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