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