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