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