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