1 /* 2 * Copyright (c) 2015, 2017, Oracle and/or its affiliates. All rights reserved. 3 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. 4 * 5 * This code is free software; you can redistribute it and/or modify it 6 * under the terms of the GNU General Public License version 2 only, as 7 * published by the Free Software Foundation. Oracle designates this 8 * particular file as subject to the "Classpath" exception as provided 9 * by Oracle in the LICENSE file that accompanied this code. 10 * 11 * This code is distributed in the hope that it will be useful, but WITHOUT 12 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 13 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License 14 * version 2 for more details (a copy is included in the LICENSE file that 15 * accompanied this code). 16 * 17 * You should have received a copy of the GNU General Public License version 18 * 2 along with this work; if not, write to the Free Software Foundation, 19 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. 20 * 21 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA 22 * or visit www.oracle.com if you need additional information or have any 23 * questions. 24 */ 25 26 package jdk.tools.jmod; 27 28 import java.io.ByteArrayInputStream; 29 import java.io.ByteArrayOutputStream; 30 import java.io.File; 31 import java.io.IOException; 32 import java.io.InputStream; 33 import java.io.OutputStream; 34 import java.io.PrintWriter; 35 import java.io.UncheckedIOException; 36 import java.lang.module.Configuration; 37 import java.lang.module.FindException; 38 import java.lang.module.ModuleReader; 39 import java.lang.module.ModuleReference; 40 import java.lang.module.ModuleFinder; 41 import java.lang.module.ModuleDescriptor; 42 import java.lang.module.ModuleDescriptor.Exports; 43 import java.lang.module.ModuleDescriptor.Opens; 44 import java.lang.module.ModuleDescriptor.Provides; 45 import java.lang.module.ModuleDescriptor.Requires; 46 import java.lang.module.ModuleDescriptor.Version; 47 import java.lang.module.ResolutionException; 48 import java.lang.module.ResolvedModule; 49 import java.net.URI; 50 import java.nio.file.FileSystems; 51 import java.nio.file.FileVisitOption; 52 import java.nio.file.FileVisitResult; 53 import java.nio.file.Files; 54 import java.nio.file.InvalidPathException; 55 import java.nio.file.Path; 56 import java.nio.file.PathMatcher; 57 import java.nio.file.Paths; 58 import java.nio.file.SimpleFileVisitor; 59 import java.nio.file.StandardCopyOption; 60 import java.nio.file.attribute.BasicFileAttributes; 61 import java.text.MessageFormat; 62 import java.util.ArrayList; 63 import java.util.Collection; 64 import java.util.Collections; 65 import java.util.Comparator; 66 import java.util.HashSet; 67 import java.util.LinkedHashMap; 68 import java.util.List; 69 import java.util.Locale; 70 import java.util.Map; 71 import java.util.MissingResourceException; 72 import java.util.Optional; 73 import java.util.ResourceBundle; 74 import java.util.Set; 75 import java.util.TreeSet; 76 import java.util.function.Consumer; 77 import java.util.function.Predicate; 78 import java.util.function.Supplier; 79 import java.util.jar.JarEntry; 80 import java.util.jar.JarFile; 81 import java.util.jar.JarOutputStream; 82 import java.util.stream.Collectors; 83 import java.util.regex.Pattern; 84 import java.util.regex.PatternSyntaxException; 85 import java.util.zip.ZipEntry; 86 import java.util.zip.ZipException; 87 import java.util.zip.ZipFile; 88 89 import jdk.internal.jmod.JmodFile; 90 import jdk.internal.jmod.JmodFile.Section; 91 import jdk.internal.joptsimple.BuiltinHelpFormatter; 92 import jdk.internal.joptsimple.NonOptionArgumentSpec; 93 import jdk.internal.joptsimple.OptionDescriptor; 94 import jdk.internal.joptsimple.OptionException; 95 import jdk.internal.joptsimple.OptionParser; 96 import jdk.internal.joptsimple.OptionSet; 97 import jdk.internal.joptsimple.OptionSpec; 98 import jdk.internal.joptsimple.ValueConverter; 99 import jdk.internal.module.ModuleHashes; 100 import jdk.internal.module.ModuleHashesBuilder; 101 import jdk.internal.module.ModuleInfo; 102 import jdk.internal.module.ModuleInfoExtender; 103 import jdk.internal.module.ModulePath; 104 import jdk.internal.module.ModuleResolution; 105 import jdk.internal.module.ModuleTarget; 106 import jdk.internal.module.Resources; 107 import jdk.tools.jlink.internal.Utils; 108 109 import static java.util.stream.Collectors.joining; 110 111 /** 112 * Implementation for the jmod tool. 113 */ 114 public class JmodTask { 115 116 static class CommandException extends RuntimeException { 117 private static final long serialVersionUID = 0L; 118 boolean showUsage; 119 120 CommandException(String key, Object... args) { 121 super(getMessageOrKey(key, args)); 122 } 123 124 CommandException showUsage(boolean b) { 125 showUsage = b; 126 return this; 127 } 128 129 private static String getMessageOrKey(String key, Object... args) { 130 try { 131 return MessageFormat.format(ResourceBundleHelper.bundle.getString(key), args); 132 } catch (MissingResourceException e) { 133 return key; 134 } 135 } 136 } 137 138 private static final String PROGNAME = "jmod"; 139 private static final String MODULE_INFO = "module-info.class"; 140 141 private static final Path CWD = Paths.get(""); 142 143 private Options options; 144 private PrintWriter out = new PrintWriter(System.out, true); 145 void setLog(PrintWriter out, PrintWriter err) { 146 this.out = out; 147 } 148 149 /* Result codes. */ 150 static final int EXIT_OK = 0, // Completed with no errors. 151 EXIT_ERROR = 1, // Completed but reported errors. 152 EXIT_CMDERR = 2, // Bad command-line arguments 153 EXIT_SYSERR = 3, // System error or resource exhaustion. 154 EXIT_ABNORMAL = 4;// terminated abnormally 155 156 enum Mode { 157 CREATE, 158 EXTRACT, 159 LIST, 160 DESCRIBE, 161 HASH 162 }; 163 164 static class Options { 165 Mode mode; 166 Path jmodFile; 167 boolean help; 168 boolean helpExtra; 169 boolean version; 170 List<Path> classpath; 171 List<Path> cmds; 172 List<Path> configs; 173 List<Path> libs; 174 List<Path> headerFiles; 175 List<Path> manPages; 176 List<Path> legalNotices;; 177 ModuleFinder moduleFinder; 178 Version moduleVersion; 179 String mainClass; 180 String osName; 181 String osArch; 182 Pattern modulesToHash; 183 ModuleResolution moduleResolution; 184 boolean dryrun; 185 List<PathMatcher> excludes; 186 Path extractDir; 187 } 188 189 public int run(String[] args) { 190 191 try { 192 handleOptions(args); 193 if (options == null) { 194 showUsageSummary(); 195 return EXIT_CMDERR; 196 } 197 if (options.help || options.helpExtra) { 198 showHelp(); 199 return EXIT_OK; 200 } 201 if (options.version) { 202 showVersion(); 203 return EXIT_OK; 204 } 205 206 boolean ok; 207 switch (options.mode) { 208 case CREATE: 209 ok = create(); 210 break; 211 case EXTRACT: 212 ok = extract(); 213 break; 214 case LIST: 215 ok = list(); 216 break; 217 case DESCRIBE: 218 ok = describe(); 219 break; 220 case HASH: 221 ok = hashModules(); 222 break; 223 default: 224 throw new AssertionError("Unknown mode: " + options.mode.name()); 225 } 226 227 return ok ? EXIT_OK : EXIT_ERROR; 228 } catch (CommandException e) { 229 reportError(e.getMessage()); 230 if (e.showUsage) 231 showUsageSummary(); 232 return EXIT_CMDERR; 233 } catch (Exception x) { 234 reportError(x.getMessage()); 235 x.printStackTrace(); 236 return EXIT_ABNORMAL; 237 } finally { 238 out.flush(); 239 } 240 } 241 242 private boolean list() throws IOException { 243 ZipFile zip = null; 244 try { 245 try { 246 zip = new ZipFile(options.jmodFile.toFile()); 247 } catch (IOException x) { 248 throw new IOException("error opening jmod file", x); 249 } 250 251 // Trivially print the archive entries for now, pending a more complete implementation 252 zip.stream().forEach(e -> out.println(e.getName())); 253 return true; 254 } finally { 255 if (zip != null) 256 zip.close(); 257 } 258 } 259 260 private boolean extract() throws IOException { 261 Path dir = options.extractDir != null ? options.extractDir : CWD; 262 try (JmodFile jf = new JmodFile(options.jmodFile)) { 263 jf.stream().forEach(e -> { 264 try { 265 ZipEntry entry = e.zipEntry(); 266 String name = entry.getName(); 267 int index = name.lastIndexOf("/"); 268 if (index != -1) { 269 Path p = dir.resolve(name.substring(0, index)); 270 if (Files.notExists(p)) 271 Files.createDirectories(p); 272 } 273 274 try (OutputStream os = Files.newOutputStream(dir.resolve(name))) { 275 jf.getInputStream(e).transferTo(os); 276 } 277 } catch (IOException x) { 278 throw new UncheckedIOException(x); 279 } 280 }); 281 282 return true; 283 } 284 } 285 286 private boolean hashModules() { 287 if (options.dryrun) { 288 out.println("Dry run:"); 289 } 290 291 Hasher hasher = new Hasher(options.moduleFinder); 292 hasher.computeHashes().forEach((mn, hashes) -> { 293 if (options.dryrun) { 294 out.format("%s%n", mn); 295 hashes.names().stream() 296 .sorted() 297 .forEach(name -> out.format(" hashes %s %s %s%n", 298 name, hashes.algorithm(), toHex(hashes.hashFor(name)))); 299 } else { 300 try { 301 hasher.updateModuleInfo(mn, hashes); 302 } catch (IOException ex) { 303 throw new UncheckedIOException(ex); 304 } 305 } 306 }); 307 return true; 308 } 309 310 private boolean describe() throws IOException { 311 try (JmodFile jf = new JmodFile(options.jmodFile)) { 312 try (InputStream in = jf.getInputStream(Section.CLASSES, MODULE_INFO)) { 313 ModuleInfo.Attributes attrs = ModuleInfo.read(in, null); 314 printModuleDescriptor(attrs.descriptor(), 315 attrs.target(), 316 attrs.recordedHashes()); 317 return true; 318 } catch (IOException e) { 319 throw new CommandException("err.module.descriptor.not.found"); 320 } 321 } 322 } 323 324 static <T> String toString(Collection<T> c) { 325 if (c.isEmpty()) { return ""; } 326 return c.stream().map(e -> e.toString().toLowerCase(Locale.ROOT)) 327 .collect(joining(" ")); 328 } 329 330 private void printModuleDescriptor(ModuleDescriptor md, 331 ModuleTarget target, 332 ModuleHashes hashes) 333 throws IOException 334 { 335 StringBuilder sb = new StringBuilder(); 336 sb.append("\n").append(md.toNameAndVersion()); 337 338 md.requires().stream() 339 .sorted(Comparator.comparing(Requires::name)) 340 .forEach(r -> { 341 sb.append("\n requires "); 342 if (!r.modifiers().isEmpty()) 343 sb.append(toString(r.modifiers())).append(" "); 344 sb.append(r.name()); 345 }); 346 347 md.uses().stream().sorted() 348 .forEach(s -> sb.append("\n uses ").append(s)); 349 350 md.exports().stream() 351 .sorted(Comparator.comparing(Exports::source)) 352 .forEach(p -> sb.append("\n exports ").append(p)); 353 354 md.opens().stream() 355 .sorted(Comparator.comparing(Opens::source)) 356 .forEach(p -> sb.append("\n opens ").append(p)); 357 358 Set<String> concealed = new HashSet<>(md.packages()); 359 md.exports().stream().map(Exports::source).forEach(concealed::remove); 360 md.opens().stream().map(Opens::source).forEach(concealed::remove); 361 concealed.stream().sorted() 362 .forEach(p -> sb.append("\n contains ").append(p)); 363 364 md.provides().stream() 365 .sorted(Comparator.comparing(Provides::service)) 366 .forEach(p -> sb.append("\n provides ").append(p.service()) 367 .append(" with ") 368 .append(toString(p.providers()))); 369 370 md.mainClass().ifPresent(v -> sb.append("\n main-class " + v)); 371 372 if (target != null) { 373 String osName = target.osName(); 374 if (osName != null) 375 sb.append("\n operating-system-name " + osName); 376 String osArch = target.osArch(); 377 if (osArch != null) 378 sb.append("\n operating-system-architecture " + osArch); 379 } 380 381 if (hashes != null) { 382 hashes.names().stream().sorted().forEach( 383 mod -> sb.append("\n hashes ").append(mod).append(" ") 384 .append(hashes.algorithm()).append(" ") 385 .append(toHex(hashes.hashFor(mod)))); 386 } 387 388 out.println(sb.toString()); 389 } 390 391 private String toHex(byte[] ba) { 392 StringBuilder sb = new StringBuilder(ba.length); 393 for (byte b: ba) { 394 sb.append(String.format("%02x", b & 0xff)); 395 } 396 return sb.toString(); 397 } 398 399 private boolean create() throws IOException { 400 JmodFileWriter jmod = new JmodFileWriter(); 401 402 // create jmod with temporary name to avoid it being examined 403 // when scanning the module path 404 Path target = options.jmodFile; 405 Path tempTarget = jmodTempFilePath(target); 406 try { 407 try (JmodOutputStream jos = JmodOutputStream.newOutputStream(tempTarget)) { 408 jmod.write(jos); 409 } 410 Files.move(tempTarget, target); 411 } catch (Exception e) { 412 try { 413 Files.deleteIfExists(tempTarget); 414 } catch (IOException ioe) { 415 e.addSuppressed(ioe); 416 } 417 throw e; 418 } 419 return true; 420 } 421 422 /* 423 * Create a JMOD .tmp file for the given target JMOD file 424 */ 425 private static Path jmodTempFilePath(Path target) throws IOException { 426 return target.resolveSibling("." + target.getFileName() + ".tmp"); 427 } 428 429 private class JmodFileWriter { 430 final List<Path> cmds = options.cmds; 431 final List<Path> libs = options.libs; 432 final List<Path> configs = options.configs; 433 final List<Path> classpath = options.classpath; 434 final List<Path> headerFiles = options.headerFiles; 435 final List<Path> manPages = options.manPages; 436 final List<Path> legalNotices = options.legalNotices; 437 438 final Version moduleVersion = options.moduleVersion; 439 final String mainClass = options.mainClass; 440 final String osName = options.osName; 441 final String osArch = options.osArch; 442 final List<PathMatcher> excludes = options.excludes; 443 final ModuleResolution moduleResolution = options.moduleResolution; 444 445 JmodFileWriter() { } 446 447 /** 448 * Writes the jmod to the given output stream. 449 */ 450 void write(JmodOutputStream out) throws IOException { 451 // module-info.class 452 writeModuleInfo(out, findPackages(classpath)); 453 454 // classes 455 processClasses(out, classpath); 456 457 processSection(out, Section.CONFIG, configs); 458 processSection(out, Section.HEADER_FILES, headerFiles); 459 processSection(out, Section.LEGAL_NOTICES, legalNotices); 460 processSection(out, Section.MAN_PAGES, manPages); 461 processSection(out, Section.NATIVE_CMDS, cmds); 462 processSection(out, Section.NATIVE_LIBS, libs); 463 464 } 465 466 /** 467 * Returns a supplier of an input stream to the module-info.class 468 * on the class path of directories and JAR files. 469 */ 470 Supplier<InputStream> newModuleInfoSupplier() throws IOException { 471 ByteArrayOutputStream baos = new ByteArrayOutputStream(); 472 for (Path e: classpath) { 473 if (Files.isDirectory(e)) { 474 Path mi = e.resolve(MODULE_INFO); 475 if (Files.isRegularFile(mi)) { 476 Files.copy(mi, baos); 477 break; 478 } 479 } else if (Files.isRegularFile(e) && e.toString().endsWith(".jar")) { 480 try (JarFile jf = new JarFile(e.toFile())) { 481 ZipEntry entry = jf.getEntry(MODULE_INFO); 482 if (entry != null) { 483 jf.getInputStream(entry).transferTo(baos); 484 break; 485 } 486 } catch (ZipException x) { 487 // Skip. Do nothing. No packages will be added. 488 } 489 } 490 } 491 if (baos.size() == 0) { 492 return null; 493 } else { 494 byte[] bytes = baos.toByteArray(); 495 return () -> new ByteArrayInputStream(bytes); 496 } 497 } 498 499 /** 500 * Writes the updated module-info.class to the ZIP output stream. 501 * 502 * The updated module-info.class will have a Packages attribute 503 * with the set of module-private/non-exported packages. 504 * 505 * If --module-version, --main-class, or other options were provided 506 * then the corresponding class file attributes are added to the 507 * module-info here. 508 */ 509 void writeModuleInfo(JmodOutputStream out, Set<String> packages) 510 throws IOException 511 { 512 Supplier<InputStream> miSupplier = newModuleInfoSupplier(); 513 if (miSupplier == null) { 514 throw new IOException(MODULE_INFO + " not found"); 515 } 516 517 ModuleDescriptor descriptor; 518 try (InputStream in = miSupplier.get()) { 519 descriptor = ModuleDescriptor.read(in); 520 } 521 522 // copy the module-info.class into the jmod with the additional 523 // attributes for the version, main class and other meta data 524 try (InputStream in = miSupplier.get()) { 525 ModuleInfoExtender extender = ModuleInfoExtender.newExtender(in); 526 527 // Add (or replace) the Packages attribute 528 if (packages != null) { 529 validatePackages(descriptor, packages); 530 extender.packages(packages); 531 } 532 533 // --main-class 534 if (mainClass != null) 535 extender.mainClass(mainClass); 536 537 // --os-name, --os-arch 538 if (osName != null || osArch != null) 539 extender.targetPlatform(osName, osArch); 540 541 // --module-version 542 if (moduleVersion != null) 543 extender.version(moduleVersion); 544 545 // --hash-modules 546 if (options.modulesToHash != null) { 547 // To compute hashes, it creates a Configuration to resolve 548 // a module graph. The post-resolution check requires 549 // the packages in ModuleDescriptor be available for validation. 550 ModuleDescriptor md; 551 try (InputStream is = miSupplier.get()) { 552 md = ModuleDescriptor.read(is, () -> packages); 553 } 554 555 ModuleHashes moduleHashes = computeHashes(md); 556 if (moduleHashes != null) { 557 extender.hashes(moduleHashes); 558 } else { 559 warning("warn.no.module.hashes", descriptor.name()); 560 } 561 } 562 563 if (moduleResolution != null && moduleResolution.value() != 0) { 564 extender.moduleResolution(moduleResolution); 565 } 566 567 // write the (possibly extended or modified) module-info.class 568 out.writeEntry(extender.toByteArray(), Section.CLASSES, MODULE_INFO); 569 } 570 } 571 572 private void validatePackages(ModuleDescriptor descriptor, Set<String> packages) { 573 Set<String> nonExistPackages = new TreeSet<>(); 574 descriptor.exports().stream() 575 .map(Exports::source) 576 .filter(pn -> !packages.contains(pn)) 577 .forEach(nonExistPackages::add); 578 579 descriptor.opens().stream() 580 .map(Opens::source) 581 .filter(pn -> !packages.contains(pn)) 582 .forEach(nonExistPackages::add); 583 584 if (!nonExistPackages.isEmpty()) { 585 throw new CommandException("err.missing.export.or.open.packages", 586 descriptor.name(), nonExistPackages); 587 } 588 } 589 590 /* 591 * Hasher resolves a module graph using the --hash-modules PATTERN 592 * as the roots. 593 * 594 * The jmod file is being created and does not exist in the 595 * given modulepath. 596 */ 597 private ModuleHashes computeHashes(ModuleDescriptor descriptor) { 598 String mn = descriptor.name(); 599 URI uri = options.jmodFile.toUri(); 600 ModuleReference mref = new ModuleReference(descriptor, uri) { 601 @Override 602 public ModuleReader open() { 603 throw new UnsupportedOperationException("opening " + mn); 604 } 605 }; 606 607 // compose a module finder with the module path and also 608 // a module finder that can find the jmod file being created 609 ModuleFinder finder = ModuleFinder.compose(options.moduleFinder, 610 new ModuleFinder() { 611 @Override 612 public Optional<ModuleReference> find(String name) { 613 if (descriptor.name().equals(name)) 614 return Optional.of(mref); 615 else return Optional.empty(); 616 } 617 618 @Override 619 public Set<ModuleReference> findAll() { 620 return Collections.singleton(mref); 621 } 622 }); 623 624 return new Hasher(mn, finder).computeHashes().get(mn); 625 } 626 627 /** 628 * Returns the set of all packages on the given class path. 629 */ 630 Set<String> findPackages(List<Path> classpath) { 631 Set<String> packages = new HashSet<>(); 632 for (Path path : classpath) { 633 if (Files.isDirectory(path)) { 634 packages.addAll(findPackages(path)); 635 } else if (Files.isRegularFile(path) && path.toString().endsWith(".jar")) { 636 try (JarFile jf = new JarFile(path.toString())) { 637 packages.addAll(findPackages(jf)); 638 } catch (ZipException x) { 639 // Skip. Do nothing. No packages will be added. 640 } catch (IOException ioe) { 641 throw new UncheckedIOException(ioe); 642 } 643 } 644 } 645 return packages; 646 } 647 648 /** 649 * Returns the set of packages in the given directory tree. 650 */ 651 Set<String> findPackages(Path dir) { 652 try { 653 return Files.find(dir, Integer.MAX_VALUE, 654 ((path, attrs) -> attrs.isRegularFile())) 655 .map(dir::relativize) 656 .filter(path -> isResource(path.toString())) 657 .map(path -> toPackageName(path)) 658 .filter(pkg -> pkg.length() > 0) 659 .distinct() 660 .collect(Collectors.toSet()); 661 } catch (IOException ioe) { 662 throw new UncheckedIOException(ioe); 663 } 664 } 665 666 /** 667 * Returns the set of packages in the given JAR file. 668 */ 669 Set<String> findPackages(JarFile jf) { 670 return jf.stream() 671 .filter(e -> !e.isDirectory() && isResource(e.getName())) 672 .map(e -> toPackageName(e)) 673 .filter(pkg -> pkg.length() > 0) 674 .distinct() 675 .collect(Collectors.toSet()); 676 } 677 678 /** 679 * Returns true if it's a .class or a resource with an effective 680 * package name. 681 */ 682 boolean isResource(String name) { 683 name = name.replace(File.separatorChar, '/'); 684 return name.endsWith(".class") || Resources.canEncapsulate(name); 685 } 686 687 688 String toPackageName(Path path) { 689 String name = path.toString(); 690 int index = name.lastIndexOf(File.separatorChar); 691 if (index != -1) 692 return name.substring(0, index).replace(File.separatorChar, '.'); 693 694 if (name.endsWith(".class") && !name.equals(MODULE_INFO)) { 695 IOException e = new IOException(name + " in the unnamed package"); 696 throw new UncheckedIOException(e); 697 } 698 return ""; 699 } 700 701 String toPackageName(ZipEntry entry) { 702 String name = entry.getName(); 703 int index = name.lastIndexOf("/"); 704 if (index != -1) 705 return name.substring(0, index).replace('/', '.'); 706 707 if (name.endsWith(".class") && !name.equals(MODULE_INFO)) { 708 IOException e = new IOException(name + " in the unnamed package"); 709 throw new UncheckedIOException(e); 710 } 711 return ""; 712 } 713 714 void processClasses(JmodOutputStream out, List<Path> classpaths) 715 throws IOException 716 { 717 if (classpaths == null) 718 return; 719 720 for (Path p : classpaths) { 721 if (Files.isDirectory(p)) { 722 processSection(out, Section.CLASSES, p); 723 } else if (Files.isRegularFile(p) && p.toString().endsWith(".jar")) { 724 try (JarFile jf = new JarFile(p.toFile())) { 725 JarEntryConsumer jec = new JarEntryConsumer(out, jf); 726 jf.stream().filter(jec).forEach(jec); 727 } 728 } 729 } 730 } 731 732 void processSection(JmodOutputStream out, Section section, List<Path> paths) 733 throws IOException 734 { 735 if (paths == null) 736 return; 737 738 for (Path p : paths) { 739 processSection(out, section, p); 740 } 741 } 742 743 void processSection(JmodOutputStream out, Section section, Path path) 744 throws IOException 745 { 746 Files.walkFileTree(path, Set.of(FileVisitOption.FOLLOW_LINKS), 747 Integer.MAX_VALUE, new SimpleFileVisitor<Path>() { 748 @Override 749 public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) 750 throws IOException 751 { 752 Path relPath = path.relativize(file); 753 if (relPath.toString().equals(MODULE_INFO) 754 && !Section.CLASSES.equals(section)) 755 warning("warn.ignore.entry", MODULE_INFO, section); 756 757 if (!relPath.toString().equals(MODULE_INFO) 758 && !matches(relPath, excludes)) { 759 try (InputStream in = Files.newInputStream(file)) { 760 out.writeEntry(in, section, relPath.toString()); 761 } catch (IOException x) { 762 if (x.getMessage().contains("duplicate entry")) { 763 warning("warn.ignore.duplicate.entry", 764 relPath.toString(), section); 765 return FileVisitResult.CONTINUE; 766 } 767 throw x; 768 } 769 } 770 return FileVisitResult.CONTINUE; 771 } 772 }); 773 } 774 775 boolean matches(Path path, List<PathMatcher> matchers) { 776 if (matchers != null) { 777 for (PathMatcher pm : matchers) { 778 if (pm.matches(path)) 779 return true; 780 } 781 } 782 return false; 783 } 784 785 class JarEntryConsumer implements Consumer<JarEntry>, Predicate<JarEntry> { 786 final JmodOutputStream out; 787 final JarFile jarfile; 788 JarEntryConsumer(JmodOutputStream out, JarFile jarfile) { 789 this.out = out; 790 this.jarfile = jarfile; 791 } 792 @Override 793 public void accept(JarEntry je) { 794 try (InputStream in = jarfile.getInputStream(je)) { 795 out.writeEntry(in, Section.CLASSES, je.getName()); 796 } catch (IOException e) { 797 throw new UncheckedIOException(e); 798 } 799 } 800 @Override 801 public boolean test(JarEntry je) { 802 String name = je.getName(); 803 // ## no support for excludes. Is it really needed? 804 return !name.endsWith(MODULE_INFO) && !je.isDirectory(); 805 } 806 } 807 } 808 809 /** 810 * Compute and record hashes 811 */ 812 private class Hasher { 813 final Configuration configuration; 814 final ModuleHashesBuilder hashesBuilder; 815 final Set<String> modules; 816 final String moduleName; // a specific module to record hashes, if set 817 818 /** 819 * This constructor is for jmod hash command. 820 * 821 * This Hasher will determine which modules to record hashes, i.e. 822 * the module in a subgraph of modules to be hashed and that 823 * has no outgoing edges. It will record in each of these modules, 824 * say `M`, with the the hashes of modules that depend upon M 825 * directly or indirectly matching the specified --hash-modules pattern. 826 */ 827 Hasher(ModuleFinder finder) { 828 this(null, finder); 829 } 830 831 /** 832 * Constructs a Hasher to compute hashes. 833 * 834 * If a module name `M` is specified, it will compute the hashes of 835 * modules that depend upon M directly or indirectly matching the 836 * specified --hash-modules pattern and record in the ModuleHashes 837 * attribute in M's module-info.class. 838 * 839 * @param name name of the module to record hashes 840 * @param finder module finder for the specified --module-path 841 */ 842 Hasher(String name, ModuleFinder finder) { 843 // Determine the modules that matches the pattern {@code modulesToHash} 844 Set<String> roots = finder.findAll().stream() 845 .map(mref -> mref.descriptor().name()) 846 .filter(mn -> options.modulesToHash.matcher(mn).find()) 847 .collect(Collectors.toSet()); 848 849 // use system module path unless it creates a JMOD file for 850 // a module that is present in the system image e.g. upgradeable 851 // module 852 ModuleFinder system; 853 if (name != null && ModuleFinder.ofSystem().find(name).isPresent()) { 854 system = ModuleFinder.of(); 855 } else { 856 system = ModuleFinder.ofSystem(); 857 } 858 // get a resolved module graph 859 Configuration config = null; 860 try { 861 config = Configuration.empty().resolve(system, finder, roots); 862 } catch (FindException | ResolutionException e) { 863 throw new CommandException("err.module.resolution.fail", e.getMessage()); 864 } 865 866 this.moduleName = name; 867 this.configuration = config; 868 869 // filter modules resolved from the system module finder 870 this.modules = config.modules().stream() 871 .map(ResolvedModule::name) 872 .filter(mn -> roots.contains(mn) && !system.find(mn).isPresent()) 873 .collect(Collectors.toSet()); 874 875 this.hashesBuilder = new ModuleHashesBuilder(config, modules); 876 } 877 878 /** 879 * Returns a map of a module M to record hashes of the modules 880 * that depend upon M directly or indirectly. 881 * 882 * For jmod hash command, the returned map contains one entry 883 * for each module M that has no outgoing edges to any of the 884 * modules matching the specified --hash-modules pattern. 885 * 886 * Each entry represents a leaf node in a connected subgraph containing 887 * M and other candidate modules from the module graph where M's outgoing 888 * edges to any module other than the ones matching the specified 889 * --hash-modules pattern are excluded. 890 */ 891 Map<String, ModuleHashes> computeHashes() { 892 if (hashesBuilder == null) 893 return null; 894 895 if (moduleName != null) { 896 return hashesBuilder.computeHashes(Set.of(moduleName)); 897 } else { 898 return hashesBuilder.computeHashes(modules); 899 } 900 } 901 902 /** 903 * Reads the given input stream of module-info.class and write 904 * the extended module-info.class with the given ModuleHashes 905 * 906 * @param in InputStream of module-info.class 907 * @param out OutputStream to write the extended module-info.class 908 * @param hashes ModuleHashes 909 */ 910 private void recordHashes(InputStream in, OutputStream out, ModuleHashes hashes) 911 throws IOException 912 { 913 ModuleInfoExtender extender = ModuleInfoExtender.newExtender(in); 914 extender.hashes(hashes); 915 extender.write(out); 916 } 917 918 void updateModuleInfo(String name, ModuleHashes moduleHashes) 919 throws IOException 920 { 921 Path target = moduleToPath(name); 922 Path tempTarget = jmodTempFilePath(target); 923 try { 924 if (target.getFileName().toString().endsWith(".jmod")) { 925 updateJmodFile(target, tempTarget, moduleHashes); 926 } else { 927 updateModularJar(target, tempTarget, moduleHashes); 928 } 929 } catch (IOException|RuntimeException e) { 930 try { 931 Files.deleteIfExists(tempTarget); 932 } catch (IOException ioe) { 933 e.addSuppressed(ioe); 934 } 935 throw e; 936 } 937 938 out.println(getMessage("module.hashes.recorded", name)); 939 Files.move(tempTarget, target, StandardCopyOption.REPLACE_EXISTING); 940 } 941 942 private void updateModularJar(Path target, Path tempTarget, 943 ModuleHashes moduleHashes) 944 throws IOException 945 { 946 try (JarFile jf = new JarFile(target.toFile()); 947 OutputStream out = Files.newOutputStream(tempTarget); 948 JarOutputStream jos = new JarOutputStream(out)) 949 { 950 jf.stream().forEach(e -> { 951 try (InputStream in = jf.getInputStream(e)) { 952 if (e.getName().equals(MODULE_INFO)) { 953 // what about module-info.class in versioned entries? 954 ZipEntry ze = new ZipEntry(e.getName()); 955 ze.setTime(System.currentTimeMillis()); 956 jos.putNextEntry(ze); 957 recordHashes(in, jos, moduleHashes); 958 jos.closeEntry(); 959 } else { 960 jos.putNextEntry(e); 961 jos.write(in.readAllBytes()); 962 jos.closeEntry(); 963 } 964 } catch (IOException x) { 965 throw new UncheckedIOException(x); 966 } 967 }); 968 } 969 } 970 971 private void updateJmodFile(Path target, Path tempTarget, 972 ModuleHashes moduleHashes) 973 throws IOException 974 { 975 976 try (JmodFile jf = new JmodFile(target); 977 JmodOutputStream jos = JmodOutputStream.newOutputStream(tempTarget)) 978 { 979 jf.stream().forEach(e -> { 980 try (InputStream in = jf.getInputStream(e.section(), e.name())) { 981 if (e.name().equals(MODULE_INFO)) { 982 // replace module-info.class 983 ModuleInfoExtender extender = 984 ModuleInfoExtender.newExtender(in); 985 extender.hashes(moduleHashes); 986 jos.writeEntry(extender.toByteArray(), e.section(), e.name()); 987 } else { 988 jos.writeEntry(in, e); 989 } 990 } catch (IOException x) { 991 throw new UncheckedIOException(x); 992 } 993 }); 994 } 995 } 996 997 private Path moduleToPath(String name) { 998 ResolvedModule rm = configuration.findModule(name).orElseThrow( 999 () -> new InternalError("Selected module " + name + " not on module path")); 1000 1001 URI uri = rm.reference().location().get(); 1002 Path path = Paths.get(uri); 1003 String fn = path.getFileName().toString(); 1004 if (!fn.endsWith(".jar") && !fn.endsWith(".jmod")) { 1005 throw new InternalError(path + " is not a modular JAR or jmod file"); 1006 } 1007 return path; 1008 } 1009 } 1010 1011 /** 1012 * An abstract converter that given a string representing a list of paths, 1013 * separated by the File.pathSeparator, returns a List of java.nio.Path's. 1014 * Specific subclasses should do whatever validation is required on the 1015 * individual path elements, if any. 1016 */ 1017 static abstract class AbstractPathConverter implements ValueConverter<List<Path>> { 1018 @Override 1019 public List<Path> convert(String value) { 1020 List<Path> paths = new ArrayList<>(); 1021 String[] pathElements = value.split(File.pathSeparator); 1022 for (String pathElement : pathElements) { 1023 paths.add(toPath(pathElement)); 1024 } 1025 return paths; 1026 } 1027 1028 @SuppressWarnings("unchecked") 1029 @Override 1030 public Class<List<Path>> valueType() { 1031 return (Class<List<Path>>)(Object)List.class; 1032 } 1033 1034 @Override public String valuePattern() { return "path"; } 1035 1036 abstract Path toPath(String path); 1037 } 1038 1039 static class ClassPathConverter extends AbstractPathConverter { 1040 static final ValueConverter<List<Path>> INSTANCE = new ClassPathConverter(); 1041 1042 @Override 1043 public Path toPath(String value) { 1044 try { 1045 Path path = CWD.resolve(value); 1046 if (Files.notExists(path)) 1047 throw new CommandException("err.path.not.found", path); 1048 if (!(Files.isDirectory(path) || 1049 (Files.isRegularFile(path) && path.toString().endsWith(".jar")))) 1050 throw new CommandException("err.invalid.class.path.entry", path); 1051 return path; 1052 } catch (InvalidPathException x) { 1053 throw new CommandException("err.path.not.valid", value); 1054 } 1055 } 1056 } 1057 1058 static class DirPathConverter extends AbstractPathConverter { 1059 static final ValueConverter<List<Path>> INSTANCE = new DirPathConverter(); 1060 1061 @Override 1062 public Path toPath(String value) { 1063 try { 1064 Path path = CWD.resolve(value); 1065 if (Files.notExists(path)) 1066 throw new CommandException("err.path.not.found", path); 1067 if (!Files.isDirectory(path)) 1068 throw new CommandException("err.path.not.a.dir", path); 1069 return path; 1070 } catch (InvalidPathException x) { 1071 throw new CommandException("err.path.not.valid", value); 1072 } 1073 } 1074 } 1075 1076 static class ExtractDirPathConverter implements ValueConverter<Path> { 1077 1078 @Override 1079 public Path convert(String value) { 1080 try { 1081 Path path = CWD.resolve(value); 1082 if (Files.exists(path)) { 1083 if (!Files.isDirectory(path)) 1084 throw new CommandException("err.cannot.create.dir", path); 1085 } 1086 return path; 1087 } catch (InvalidPathException x) { 1088 throw new CommandException("err.path.not.valid", value); 1089 } 1090 } 1091 1092 @Override public Class<Path> valueType() { return Path.class; } 1093 1094 @Override public String valuePattern() { return "path"; } 1095 } 1096 1097 static class ModuleVersionConverter implements ValueConverter<Version> { 1098 @Override 1099 public Version convert(String value) { 1100 try { 1101 return Version.parse(value); 1102 } catch (IllegalArgumentException x) { 1103 throw new CommandException("err.invalid.version", x.getMessage()); 1104 } 1105 } 1106 1107 @Override public Class<Version> valueType() { return Version.class; } 1108 1109 @Override public String valuePattern() { return "module-version"; } 1110 } 1111 1112 static class WarnIfResolvedReasonConverter 1113 implements ValueConverter<ModuleResolution> 1114 { 1115 @Override 1116 public ModuleResolution convert(String value) { 1117 if (value.equals("deprecated")) 1118 return ModuleResolution.empty().withDeprecated(); 1119 else if (value.equals("deprecated-for-removal")) 1120 return ModuleResolution.empty().withDeprecatedForRemoval(); 1121 else if (value.equals("incubating")) 1122 return ModuleResolution.empty().withIncubating(); 1123 else 1124 throw new CommandException("err.bad.WarnIfResolvedReason", value); 1125 } 1126 1127 @Override public Class<ModuleResolution> valueType() { 1128 return ModuleResolution.class; 1129 } 1130 1131 @Override public String valuePattern() { return "reason"; } 1132 } 1133 1134 static class PatternConverter implements ValueConverter<Pattern> { 1135 @Override 1136 public Pattern convert(String value) { 1137 try { 1138 if (value.startsWith("regex:")) { 1139 value = value.substring("regex:".length()).trim(); 1140 } 1141 1142 return Pattern.compile(value); 1143 } catch (PatternSyntaxException e) { 1144 throw new CommandException("err.bad.pattern", value); 1145 } 1146 } 1147 1148 @Override public Class<Pattern> valueType() { return Pattern.class; } 1149 1150 @Override public String valuePattern() { return "regex-pattern"; } 1151 } 1152 1153 static class PathMatcherConverter implements ValueConverter<PathMatcher> { 1154 @Override 1155 public PathMatcher convert(String pattern) { 1156 try { 1157 return Utils.getPathMatcher(FileSystems.getDefault(), pattern); 1158 } catch (PatternSyntaxException e) { 1159 throw new CommandException("err.bad.pattern", pattern); 1160 } 1161 } 1162 1163 @Override public Class<PathMatcher> valueType() { return PathMatcher.class; } 1164 1165 @Override public String valuePattern() { return "pattern-list"; } 1166 } 1167 1168 /* Support for @<file> in jmod help */ 1169 private static final String CMD_FILENAME = "@<filename>"; 1170 1171 /** 1172 * This formatter is adding the @filename option and does the required 1173 * formatting. 1174 */ 1175 private static final class JmodHelpFormatter extends BuiltinHelpFormatter { 1176 1177 private final Options opts; 1178 1179 private JmodHelpFormatter(Options opts) { 1180 super(80, 2); 1181 this.opts = opts; 1182 } 1183 1184 @Override 1185 public String format(Map<String, ? extends OptionDescriptor> options) { 1186 Map<String, OptionDescriptor> all = new LinkedHashMap<>(); 1187 all.putAll(options); 1188 1189 // extra options 1190 if (!opts.helpExtra) { 1191 all.remove("do-not-resolve-by-default"); 1192 all.remove("warn-if-resolved"); 1193 } 1194 1195 all.put(CMD_FILENAME, new OptionDescriptor() { 1196 @Override 1197 public Collection<String> options() { 1198 List<String> ret = new ArrayList<>(); 1199 ret.add(CMD_FILENAME); 1200 return ret; 1201 } 1202 @Override 1203 public String description() { return getMessage("main.opt.cmdfile"); } 1204 @Override 1205 public List<?> defaultValues() { return Collections.emptyList(); } 1206 @Override 1207 public boolean isRequired() { return false; } 1208 @Override 1209 public boolean acceptsArguments() { return false; } 1210 @Override 1211 public boolean requiresArgument() { return false; } 1212 @Override 1213 public String argumentDescription() { return null; } 1214 @Override 1215 public String argumentTypeIndicator() { return null; } 1216 @Override 1217 public boolean representsNonOptions() { return false; } 1218 }); 1219 String content = super.format(all); 1220 StringBuilder builder = new StringBuilder(); 1221 1222 builder.append(getMessage("main.opt.mode")).append("\n "); 1223 builder.append(getMessage("main.opt.mode.create")).append("\n "); 1224 builder.append(getMessage("main.opt.mode.extract")).append("\n "); 1225 builder.append(getMessage("main.opt.mode.list")).append("\n "); 1226 builder.append(getMessage("main.opt.mode.describe")).append("\n "); 1227 builder.append(getMessage("main.opt.mode.hash")).append("\n\n"); 1228 1229 String cmdfile = null; 1230 String[] lines = content.split("\n"); 1231 for (String line : lines) { 1232 if (line.startsWith("--@")) { 1233 cmdfile = line.replace("--" + CMD_FILENAME, CMD_FILENAME + " "); 1234 } else if (line.startsWith("Option") || line.startsWith("------")) { 1235 builder.append(" ").append(line).append("\n"); 1236 } else if (!line.matches("Non-option arguments")){ 1237 builder.append(" ").append(line).append("\n"); 1238 } 1239 } 1240 if (cmdfile != null) { 1241 builder.append(" ").append(cmdfile).append("\n"); 1242 } 1243 return builder.toString(); 1244 } 1245 } 1246 1247 private final OptionParser parser = new OptionParser("hp"); 1248 1249 private void handleOptions(String[] args) { 1250 options = new Options(); 1251 parser.formatHelpWith(new JmodHelpFormatter(options)); 1252 1253 OptionSpec<List<Path>> classPath 1254 = parser.accepts("class-path", getMessage("main.opt.class-path")) 1255 .withRequiredArg() 1256 .withValuesConvertedBy(ClassPathConverter.INSTANCE); 1257 1258 OptionSpec<List<Path>> cmds 1259 = parser.accepts("cmds", getMessage("main.opt.cmds")) 1260 .withRequiredArg() 1261 .withValuesConvertedBy(DirPathConverter.INSTANCE); 1262 1263 OptionSpec<List<Path>> config 1264 = parser.accepts("config", getMessage("main.opt.config")) 1265 .withRequiredArg() 1266 .withValuesConvertedBy(DirPathConverter.INSTANCE); 1267 1268 OptionSpec<Path> dir 1269 = parser.accepts("dir", getMessage("main.opt.extractDir")) 1270 .withRequiredArg() 1271 .withValuesConvertedBy(new ExtractDirPathConverter()); 1272 1273 OptionSpec<Void> dryrun 1274 = parser.accepts("dry-run", getMessage("main.opt.dry-run")); 1275 1276 OptionSpec<PathMatcher> excludes 1277 = parser.accepts("exclude", getMessage("main.opt.exclude")) 1278 .withRequiredArg() 1279 .withValuesConvertedBy(new PathMatcherConverter()); 1280 1281 OptionSpec<Pattern> hashModules 1282 = parser.accepts("hash-modules", getMessage("main.opt.hash-modules")) 1283 .withRequiredArg() 1284 .withValuesConvertedBy(new PatternConverter()); 1285 1286 OptionSpec<Void> help 1287 = parser.acceptsAll(Set.of("h", "help"), getMessage("main.opt.help")) 1288 .forHelp(); 1289 1290 OptionSpec<Void> helpExtra 1291 = parser.accepts("help-extra", getMessage("main.opt.help-extra")); 1292 1293 OptionSpec<List<Path>> headerFiles 1294 = parser.accepts("header-files", getMessage("main.opt.header-files")) 1295 .withRequiredArg() 1296 .withValuesConvertedBy(DirPathConverter.INSTANCE); 1297 1298 OptionSpec<List<Path>> libs 1299 = parser.accepts("libs", getMessage("main.opt.libs")) 1300 .withRequiredArg() 1301 .withValuesConvertedBy(DirPathConverter.INSTANCE); 1302 1303 OptionSpec<List<Path>> legalNotices 1304 = parser.accepts("legal-notices", getMessage("main.opt.legal-notices")) 1305 .withRequiredArg() 1306 .withValuesConvertedBy(DirPathConverter.INSTANCE); 1307 1308 1309 OptionSpec<String> mainClass 1310 = parser.accepts("main-class", getMessage("main.opt.main-class")) 1311 .withRequiredArg() 1312 .describedAs(getMessage("main.opt.main-class.arg")); 1313 1314 OptionSpec<List<Path>> manPages 1315 = parser.accepts("man-pages", getMessage("main.opt.man-pages")) 1316 .withRequiredArg() 1317 .withValuesConvertedBy(DirPathConverter.INSTANCE); 1318 1319 OptionSpec<List<Path>> modulePath 1320 = parser.acceptsAll(Set.of("p", "module-path"), 1321 getMessage("main.opt.module-path")) 1322 .withRequiredArg() 1323 .withValuesConvertedBy(DirPathConverter.INSTANCE); 1324 1325 OptionSpec<Version> moduleVersion 1326 = parser.accepts("module-version", getMessage("main.opt.module-version")) 1327 .withRequiredArg() 1328 .withValuesConvertedBy(new ModuleVersionConverter()); 1329 1330 OptionSpec<String> osName 1331 = parser.accepts("os-name", getMessage("main.opt.os-name")) 1332 .withRequiredArg() 1333 .describedAs(getMessage("main.opt.os-name.arg")); 1334 1335 OptionSpec<String> osArch 1336 = parser.accepts("os-arch", getMessage("main.opt.os-arch")) 1337 .withRequiredArg() 1338 .describedAs(getMessage("main.opt.os-arch.arg")); 1339 1340 OptionSpec<Void> doNotResolveByDefault 1341 = parser.accepts("do-not-resolve-by-default", 1342 getMessage("main.opt.do-not-resolve-by-default")); 1343 1344 OptionSpec<ModuleResolution> warnIfResolved 1345 = parser.accepts("warn-if-resolved", getMessage("main.opt.warn-if-resolved")) 1346 .withRequiredArg() 1347 .withValuesConvertedBy(new WarnIfResolvedReasonConverter()); 1348 1349 OptionSpec<Void> version 1350 = parser.accepts("version", getMessage("main.opt.version")); 1351 1352 NonOptionArgumentSpec<String> nonOptions 1353 = parser.nonOptions(); 1354 1355 try { 1356 OptionSet opts = parser.parse(args); 1357 1358 if (opts.has(help) || opts.has(helpExtra) || opts.has(version)) { 1359 options.help = opts.has(help); 1360 options.helpExtra = opts.has(helpExtra); 1361 options.version = opts.has(version); 1362 return; // informational message will be shown 1363 } 1364 1365 List<String> words = opts.valuesOf(nonOptions); 1366 if (words.isEmpty()) 1367 throw new CommandException("err.missing.mode").showUsage(true); 1368 String verb = words.get(0); 1369 try { 1370 options.mode = Enum.valueOf(Mode.class, verb.toUpperCase()); 1371 } catch (IllegalArgumentException e) { 1372 throw new CommandException("err.invalid.mode", verb).showUsage(true); 1373 } 1374 1375 if (opts.has(classPath)) 1376 options.classpath = getLastElement(opts.valuesOf(classPath)); 1377 if (opts.has(cmds)) 1378 options.cmds = getLastElement(opts.valuesOf(cmds)); 1379 if (opts.has(config)) 1380 options.configs = getLastElement(opts.valuesOf(config)); 1381 if (opts.has(dir)) 1382 options.extractDir = getLastElement(opts.valuesOf(dir)); 1383 if (opts.has(dryrun)) 1384 options.dryrun = true; 1385 if (opts.has(excludes)) 1386 options.excludes = opts.valuesOf(excludes); // excludes is repeatable 1387 if (opts.has(libs)) 1388 options.libs = getLastElement(opts.valuesOf(libs)); 1389 if (opts.has(headerFiles)) 1390 options.headerFiles = getLastElement(opts.valuesOf(headerFiles)); 1391 if (opts.has(manPages)) 1392 options.manPages = getLastElement(opts.valuesOf(manPages)); 1393 if (opts.has(legalNotices)) 1394 options.legalNotices = getLastElement(opts.valuesOf(legalNotices)); 1395 if (opts.has(modulePath)) { 1396 Path[] dirs = getLastElement(opts.valuesOf(modulePath)).toArray(new Path[0]); 1397 options.moduleFinder = ModulePath.of(Runtime.version(), true, dirs); 1398 } 1399 if (opts.has(moduleVersion)) 1400 options.moduleVersion = getLastElement(opts.valuesOf(moduleVersion)); 1401 if (opts.has(mainClass)) 1402 options.mainClass = getLastElement(opts.valuesOf(mainClass)); 1403 if (opts.has(osName)) 1404 options.osName = getLastElement(opts.valuesOf(osName)); 1405 if (opts.has(osArch)) 1406 options.osArch = getLastElement(opts.valuesOf(osArch)); 1407 if (opts.has(warnIfResolved)) 1408 options.moduleResolution = getLastElement(opts.valuesOf(warnIfResolved)); 1409 if (opts.has(doNotResolveByDefault)) { 1410 if (options.moduleResolution == null) 1411 options.moduleResolution = ModuleResolution.empty(); 1412 options.moduleResolution = options.moduleResolution.withDoNotResolveByDefault(); 1413 } 1414 if (opts.has(hashModules)) { 1415 options.modulesToHash = getLastElement(opts.valuesOf(hashModules)); 1416 // if storing hashes then the module path is required 1417 if (options.moduleFinder == null) 1418 throw new CommandException("err.modulepath.must.be.specified") 1419 .showUsage(true); 1420 } 1421 1422 if (options.mode.equals(Mode.HASH)) { 1423 if (options.moduleFinder == null || options.modulesToHash == null) 1424 throw new CommandException("err.modulepath.must.be.specified") 1425 .showUsage(true); 1426 } else { 1427 if (words.size() <= 1) 1428 throw new CommandException("err.jmod.must.be.specified").showUsage(true); 1429 Path path = Paths.get(words.get(1)); 1430 1431 if (options.mode.equals(Mode.CREATE) && Files.exists(path)) 1432 throw new CommandException("err.file.already.exists", path); 1433 else if ((options.mode.equals(Mode.LIST) || 1434 options.mode.equals(Mode.DESCRIBE) || 1435 options.mode.equals((Mode.EXTRACT))) 1436 && Files.notExists(path)) 1437 throw new CommandException("err.jmod.not.found", path); 1438 1439 if (options.dryrun) { 1440 throw new CommandException("err.invalid.dryrun.option"); 1441 } 1442 options.jmodFile = path; 1443 1444 if (words.size() > 2) 1445 throw new CommandException("err.unknown.option", 1446 words.subList(2, words.size())).showUsage(true); 1447 } 1448 1449 if (options.mode.equals(Mode.CREATE) && options.classpath == null) 1450 throw new CommandException("err.classpath.must.be.specified").showUsage(true); 1451 if (options.mainClass != null && !isValidJavaIdentifier(options.mainClass)) 1452 throw new CommandException("err.invalid.main-class", options.mainClass); 1453 if (options.mode.equals(Mode.EXTRACT) && options.extractDir != null) { 1454 try { 1455 Files.createDirectories(options.extractDir); 1456 } catch (IOException ioe) { 1457 throw new CommandException("err.cannot.create.dir", options.extractDir); 1458 } 1459 } 1460 } catch (OptionException e) { 1461 throw new CommandException(e.getMessage()); 1462 } 1463 } 1464 1465 /** 1466 * Returns true if, and only if, the given main class is a legal. 1467 */ 1468 static boolean isValidJavaIdentifier(String mainClass) { 1469 if (mainClass.length() == 0) 1470 return false; 1471 1472 if (!Character.isJavaIdentifierStart(mainClass.charAt(0))) 1473 return false; 1474 1475 int n = mainClass.length(); 1476 for (int i=1; i < n; i++) { 1477 char c = mainClass.charAt(i); 1478 if (!Character.isJavaIdentifierPart(c) && c != '.') 1479 return false; 1480 } 1481 if (mainClass.charAt(n-1) == '.') 1482 return false; 1483 1484 return true; 1485 } 1486 1487 static <E> E getLastElement(List<E> list) { 1488 if (list.size() == 0) 1489 throw new InternalError("Unexpected 0 list size"); 1490 return list.get(list.size() - 1); 1491 } 1492 1493 private void reportError(String message) { 1494 out.println(getMessage("error.prefix") + " " + message); 1495 } 1496 1497 private void warning(String key, Object... args) { 1498 out.println(getMessage("warn.prefix") + " " + getMessage(key, args)); 1499 } 1500 1501 private void showUsageSummary() { 1502 out.println(getMessage("main.usage.summary", PROGNAME)); 1503 } 1504 1505 private void showHelp() { 1506 out.println(getMessage("main.usage", PROGNAME)); 1507 try { 1508 parser.printHelpOn(out); 1509 } catch (IOException x) { 1510 throw new AssertionError(x); 1511 } 1512 } 1513 1514 private void showVersion() { 1515 out.println(version()); 1516 } 1517 1518 private String version() { 1519 return System.getProperty("java.version"); 1520 } 1521 1522 private static String getMessage(String key, Object... args) { 1523 try { 1524 return MessageFormat.format(ResourceBundleHelper.bundle.getString(key), args); 1525 } catch (MissingResourceException e) { 1526 throw new InternalError("Missing message: " + key); 1527 } 1528 } 1529 1530 private static class ResourceBundleHelper { 1531 static final ResourceBundle bundle; 1532 1533 static { 1534 Locale locale = Locale.getDefault(); 1535 try { 1536 bundle = ResourceBundle.getBundle("jdk.tools.jmod.resources.jmod", locale); 1537 } catch (MissingResourceException e) { 1538 throw new InternalError("Cannot find jmod resource bundle for locale " + locale); 1539 } 1540 } 1541 } 1542 }