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