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