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