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