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