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