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