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 26 package jdk.tools.jmod; 27 28 import java.io.BufferedInputStream; 29 import java.io.ByteArrayInputStream; 30 import java.io.ByteArrayOutputStream; 31 import java.io.File; 32 import java.io.IOException; 33 import java.io.InputStream; 34 import java.io.OutputStream; 35 import java.io.PrintStream; 36 import java.io.UncheckedIOException; 37 import java.lang.module.FindException; 38 import java.lang.module.ModuleReference; 39 import java.lang.module.ModuleFinder; 40 import java.lang.module.ModuleDescriptor.Requires; 41 import java.lang.module.ModuleDescriptor; 42 import java.lang.module.ModuleDescriptor.Version; 43 import java.lang.reflect.InvocationTargetException; 44 import java.lang.reflect.Method; 45 import java.net.URI; 46 import java.nio.file.FileSystems; 47 import java.nio.file.FileVisitResult; 48 import java.nio.file.Files; 49 import java.nio.file.InvalidPathException; 50 import java.nio.file.Path; 51 import java.nio.file.PathMatcher; 52 import java.nio.file.Paths; 53 import java.nio.file.SimpleFileVisitor; 54 import java.nio.file.attribute.BasicFileAttributes; 55 import java.text.MessageFormat; 56 import java.util.ArrayList; 57 import java.util.Arrays; 58 import java.util.Collection; 59 import java.util.Collections; 60 import java.util.Formatter; 61 import java.util.HashMap; 62 import java.util.HashSet; 63 import java.util.List; 64 import java.util.Locale; 65 import java.util.Map; 66 import java.util.MissingResourceException; 67 import java.util.Optional; 68 import java.util.ResourceBundle; 69 import java.util.Set; 70 import java.util.function.Consumer; 71 import java.util.function.Predicate; 72 import java.util.function.Supplier; 73 import java.util.jar.JarEntry; 74 import java.util.jar.JarFile; 75 import java.util.stream.Collectors; 76 import java.util.regex.Pattern; 77 import java.util.regex.PatternSyntaxException; 78 import java.util.zip.ZipEntry; 79 import java.util.zip.ZipException; 80 import java.util.zip.ZipFile; 81 import java.util.zip.ZipInputStream; 82 import java.util.zip.ZipOutputStream; 83 84 import jdk.internal.joptsimple.BuiltinHelpFormatter; 85 import jdk.internal.joptsimple.NonOptionArgumentSpec; 86 import jdk.internal.joptsimple.OptionDescriptor; 87 import jdk.internal.joptsimple.OptionException; 88 import jdk.internal.joptsimple.OptionParser; 89 import jdk.internal.joptsimple.OptionSet; 90 import jdk.internal.joptsimple.OptionSpec; 91 import jdk.internal.joptsimple.ValueConverter; 92 import jdk.internal.module.ConfigurableModuleFinder; 93 import jdk.internal.module.ConfigurableModuleFinder.Phase; 94 import jdk.internal.module.Hasher; 95 import jdk.internal.module.Hasher.DependencyHashes; 96 import jdk.internal.module.ModuleInfoExtender; 97 98 import static java.util.function.Function.identity; 99 import static java.util.stream.Collectors.joining; 100 import static java.util.stream.Collectors.toList; 101 import static java.util.stream.Collectors.toMap; 102 103 /** 104 * Implementation for the jmod tool. 105 */ 106 public class JmodTask { 107 108 static class CommandException extends RuntimeException { 109 private static final long serialVersionUID = 0L; 110 boolean showUsage; 111 112 CommandException(String key, Object... args) { 113 super(getMessageOrKey(key, args)); 114 } 115 116 CommandException showUsage(boolean b) { 117 showUsage = b; 118 return this; 119 } 120 121 private static String getMessageOrKey(String key, Object... args) { 122 try { 123 return MessageFormat.format(ResourceBundleHelper.bundle.getString(key), args); 124 } catch (MissingResourceException e) { 125 return key; 126 } 127 } 128 } 129 130 static <T extends Throwable> void fail(Class<T> type, 131 String format, 132 Object... args) throws T { 133 String msg = new Formatter().format(format, args).toString(); 134 try { 135 T t = type.getConstructor(String.class).newInstance(msg); 136 throw t; 137 } catch (InstantiationException | 138 InvocationTargetException | 139 NoSuchMethodException | 140 IllegalAccessException e) { 141 throw new InternalError("Unable to create an instance of " + type, e); 142 } 143 } 144 145 private static final String PROGNAME = "jmod"; 146 private static final String MODULE_INFO = "module-info.class"; 147 148 private Options options; 149 private PrintStream out = System.out; 150 void setLog(PrintStream out) { 151 this.out = out; 152 } 153 154 /* Result codes. */ 155 static final int EXIT_OK = 0, // Completed with no errors. 156 EXIT_ERROR = 1, // Completed but reported errors. 157 EXIT_CMDERR = 2, // Bad command-line arguments 158 EXIT_SYSERR = 3, // System error or resource exhaustion. 159 EXIT_ABNORMAL = 4;// terminated abnormally 160 161 enum Mode { 162 CREATE, 163 LIST, 164 DESCRIBE 165 }; 166 167 static class Options { 168 Mode mode; 169 Path jmodFile; 170 boolean help; 171 boolean version; 172 List<Path> classpath; 173 List<Path> cmds; 174 List<Path> configs; 175 List<Path> libs; 176 ModuleFinder moduleFinder; 177 Version moduleVersion; 178 String mainClass; 179 String osName; 180 String osArch; 181 String osVersion; 182 Pattern dependenciesToHash; 183 List<PathMatcher> excludes; 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 LIST: 209 ok = list(); 210 break; 211 case DESCRIBE: 212 ok = describe(); 213 break; 214 default: 215 throw new AssertionError("Unknown mode: " + options.mode.name()); 216 } 217 218 return ok ? EXIT_OK : EXIT_ERROR; 219 } catch (CommandException e) { 220 reportError(e.getMessage()); 221 if (e.showUsage) 222 showUsageSummary(); 223 return EXIT_CMDERR; 224 } catch (Exception x) { 225 reportError(x.getMessage()); 226 x.printStackTrace(); 227 return EXIT_ABNORMAL; 228 } finally { 229 out.flush(); 230 } 231 } 232 233 private boolean list() throws IOException { 234 ZipFile zip = null; 235 try { 236 try { 237 zip = new ZipFile(options.jmodFile.toFile()); 238 } catch (IOException x) { 239 throw new IOException("error opening jmod file", x); 240 } 241 242 // Trivially print the archive entries for now, pending a more complete implementation 243 zip.stream().forEach(e -> out.println(e.getName())); 244 return true; 245 } finally { 246 if (zip != null) 247 zip.close(); 248 } 249 } 250 251 private Map<String, Path> modulesToPath(Set<ModuleDescriptor> modules) { 252 ModuleFinder finder = options.moduleFinder; 253 254 Map<String,Path> modPaths = new HashMap<>(); 255 for (ModuleDescriptor m : modules) { 256 String name = m.name(); 257 258 Optional<ModuleReference> omref = finder.find(name); 259 if (!omref.isPresent()) { 260 // this should not happen, module path bug? 261 fail(InternalError.class, 262 "Selected module %s not on module path", 263 name); 264 } 265 266 URI uri = omref.getWhenPresent().location().getWhenPresent(); 267 modPaths.put(name, Paths.get(uri)); 268 269 } 270 return modPaths; 271 } 272 273 private boolean describe() throws IOException { 274 ZipFile zip = null; 275 try { 276 try { 277 zip = new ZipFile(options.jmodFile.toFile()); 278 } catch (IOException x) { 279 throw new IOException("error opening jmod file", x); 280 } 281 282 try (InputStream in = Files.newInputStream(options.jmodFile)) { 283 boolean found = printModuleDescriptor(in); 284 if (!found) 285 throw new CommandException("err.module.descriptor.not.found"); 286 return found; 287 } 288 } finally { 289 if (zip != null) 290 zip.close(); 291 } 292 } 293 294 static <T> String toString(Set<T> set) { 295 if (set.isEmpty()) { return ""; } 296 return set.stream().map(e -> e.toString().toLowerCase(Locale.ROOT)) 297 .collect(joining(" ")); 298 } 299 300 private boolean printModuleDescriptor(InputStream in) 301 throws IOException 302 { 303 final String mi = Section.CLASSES.jmodDir() + "/" + MODULE_INFO; 304 try (BufferedInputStream bis = new BufferedInputStream(in); 305 ZipInputStream zis = new ZipInputStream(bis)) { 306 307 ZipEntry e; 308 while ((e = zis.getNextEntry()) != null) { 309 if (e.getName().equals(mi)) { 310 ModuleDescriptor md = ModuleDescriptor.read(zis); 311 StringBuilder sb = new StringBuilder(); 312 sb.append("\n").append(md.toNameAndVersion()); 313 314 List<Requires> requires = md.requires().stream().sorted().collect(toList()); 315 if (!requires.isEmpty()) { 316 requires.forEach(r -> { 317 sb.append("\n requires "); 318 if (!r.modifiers().isEmpty()) 319 sb.append(toString(r.modifiers())).append(" "); 320 sb.append(r.name()); 321 }); 322 } 323 324 List<String> l = md.uses().stream().sorted().collect(toList()); 325 if (!l.isEmpty()) { 326 l.forEach(sv -> sb.append("\n uses ").append(sv)); 327 } 328 329 List<ModuleDescriptor.Exports> exports = sortExports(md.exports()); 330 if (!exports.isEmpty()) { 331 exports.forEach(ex -> sb.append("\n exports ").append(ex)); 332 } 333 334 l = md.conceals().stream().sorted().collect(toList()); 335 if (!l.isEmpty()) { 336 l.forEach(p -> sb.append("\n conceals ").append(p)); 337 } 338 339 Map<String, ModuleDescriptor.Provides> provides = md.provides(); 340 if (!provides.isEmpty()) { 341 provides.values().forEach(p -> 342 sb.append("\n provides ").append(p.service()) 343 .append(" with ") 344 .append(toString(p.providers()))); 345 } 346 347 Optional<String> mc = md.mainClass(); 348 if (mc.isPresent()) 349 sb.append("\n main-class " + mc.getWhenPresent()); 350 351 352 353 Optional<String> osname = md.osName(); 354 if (osname.isPresent()) 355 sb.append("\n operating-system-name " + osname.getWhenPresent()); 356 357 Optional<String> osarch = md.osArch(); 358 if (osarch.isPresent()) 359 sb.append("\n operating-system-architecture " + osarch.getWhenPresent()); 360 361 Optional<String> osversion = md.osVersion(); 362 if (osversion.isPresent()) 363 sb.append("\n operating-system-version " + osversion.getWhenPresent()); 364 365 try { 366 Method m = ModuleDescriptor.class.getDeclaredMethod("hashes"); 367 m.setAccessible(true); 368 @SuppressWarnings("unchecked") 369 Optional<Hasher.DependencyHashes> optHashes = 370 (Optional<Hasher.DependencyHashes>) m.invoke(md); 371 372 if (optHashes.isPresent()) { 373 Hasher.DependencyHashes hashes = optHashes.getWhenPresent(); 374 hashes.names().stream().forEach(mod -> 375 sb.append("\n hashes ").append(mod).append(" ") 376 .append(hashes.algorithm()).append(" ") 377 .append(hashes.hashFor(mod))); 378 } 379 } catch (ReflectiveOperationException x) { 380 throw new InternalError(x); 381 } 382 out.println(sb.toString()); 383 return true; 384 } 385 } 386 } 387 return false; 388 } 389 390 static List<ModuleDescriptor.Exports> sortExports(Set<ModuleDescriptor.Exports> exports) { 391 Map<String,ModuleDescriptor.Exports> map = 392 exports.stream() 393 .collect(toMap(ModuleDescriptor.Exports::source, 394 identity())); 395 List<String> sources = exports.stream() 396 .map(ModuleDescriptor.Exports::source) 397 .sorted() 398 .collect(toList()); 399 400 List<ModuleDescriptor.Exports> l = new ArrayList<>(); 401 sources.forEach(e -> l.add(map.get(e))); 402 return l; 403 } 404 405 private boolean create() throws IOException { 406 JmodFileWriter jmod = new JmodFileWriter(); 407 408 // create jmod with temporary name to avoid it being examined 409 // when scanning the module path 410 Path target = options.jmodFile; 411 Path tempTarget = target.resolveSibling(target.getFileName() + ".tmp"); 412 try { 413 try (OutputStream out = Files.newOutputStream(tempTarget)) { 414 jmod.write(out); 415 } 416 Files.move(tempTarget, target); 417 } catch (Exception e) { 418 if (Files.exists(tempTarget)) { 419 try { 420 Files.delete(tempTarget); 421 } catch (IOException ioe) { 422 e.addSuppressed(ioe); 423 } 424 } 425 throw e; 426 } 427 return true; 428 } 429 430 private class JmodFileWriter { 431 final ModuleFinder moduleFinder = options.moduleFinder; 432 final List<Path> cmds = options.cmds; 433 final List<Path> libs = options.libs; 434 final List<Path> configs = options.configs; 435 final List<Path> classpath = options.classpath; 436 final Version moduleVersion = options.moduleVersion; 437 final String mainClass = options.mainClass; 438 final String osName = options.osName; 439 final String osArch = options.osArch; 440 final String osVersion = options.osVersion; 441 final Pattern dependenciesToHash = options.dependenciesToHash; 442 final List<PathMatcher> excludes = options.excludes; 443 444 JmodFileWriter() { } 445 446 /** 447 * Writes the jmod to the given output stream. 448 */ 449 void write(OutputStream out) throws IOException { 450 try (ZipOutputStream zos = new ZipOutputStream(out)) { 451 452 // module-info.class 453 writeModuleInfo(zos, findPackages(classpath)); 454 455 // classes 456 processClasses(zos, classpath); 457 458 processSection(zos, Section.NATIVE_CMDS, cmds); 459 processSection(zos, Section.NATIVE_LIBS, libs); 460 processSection(zos, Section.CONFIG, configs); 461 } 462 } 463 464 /** 465 * Returns a supplier of an input stream to the module-info.class 466 * on the class path of directories and JAR files. 467 */ 468 Supplier<InputStream> newModuleInfoSupplier() throws IOException { 469 ByteArrayOutputStream baos = new ByteArrayOutputStream(); 470 for (Path e: classpath) { 471 if (Files.isDirectory(e)) { 472 Path mi = e.resolve(MODULE_INFO); 473 if (Files.isRegularFile(mi)) { 474 Files.copy(mi, baos); 475 break; 476 } 477 } else if (Files.isRegularFile(e) && e.toString().endsWith(".jar")) { 478 try (JarFile jf = new JarFile(e.toFile())) { 479 ZipEntry entry = jf.getEntry(MODULE_INFO); 480 if (entry != null) { 481 jf.getInputStream(entry).transferTo(baos); 482 break; 483 } 484 } catch (ZipException x) { 485 // Skip. Do nothing. No packages will be added. 486 } 487 } 488 } 489 if (baos.size() == 0) { 490 return null; 491 } else { 492 byte[] bytes = baos.toByteArray(); 493 return () -> new ByteArrayInputStream(bytes); 494 } 495 } 496 497 /** 498 * Writes the updated module-info.class to the ZIP output stream. 499 * 500 * The updated module-info.class will have a ConcealedPackages attribute 501 * with the set of module-private/non-exported packages. 502 * 503 * If --module-version, --main-class, or other options were provided 504 * then the corresponding class file attributes are added to the 505 * module-info here. 506 */ 507 void writeModuleInfo(ZipOutputStream zos, Set<String> packages) 508 throws IOException 509 { 510 Supplier<InputStream> miSupplier = newModuleInfoSupplier(); 511 if (miSupplier == null) { 512 throw new IOException(MODULE_INFO + " not found"); 513 } 514 515 ModuleDescriptor descriptor; 516 try (InputStream in = miSupplier.get()) { 517 descriptor = ModuleDescriptor.read(in); 518 } 519 520 // copy the module-info.class into the jmod with the additional 521 // attributes for the version, main class and other meta data 522 try (InputStream in = miSupplier.get()) { 523 ModuleInfoExtender extender = ModuleInfoExtender.newExtender(in); 524 525 // Add (or replace) the ConcealedPackages attribute 526 if (packages != null) { 527 Set<String> exported = descriptor.exports().stream() 528 .map(ModuleDescriptor.Exports::source) 529 .collect(Collectors.toSet()); 530 Set<String> concealed = packages.stream() 531 .filter(p -> !exported.contains(p)) 532 .collect(Collectors.toSet()); 533 extender.conceals(concealed); 534 } 535 536 // --main-class 537 if (mainClass != null) 538 extender.mainClass(mainClass); 539 540 // --os-name, --os-arch, --os-version 541 if (osName != null || osArch != null || osVersion != null) 542 extender.targetPlatform(osName, osArch, osVersion); 543 544 // --module-version 545 if (moduleVersion != null) 546 extender.version(moduleVersion); 547 548 // --hash-dependencies 549 if (dependenciesToHash != null) { 550 String name = descriptor.name(); 551 Set<Requires> dependences = descriptor.requires(); 552 extender.hashes(hashDependences(name, dependences)); 553 } 554 555 // write the (possibly extended or modified) module-info.class 556 String e = Section.CLASSES.jmodDir() + "/" + MODULE_INFO; 557 ZipEntry ze = new ZipEntry(e); 558 zos.putNextEntry(ze); 559 extender.write(zos); 560 zos.closeEntry(); 561 } 562 } 563 564 /** 565 * Examines the module dependences of the given module 566 * and computes the hash of any module that matches the 567 * pattern {@code dependenciesToHash}. 568 */ 569 DependencyHashes hashDependences(String name, Set<Requires> moduleDependences) 570 throws IOException 571 { 572 Set<ModuleDescriptor> descriptors = new HashSet<>(); 573 for (Requires md: moduleDependences) { 574 String dn = md.name(); 575 if (dependenciesToHash.matcher(dn).find()) { 576 try { 577 Optional<ModuleReference> omref = moduleFinder.find(dn); 578 if (!omref.isPresent()) { 579 throw new RuntimeException("Hashing module " + name 580 + " dependencies, unable to find module " + dn 581 + " on module path"); 582 } 583 descriptors.add(omref.getWhenPresent().descriptor()); 584 } catch (FindException x) { 585 throw new IOException("error reading module path", x); 586 } 587 } 588 } 589 590 Map<String, Path> map = modulesToPath(descriptors); 591 if (map.size() == 0) { 592 return null; 593 } else { 594 // use SHA-256 for now, easy to make this configurable if needed 595 return Hasher.generate(map, "SHA-256"); 596 } 597 } 598 599 /** 600 * Returns the set of all packages on the given class path. 601 */ 602 Set<String> findPackages(List<Path> classpath) { 603 Set<String> packages = new HashSet<>(); 604 for (Path path : classpath) { 605 if (Files.isDirectory(path)) { 606 packages.addAll(findPackages(path)); 607 } else if (Files.isRegularFile(path) && path.toString().endsWith(".jar")) { 608 try (JarFile jf = new JarFile(path.toString())) { 609 packages.addAll(findPackages(jf)); 610 } catch (ZipException x) { 611 // Skip. Do nothing. No packages will be added. 612 } catch (IOException ioe) { 613 throw new UncheckedIOException(ioe); 614 } 615 } 616 } 617 return packages; 618 } 619 620 /** 621 * Returns the set of packages in the given directory tree. 622 */ 623 Set<String> findPackages(Path dir) { 624 try { 625 return Files.find(dir, Integer.MAX_VALUE, 626 ((path, attrs) -> attrs.isRegularFile() && 627 path.toString().endsWith(".class"))) 628 .map(path -> toPackageName(dir.relativize(path))) 629 .filter(pkg -> pkg.length() > 0) // module-info 630 .distinct() 631 .collect(Collectors.toSet()); 632 } catch (IOException ioe) { 633 throw new UncheckedIOException(ioe); 634 } 635 } 636 637 /** 638 * Returns the set of packages in the given JAR file. 639 */ 640 Set<String> findPackages(JarFile jf) { 641 return jf.stream() 642 .filter(e -> e.getName().endsWith(".class")) 643 .map(e -> toPackageName(e)) 644 .filter(pkg -> pkg.length() > 0) // module-info 645 .distinct() 646 .collect(Collectors.toSet()); 647 } 648 649 String toPackageName(Path path) { 650 String name = path.toString(); 651 assert name.endsWith(".class"); 652 int index = name.lastIndexOf(File.separatorChar); 653 if (index != -1) 654 return name.substring(0, index).replace(File.separatorChar, '.'); 655 656 if (!name.equals(MODULE_INFO)) { 657 IOException e = new IOException(name + " in the unnamed package"); 658 throw new UncheckedIOException(e); 659 } 660 return ""; 661 } 662 663 String toPackageName(ZipEntry entry) { 664 String name = entry.getName(); 665 assert name.endsWith(".class"); 666 int index = name.lastIndexOf("/"); 667 if (index != -1) 668 return name.substring(0, index).replace('/', '.'); 669 else 670 return ""; 671 } 672 673 void processClasses(ZipOutputStream zos, List<Path> classpaths) 674 throws IOException 675 { 676 if (classpaths == null) 677 return; 678 679 for (Path p : classpaths) { 680 if (Files.isDirectory(p)) { 681 processSection(zos, Section.CLASSES, p); 682 } else if (Files.isRegularFile(p) && p.toString().endsWith(".jar")) { 683 try (JarFile jf = new JarFile(p.toFile())) { 684 JarEntryConsumer jec = new JarEntryConsumer(zos, jf); 685 jf.stream().filter(jec).forEach(jec); 686 } 687 } 688 } 689 } 690 691 void processSection(ZipOutputStream zos, Section section, List<Path> paths) 692 throws IOException 693 { 694 if (paths == null) 695 return; 696 697 for (Path p : paths) 698 processSection(zos, section, p); 699 } 700 701 void processSection(ZipOutputStream zos, Section section, Path top) 702 throws IOException 703 { 704 final String prefix = section.jmodDir(); 705 706 Files.walkFileTree(top, new SimpleFileVisitor<Path>() { 707 @Override 708 public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) 709 throws IOException 710 { 711 Path relPath = top.relativize(file); 712 if (!relPath.toString().equals(MODULE_INFO) 713 && !matches(relPath, excludes)) { 714 try (InputStream in = Files.newInputStream(file)) { 715 writeZipEntry(zos, in, prefix, relPath.toString()); 716 } 717 } 718 return FileVisitResult.CONTINUE; 719 } 720 }); 721 } 722 723 boolean matches(Path path, List<PathMatcher> matchers) { 724 if (matchers != null) { 725 for (PathMatcher pm : matchers) { 726 if (pm.matches(path)) 727 return true; 728 } 729 } 730 return false; 731 } 732 733 void writeZipEntry(ZipOutputStream zos, InputStream in, String prefix, String other) 734 throws IOException 735 { 736 String name = Paths.get(prefix, other).toString() 737 .replace(File.separatorChar, '/'); 738 ZipEntry ze = new ZipEntry(name); 739 zos.putNextEntry(ze); 740 in.transferTo(zos); 741 zos.closeEntry(); 742 } 743 744 class JarEntryConsumer implements Consumer<JarEntry>, Predicate<JarEntry> { 745 final ZipOutputStream zos; 746 final JarFile jarfile; 747 JarEntryConsumer(ZipOutputStream zos, JarFile jarfile) { 748 this.zos = zos; 749 this.jarfile = jarfile; 750 } 751 @Override 752 public void accept(JarEntry je) { 753 try (InputStream in = jarfile.getInputStream(je)) { 754 writeZipEntry(zos, in, Section.CLASSES.jmodDir(), je.getName()); 755 } catch (IOException e) { 756 throw new UncheckedIOException(e); 757 } 758 } 759 @Override 760 public boolean test(JarEntry je) { 761 String name = je.getName(); 762 // ## no support for excludes. Is it really needed? 763 return !name.endsWith(MODULE_INFO) && !je.isDirectory(); 764 } 765 } 766 } 767 768 enum Section { 769 NATIVE_LIBS("native"), 770 NATIVE_CMDS("bin"), 771 CLASSES("classes"), 772 CONFIG("conf"), 773 UNKNOWN("unknown"); 774 775 private final String jmodDir; 776 777 Section(String jmodDir) { 778 this.jmodDir = jmodDir; 779 } 780 781 String jmodDir() { return jmodDir; } 782 } 783 784 static class ClassPathConverter implements ValueConverter<Path> { 785 static final ValueConverter<Path> INSTANCE = new ClassPathConverter(); 786 787 private static final Path CWD = Paths.get(""); 788 789 @Override 790 public Path convert(String value) { 791 try { 792 Path path = CWD.resolve(value); 793 if (Files.notExists(path)) 794 throw new CommandException("err.path.not.found", path); 795 if (! (Files.isDirectory(path) || 796 (Files.isRegularFile(path) && path.toString().endsWith(".jar")))) 797 throw new CommandException("err.invalid.class.path.entry", path); 798 return path; 799 } catch (InvalidPathException x) { 800 throw new CommandException("err.path.not.valid", value); 801 } 802 } 803 804 @Override public Class<Path> valueType() { return Path.class; } 805 806 @Override public String valuePattern() { return "path"; } 807 } 808 809 static class DirPathConverter implements ValueConverter<Path> { 810 static final ValueConverter<Path> INSTANCE = new DirPathConverter(); 811 812 private static final Path CWD = Paths.get(""); 813 814 @Override 815 public Path convert(String value) { 816 try { 817 Path path = CWD.resolve(value); 818 if (Files.notExists(path)) 819 throw new CommandException("err.path.not.found", path); 820 if (!Files.isDirectory(path)) 821 throw new CommandException("err.path.not.a.dir", path); 822 return path; 823 } catch (InvalidPathException x) { 824 throw new CommandException("err.path.not.valid", value); 825 } 826 } 827 828 @Override public Class<Path> valueType() { return Path.class; } 829 830 @Override public String valuePattern() { return "path"; } 831 } 832 833 static class ModuleVersionConverter implements ValueConverter<Version> { 834 @Override 835 public Version convert(String value) { 836 try { 837 return Version.parse(value); 838 } catch (IllegalArgumentException x) { 839 throw new CommandException("err.invalid.version", x.getMessage()); 840 } 841 } 842 843 @Override public Class<Version> valueType() { return Version.class; } 844 845 @Override public String valuePattern() { return "module-version"; } 846 } 847 848 static class PatternConverter implements ValueConverter<Pattern> { 849 @Override 850 public Pattern convert(String value) { 851 try { 852 return Pattern.compile(value); 853 } catch (PatternSyntaxException e) { 854 throw new CommandException("err.bad.pattern", value); 855 } 856 } 857 858 @Override public Class<Pattern> valueType() { return Pattern.class; } 859 860 @Override public String valuePattern() { return "pattern"; } 861 } 862 863 static class GlobConverter implements ValueConverter<PathMatcher> { 864 @Override 865 public PathMatcher convert(String pattern) { 866 try { 867 return FileSystems.getDefault() 868 .getPathMatcher("glob:" + pattern); 869 } catch (PatternSyntaxException e) { 870 throw new CommandException("err.bad.pattern", pattern); 871 } 872 } 873 874 @Override public Class<PathMatcher> valueType() { return PathMatcher.class; } 875 876 @Override public String valuePattern() { return "pattern"; } 877 } 878 879 /* Support for @<file> in jmod help */ 880 private static final String CMD_FILENAME = "@<filename>"; 881 882 /** 883 * This formatter is adding the @filename option and does the required 884 * formatting. 885 */ 886 private static final class JmodHelpFormatter extends BuiltinHelpFormatter { 887 888 private JmodHelpFormatter() { super(80, 2); } 889 890 @Override 891 public String format(Map<String, ? extends OptionDescriptor> options) { 892 Map<String, OptionDescriptor> all = new HashMap<>(); 893 all.putAll(options); 894 all.put(CMD_FILENAME, new OptionDescriptor() { 895 @Override 896 public Collection<String> options() { 897 List<String> ret = new ArrayList<>(); 898 ret.add(CMD_FILENAME); 899 return ret; 900 } 901 @Override 902 public String description() { return getMessage("main.opt.cmdfile"); } 903 @Override 904 public List<?> defaultValues() { return Collections.emptyList(); } 905 @Override 906 public boolean isRequired() { return false; } 907 @Override 908 public boolean acceptsArguments() { return false; } 909 @Override 910 public boolean requiresArgument() { return false; } 911 @Override 912 public String argumentDescription() { return null; } 913 @Override 914 public String argumentTypeIndicator() { return null; } 915 @Override 916 public boolean representsNonOptions() { return false; } 917 }); 918 String content = super.format(all); 919 StringBuilder builder = new StringBuilder(); 920 921 builder.append("\n").append(" Main operation modes:\n "); 922 builder.append(getMessage("main.opt.mode.create")).append("\n "); 923 builder.append(getMessage("main.opt.mode.list")).append("\n "); 924 builder.append(getMessage("main.opt.mode.describe")).append("\n\n"); 925 926 String cmdfile = null; 927 String[] lines = content.split("\n"); 928 for (String line : lines) { 929 if (line.startsWith("--@")) { 930 cmdfile = line.replace("--" + CMD_FILENAME, CMD_FILENAME + " "); 931 } else if (line.startsWith("Option") || line.startsWith("------")) { 932 builder.append(" ").append(line).append("\n"); 933 } else if (!line.matches("Non-option arguments")){ 934 builder.append(" ").append(line).append("\n"); 935 } 936 } 937 if (cmdfile != null) { 938 builder.append(" ").append(cmdfile).append("\n"); 939 } 940 return builder.toString(); 941 } 942 } 943 944 private final OptionParser parser = new OptionParser(); 945 946 private void handleOptions(String[] args) { 947 parser.formatHelpWith(new JmodHelpFormatter()); 948 949 OptionSpec<Path> classPath 950 = parser.accepts("class-path", getMessage("main.opt.class-path")) 951 .withRequiredArg() 952 .withValuesSeparatedBy(File.pathSeparatorChar) 953 .withValuesConvertedBy(ClassPathConverter.INSTANCE); 954 955 OptionSpec<Path> cmds 956 = parser.accepts("cmds", getMessage("main.opt.cmds")) 957 .withRequiredArg() 958 .withValuesSeparatedBy(File.pathSeparatorChar) 959 .withValuesConvertedBy(DirPathConverter.INSTANCE); 960 961 OptionSpec<Path> config 962 = parser.accepts("config", getMessage("main.opt.config")) 963 .withRequiredArg() 964 .withValuesSeparatedBy(File.pathSeparatorChar) 965 .withValuesConvertedBy(DirPathConverter.INSTANCE); 966 967 OptionSpec<PathMatcher> excludes 968 = parser.accepts("exclude", getMessage("main.opt.exclude")) 969 .withRequiredArg() 970 .withValuesConvertedBy(new GlobConverter()); 971 972 OptionSpec<Pattern> hashDependencies 973 = parser.accepts("hash-dependencies", getMessage("main.opt.hash-dependencies")) 974 .withRequiredArg() 975 .withValuesConvertedBy(new PatternConverter()); 976 977 OptionSpec<Void> help 978 = parser.accepts("help", getMessage("main.opt.help")) 979 .forHelp(); 980 981 OptionSpec<Path> libs 982 = parser.accepts("libs", getMessage("main.opt.libs")) 983 .withRequiredArg() 984 .withValuesSeparatedBy(File.pathSeparatorChar) 985 .withValuesConvertedBy(DirPathConverter.INSTANCE); 986 987 OptionSpec<String> mainClass 988 = parser.accepts("main-class", getMessage("main.opt.main-class")) 989 .withRequiredArg() 990 .describedAs(getMessage("main.opt.main-class.arg")); 991 992 OptionSpec<Path> modulePath // TODO: short version of --mp ?? 993 = parser.acceptsAll(Arrays.asList("mp", "modulepath"), 994 getMessage("main.opt.modulepath")) 995 .withRequiredArg() 996 .withValuesSeparatedBy(File.pathSeparatorChar) 997 .withValuesConvertedBy(DirPathConverter.INSTANCE); 998 999 OptionSpec<Version> moduleVersion 1000 = parser.accepts("module-version", getMessage("main.opt.module-version")) 1001 .withRequiredArg() 1002 .withValuesConvertedBy(new ModuleVersionConverter()); 1003 1004 OptionSpec<String> osName 1005 = parser.accepts("os-name", getMessage("main.opt.os-name")) 1006 .withRequiredArg() 1007 .describedAs(getMessage("main.opt.os-name.arg")); 1008 1009 OptionSpec<String> osArch 1010 = parser.accepts("os-arch", getMessage("main.opt.os-arch")) 1011 .withRequiredArg() 1012 .describedAs(getMessage("main.opt.os-arch.arg")); 1013 1014 OptionSpec<String> osVersion 1015 = parser.accepts("os-version", getMessage("main.opt.os-version")) 1016 .withRequiredArg() 1017 .describedAs(getMessage("main.opt.os-version.arg")); 1018 1019 OptionSpec<Void> version 1020 = parser.accepts("version", getMessage("main.opt.version")); 1021 1022 NonOptionArgumentSpec<String> nonOptions 1023 = parser.nonOptions(); 1024 1025 try { 1026 OptionSet opts = parser.parse(args); 1027 1028 if (opts.has(help) || opts.has(version)) { 1029 options = new Options(); 1030 options.help = opts.has(help); 1031 options.version = opts.has(version); 1032 return; // informational message will be shown 1033 } 1034 1035 List<String> words = opts.valuesOf(nonOptions); 1036 if (words.isEmpty()) 1037 throw new CommandException("err.missing.mode").showUsage(true); 1038 String verb = words.get(0); 1039 options = new Options(); 1040 try { 1041 options.mode = Enum.valueOf(Mode.class, verb.toUpperCase()); 1042 } catch (IllegalArgumentException e) { 1043 throw new CommandException("err.invalid.mode", verb).showUsage(true); 1044 } 1045 1046 if (opts.has(classPath)) 1047 options.classpath = opts.valuesOf(classPath); 1048 if (opts.has(cmds)) 1049 options.cmds = opts.valuesOf(cmds); 1050 if (opts.has(config)) 1051 options.configs = opts.valuesOf(config); 1052 if (opts.has(excludes)) 1053 options.excludes = opts.valuesOf(excludes); 1054 if (opts.has(libs)) 1055 options.libs = opts.valuesOf(libs); 1056 if (opts.has(modulePath)) { 1057 Path[] dirs = opts.valuesOf(modulePath).toArray(new Path[0]); 1058 options.moduleFinder = ModuleFinder.of(dirs); 1059 if (options.moduleFinder instanceof ConfigurableModuleFinder) 1060 ((ConfigurableModuleFinder)options.moduleFinder).configurePhase(Phase.LINK_TIME); 1061 } 1062 if (opts.has(moduleVersion)) 1063 options.moduleVersion = opts.valueOf(moduleVersion); 1064 if (opts.has(mainClass)) 1065 options.mainClass = opts.valueOf(mainClass); 1066 if (opts.has(osName)) 1067 options.osName = opts.valueOf(osName); 1068 if (opts.has(osArch)) 1069 options.osArch = opts.valueOf(osArch); 1070 if (opts.has(osVersion)) 1071 options.osVersion = opts.valueOf(osVersion); 1072 if (opts.has(hashDependencies)) { 1073 options.dependenciesToHash = opts.valueOf(hashDependencies); 1074 // if storing hashes of dependencies then the module path is required 1075 if (options.moduleFinder == null) 1076 throw new CommandException("err.modulepath.must.be.specified").showUsage(true); 1077 } 1078 1079 if (words.size() <= 1) 1080 throw new CommandException("err.jmod.must.be.specified").showUsage(true); 1081 Path path = Paths.get(words.get(1)); 1082 if (options.mode.equals(Mode.CREATE) && Files.exists(path)) 1083 throw new CommandException("err.file.already.exists", path); 1084 else if ((options.mode.equals(Mode.LIST) || 1085 options.mode.equals(Mode.DESCRIBE)) 1086 && Files.notExists(path)) 1087 throw new CommandException("err.jmod.not.found", path); 1088 options.jmodFile = path; 1089 1090 if (words.size() > 2) 1091 throw new CommandException("err.unknown.option", 1092 words.subList(2, words.size())).showUsage(true); 1093 1094 if (options.mode.equals(Mode.CREATE) && options.classpath == null) 1095 throw new CommandException("err.classpath.must.be.specified").showUsage(true); 1096 if (options.mainClass != null && !isValidJavaIdentifier(options.mainClass)) 1097 throw new CommandException("err.invalid.main-class", options.mainClass); 1098 } catch (OptionException e) { 1099 throw new CommandException(e.getMessage()); 1100 } 1101 } 1102 1103 /** 1104 * Returns true if, and only if, the given main class is a legal. 1105 */ 1106 static boolean isValidJavaIdentifier(String mainClass) { 1107 if (mainClass.length() == 0) 1108 return false; 1109 1110 if (!Character.isJavaIdentifierStart(mainClass.charAt(0))) 1111 return false; 1112 1113 int n = mainClass.length(); 1114 for (int i=1; i < n; i++) { 1115 char c = mainClass.charAt(i); 1116 if (!Character.isJavaIdentifierPart(c) && c != '.') 1117 return false; 1118 } 1119 if (mainClass.charAt(n-1) == '.') 1120 return false; 1121 1122 return true; 1123 } 1124 1125 private void reportError(String message) { 1126 out.println(getMessage("error.prefix") + " " + message); 1127 } 1128 1129 private void warning(String key, Object... args) { 1130 out.println(getMessage("warn.prefix") + " " + getMessage(key, args)); 1131 } 1132 1133 private void showUsageSummary() { 1134 out.println(getMessage("main.usage.summary", PROGNAME)); 1135 } 1136 1137 private void showHelp() { 1138 out.println(getMessage("main.usage", PROGNAME)); 1139 try { 1140 parser.printHelpOn(out); 1141 } catch (IOException x) { 1142 throw new AssertionError(x); 1143 } 1144 } 1145 1146 private void showVersion() { 1147 out.println(version()); 1148 } 1149 1150 private String version() { 1151 return System.getProperty("java.version"); 1152 } 1153 1154 private static String getMessage(String key, Object... args) { 1155 try { 1156 return MessageFormat.format(ResourceBundleHelper.bundle.getString(key), args); 1157 } catch (MissingResourceException e) { 1158 throw new InternalError("Missing message: " + key); 1159 } 1160 } 1161 1162 private static class ResourceBundleHelper { 1163 static final ResourceBundle bundle; 1164 1165 static { 1166 Locale locale = Locale.getDefault(); 1167 try { 1168 bundle = ResourceBundle.getBundle("jdk.tools.jmod.resources.jmod", locale); 1169 } catch (MissingResourceException e) { 1170 throw new InternalError("Cannot find jmod resource bundle for locale " + locale); 1171 } 1172 } 1173 } 1174 }