rev 58246 : 8240333: jmod incorrectly updates .jar and .jmod files during hashing

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