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