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