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