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