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