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