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