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