1 /* 2 * Copyright (c) 2015, 2018, 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 jos.putNextEntry(e); 991 jos.write(in.readAllBytes()); 992 jos.closeEntry(); 993 } 994 } catch (IOException x) { 995 throw new UncheckedIOException(x); 996 } 997 }); 998 } 999 } 1000 1001 private void updateJmodFile(Path target, Path tempTarget, 1002 ModuleHashes moduleHashes) 1003 throws IOException 1004 { 1005 1006 try (JmodFile jf = new JmodFile(target); 1007 JmodOutputStream jos = JmodOutputStream.newOutputStream(tempTarget)) 1008 { 1009 jf.stream().forEach(e -> { 1010 try (InputStream in = jf.getInputStream(e.section(), e.name())) { 1011 if (e.name().equals(MODULE_INFO)) { 1012 // replace module-info.class 1013 ModuleInfoExtender extender = 1014 ModuleInfoExtender.newExtender(in); 1015 extender.hashes(moduleHashes); 1016 jos.writeEntry(extender.toByteArray(), e.section(), e.name()); 1017 } else { 1018 jos.writeEntry(in, e); 1019 } 1020 } catch (IOException x) { 1021 throw new UncheckedIOException(x); 1022 } 1023 }); 1024 } 1025 } 1026 1027 private Path moduleToPath(String name) { 1028 ResolvedModule rm = configuration.findModule(name).orElseThrow( 1029 () -> new InternalError("Selected module " + name + " not on module path")); 1030 1031 URI uri = rm.reference().location().get(); 1032 Path path = Paths.get(uri); 1033 String fn = path.getFileName().toString(); 1034 if (!fn.endsWith(".jar") && !fn.endsWith(".jmod")) { 1035 throw new InternalError(path + " is not a modular JAR or jmod file"); 1036 } 1037 return path; 1038 } 1039 } 1040 1041 /** 1042 * An abstract converter that given a string representing a list of paths, 1043 * separated by the File.pathSeparator, returns a List of java.nio.Path's. 1044 * Specific subclasses should do whatever validation is required on the 1045 * individual path elements, if any. 1046 */ 1047 static abstract class AbstractPathConverter implements ValueConverter<List<Path>> { 1048 @Override 1049 public List<Path> convert(String value) { 1050 List<Path> paths = new ArrayList<>(); 1051 String[] pathElements = value.split(File.pathSeparator); 1052 for (String pathElement : pathElements) { 1053 paths.add(toPath(pathElement)); 1054 } 1055 return paths; 1056 } 1057 1058 @SuppressWarnings("unchecked") 1059 @Override 1060 public Class<List<Path>> valueType() { 1061 return (Class<List<Path>>)(Object)List.class; 1062 } 1063 1064 @Override public String valuePattern() { return "path"; } 1065 1066 abstract Path toPath(String path); 1067 } 1068 1069 static class ClassPathConverter extends AbstractPathConverter { 1070 static final ValueConverter<List<Path>> INSTANCE = new ClassPathConverter(); 1071 1072 @Override 1073 public Path toPath(String value) { 1074 try { 1075 Path path = CWD.resolve(value); 1076 if (Files.notExists(path)) 1077 throw new CommandException("err.path.not.found", path); 1078 if (!(Files.isDirectory(path) || 1079 (Files.isRegularFile(path) && path.toString().endsWith(".jar")))) 1080 throw new CommandException("err.invalid.class.path.entry", path); 1081 return path; 1082 } catch (InvalidPathException x) { 1083 throw new CommandException("err.path.not.valid", value); 1084 } 1085 } 1086 } 1087 1088 static class DirPathConverter extends AbstractPathConverter { 1089 static final ValueConverter<List<Path>> INSTANCE = new DirPathConverter(); 1090 1091 @Override 1092 public Path toPath(String value) { 1093 try { 1094 Path path = CWD.resolve(value); 1095 if (Files.notExists(path)) 1096 throw new CommandException("err.path.not.found", path); 1097 if (!Files.isDirectory(path)) 1098 throw new CommandException("err.path.not.a.dir", path); 1099 return path; 1100 } catch (InvalidPathException x) { 1101 throw new CommandException("err.path.not.valid", value); 1102 } 1103 } 1104 } 1105 1106 static class ExtractDirPathConverter implements ValueConverter<Path> { 1107 1108 @Override 1109 public Path convert(String value) { 1110 try { 1111 Path path = CWD.resolve(value); 1112 if (Files.exists(path)) { 1113 if (!Files.isDirectory(path)) 1114 throw new CommandException("err.cannot.create.dir", path); 1115 } 1116 return path; 1117 } catch (InvalidPathException x) { 1118 throw new CommandException("err.path.not.valid", value); 1119 } 1120 } 1121 1122 @Override public Class<Path> valueType() { return Path.class; } 1123 1124 @Override public String valuePattern() { return "path"; } 1125 } 1126 1127 static class ModuleVersionConverter implements ValueConverter<Version> { 1128 @Override 1129 public Version convert(String value) { 1130 try { 1131 return Version.parse(value); 1132 } catch (IllegalArgumentException x) { 1133 throw new CommandException("err.invalid.version", x.getMessage()); 1134 } 1135 } 1136 1137 @Override public Class<Version> valueType() { return Version.class; } 1138 1139 @Override public String valuePattern() { return "module-version"; } 1140 } 1141 1142 static class WarnIfResolvedReasonConverter 1143 implements ValueConverter<ModuleResolution> 1144 { 1145 @Override 1146 public ModuleResolution convert(String value) { 1147 if (value.equals("deprecated")) 1148 return ModuleResolution.empty().withDeprecated(); 1149 else if (value.equals("deprecated-for-removal")) 1150 return ModuleResolution.empty().withDeprecatedForRemoval(); 1151 else if (value.equals("incubating")) 1152 return ModuleResolution.empty().withIncubating(); 1153 else 1154 throw new CommandException("err.bad.WarnIfResolvedReason", value); 1155 } 1156 1157 @Override public Class<ModuleResolution> valueType() { 1158 return ModuleResolution.class; 1159 } 1160 1161 @Override public String valuePattern() { return "reason"; } 1162 } 1163 1164 static class PatternConverter implements ValueConverter<Pattern> { 1165 @Override 1166 public Pattern convert(String value) { 1167 try { 1168 if (value.startsWith("regex:")) { 1169 value = value.substring("regex:".length()).trim(); 1170 } 1171 1172 return Pattern.compile(value); 1173 } catch (PatternSyntaxException e) { 1174 throw new CommandException("err.bad.pattern", value); 1175 } 1176 } 1177 1178 @Override public Class<Pattern> valueType() { return Pattern.class; } 1179 1180 @Override public String valuePattern() { return "regex-pattern"; } 1181 } 1182 1183 static class PathMatcherConverter implements ValueConverter<PathMatcher> { 1184 @Override 1185 public PathMatcher convert(String pattern) { 1186 try { 1187 return Utils.getPathMatcher(FileSystems.getDefault(), pattern); 1188 } catch (PatternSyntaxException e) { 1189 throw new CommandException("err.bad.pattern", pattern); 1190 } 1191 } 1192 1193 @Override public Class<PathMatcher> valueType() { return PathMatcher.class; } 1194 1195 @Override public String valuePattern() { return "pattern-list"; } 1196 } 1197 1198 /* Support for @<file> in jmod help */ 1199 private static final String CMD_FILENAME = "@<filename>"; 1200 1201 /** 1202 * This formatter is adding the @filename option and does the required 1203 * formatting. 1204 */ 1205 private static final class JmodHelpFormatter extends BuiltinHelpFormatter { 1206 1207 private final Options opts; 1208 1209 private JmodHelpFormatter(Options opts) { 1210 super(80, 2); 1211 this.opts = opts; 1212 } 1213 1214 @Override 1215 public String format(Map<String, ? extends OptionDescriptor> options) { 1216 Map<String, OptionDescriptor> all = new LinkedHashMap<>(); 1217 all.putAll(options); 1218 1219 // extra options 1220 if (!opts.helpExtra) { 1221 all.remove("do-not-resolve-by-default"); 1222 all.remove("warn-if-resolved"); 1223 } 1224 1225 all.put(CMD_FILENAME, new OptionDescriptor() { 1226 @Override 1227 public List<String> options() { 1228 List<String> ret = new ArrayList<>(); 1229 ret.add(CMD_FILENAME); 1230 return ret; 1231 } 1232 @Override 1233 public String description() { return getMessage("main.opt.cmdfile"); } 1234 @Override 1235 public List<?> defaultValues() { return Collections.emptyList(); } 1236 @Override 1237 public boolean isRequired() { return false; } 1238 @Override 1239 public boolean acceptsArguments() { return false; } 1240 @Override 1241 public boolean requiresArgument() { return false; } 1242 @Override 1243 public String argumentDescription() { return null; } 1244 @Override 1245 public String argumentTypeIndicator() { return null; } 1246 @Override 1247 public boolean representsNonOptions() { return false; } 1248 }); 1249 String content = super.format(all); 1250 StringBuilder builder = new StringBuilder(); 1251 1252 builder.append(getMessage("main.opt.mode")).append("\n "); 1253 builder.append(getMessage("main.opt.mode.create")).append("\n "); 1254 builder.append(getMessage("main.opt.mode.extract")).append("\n "); 1255 builder.append(getMessage("main.opt.mode.list")).append("\n "); 1256 builder.append(getMessage("main.opt.mode.describe")).append("\n "); 1257 builder.append(getMessage("main.opt.mode.hash")).append("\n\n"); 1258 1259 String cmdfile = null; 1260 String[] lines = content.split("\n"); 1261 for (String line : lines) { 1262 if (line.startsWith("--@")) { 1263 cmdfile = line.replace("--" + CMD_FILENAME, CMD_FILENAME + " "); 1264 } else if (line.startsWith("Option") || line.startsWith("------")) { 1265 builder.append(" ").append(line).append("\n"); 1266 } else if (!line.matches("Non-option arguments")){ 1267 builder.append(" ").append(line).append("\n"); 1268 } 1269 } 1270 if (cmdfile != null) { 1271 builder.append(" ").append(cmdfile).append("\n"); 1272 } 1273 return builder.toString(); 1274 } 1275 } 1276 1277 private final OptionParser parser = new OptionParser("hp"); 1278 1279 private void handleOptions(String[] args) { 1280 options = new Options(); 1281 parser.formatHelpWith(new JmodHelpFormatter(options)); 1282 1283 OptionSpec<List<Path>> classPath 1284 = parser.accepts("class-path", getMessage("main.opt.class-path")) 1285 .withRequiredArg() 1286 .withValuesConvertedBy(ClassPathConverter.INSTANCE); 1287 1288 OptionSpec<List<Path>> cmds 1289 = parser.accepts("cmds", getMessage("main.opt.cmds")) 1290 .withRequiredArg() 1291 .withValuesConvertedBy(DirPathConverter.INSTANCE); 1292 1293 OptionSpec<List<Path>> config 1294 = parser.accepts("config", getMessage("main.opt.config")) 1295 .withRequiredArg() 1296 .withValuesConvertedBy(DirPathConverter.INSTANCE); 1297 1298 OptionSpec<Path> dir 1299 = parser.accepts("dir", getMessage("main.opt.extractDir")) 1300 .withRequiredArg() 1301 .withValuesConvertedBy(new ExtractDirPathConverter()); 1302 1303 OptionSpec<Void> dryrun 1304 = parser.accepts("dry-run", getMessage("main.opt.dry-run")); 1305 1306 OptionSpec<PathMatcher> excludes 1307 = parser.accepts("exclude", getMessage("main.opt.exclude")) 1308 .withRequiredArg() 1309 .withValuesConvertedBy(new PathMatcherConverter()); 1310 1311 OptionSpec<Pattern> hashModules 1312 = parser.accepts("hash-modules", getMessage("main.opt.hash-modules")) 1313 .withRequiredArg() 1314 .withValuesConvertedBy(new PatternConverter()); 1315 1316 OptionSpec<Void> help 1317 = parser.acceptsAll(List.of("h", "help", "?"), getMessage("main.opt.help")) 1318 .forHelp(); 1319 1320 OptionSpec<Void> helpExtra 1321 = parser.accepts("help-extra", getMessage("main.opt.help-extra")); 1322 1323 OptionSpec<List<Path>> headerFiles 1324 = parser.accepts("header-files", getMessage("main.opt.header-files")) 1325 .withRequiredArg() 1326 .withValuesConvertedBy(DirPathConverter.INSTANCE); 1327 1328 OptionSpec<List<Path>> libs 1329 = parser.accepts("libs", getMessage("main.opt.libs")) 1330 .withRequiredArg() 1331 .withValuesConvertedBy(DirPathConverter.INSTANCE); 1332 1333 OptionSpec<List<Path>> legalNotices 1334 = parser.accepts("legal-notices", getMessage("main.opt.legal-notices")) 1335 .withRequiredArg() 1336 .withValuesConvertedBy(DirPathConverter.INSTANCE); 1337 1338 1339 OptionSpec<String> mainClass 1340 = parser.accepts("main-class", getMessage("main.opt.main-class")) 1341 .withRequiredArg() 1342 .describedAs(getMessage("main.opt.main-class.arg")); 1343 1344 OptionSpec<List<Path>> manPages 1345 = parser.accepts("man-pages", getMessage("main.opt.man-pages")) 1346 .withRequiredArg() 1347 .withValuesConvertedBy(DirPathConverter.INSTANCE); 1348 1349 OptionSpec<List<Path>> modulePath 1350 = parser.acceptsAll(List.of("p", "module-path"), 1351 getMessage("main.opt.module-path")) 1352 .withRequiredArg() 1353 .withValuesConvertedBy(DirPathConverter.INSTANCE); 1354 1355 OptionSpec<Version> moduleVersion 1356 = parser.accepts("module-version", getMessage("main.opt.module-version")) 1357 .withRequiredArg() 1358 .withValuesConvertedBy(new ModuleVersionConverter()); 1359 1360 OptionSpec<String> targetPlatform 1361 = parser.accepts("target-platform", getMessage("main.opt.target-platform")) 1362 .withRequiredArg() 1363 .describedAs(getMessage("main.opt.target-platform.arg")); 1364 1365 OptionSpec<Void> doNotResolveByDefault 1366 = parser.accepts("do-not-resolve-by-default", 1367 getMessage("main.opt.do-not-resolve-by-default")); 1368 1369 OptionSpec<ModuleResolution> warnIfResolved 1370 = parser.accepts("warn-if-resolved", getMessage("main.opt.warn-if-resolved")) 1371 .withRequiredArg() 1372 .withValuesConvertedBy(new WarnIfResolvedReasonConverter()); 1373 1374 OptionSpec<Void> version 1375 = parser.accepts("version", getMessage("main.opt.version")); 1376 1377 NonOptionArgumentSpec<String> nonOptions 1378 = parser.nonOptions(); 1379 1380 try { 1381 OptionSet opts = parser.parse(args); 1382 1383 if (opts.has(help) || opts.has(helpExtra) || opts.has(version)) { 1384 options.help = opts.has(help); 1385 options.helpExtra = opts.has(helpExtra); 1386 options.version = opts.has(version); 1387 return; // informational message will be shown 1388 } 1389 1390 List<String> words = opts.valuesOf(nonOptions); 1391 if (words.isEmpty()) 1392 throw new CommandException("err.missing.mode").showUsage(true); 1393 String verb = words.get(0); 1394 try { 1395 options.mode = Enum.valueOf(Mode.class, verb.toUpperCase()); 1396 } catch (IllegalArgumentException e) { 1397 throw new CommandException("err.invalid.mode", verb).showUsage(true); 1398 } 1399 1400 if (opts.has(classPath)) 1401 options.classpath = getLastElement(opts.valuesOf(classPath)); 1402 if (opts.has(cmds)) 1403 options.cmds = getLastElement(opts.valuesOf(cmds)); 1404 if (opts.has(config)) 1405 options.configs = getLastElement(opts.valuesOf(config)); 1406 if (opts.has(dir)) 1407 options.extractDir = getLastElement(opts.valuesOf(dir)); 1408 if (opts.has(dryrun)) 1409 options.dryrun = true; 1410 if (opts.has(excludes)) 1411 options.excludes = opts.valuesOf(excludes); // excludes is repeatable 1412 if (opts.has(libs)) 1413 options.libs = getLastElement(opts.valuesOf(libs)); 1414 if (opts.has(headerFiles)) 1415 options.headerFiles = getLastElement(opts.valuesOf(headerFiles)); 1416 if (opts.has(manPages)) 1417 options.manPages = getLastElement(opts.valuesOf(manPages)); 1418 if (opts.has(legalNotices)) 1419 options.legalNotices = getLastElement(opts.valuesOf(legalNotices)); 1420 if (opts.has(modulePath)) { 1421 Path[] dirs = getLastElement(opts.valuesOf(modulePath)).toArray(new Path[0]); 1422 options.moduleFinder = ModulePath.of(Runtime.version(), true, dirs); 1423 } 1424 if (opts.has(moduleVersion)) 1425 options.moduleVersion = getLastElement(opts.valuesOf(moduleVersion)); 1426 if (opts.has(mainClass)) 1427 options.mainClass = getLastElement(opts.valuesOf(mainClass)); 1428 if (opts.has(targetPlatform)) 1429 options.targetPlatform = getLastElement(opts.valuesOf(targetPlatform)); 1430 if (opts.has(warnIfResolved)) 1431 options.moduleResolution = getLastElement(opts.valuesOf(warnIfResolved)); 1432 if (opts.has(doNotResolveByDefault)) { 1433 if (options.moduleResolution == null) 1434 options.moduleResolution = ModuleResolution.empty(); 1435 options.moduleResolution = options.moduleResolution.withDoNotResolveByDefault(); 1436 } 1437 if (opts.has(hashModules)) { 1438 options.modulesToHash = getLastElement(opts.valuesOf(hashModules)); 1439 // if storing hashes then the module path is required 1440 if (options.moduleFinder == null) 1441 throw new CommandException("err.modulepath.must.be.specified") 1442 .showUsage(true); 1443 } 1444 1445 if (options.mode.equals(Mode.HASH)) { 1446 if (options.moduleFinder == null || options.modulesToHash == null) 1447 throw new CommandException("err.modulepath.must.be.specified") 1448 .showUsage(true); 1449 } else { 1450 if (words.size() <= 1) 1451 throw new CommandException("err.jmod.must.be.specified").showUsage(true); 1452 Path path = Paths.get(words.get(1)); 1453 1454 if (options.mode.equals(Mode.CREATE) && Files.exists(path)) 1455 throw new CommandException("err.file.already.exists", path); 1456 else if ((options.mode.equals(Mode.LIST) || 1457 options.mode.equals(Mode.DESCRIBE) || 1458 options.mode.equals((Mode.EXTRACT))) 1459 && Files.notExists(path)) 1460 throw new CommandException("err.jmod.not.found", path); 1461 1462 if (options.dryrun) { 1463 throw new CommandException("err.invalid.dryrun.option"); 1464 } 1465 options.jmodFile = path; 1466 1467 if (words.size() > 2) 1468 throw new CommandException("err.unknown.option", 1469 words.subList(2, words.size())).showUsage(true); 1470 } 1471 1472 if (options.mode.equals(Mode.CREATE) && options.classpath == null) 1473 throw new CommandException("err.classpath.must.be.specified").showUsage(true); 1474 if (options.mainClass != null && !isValidJavaIdentifier(options.mainClass)) 1475 throw new CommandException("err.invalid.main-class", options.mainClass); 1476 if (options.mode.equals(Mode.EXTRACT) && options.extractDir != null) { 1477 try { 1478 Files.createDirectories(options.extractDir); 1479 } catch (IOException ioe) { 1480 throw new CommandException("err.cannot.create.dir", options.extractDir); 1481 } 1482 } 1483 } catch (OptionException e) { 1484 throw new CommandException(e.getMessage()); 1485 } 1486 } 1487 1488 /** 1489 * Returns true if, and only if, the given main class is a legal. 1490 */ 1491 static boolean isValidJavaIdentifier(String mainClass) { 1492 if (mainClass.length() == 0) 1493 return false; 1494 1495 if (!Character.isJavaIdentifierStart(mainClass.charAt(0))) 1496 return false; 1497 1498 int n = mainClass.length(); 1499 for (int i=1; i < n; i++) { 1500 char c = mainClass.charAt(i); 1501 if (!Character.isJavaIdentifierPart(c) && c != '.') 1502 return false; 1503 } 1504 if (mainClass.charAt(n-1) == '.') 1505 return false; 1506 1507 return true; 1508 } 1509 1510 static <E> E getLastElement(List<E> list) { 1511 if (list.size() == 0) 1512 throw new InternalError("Unexpected 0 list size"); 1513 return list.get(list.size() - 1); 1514 } 1515 1516 private void reportError(String message) { 1517 out.println(getMessage("error.prefix") + " " + message); 1518 } 1519 1520 private void warning(String key, Object... args) { 1521 out.println(getMessage("warn.prefix") + " " + getMessage(key, args)); 1522 } 1523 1524 private void showUsageSummary() { 1525 out.println(getMessage("main.usage.summary", PROGNAME)); 1526 } 1527 1528 private void showHelp() { 1529 out.println(getMessage("main.usage", PROGNAME)); 1530 try { 1531 parser.printHelpOn(out); 1532 } catch (IOException x) { 1533 throw new AssertionError(x); 1534 } 1535 } 1536 1537 private void showVersion() { 1538 out.println(version()); 1539 } 1540 1541 private String version() { 1542 return System.getProperty("java.version"); 1543 } 1544 1545 private static String getMessage(String key, Object... args) { 1546 try { 1547 return MessageFormat.format(ResourceBundleHelper.bundle.getString(key), args); 1548 } catch (MissingResourceException e) { 1549 throw new InternalError("Missing message: " + key); 1550 } 1551 } 1552 1553 private static class ResourceBundleHelper { 1554 static final ResourceBundle bundle; 1555 1556 static { 1557 Locale locale = Locale.getDefault(); 1558 try { 1559 bundle = ResourceBundle.getBundle("jdk.tools.jmod.resources.jmod", locale); 1560 } catch (MissingResourceException e) { 1561 throw new InternalError("Cannot find jmod resource bundle for locale " + locale); 1562 } 1563 } 1564 } 1565 }