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