1 /*
   2  * Copyright (c) 2015, 2016, Oracle and/or its affiliates. All rights reserved.
   3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
   4  *
   5  * This code is free software; you can redistribute it and/or modify it
   6  * under the terms of the GNU General Public License version 2 only, as
   7  * published by the Free Software Foundation.  Oracle designates this
   8  * particular file as subject to the "Classpath" exception as provided
   9  * by Oracle in the LICENSE file that accompanied this code.
  10  *
  11  * This code is distributed in the hope that it will be useful, but WITHOUT
  12  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
  13  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
  14  * version 2 for more details (a copy is included in the LICENSE file that
  15  * accompanied this code).
  16  *
  17  * You should have received a copy of the GNU General Public License version
  18  * 2 along with this work; if not, write to the Free Software Foundation,
  19  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
  20  *
  21  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
  22  * or visit www.oracle.com if you need additional information or have any
  23  * questions.
  24  */
  25 
  26 package jdk.tools.jmod;
  27 
  28 import java.io.BufferedInputStream;
  29 import java.io.ByteArrayInputStream;
  30 import java.io.ByteArrayOutputStream;
  31 import java.io.File;
  32 import java.io.IOException;
  33 import java.io.InputStream;
  34 import java.io.OutputStream;
  35 import java.io.PrintStream;
  36 import java.io.UncheckedIOException;
  37 import java.lang.module.FindException;
  38 import java.lang.module.ModuleReference;
  39 import java.lang.module.ModuleFinder;
  40 import java.lang.module.ModuleDescriptor.Requires;
  41 import java.lang.module.ModuleDescriptor;
  42 import java.lang.module.ModuleDescriptor.Version;
  43 import java.lang.reflect.InvocationTargetException;
  44 import java.lang.reflect.Method;
  45 import java.net.URI;
  46 import java.nio.file.FileSystems;
  47 import java.nio.file.FileVisitResult;
  48 import java.nio.file.Files;
  49 import java.nio.file.InvalidPathException;
  50 import java.nio.file.Path;
  51 import java.nio.file.PathMatcher;
  52 import java.nio.file.Paths;
  53 import java.nio.file.SimpleFileVisitor;
  54 import java.nio.file.attribute.BasicFileAttributes;
  55 import java.text.MessageFormat;
  56 import java.util.ArrayList;
  57 import java.util.Arrays;
  58 import java.util.Collection;
  59 import java.util.Collections;
  60 import java.util.Formatter;
  61 import java.util.HashMap;
  62 import java.util.HashSet;
  63 import java.util.List;
  64 import java.util.Locale;
  65 import java.util.Map;
  66 import java.util.MissingResourceException;
  67 import java.util.Optional;
  68 import java.util.ResourceBundle;
  69 import java.util.Set;
  70 import java.util.function.Consumer;
  71 import java.util.function.Predicate;
  72 import java.util.function.Supplier;
  73 import java.util.jar.JarEntry;
  74 import java.util.jar.JarFile;
  75 import java.util.stream.Collectors;
  76 import java.util.regex.Pattern;
  77 import java.util.regex.PatternSyntaxException;
  78 import java.util.zip.ZipEntry;
  79 import java.util.zip.ZipException;
  80 import java.util.zip.ZipFile;
  81 import java.util.zip.ZipInputStream;
  82 import java.util.zip.ZipOutputStream;
  83 
  84 import jdk.internal.joptsimple.BuiltinHelpFormatter;
  85 import jdk.internal.joptsimple.NonOptionArgumentSpec;
  86 import jdk.internal.joptsimple.OptionDescriptor;
  87 import jdk.internal.joptsimple.OptionException;
  88 import jdk.internal.joptsimple.OptionParser;
  89 import jdk.internal.joptsimple.OptionSet;
  90 import jdk.internal.joptsimple.OptionSpec;
  91 import jdk.internal.joptsimple.ValueConverter;
  92 import jdk.internal.module.ConfigurableModuleFinder;
  93 import jdk.internal.module.ConfigurableModuleFinder.Phase;
  94 import jdk.internal.module.Hasher;
  95 import jdk.internal.module.Hasher.DependencyHashes;
  96 import jdk.internal.module.ModuleInfoExtender;
  97 
  98 import static java.util.function.Function.identity;
  99 import static java.util.stream.Collectors.joining;
 100 import static java.util.stream.Collectors.toList;
 101 import static java.util.stream.Collectors.toMap;
 102 
 103 /**
 104  * Implementation for the jmod tool.
 105  */
 106 public class JmodTask {
 107 
 108     static class CommandException extends RuntimeException {
 109         private static final long serialVersionUID = 0L;
 110         boolean showUsage;
 111 
 112         CommandException(String key, Object... args) {
 113             super(getMessageOrKey(key, args));
 114         }
 115 
 116         CommandException showUsage(boolean b) {
 117             showUsage = b;
 118             return this;
 119         }
 120 
 121         private static String getMessageOrKey(String key, Object... args) {
 122             try {
 123                 return MessageFormat.format(ResourceBundleHelper.bundle.getString(key), args);
 124             } catch (MissingResourceException e) {
 125                 return key;
 126             }
 127         }
 128     }
 129 
 130     static <T extends Throwable> void fail(Class<T> type,
 131                                            String format,
 132                                            Object... args) throws T {
 133         String msg = new Formatter().format(format, args).toString();
 134         try {
 135             T t = type.getConstructor(String.class).newInstance(msg);
 136             throw t;
 137         } catch (InstantiationException |
 138                  InvocationTargetException |
 139                  NoSuchMethodException |
 140                  IllegalAccessException e) {
 141             throw new InternalError("Unable to create an instance of " + type, e);
 142         }
 143     }
 144 
 145     private static final String PROGNAME = "jmod";
 146     private static final String MODULE_INFO = "module-info.class";
 147 
 148     private Options options;
 149     private PrintStream out = System.out;
 150     void setLog(PrintStream out) {
 151         this.out = out;
 152     }
 153 
 154     /* Result codes. */
 155     static final int EXIT_OK = 0, // Completed with no errors.
 156                      EXIT_ERROR = 1, // Completed but reported errors.
 157                      EXIT_CMDERR = 2, // Bad command-line arguments
 158                      EXIT_SYSERR = 3, // System error or resource exhaustion.
 159                      EXIT_ABNORMAL = 4;// terminated abnormally
 160 
 161     enum Mode {
 162         CREATE,
 163         LIST,
 164         DESCRIBE
 165     };
 166 
 167     static class Options {
 168         Mode mode;
 169         Path jmodFile;
 170         boolean help;
 171         boolean version;
 172         List<Path> classpath;
 173         List<Path> cmds;
 174         List<Path> configs;
 175         List<Path> libs;
 176         ModuleFinder moduleFinder;
 177         Version moduleVersion;
 178         String mainClass;
 179         String osName;
 180         String osArch;
 181         String osVersion;
 182         Pattern dependenciesToHash;
 183         List<PathMatcher> excludes;
 184     }
 185 
 186     public int run(String[] args) {
 187 
 188         try {
 189             handleOptions(args);
 190             if (options == null) {
 191                 showUsageSummary();
 192                 return EXIT_CMDERR;
 193             }
 194             if (options.help) {
 195                 showHelp();
 196                 return EXIT_OK;
 197             }
 198             if (options.version) {
 199                 showVersion();
 200                 return EXIT_OK;
 201             }
 202 
 203             boolean ok;
 204             switch (options.mode) {
 205                 case CREATE:
 206                     ok = create();
 207                     break;
 208                 case LIST:
 209                     ok = list();
 210                     break;
 211                 case DESCRIBE:
 212                     ok = describe();
 213                     break;
 214                 default:
 215                     throw new AssertionError("Unknown mode: " + options.mode.name());
 216             }
 217 
 218             return ok ? EXIT_OK : EXIT_ERROR;
 219         } catch (CommandException e) {
 220             reportError(e.getMessage());
 221             if (e.showUsage)
 222                 showUsageSummary();
 223             return EXIT_CMDERR;
 224         } catch (Exception x) {
 225             reportError(x.getMessage());
 226             x.printStackTrace();
 227             return EXIT_ABNORMAL;
 228         } finally {
 229             out.flush();
 230         }
 231     }
 232 
 233     private boolean list() throws IOException {
 234         ZipFile zip = null;
 235         try {
 236             try {
 237                 zip = new ZipFile(options.jmodFile.toFile());
 238             } catch (IOException x) {
 239                 throw new IOException("error opening jmod file", x);
 240             }
 241 
 242             // Trivially print the archive entries for now, pending a more complete implementation
 243             zip.stream().forEach(e -> out.println(e.getName()));
 244             return true;
 245         } finally {
 246             if (zip != null)
 247                 zip.close();
 248         }
 249     }
 250 
 251     private Map<String, Path> modulesToPath(Set<ModuleDescriptor> modules) {
 252         ModuleFinder finder = options.moduleFinder;
 253 
 254         Map<String,Path> modPaths = new HashMap<>();
 255         for (ModuleDescriptor m : modules) {
 256             String name = m.name();
 257 
 258             Optional<ModuleReference> omref = finder.find(name);
 259             if (!omref.isPresent()) {
 260                 // this should not happen, module path bug?
 261                 fail(InternalError.class,
 262                      "Selected module %s not on module path",
 263                      name);
 264             }
 265 
 266             URI uri = omref.get().location().get();
 267             modPaths.put(name, Paths.get(uri));
 268 
 269         }
 270         return modPaths;
 271     }
 272 
 273     private boolean describe() throws IOException {
 274         ZipFile zip = null;
 275         try {
 276             try {
 277                 zip = new ZipFile(options.jmodFile.toFile());
 278             } catch (IOException x) {
 279                 throw new IOException("error opening jmod file", x);
 280             }
 281 
 282             try (InputStream in = Files.newInputStream(options.jmodFile)) {
 283                 boolean found = printModuleDescriptor(in);
 284                 if (!found)
 285                     throw new CommandException("err.module.descriptor.not.found");
 286                 return found;
 287             }
 288         } finally {
 289             if (zip != null)
 290                 zip.close();
 291         }
 292     }
 293 
 294     static <T> String toString(Set<T> set) {
 295         if (set.isEmpty()) { return ""; }
 296         return set.stream().map(e -> e.toString().toLowerCase(Locale.ROOT))
 297                   .collect(joining(" "));
 298     }
 299 
 300     private boolean printModuleDescriptor(InputStream in)
 301         throws IOException
 302     {
 303         final String mi = Section.CLASSES.jmodDir() + "/" + MODULE_INFO;
 304         try (BufferedInputStream bis = new BufferedInputStream(in);
 305              ZipInputStream zis = new ZipInputStream(bis)) {
 306 
 307             ZipEntry e;
 308             while ((e = zis.getNextEntry()) != null) {
 309                 if (e.getName().equals(mi)) {
 310                     ModuleDescriptor md = ModuleDescriptor.read(zis);
 311                     StringBuilder sb = new StringBuilder();
 312                     sb.append("\n").append(md.toNameAndVersion());
 313 
 314                     List<Requires> requires = md.requires().stream().sorted().collect(toList());
 315                     if (!requires.isEmpty()) {
 316                         requires.forEach(r -> {
 317                                 sb.append("\n  requires ");
 318                                 if (!r.modifiers().isEmpty())
 319                                   sb.append(toString(r.modifiers())).append(" ");
 320                                 sb.append(r.name());
 321                             });
 322                     }
 323 
 324                     List<String> l = md.uses().stream().sorted().collect(toList());
 325                     if (!l.isEmpty()) {
 326                         l.forEach(sv -> sb.append("\n  uses ").append(sv));
 327                     }
 328 
 329                     List<ModuleDescriptor.Exports> exports = sortExports(md.exports());
 330                     if (!exports.isEmpty()) {
 331                         exports.forEach(ex -> sb.append("\n  exports ").append(ex));
 332                     }
 333 
 334                     l = md.conceals().stream().sorted().collect(toList());
 335                     if (!l.isEmpty()) {
 336                         l.forEach(p -> sb.append("\n  conceals ").append(p));
 337                     }
 338 
 339                     Map<String, ModuleDescriptor.Provides> provides = md.provides();
 340                     if (!provides.isEmpty()) {
 341                         provides.values().forEach(p ->
 342                                 sb.append("\n  provides ").append(p.service())
 343                                   .append(" with ")
 344                                   .append(toString(p.providers())));
 345                     }
 346 
 347                     Optional<String> mc = md.mainClass();
 348                     if (mc.isPresent())
 349                         sb.append("\n  main-class " + mc.get());
 350 
 351 
 352 
 353                     Optional<String> osname = md.osName();
 354                     if (osname.isPresent())
 355                         sb.append("\n  operating-system-name " + osname.get());
 356 
 357                     Optional<String> osarch = md.osArch();
 358                     if (osarch.isPresent())
 359                         sb.append("\n  operating-system-architecture " + osarch.get());
 360 
 361                     Optional<String> osversion = md.osVersion();
 362                     if (osversion.isPresent())
 363                         sb.append("\n  operating-system-version " + osversion.get());
 364 
 365                     try {
 366                         Method m = ModuleDescriptor.class.getDeclaredMethod("hashes");
 367                         m.setAccessible(true);
 368                         @SuppressWarnings("unchecked")
 369                         Optional<Hasher.DependencyHashes> optHashes =
 370                                 (Optional<Hasher.DependencyHashes>) m.invoke(md);
 371 
 372                         if (optHashes.isPresent()) {
 373                             Hasher.DependencyHashes hashes = optHashes.get();
 374                             hashes.names().stream().forEach(mod ->
 375                                     sb.append("\n  hashes ").append(mod).append(" ")
 376                                       .append(hashes.algorithm()).append(" ")
 377                                       .append(hashes.hashFor(mod)));
 378                         }
 379                     } catch (ReflectiveOperationException x) {
 380                         throw new InternalError(x);
 381                     }
 382                     out.println(sb.toString());
 383                     return true;
 384                 }
 385             }
 386         }
 387         return false;
 388     }
 389 
 390     static List<ModuleDescriptor.Exports> sortExports(Set<ModuleDescriptor.Exports> exports) {
 391         Map<String,ModuleDescriptor.Exports> map =
 392                 exports.stream()
 393                        .collect(toMap(ModuleDescriptor.Exports::source,
 394                                       identity()));
 395         List<String> sources = exports.stream()
 396                                       .map(ModuleDescriptor.Exports::source)
 397                                       .sorted()
 398                                       .collect(toList());
 399 
 400         List<ModuleDescriptor.Exports> l = new ArrayList<>();
 401         sources.forEach(e -> l.add(map.get(e)));
 402         return l;
 403     }
 404 
 405     private boolean create() throws IOException {
 406         JmodFileWriter jmod = new JmodFileWriter();
 407 
 408         // create jmod with temporary name to avoid it being examined
 409         // when scanning the module path
 410         Path target = options.jmodFile;
 411         Path tempTarget = target.resolveSibling(target.getFileName() + ".tmp");
 412         try {
 413             try (OutputStream out = Files.newOutputStream(tempTarget)) {
 414                 jmod.write(out);
 415             }
 416             Files.move(tempTarget, target);
 417         } catch (Exception e) {
 418             if (Files.exists(tempTarget)) {
 419                 try {
 420                     Files.delete(tempTarget);
 421                 } catch (IOException ioe) {
 422                     e.addSuppressed(ioe);
 423                 }
 424             }
 425             throw e;
 426         }
 427         return true;
 428     }
 429 
 430     private class JmodFileWriter {
 431         final ModuleFinder moduleFinder = options.moduleFinder;
 432         final List<Path> cmds = options.cmds;
 433         final List<Path> libs = options.libs;
 434         final List<Path> configs = options.configs;
 435         final List<Path> classpath = options.classpath;
 436         final Version moduleVersion = options.moduleVersion;
 437         final String mainClass = options.mainClass;
 438         final String osName = options.osName;
 439         final String osArch = options.osArch;
 440         final String osVersion = options.osVersion;
 441         final Pattern dependenciesToHash = options.dependenciesToHash;
 442         final List<PathMatcher> excludes = options.excludes;
 443 
 444         JmodFileWriter() { }
 445 
 446         /**
 447          * Writes the jmod to the given output stream.
 448          */
 449         void write(OutputStream out) throws IOException {
 450             try (ZipOutputStream zos = new ZipOutputStream(out)) {
 451 
 452                 // module-info.class
 453                 writeModuleInfo(zos, findPackages(classpath));
 454 
 455                 // classes
 456                 processClasses(zos, classpath);
 457 
 458                 processSection(zos, Section.NATIVE_CMDS, cmds);
 459                 processSection(zos, Section.NATIVE_LIBS, libs);
 460                 processSection(zos, Section.CONFIG, configs);
 461             }
 462         }
 463 
 464         /**
 465          * Returns a supplier of an input stream to the module-info.class
 466          * on the class path of directories and JAR files.
 467          */
 468         Supplier<InputStream> newModuleInfoSupplier() throws IOException {
 469             ByteArrayOutputStream baos = new ByteArrayOutputStream();
 470             for (Path e: classpath) {
 471                 if (Files.isDirectory(e)) {
 472                     Path mi = e.resolve(MODULE_INFO);
 473                     if (Files.isRegularFile(mi)) {
 474                         Files.copy(mi, baos);
 475                         break;
 476                     }
 477                 } else if (Files.isRegularFile(e) && e.toString().endsWith(".jar")) {
 478                     try (JarFile jf = new JarFile(e.toFile())) {
 479                         ZipEntry entry = jf.getEntry(MODULE_INFO);
 480                         if (entry != null) {
 481                             jf.getInputStream(entry).transferTo(baos);
 482                             break;
 483                         }
 484                     } catch (ZipException x) {
 485                         // Skip. Do nothing. No packages will be added.
 486                     }
 487                 }
 488             }
 489             if (baos.size() == 0) {
 490                 return null;
 491             } else {
 492                 byte[] bytes = baos.toByteArray();
 493                 return () -> new ByteArrayInputStream(bytes);
 494             }
 495         }
 496 
 497         /**
 498          * Writes the updated module-info.class to the ZIP output stream.
 499          *
 500          * The updated module-info.class will have a ConcealedPackages attribute
 501          * with the set of module-private/non-exported packages.
 502          *
 503          * If --module-version, --main-class, or other options were provided
 504          * then the corresponding class file attributes are added to the
 505          * module-info here.
 506          */
 507         void writeModuleInfo(ZipOutputStream zos, Set<String> packages)
 508             throws IOException
 509         {
 510             Supplier<InputStream> miSupplier = newModuleInfoSupplier();
 511             if (miSupplier == null) {
 512                 throw new IOException(MODULE_INFO + " not found");
 513             }
 514 
 515             ModuleDescriptor descriptor;
 516             try (InputStream in = miSupplier.get()) {
 517                 descriptor = ModuleDescriptor.read(in);
 518             }
 519 
 520             // copy the module-info.class into the jmod with the additional
 521             // attributes for the version, main class and other meta data
 522             try (InputStream in = miSupplier.get()) {
 523                 ModuleInfoExtender extender = ModuleInfoExtender.newExtender(in);
 524 
 525                 // Add (or replace) the ConcealedPackages attribute
 526                 if (packages != null) {
 527                     Set<String> exported = descriptor.exports().stream()
 528                         .map(ModuleDescriptor.Exports::source)
 529                         .collect(Collectors.toSet());
 530                     Set<String> concealed = packages.stream()
 531                         .filter(p -> !exported.contains(p))
 532                         .collect(Collectors.toSet());
 533                     extender.conceals(concealed);
 534                 }
 535 
 536                 // --main-class
 537                 if (mainClass != null)
 538                     extender.mainClass(mainClass);
 539 
 540                 // --os-name, --os-arch, --os-version
 541                 if (osName != null || osArch != null || osVersion != null)
 542                     extender.targetPlatform(osName, osArch, osVersion);
 543 
 544                 // --module-version
 545                 if (moduleVersion != null)
 546                     extender.version(moduleVersion);
 547 
 548                 // --hash-dependencies
 549                 if (dependenciesToHash != null) {
 550                     String name = descriptor.name();
 551                     Set<Requires> dependences = descriptor.requires();
 552                     extender.hashes(hashDependences(name, dependences));
 553                 }
 554 
 555                 // write the (possibly extended or modified) module-info.class
 556                 String e = Section.CLASSES.jmodDir() + "/" + MODULE_INFO;
 557                 ZipEntry ze = new ZipEntry(e);
 558                 zos.putNextEntry(ze);
 559                 extender.write(zos);
 560                 zos.closeEntry();
 561             }
 562         }
 563 
 564         /**
 565          * Examines the module dependences of the given module
 566          * and computes the hash of any module that matches the
 567          * pattern {@code dependenciesToHash}.
 568          */
 569         DependencyHashes hashDependences(String name, Set<Requires> moduleDependences)
 570             throws IOException
 571         {
 572             Set<ModuleDescriptor> descriptors = new HashSet<>();
 573             for (Requires md: moduleDependences) {
 574                 String dn = md.name();
 575                 if (dependenciesToHash.matcher(dn).find()) {
 576                     try {
 577                         Optional<ModuleReference> omref = moduleFinder.find(dn);
 578                         if (!omref.isPresent()) {
 579                             throw new RuntimeException("Hashing module " + name
 580                                 + " dependencies, unable to find module " + dn
 581                                 + " on module path");
 582                         }
 583                         descriptors.add(omref.get().descriptor());
 584                     } catch (FindException x) {
 585                         throw new IOException("error reading module path", x);
 586                     }
 587                 }
 588             }
 589 
 590             Map<String, Path> map = modulesToPath(descriptors);
 591             if (map.size() == 0) {
 592                 return null;
 593             } else {
 594                 // use SHA-256 for now, easy to make this configurable if needed
 595                 return Hasher.generate(map, "SHA-256");
 596             }
 597         }
 598 
 599         /**
 600          * Returns the set of all packages on the given class path.
 601          */
 602         Set<String> findPackages(List<Path> classpath) {
 603             Set<String> packages = new HashSet<>();
 604             for (Path path : classpath) {
 605                 if (Files.isDirectory(path)) {
 606                     packages.addAll(findPackages(path));
 607                 } else if (Files.isRegularFile(path) && path.toString().endsWith(".jar")) {
 608                     try (JarFile jf = new JarFile(path.toString())) {
 609                         packages.addAll(findPackages(jf));
 610                     } catch (ZipException x) {
 611                         // Skip. Do nothing. No packages will be added.
 612                     } catch (IOException ioe) {
 613                         throw new UncheckedIOException(ioe);
 614                     }
 615                 }
 616             }
 617             return packages;
 618         }
 619 
 620         /**
 621          * Returns the set of packages in the given directory tree.
 622          */
 623         Set<String> findPackages(Path dir) {
 624             try {
 625                 return Files.find(dir, Integer.MAX_VALUE,
 626                         ((path, attrs) -> attrs.isRegularFile() &&
 627                                 path.toString().endsWith(".class")))
 628                         .map(path -> toPackageName(dir.relativize(path)))
 629                         .filter(pkg -> pkg.length() > 0)   // module-info
 630                         .distinct()
 631                         .collect(Collectors.toSet());
 632             } catch (IOException ioe) {
 633                 throw new UncheckedIOException(ioe);
 634             }
 635         }
 636 
 637         /**
 638          * Returns the set of packages in the given JAR file.
 639          */
 640         Set<String> findPackages(JarFile jf) {
 641             return jf.stream()
 642                      .filter(e -> e.getName().endsWith(".class"))
 643                      .map(e -> toPackageName(e))
 644                      .filter(pkg -> pkg.length() > 0)   // module-info
 645                      .distinct()
 646                      .collect(Collectors.toSet());
 647         }
 648 
 649         String toPackageName(Path path) {
 650             String name = path.toString();
 651             assert name.endsWith(".class");
 652             int index = name.lastIndexOf(File.separatorChar);
 653             if (index != -1)
 654                 return name.substring(0, index).replace(File.separatorChar, '.');
 655 
 656             if (!name.equals(MODULE_INFO)) {
 657                 IOException e = new IOException(name  + " in the unnamed package");
 658                 throw new UncheckedIOException(e);
 659             }
 660             return "";
 661         }
 662 
 663         String toPackageName(ZipEntry entry) {
 664             String name = entry.getName();
 665             assert name.endsWith(".class");
 666             int index = name.lastIndexOf("/");
 667             if (index != -1)
 668                 return name.substring(0, index).replace('/', '.');
 669             else
 670                 return "";
 671         }
 672 
 673         void processClasses(ZipOutputStream zos, List<Path> classpaths)
 674             throws IOException
 675         {
 676             if (classpaths == null)
 677                 return;
 678 
 679             for (Path p : classpaths) {
 680                 if (Files.isDirectory(p)) {
 681                     processSection(zos, Section.CLASSES, p);
 682                 } else if (Files.isRegularFile(p) && p.toString().endsWith(".jar")) {
 683                     try (JarFile jf = new JarFile(p.toFile())) {
 684                         JarEntryConsumer jec = new JarEntryConsumer(zos, jf);
 685                         jf.stream().filter(jec).forEach(jec);
 686                     }
 687                 }
 688             }
 689         }
 690 
 691         void processSection(ZipOutputStream zos, Section section, List<Path> paths)
 692             throws IOException
 693         {
 694             if (paths == null)
 695                 return;
 696 
 697             for (Path p : paths)
 698                 processSection(zos, section, p);
 699         }
 700 
 701         void processSection(ZipOutputStream zos, Section section, Path top)
 702             throws IOException
 703         {
 704             final String prefix = section.jmodDir();
 705 
 706             Files.walkFileTree(top, new SimpleFileVisitor<Path>() {
 707                 @Override
 708                 public FileVisitResult visitFile(Path file, BasicFileAttributes attrs)
 709                     throws IOException
 710                 {
 711                     Path relPath = top.relativize(file);
 712                     if (!relPath.toString().equals(MODULE_INFO)
 713                             && !matches(relPath, excludes)) {
 714                         try (InputStream in = Files.newInputStream(file)) {
 715                             writeZipEntry(zos, in, prefix, relPath.toString());
 716                         }
 717                     }
 718                     return FileVisitResult.CONTINUE;
 719                 }
 720             });
 721         }
 722 
 723         boolean matches(Path path, List<PathMatcher> matchers) {
 724             if (matchers != null) {
 725                 for (PathMatcher pm : matchers) {
 726                     if (pm.matches(path))
 727                         return true;
 728                 }
 729             }
 730             return false;
 731         }
 732 
 733         void writeZipEntry(ZipOutputStream zos, InputStream in, String prefix, String other)
 734             throws IOException
 735         {
 736             String name = Paths.get(prefix, other).toString()
 737                                .replace(File.separatorChar, '/');
 738             ZipEntry ze = new ZipEntry(name);
 739             zos.putNextEntry(ze);
 740             in.transferTo(zos);
 741             zos.closeEntry();
 742         }
 743 
 744         class JarEntryConsumer implements Consumer<JarEntry>, Predicate<JarEntry> {
 745             final ZipOutputStream zos;
 746             final JarFile jarfile;
 747             JarEntryConsumer(ZipOutputStream zos, JarFile jarfile) {
 748                 this.zos = zos;
 749                 this.jarfile = jarfile;
 750             }
 751             @Override
 752             public void accept(JarEntry je) {
 753                 try (InputStream in = jarfile.getInputStream(je)) {
 754                     writeZipEntry(zos, in, Section.CLASSES.jmodDir(), je.getName());
 755                 } catch (IOException e) {
 756                     throw new UncheckedIOException(e);
 757                 }
 758             }
 759             @Override
 760             public boolean test(JarEntry je) {
 761                 String name = je.getName();
 762                 // ## no support for excludes. Is it really needed?
 763                 return !name.endsWith(MODULE_INFO) && !je.isDirectory();
 764             }
 765         }
 766     }
 767 
 768     enum Section {
 769         NATIVE_LIBS("native"),
 770         NATIVE_CMDS("bin"),
 771         CLASSES("classes"),
 772         CONFIG("conf"),
 773         UNKNOWN("unknown");
 774 
 775         private final String jmodDir;
 776 
 777         Section(String jmodDir) {
 778             this.jmodDir = jmodDir;
 779         }
 780 
 781         String jmodDir() { return jmodDir; }
 782     }
 783 
 784     static class ClassPathConverter implements ValueConverter<Path> {
 785         static final ValueConverter<Path> INSTANCE = new ClassPathConverter();
 786 
 787         private static final Path CWD = Paths.get("");
 788 
 789         @Override
 790         public Path convert(String value) {
 791             try {
 792                 Path path = CWD.resolve(value);
 793                 if (Files.notExists(path))
 794                     throw new CommandException("err.path.not.found", path);
 795                 if (! (Files.isDirectory(path) ||
 796                        (Files.isRegularFile(path) && path.toString().endsWith(".jar"))))
 797                     throw new CommandException("err.invalid.class.path.entry", path);
 798                 return path;
 799             } catch (InvalidPathException x) {
 800                 throw new CommandException("err.path.not.valid", value);
 801             }
 802         }
 803 
 804         @Override  public Class<Path> valueType() { return Path.class; }
 805 
 806         @Override  public String valuePattern() { return "path"; }
 807     }
 808 
 809     static class DirPathConverter implements ValueConverter<Path> {
 810         static final ValueConverter<Path> INSTANCE = new DirPathConverter();
 811 
 812         private static final Path CWD = Paths.get("");
 813 
 814         @Override
 815         public Path convert(String value) {
 816             try {
 817                 Path path = CWD.resolve(value);
 818                 if (Files.notExists(path))
 819                     throw new CommandException("err.path.not.found", path);
 820                 if (!Files.isDirectory(path))
 821                     throw new CommandException("err.path.not.a.dir", path);
 822                 return path;
 823             } catch (InvalidPathException x) {
 824                 throw new CommandException("err.path.not.valid", value);
 825             }
 826         }
 827 
 828         @Override  public Class<Path> valueType() { return Path.class; }
 829 
 830         @Override  public String valuePattern() { return "path"; }
 831     }
 832 
 833     static class ModuleVersionConverter implements ValueConverter<Version> {
 834         @Override
 835         public Version convert(String value) {
 836             try {
 837                 return Version.parse(value);
 838             } catch (IllegalArgumentException x) {
 839                 throw new CommandException("err.invalid.version", x.getMessage());
 840             }
 841         }
 842 
 843         @Override public Class<Version> valueType() { return Version.class; }
 844 
 845         @Override public String valuePattern() { return "module-version"; }
 846     }
 847 
 848     static class PatternConverter implements ValueConverter<Pattern> {
 849         @Override
 850         public Pattern convert(String value) {
 851             try {
 852                 return Pattern.compile(value);
 853             } catch (PatternSyntaxException e) {
 854                 throw new CommandException("err.bad.pattern", value);
 855             }
 856         }
 857 
 858         @Override public Class<Pattern> valueType() { return Pattern.class; }
 859 
 860         @Override public String valuePattern() { return "pattern"; }
 861     }
 862 
 863     static class GlobConverter implements ValueConverter<PathMatcher> {
 864         @Override
 865         public PathMatcher convert(String pattern) {
 866             try {
 867                 return FileSystems.getDefault()
 868                                   .getPathMatcher("glob:" + pattern);
 869             } catch (PatternSyntaxException e) {
 870                 throw new CommandException("err.bad.pattern", pattern);
 871             }
 872         }
 873 
 874         @Override public Class<PathMatcher> valueType() { return PathMatcher.class; }
 875 
 876         @Override public String valuePattern() { return "pattern"; }
 877     }
 878 
 879     /* Support for @<file> in jmod help */
 880     private static final String CMD_FILENAME = "@<filename>";
 881 
 882     /**
 883      * This formatter is adding the @filename option and does the required
 884      * formatting.
 885      */
 886     private static final class JmodHelpFormatter extends BuiltinHelpFormatter {
 887 
 888         private JmodHelpFormatter() { super(80, 2); }
 889 
 890         @Override
 891         public String format(Map<String, ? extends OptionDescriptor> options) {
 892             Map<String, OptionDescriptor> all = new HashMap<>();
 893             all.putAll(options);
 894             all.put(CMD_FILENAME, new OptionDescriptor() {
 895                 @Override
 896                 public Collection<String> options() {
 897                     List<String> ret = new ArrayList<>();
 898                     ret.add(CMD_FILENAME);
 899                     return ret;
 900                 }
 901                 @Override
 902                 public String description() { return getMessage("main.opt.cmdfile"); }
 903                 @Override
 904                 public List<?> defaultValues() { return Collections.emptyList(); }
 905                 @Override
 906                 public boolean isRequired() { return false; }
 907                 @Override
 908                 public boolean acceptsArguments() { return false; }
 909                 @Override
 910                 public boolean requiresArgument() { return false; }
 911                 @Override
 912                 public String argumentDescription() { return null; }
 913                 @Override
 914                 public String argumentTypeIndicator() { return null; }
 915                 @Override
 916                 public boolean representsNonOptions() { return false; }
 917             });
 918             String content = super.format(all);
 919             StringBuilder builder = new StringBuilder();
 920 
 921             builder.append("\n").append(" Main operation modes:\n  ");
 922             builder.append(getMessage("main.opt.mode.create")).append("\n  ");
 923             builder.append(getMessage("main.opt.mode.list")).append("\n  ");
 924             builder.append(getMessage("main.opt.mode.describe")).append("\n\n");
 925 
 926             String cmdfile = null;
 927             String[] lines = content.split("\n");
 928             for (String line : lines) {
 929                 if (line.startsWith("--@")) {
 930                     cmdfile = line.replace("--" + CMD_FILENAME, CMD_FILENAME + "  ");
 931                 } else if (line.startsWith("Option") || line.startsWith("------")) {
 932                     builder.append(" ").append(line).append("\n");
 933                 } else if (!line.matches("Non-option arguments")){
 934                     builder.append("  ").append(line).append("\n");
 935                 }
 936             }
 937             if (cmdfile != null) {
 938                 builder.append("  ").append(cmdfile).append("\n");
 939             }
 940             return builder.toString();
 941         }
 942     }
 943 
 944     private final OptionParser parser = new OptionParser();
 945 
 946     private void handleOptions(String[] args) {
 947         parser.formatHelpWith(new JmodHelpFormatter());
 948 
 949         OptionSpec<Path> classPath
 950                 = parser.accepts("class-path", getMessage("main.opt.class-path"))
 951                         .withRequiredArg()
 952                         .withValuesSeparatedBy(File.pathSeparatorChar)
 953                         .withValuesConvertedBy(ClassPathConverter.INSTANCE);
 954 
 955         OptionSpec<Path> cmds
 956                 = parser.accepts("cmds", getMessage("main.opt.cmds"))
 957                         .withRequiredArg()
 958                         .withValuesSeparatedBy(File.pathSeparatorChar)
 959                         .withValuesConvertedBy(DirPathConverter.INSTANCE);
 960 
 961         OptionSpec<Path> config
 962                 = parser.accepts("config", getMessage("main.opt.config"))
 963                         .withRequiredArg()
 964                         .withValuesSeparatedBy(File.pathSeparatorChar)
 965                         .withValuesConvertedBy(DirPathConverter.INSTANCE);
 966 
 967         OptionSpec<PathMatcher> excludes
 968                 = parser.accepts("exclude", getMessage("main.opt.exclude"))
 969                         .withRequiredArg()
 970                         .withValuesConvertedBy(new GlobConverter());
 971 
 972         OptionSpec<Pattern> hashDependencies
 973                 = parser.accepts("hash-dependencies", getMessage("main.opt.hash-dependencies"))
 974                         .withRequiredArg()
 975                         .withValuesConvertedBy(new PatternConverter());
 976 
 977         OptionSpec<Void> help
 978                 = parser.accepts("help", getMessage("main.opt.help"))
 979                         .forHelp();
 980 
 981         OptionSpec<Path> libs
 982                 = parser.accepts("libs", getMessage("main.opt.libs"))
 983                         .withRequiredArg()
 984                         .withValuesSeparatedBy(File.pathSeparatorChar)
 985                         .withValuesConvertedBy(DirPathConverter.INSTANCE);
 986 
 987         OptionSpec<String> mainClass
 988                 = parser.accepts("main-class", getMessage("main.opt.main-class"))
 989                         .withRequiredArg()
 990                         .describedAs(getMessage("main.opt.main-class.arg"));
 991 
 992         OptionSpec<Path> modulePath  // TODO: short version of --mp ??
 993                 = parser.acceptsAll(Arrays.asList("mp", "modulepath"),
 994                                     getMessage("main.opt.modulepath"))
 995                         .withRequiredArg()
 996                         .withValuesSeparatedBy(File.pathSeparatorChar)
 997                         .withValuesConvertedBy(DirPathConverter.INSTANCE);
 998 
 999         OptionSpec<Version> moduleVersion
1000                 = parser.accepts("module-version", getMessage("main.opt.module-version"))
1001                         .withRequiredArg()
1002                         .withValuesConvertedBy(new ModuleVersionConverter());
1003 
1004         OptionSpec<String> osName
1005                 = parser.accepts("os-name", getMessage("main.opt.os-name"))
1006                         .withRequiredArg()
1007                         .describedAs(getMessage("main.opt.os-name.arg"));
1008 
1009         OptionSpec<String> osArch
1010                 = parser.accepts("os-arch", getMessage("main.opt.os-arch"))
1011                         .withRequiredArg()
1012                         .describedAs(getMessage("main.opt.os-arch.arg"));
1013 
1014         OptionSpec<String> osVersion
1015                 = parser.accepts("os-version", getMessage("main.opt.os-version"))
1016                         .withRequiredArg()
1017                         .describedAs(getMessage("main.opt.os-version.arg"));
1018 
1019         OptionSpec<Void> version
1020                 = parser.accepts("version", getMessage("main.opt.version"));
1021 
1022         NonOptionArgumentSpec<String> nonOptions
1023                 = parser.nonOptions();
1024 
1025         try {
1026             OptionSet opts = parser.parse(args);
1027 
1028             if (opts.has(help) || opts.has(version)) {
1029                 options = new Options();
1030                 options.help = opts.has(help);
1031                 options.version = opts.has(version);
1032                 return;  // informational message will be shown
1033             }
1034 
1035             List<String> words = opts.valuesOf(nonOptions);
1036             if (words.isEmpty())
1037                 throw new CommandException("err.missing.mode").showUsage(true);
1038             String verb = words.get(0);
1039             options = new Options();
1040             try {
1041                 options.mode = Enum.valueOf(Mode.class, verb.toUpperCase());
1042             } catch (IllegalArgumentException e) {
1043                 throw new CommandException("err.invalid.mode", verb).showUsage(true);
1044             }
1045 
1046             if (opts.has(classPath))
1047                 options.classpath = opts.valuesOf(classPath);
1048             if (opts.has(cmds))
1049                 options.cmds = opts.valuesOf(cmds);
1050             if (opts.has(config))
1051                 options.configs = opts.valuesOf(config);
1052             if (opts.has(excludes))
1053                 options.excludes = opts.valuesOf(excludes);
1054             if (opts.has(libs))
1055                 options.libs = opts.valuesOf(libs);
1056             if (opts.has(modulePath)) {
1057                 Path[] dirs = opts.valuesOf(modulePath).toArray(new Path[0]);
1058                 options.moduleFinder = ModuleFinder.of(dirs);
1059                 if (options.moduleFinder instanceof ConfigurableModuleFinder)
1060                     ((ConfigurableModuleFinder)options.moduleFinder).configurePhase(Phase.LINK_TIME);
1061             }
1062             if (opts.has(moduleVersion))
1063                 options.moduleVersion = opts.valueOf(moduleVersion);
1064             if (opts.has(mainClass))
1065                 options.mainClass = opts.valueOf(mainClass);
1066             if (opts.has(osName))
1067                 options.osName = opts.valueOf(osName);
1068             if (opts.has(osArch))
1069                 options.osArch = opts.valueOf(osArch);
1070             if (opts.has(osVersion))
1071                 options.osVersion = opts.valueOf(osVersion);
1072             if (opts.has(hashDependencies)) {
1073                 options.dependenciesToHash = opts.valueOf(hashDependencies);
1074                 // if storing hashes of dependencies then the module path is required
1075                 if (options.moduleFinder == null)
1076                     throw new CommandException("err.modulepath.must.be.specified").showUsage(true);
1077             }
1078 
1079             if (words.size() <= 1)
1080                 throw new CommandException("err.jmod.must.be.specified").showUsage(true);
1081             Path path = Paths.get(words.get(1));
1082             if (options.mode.equals(Mode.CREATE) && Files.exists(path))
1083                 throw new CommandException("err.file.already.exists", path);
1084             else if ((options.mode.equals(Mode.LIST) ||
1085                           options.mode.equals(Mode.DESCRIBE))
1086                       && Files.notExists(path))
1087                 throw new CommandException("err.jmod.not.found", path);
1088             options.jmodFile = path;
1089 
1090             if (words.size() > 2)
1091                 throw new CommandException("err.unknown.option",
1092                         words.subList(2, words.size())).showUsage(true);
1093 
1094             if (options.mode.equals(Mode.CREATE) && options.classpath == null)
1095                 throw new CommandException("err.classpath.must.be.specified").showUsage(true);
1096             if (options.mainClass != null && !isValidJavaIdentifier(options.mainClass))
1097                 throw new CommandException("err.invalid.main-class", options.mainClass);
1098         } catch (OptionException e) {
1099              throw new CommandException(e.getMessage());
1100         }
1101     }
1102 
1103     /**
1104      * Returns true if, and only if, the given main class is a legal.
1105      */
1106     static boolean isValidJavaIdentifier(String mainClass) {
1107         if (mainClass.length() == 0)
1108             return false;
1109 
1110         if (!Character.isJavaIdentifierStart(mainClass.charAt(0)))
1111             return false;
1112 
1113         int n = mainClass.length();
1114         for (int i=1; i < n; i++) {
1115             char c = mainClass.charAt(i);
1116             if (!Character.isJavaIdentifierPart(c) && c != '.')
1117                 return false;
1118         }
1119         if (mainClass.charAt(n-1) == '.')
1120             return false;
1121 
1122         return true;
1123     }
1124 
1125     private void reportError(String message) {
1126         out.println(getMessage("error.prefix") + " " + message);
1127     }
1128 
1129     private void warning(String key, Object... args) {
1130         out.println(getMessage("warn.prefix") + " " + getMessage(key, args));
1131     }
1132 
1133     private void showUsageSummary() {
1134         out.println(getMessage("main.usage.summary", PROGNAME));
1135     }
1136 
1137     private void showHelp() {
1138         out.println(getMessage("main.usage", PROGNAME));
1139         try {
1140             parser.printHelpOn(out);
1141         } catch (IOException x) {
1142             throw new AssertionError(x);
1143         }
1144     }
1145 
1146     private void showVersion() {
1147         out.println(version());
1148     }
1149 
1150     private String version() {
1151         return System.getProperty("java.version");
1152     }
1153 
1154     private static String getMessage(String key, Object... args) {
1155         try {
1156             return MessageFormat.format(ResourceBundleHelper.bundle.getString(key), args);
1157         } catch (MissingResourceException e) {
1158             throw new InternalError("Missing message: " + key);
1159         }
1160     }
1161 
1162     private static class ResourceBundleHelper {
1163         static final ResourceBundle bundle;
1164 
1165         static {
1166             Locale locale = Locale.getDefault();
1167             try {
1168                 bundle = ResourceBundle.getBundle("jdk.tools.jmod.resources.jmod", locale);
1169             } catch (MissingResourceException e) {
1170                 throw new InternalError("Cannot find jmod resource bundle for locale " + locale);
1171             }
1172         }
1173     }
1174 }