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