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