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