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