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