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