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