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