1 /*
2  * Copyright (c) 2012, 2020, 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 com.sun.tools.jdeps;
27 
28 import com.sun.tools.jdeps.Analyzer.Type;
29 import static com.sun.tools.jdeps.Analyzer.Type.*;
30 import static com.sun.tools.jdeps.JdepsWriter.*;
31 import static java.util.stream.Collectors.*;
32 
33 import java.io.IOException;
34 import java.io.PrintWriter;
35 import java.lang.module.ResolutionException;
36 import java.nio.file.Files;
37 import java.nio.file.Path;
38 import java.nio.file.Paths;
39 import java.text.MessageFormat;
40 import java.util.*;
41 import java.util.jar.JarFile;
42 import java.util.regex.Pattern;
43 
44 /**
45  * Implementation for the jdeps tool for static class dependency analysis.
46  */
47 class JdepsTask {
48     static interface BadArguments {
getKey()49         String getKey();
getArgs()50         Object[] getArgs();
showUsage()51         boolean showUsage();
52     }
53     static class BadArgs extends Exception implements BadArguments {
54         static final long serialVersionUID = 8765093759964640721L;
BadArgs(String key, Object... args)55         BadArgs(String key, Object... args) {
56             super(JdepsTask.getMessage(key, args));
57             this.key = key;
58             this.args = args;
59         }
60 
showUsage(boolean b)61         BadArgs showUsage(boolean b) {
62             showUsage = b;
63             return this;
64         }
65         final String key;
66         final Object[] args;
67         boolean showUsage;
68 
69         @Override
getKey()70         public String getKey() {
71             return key;
72         }
73 
74         @Override
getArgs()75         public Object[] getArgs() {
76             return args;
77         }
78 
79         @Override
showUsage()80         public boolean showUsage() {
81             return showUsage;
82         }
83     }
84 
85     static class UncheckedBadArgs extends RuntimeException implements BadArguments {
86         static final long serialVersionUID = -1L;
87         final BadArgs cause;
UncheckedBadArgs(BadArgs cause)88         UncheckedBadArgs(BadArgs cause) {
89             super(cause);
90             this.cause = cause;
91         }
92         @Override
getKey()93         public String getKey() {
94             return cause.key;
95         }
96 
97         @Override
getArgs()98         public Object[] getArgs() {
99             return cause.args;
100         }
101 
102         @Override
showUsage()103         public boolean showUsage() {
104             return cause.showUsage;
105         }
106     }
107 
108     static abstract class Option {
Option(boolean hasArg, String... aliases)109         Option(boolean hasArg, String... aliases) {
110             this.hasArg = hasArg;
111             this.aliases = aliases;
112         }
113 
Option(boolean hasArg, CommandOption cmd)114         Option(boolean hasArg, CommandOption cmd) {
115             this(hasArg, cmd.names());
116         }
117 
isHidden()118         boolean isHidden() {
119             return false;
120         }
121 
matches(String opt)122         boolean matches(String opt) {
123             for (String a : aliases) {
124                 if (a.equals(opt))
125                     return true;
126                 if (hasArg && opt.startsWith(a + "="))
127                     return true;
128             }
129             return false;
130         }
131 
ignoreRest()132         boolean ignoreRest() {
133             return false;
134         }
135 
process(JdepsTask task, String opt, String arg)136         abstract void process(JdepsTask task, String opt, String arg) throws BadArgs;
137         final boolean hasArg;
138         final String[] aliases;
139     }
140 
141     static abstract class HiddenOption extends Option {
HiddenOption(boolean hasArg, String... aliases)142         HiddenOption(boolean hasArg, String... aliases) {
143             super(hasArg, aliases);
144         }
145 
isHidden()146         boolean isHidden() {
147             return true;
148         }
149     }
150 
151     enum CommandOption {
152         ANALYZE_DEPS(""),
153         GENERATE_DOT_FILE("-dotoutput", "--dot-output"),
154         GENERATE_MODULE_INFO("--generate-module-info"),
155         GENERATE_OPEN_MODULE("--generate-open-module"),
156         LIST_DEPS("--list-deps"),
157         LIST_REDUCED_DEPS("--list-reduced-deps"),
158         PRINT_MODULE_DEPS("--print-module-deps"),
159         CHECK_MODULES("--check");
160 
161         private final String[] names;
CommandOption(String... names)162         CommandOption(String... names) {
163             this.names = names;
164         }
165 
names()166         String[] names() {
167             return names;
168         }
169 
170         @Override
toString()171         public String toString() {
172             return names[0];
173         }
174     }
175 
176     static Option[] recognizedOptions = {
177         new Option(false, "-h", "-?", "-help", "--help") {
178             void process(JdepsTask task, String opt, String arg) {
179                 task.options.help = true;
180             }
181         },
182         new Option(true, CommandOption.GENERATE_DOT_FILE) {
183             void process(JdepsTask task, String opt, String arg) throws BadArgs {
184                 if (task.command != null) {
185                     throw new BadArgs("err.command.set", task.command, opt);
186                 }
187                 task.command = task.genDotFile(Paths.get(arg));
188             }
189         },
190         new Option(false, "-s", "-summary") {
191             void process(JdepsTask task, String opt, String arg) {
192                 task.options.showSummary = true;
193             }
194         },
195         new Option(false, "-v", "-verbose",
196                                 "-verbose:module",
197                                 "-verbose:package",
198                                 "-verbose:class") {
199             void process(JdepsTask task, String opt, String arg) throws BadArgs {
200                 switch (opt) {
201                     case "-v":
202                     case "-verbose":
203                         task.options.verbose = VERBOSE;
204                         task.options.filterSameArchive = false;
205                         task.options.filterSamePackage = false;
206                         break;
207                     case "-verbose:module":
208                         task.options.verbose = MODULE;
209                         break;
210                     case "-verbose:package":
211                         task.options.verbose = PACKAGE;
212                         break;
213                     case "-verbose:class":
214                         task.options.verbose = CLASS;
215                         break;
216                     default:
217                         throw new BadArgs("err.invalid.arg.for.option", opt);
218                 }
219             }
220         },
221         new Option(false, "-apionly", "--api-only") {
222             void process(JdepsTask task, String opt, String arg) {
223                 task.options.apiOnly = true;
224             }
225         },
226 
227         new Option(false, "-jdkinternals", "--jdk-internals") {
228             void process(JdepsTask task, String opt, String arg) {
229                 task.options.findJDKInternals = true;
230                 if (task.options.includePattern == null) {
231                     task.options.includePattern = Pattern.compile(".*");
232                 }
233             }
234         },
235 
236         // ---- paths option ----
237         new Option(true, "-cp", "-classpath", "--class-path") {
238             void process(JdepsTask task, String opt, String arg) {
239                 task.options.classpath = arg;
240             }
241         },
242         new Option(true, "--module-path") {
243             void process(JdepsTask task, String opt, String arg) throws BadArgs {
244                 task.options.modulePath = arg;
245             }
246         },
247         new Option(true, "--upgrade-module-path") {
248             void process(JdepsTask task, String opt, String arg) throws BadArgs {
249                 task.options.upgradeModulePath = arg;
250             }
251         },
252         new Option(true, "--system") {
253             void process(JdepsTask task, String opt, String arg) throws BadArgs {
254                 if (arg.equals("none")) {
255                     task.options.systemModulePath = null;
256                 } else {
257                     Path path = Paths.get(arg);
258                     if (Files.isRegularFile(path.resolve("lib").resolve("modules")))
259                         task.options.systemModulePath = arg;
260                     else
261                         throw new BadArgs("err.invalid.path", arg);
262                 }
263             }
264         },
265         new Option(true, "--add-modules") {
266             void process(JdepsTask task, String opt, String arg) throws BadArgs {
267                 Set<String> mods = Set.of(arg.split(","));
268                 task.options.addmods.addAll(mods);
269             }
270         },
271         new Option(true, "--multi-release") {
272             void process(JdepsTask task, String opt, String arg) throws BadArgs {
273                 if (arg.equalsIgnoreCase("base")) {
274                     task.options.multiRelease = JarFile.baseVersion();
275                 } else {
276                     try {
277                         int v = Integer.parseInt(arg);
278                         if (v < 9) {
279                             throw new BadArgs("err.invalid.arg.for.option", arg);
280                         }
281                     } catch (NumberFormatException x) {
282                         throw new BadArgs("err.invalid.arg.for.option", arg);
283                     }
284                     task.options.multiRelease = Runtime.Version.parse(arg);
285                 }
286             }
287         },
288         new Option(false, "-q", "-quiet") {
289             void process(JdepsTask task, String opt, String arg) {
290                 task.options.nowarning = true;
291             }
292         },
293         new Option(false, "-version", "--version") {
294             void process(JdepsTask task, String opt, String arg) {
295                 task.options.version = true;
296             }
297         },
298 
299         // ---- module-specific options ----
300 
301         new Option(true, "-m", "--module") {
302             void process(JdepsTask task, String opt, String arg) throws BadArgs {
303                 if (!task.options.rootModules.isEmpty()) {
304                     throw new BadArgs("err.option.already.specified", opt);
305                 }
306                 task.options.rootModules.add(arg);
307                 task.options.addmods.add(arg);
308             }
309         },
310         new Option(true, CommandOption.GENERATE_MODULE_INFO) {
311             void process(JdepsTask task, String opt, String arg) throws BadArgs {
312                 if (task.command != null) {
313                     throw new BadArgs("err.command.set", task.command, opt);
314                 }
315                 task.command = task.genModuleInfo(Paths.get(arg), false);
316             }
317         },
318         new Option(true, CommandOption.GENERATE_OPEN_MODULE) {
319             void process(JdepsTask task, String opt, String arg) throws BadArgs {
320                 if (task.command != null) {
321                     throw new BadArgs("err.command.set", task.command, opt);
322                 }
323                 task.command = task.genModuleInfo(Paths.get(arg), true);
324             }
325         },
326         new Option(true, CommandOption.CHECK_MODULES) {
327             void process(JdepsTask task, String opt, String arg) throws BadArgs {
328                 if (task.command != null) {
329                     throw new BadArgs("err.command.set", task.command, opt);
330                 }
331                 Set<String> mods =  Set.of(arg.split(","));
332                 task.options.addmods.addAll(mods);
333                 task.command = task.checkModuleDeps(mods);
334             }
335         },
336         new Option(false, CommandOption.LIST_DEPS) {
337             void process(JdepsTask task, String opt, String arg) throws BadArgs {
338                 if (task.command != null) {
339                     throw new BadArgs("err.command.set", task.command, opt);
340                 }
341                 task.command = task.listModuleDeps(CommandOption.LIST_DEPS);
342             }
343         },
344         new Option(false, CommandOption.LIST_REDUCED_DEPS) {
345             void process(JdepsTask task, String opt, String arg) throws BadArgs {
346                 if (task.command != null) {
347                     throw new BadArgs("err.command.set", task.command, opt);
348                 }
349                 task.command = task.listModuleDeps(CommandOption.LIST_REDUCED_DEPS);
350             }
351         },
352         new Option(false, CommandOption.PRINT_MODULE_DEPS) {
353             void process(JdepsTask task, String opt, String arg) throws BadArgs {
354                 if (task.command != null) {
355                     throw new BadArgs("err.command.set", task.command, opt);
356                 }
357                 task.command = task.listModuleDeps(CommandOption.PRINT_MODULE_DEPS);
358             }
359         },
360         new Option(false, "--ignore-missing-deps") {
361             void process(JdepsTask task, String opt, String arg) {
362                 task.options.ignoreMissingDeps = true;
363             }
364         },
365 
366         // ---- Target filtering options ----
367         new Option(true, "-p", "-package", "--package") {
368             void process(JdepsTask task, String opt, String arg) {
369                 task.options.packageNames.add(arg);
370             }
371         },
372         new Option(true, "-e", "-regex", "--regex") {
373             void process(JdepsTask task, String opt, String arg) {
374                 task.options.regex = Pattern.compile(arg);
375             }
376         },
377         new Option(true, "--require") {
378             void process(JdepsTask task, String opt, String arg) {
379                 task.options.requires.add(arg);
380                 task.options.addmods.add(arg);
381             }
382         },
383         new Option(true, "-f", "-filter") {
384             void process(JdepsTask task, String opt, String arg) {
385                 task.options.filterRegex = Pattern.compile(arg);
386             }
387         },
388         new Option(false, "-filter:package",
389                           "-filter:archive", "-filter:module",
390                           "-filter:none") {
391             void process(JdepsTask task, String opt, String arg) {
392                 switch (opt) {
393                     case "-filter:package":
394                         task.options.filterSamePackage = true;
395                         task.options.filterSameArchive = false;
396                         break;
397                     case "-filter:archive":
398                     case "-filter:module":
399                         task.options.filterSameArchive = true;
400                         task.options.filterSamePackage = false;
401                         break;
402                     case "-filter:none":
403                         task.options.filterSameArchive = false;
404                         task.options.filterSamePackage = false;
405                         break;
406                 }
407             }
408         },
409         new Option(false, "--missing-deps") {
410             void process(JdepsTask task, String opt, String arg) {
411                 task.options.findMissingDeps = true;
412             }
413         },
414 
415         // ---- Source filtering options ----
416         new Option(true, "-include") {
417             void process(JdepsTask task, String opt, String arg) throws BadArgs {
418                 task.options.includePattern = Pattern.compile(arg);
419             }
420         },
421 
422         new Option(false, "-P", "-profile") {
423             void process(JdepsTask task, String opt, String arg) throws BadArgs {
424                 task.options.showProfile = true;
425             }
426         },
427 
428         new Option(false, "-R", "-recursive", "--recursive") {
429             void process(JdepsTask task, String opt, String arg) throws BadArgs {
430                 task.options.recursive = Options.RECURSIVE;
431                 // turn off filtering
432                 task.options.filterSameArchive = false;
433                 task.options.filterSamePackage = false;
434             }
435         },
436         new Option(false, "--no-recursive") {
437             void process(JdepsTask task, String opt, String arg) throws BadArgs {
438                 task.options.recursive = Options.NO_RECURSIVE;
439             }
440         },
441         new Option(false, "-I", "--inverse") {
442             void process(JdepsTask task, String opt, String arg) {
443                 task.options.inverse = true;
444                 // equivalent to the inverse of compile-time view analysis
445                 task.options.compileTimeView = true;
446                 task.options.filterSamePackage = true;
447                 task.options.filterSameArchive = true;
448             }
449         },
450 
451         new Option(false, "--compile-time") {
452             void process(JdepsTask task, String opt, String arg) {
453                 task.options.compileTimeView = true;
454                 task.options.recursive = Options.RECURSIVE;
455                 task.options.filterSamePackage = true;
456                 task.options.filterSameArchive = true;
457             }
458         },
459 
460         new HiddenOption(false, "-fullversion") {
461             void process(JdepsTask task, String opt, String arg) {
462                 task.options.fullVersion = true;
463             }
464         },
465         new HiddenOption(false, "-showlabel") {
466             void process(JdepsTask task, String opt, String arg) {
467                 task.options.showLabel = true;
468             }
469         },
470         new HiddenOption(false, "--hide-show-module") {
471             void process(JdepsTask task, String opt, String arg) {
472                 task.options.showModule = false;
473             }
474         },
475         new HiddenOption(true, "-depth") {
476             void process(JdepsTask task, String opt, String arg) throws BadArgs {
477                 try {
478                     task.options.depth = Integer.parseInt(arg);
479                 } catch (NumberFormatException e) {
480                     throw new BadArgs("err.invalid.arg.for.option", opt);
481                 }
482             }
483         },
484     };
485 
486     private static final String PROGNAME = "jdeps";
487     private final Options options = new Options();
488     private final List<String> inputArgs = new ArrayList<>();
489 
490     private Command command;
491     private PrintWriter log;
setLog(PrintWriter out)492     void setLog(PrintWriter out) {
493         log = out;
494     }
495 
496     /**
497      * Result codes.
498      */
499     static final int EXIT_OK = 0,       // Completed with no errors.
500                      EXIT_ERROR = 1,    // Completed but reported errors.
501                      EXIT_CMDERR = 2,   // Bad command-line arguments
502                      EXIT_SYSERR = 3,   // System error or resource exhaustion.
503                      EXIT_ABNORMAL = 4; // terminated abnormally
504 
run(String... args)505     int run(String... args) {
506         if (log == null) {
507             log = new PrintWriter(System.out);
508         }
509         try {
510             handleOptions(args);
511             if (options.help) {
512                 showHelp();
513             }
514             if (options.version || options.fullVersion) {
515                 showVersion(options.fullVersion);
516             }
517             if (options.help || options.version || options.fullVersion) {
518                 return EXIT_OK;
519             }
520             if (options.numFilters() > 1) {
521                 reportError("err.invalid.filters");
522                 return EXIT_CMDERR;
523             }
524 
525             // default command to analyze dependences
526             if (command == null) {
527                 command = analyzeDeps();
528             }
529             if (!command.checkOptions()) {
530                 return EXIT_CMDERR;
531             }
532 
533             boolean ok = run();
534             return ok ? EXIT_OK : EXIT_ERROR;
535 
536         } catch (BadArgs|UncheckedBadArgs e) {
537             reportError(e.getKey(), e.getArgs());
538             if (e.showUsage()) {
539                 log.println(getMessage("main.usage.summary", PROGNAME));
540             }
541             return EXIT_CMDERR;
542         } catch (ResolutionException e) {
543             reportError("err.exception.message", e.getMessage());
544             return EXIT_CMDERR;
545         } catch (IOException e) {
546             e.printStackTrace();
547             return EXIT_CMDERR;
548         } catch (MultiReleaseException e) {
549             reportError(e.getKey(), e.getParams());
550             return EXIT_CMDERR;  // could be EXIT_ABNORMAL sometimes
551         } finally {
552             log.flush();
553         }
554     }
555 
run()556     boolean run() throws IOException {
557         try (JdepsConfiguration config = buildConfig()) {
558             if (!options.nowarning) {
559                 // detect split packages
560                 config.splitPackages().entrySet()
561                       .stream()
562                       .sorted(Map.Entry.comparingByKey())
563                       .forEach(e -> warning("warn.split.package",
564                                             e.getKey(),
565                                             e.getValue().stream().collect(joining(" "))));
566             }
567 
568             // check if any module specified in --add-modules, --require, and -m is missing
569             options.addmods.stream()
570                 .filter(mn -> !JdepsConfiguration.isToken(mn))
571                 .forEach(mn -> config.findModule(mn).orElseThrow(() ->
572                     new UncheckedBadArgs(new BadArgs("err.module.not.found", mn))));
573 
574             return command.run(config);
575         }
576     }
577 
buildConfig()578     private JdepsConfiguration buildConfig() throws IOException {
579         JdepsConfiguration.Builder builder =
580             new JdepsConfiguration.Builder(options.systemModulePath);
581 
582         builder.upgradeModulePath(options.upgradeModulePath)
583                .appModulePath(options.modulePath)
584                .addmods(options.addmods)
585                .addmods(command.addModules());
586 
587         if (options.classpath != null)
588             builder.addClassPath(options.classpath);
589 
590         if (options.multiRelease != null)
591             builder.multiRelease(options.multiRelease);
592 
593         // build the root set of archives to be analyzed
594         for (String s : inputArgs) {
595             Path p = Paths.get(s);
596             if (Files.exists(p)) {
597                 builder.addRoot(p);
598             } else {
599                 warning("warn.invalid.arg", s);
600             }
601         }
602 
603         return builder.build();
604     }
605 
606     // ---- factory methods to create a Command
607 
analyzeDeps()608     private AnalyzeDeps analyzeDeps() throws BadArgs {
609         return options.inverse ? new InverseAnalyzeDeps()
610                                : new AnalyzeDeps();
611     }
612 
genDotFile(Path dir)613     private GenDotFile genDotFile(Path dir) throws BadArgs {
614         if (Files.exists(dir) && (!Files.isDirectory(dir) || !Files.isWritable(dir))) {
615             throw new BadArgs("err.invalid.path", dir.toString());
616         }
617         return new GenDotFile(dir);
618     }
619 
genModuleInfo(Path dir, boolean openModule)620     private GenModuleInfo genModuleInfo(Path dir, boolean openModule) throws BadArgs {
621         if (Files.exists(dir) && (!Files.isDirectory(dir) || !Files.isWritable(dir))) {
622             throw new BadArgs("err.invalid.path", dir.toString());
623         }
624         return new GenModuleInfo(dir, openModule);
625     }
626 
listModuleDeps(CommandOption option)627     private ListModuleDeps listModuleDeps(CommandOption option) throws BadArgs {
628         // do transitive dependence analysis unless --no-recursive is set
629         if (options.recursive != Options.NO_RECURSIVE) {
630             options.recursive = Options.RECURSIVE;
631         }
632         // no need to record the dependences on the same archive or same package
633         options.filterSameArchive = true;
634         options.filterSamePackage = true;
635         switch (option) {
636             case LIST_DEPS:
637                 return new ListModuleDeps(option, true, false);
638             case LIST_REDUCED_DEPS:
639                 return new ListModuleDeps(option, true, true);
640             case PRINT_MODULE_DEPS:
641                 return new ListModuleDeps(option, false, true, ",");
642             default:
643                 throw new IllegalArgumentException(option.toString());
644         }
645     }
646 
checkModuleDeps(Set<String> mods)647     private CheckModuleDeps checkModuleDeps(Set<String> mods) throws BadArgs {
648         return new CheckModuleDeps(mods);
649     }
650 
651     abstract class Command {
652         final CommandOption option;
Command(CommandOption option)653         protected Command(CommandOption option) {
654             this.option = option;
655         }
656 
657         /**
658          * Returns true if the command-line options are all valid;
659          * otherwise, returns false.
660          */
checkOptions()661         abstract boolean checkOptions();
662 
663         /**
664          * Do analysis
665          */
run(JdepsConfiguration config)666         abstract boolean run(JdepsConfiguration config) throws IOException;
667 
668         /**
669          * Includes all modules on system module path and application module path
670          *
671          * When a named module is analyzed, it will analyze the dependences
672          * only.  The method should be overridden when this command should
673          * analyze all modules instead.
674          */
addModules()675         Set<String> addModules() {
676             return Set.of();
677         }
678 
679         @Override
toString()680         public String toString() {
681             return option.toString();
682         }
683     }
684 
685 
686     /**
687      * Analyze dependences
688      */
689     class AnalyzeDeps extends Command {
690         JdepsWriter writer;
AnalyzeDeps()691         AnalyzeDeps() {
692             this(CommandOption.ANALYZE_DEPS);
693         }
694 
AnalyzeDeps(CommandOption option)695         AnalyzeDeps(CommandOption option) {
696             super(option);
697         }
698 
699         @Override
checkOptions()700         boolean checkOptions() {
701             if (options.findJDKInternals || options.findMissingDeps) {
702                 // cannot set any filter, -verbose and -summary option
703                 if (options.showSummary || options.verbose != null) {
704                     reportError("err.invalid.options", "-summary or -verbose",
705                         options.findJDKInternals ? "-jdkinternals" : "--missing-deps");
706                     return false;
707                 }
708                 if (options.hasFilter()) {
709                     reportError("err.invalid.options", "--package, --regex, --require",
710                         options.findJDKInternals ? "-jdkinternals" : "--missing-deps");
711                     return false;
712                 }
713             }
714             if (options.showSummary) {
715                 // -summary cannot use with -verbose option
716                 if (options.verbose != null) {
717                     reportError("err.invalid.options", "-v, -verbose", "-s, -summary");
718                     return false;
719                 }
720             }
721 
722             if (!inputArgs.isEmpty() && !options.rootModules.isEmpty()) {
723                 reportError("err.invalid.arg.for.option", "-m");
724             }
725             if (inputArgs.isEmpty() && !options.hasSourcePath()) {
726                 showHelp();
727                 return false;
728             }
729             return true;
730         }
731 
732         /*
733          * Default is to show package-level dependencies
734          */
getAnalyzerType()735         Type getAnalyzerType() {
736             if (options.showSummary)
737                 return Type.SUMMARY;
738 
739             if (options.findJDKInternals || options.findMissingDeps)
740                 return Type.CLASS;
741 
742             // default to package-level verbose
743            return options.verbose != null ? options.verbose : PACKAGE;
744         }
745 
746         @Override
run(JdepsConfiguration config)747         boolean run(JdepsConfiguration config) throws IOException {
748             Type type = getAnalyzerType();
749             // default to package-level verbose
750             JdepsWriter writer = new SimpleWriter(log,
751                                                   type,
752                                                   options.showProfile,
753                                                   options.showModule);
754 
755             return run(config, writer, type);
756         }
757 
run(JdepsConfiguration config, JdepsWriter writer, Type type)758         boolean run(JdepsConfiguration config, JdepsWriter writer, Type type)
759             throws IOException
760         {
761             // analyze the dependencies
762             DepsAnalyzer analyzer = new DepsAnalyzer(config,
763                                                      dependencyFilter(config),
764                                                      writer,
765                                                      type,
766                                                      options.apiOnly);
767 
768             boolean ok = analyzer.run(options.compileTimeView, options.depth());
769 
770             // print skipped entries, if any
771             if (!options.nowarning) {
772                 analyzer.archives()
773                     .forEach(archive -> archive.reader()
774                         .skippedEntries().stream()
775                         .forEach(name -> warning("warn.skipped.entry", name)));
776             }
777 
778             if (options.findJDKInternals && !options.nowarning) {
779                 Map<String, String> jdkInternals = new TreeMap<>();
780                 Set<String> deps = analyzer.dependences();
781                 // find the ones with replacement
782                 deps.forEach(cn -> replacementFor(cn).ifPresent(
783                     repl -> jdkInternals.put(cn, repl))
784                 );
785 
786                 if (!deps.isEmpty()) {
787                     log.println();
788                     warning("warn.replace.useJDKInternals", getMessage("jdeps.wiki.url"));
789                 }
790 
791                 if (!jdkInternals.isEmpty()) {
792                     log.println();
793                     String internalApiTitle = getMessage("internal.api.column.header");
794                     String replacementApiTitle = getMessage("public.api.replacement.column.header");
795                     log.format("%-40s %s%n", internalApiTitle, replacementApiTitle);
796                     log.format("%-40s %s%n",
797                                internalApiTitle.replaceAll(".", "-"),
798                                replacementApiTitle.replaceAll(".", "-"));
799                     jdkInternals.entrySet().stream()
800                         .forEach(e -> {
801                             String key = e.getKey();
802                             String[] lines = e.getValue().split("\\n");
803                             for (String s : lines) {
804                                 log.format("%-40s %s%n", key, s);
805                                 key = "";
806                             }
807                         });
808                 }
809             }
810             return ok;
811         }
812     }
813 
814 
815     class InverseAnalyzeDeps extends AnalyzeDeps {
InverseAnalyzeDeps()816         InverseAnalyzeDeps() {
817         }
818 
819         @Override
checkOptions()820         boolean checkOptions() {
821             if (options.recursive != -1 || options.depth != -1) {
822                 reportError("err.invalid.options", "--recursive and --no-recursive", "--inverse");
823                 return false;
824             }
825 
826             if (options.numFilters() == 0) {
827                 reportError("err.filter.not.specified");
828                 return false;
829             }
830 
831             if (!super.checkOptions()) {
832                 return false;
833             }
834 
835             return true;
836         }
837 
838         @Override
run(JdepsConfiguration config)839         boolean run(JdepsConfiguration config) throws IOException {
840             Type type = getAnalyzerType();
841 
842             InverseDepsAnalyzer analyzer =
843                 new InverseDepsAnalyzer(config,
844                                         dependencyFilter(config),
845                                         writer,
846                                         type,
847                                         options.apiOnly);
848             boolean ok = analyzer.run();
849 
850             log.println();
851             if (!options.requires.isEmpty())
852                 log.println(getMessage("inverse.transitive.dependencies.on",
853                                        options.requires));
854             else
855                 log.println(getMessage("inverse.transitive.dependencies.matching",
856                                        options.regex != null
857                                            ? options.regex.toString()
858                                            : "packages " + options.packageNames));
859 
860             analyzer.inverseDependences()
861                     .stream()
862                     .sorted(comparator())
863                     .map(this::toInversePath)
864                     .forEach(log::println);
865             return ok;
866         }
867 
toInversePath(Deque<Archive> path)868         private String toInversePath(Deque<Archive> path) {
869             return path.stream()
870                        .map(Archive::getName)
871                        .collect(joining(" <- "));
872         }
873 
874         /*
875          * Returns a comparator for sorting the inversed path, grouped by
876          * the first module name, then the shortest path and then sort by
877          * the module names of each path
878          */
comparator()879         private Comparator<Deque<Archive>> comparator() {
880             return Comparator.<Deque<Archive>, String>
881                 comparing(deque -> deque.peekFirst().getName())
882                     .thenComparingInt(Deque::size)
883                     .thenComparing(this::toInversePath);
884         }
885 
886         /*
887          * Returns true if --require is specified so that all modules are
888          * analyzed to find all modules that depend on the modules specified in the
889          * --require option directly and indirectly
890          */
addModules()891         Set<String> addModules() {
892             return options.requires.size() > 0 ? Set.of("ALL-SYSTEM") : Set.of();
893         }
894     }
895 
896 
897     class GenModuleInfo extends Command {
898         final Path dir;
899         final boolean openModule;
GenModuleInfo(Path dir, boolean openModule)900         GenModuleInfo(Path dir, boolean openModule) {
901             super(CommandOption.GENERATE_MODULE_INFO);
902             this.dir = dir;
903             this.openModule = openModule;
904         }
905 
906         @Override
checkOptions()907         boolean checkOptions() {
908             if (options.classpath != null) {
909                 reportError("err.invalid.options", "-classpath",
910                             option);
911                 return false;
912             }
913             if (options.hasFilter()) {
914                 reportError("err.invalid.options", "--package, --regex, --require",
915                             option);
916                 return false;
917             }
918             if (!options.rootModules.isEmpty()) {
919                 reportError("err.invalid.options", "-m or --module",
920                             option);
921                 return false;
922             }
923             return true;
924         }
925 
926         @Override
run(JdepsConfiguration config)927         boolean run(JdepsConfiguration config) throws IOException {
928             // check if any JAR file contains unnamed package
929             for (String arg : inputArgs) {
930                 try (ClassFileReader reader = ClassFileReader.newInstance(Paths.get(arg), config.getVersion())) {
931                     Optional<String> classInUnnamedPackage =
932                         reader.entries().stream()
933                               .filter(n -> n.endsWith(".class"))
934                               .filter(cn -> toPackageName(cn).isEmpty())
935                               .findFirst();
936 
937                     if (classInUnnamedPackage.isPresent()) {
938                         if (classInUnnamedPackage.get().equals("module-info.class")) {
939                             reportError("err.genmoduleinfo.not.jarfile", arg);
940                         } else {
941                             reportError("err.genmoduleinfo.unnamed.package", arg);
942                         }
943                         return false;
944                     }
945                 }
946             }
947 
948             ModuleInfoBuilder builder
949                  = new ModuleInfoBuilder(config, inputArgs, dir, openModule);
950             boolean ok = builder.run(options.ignoreMissingDeps, log, options.nowarning);
951             if (!ok) {
952                 reportError("err.missing.dependences");
953                 log.println();
954                 builder.visitMissingDeps(new SimpleDepVisitor());
955             }
956             return ok;
957         }
958 
toPackageName(String name)959         private String toPackageName(String name) {
960             int i = name.lastIndexOf('/');
961             return i > 0 ? name.replace('/', '.').substring(0, i) : "";
962         }
963     }
964 
965     class CheckModuleDeps extends Command {
966         final Set<String> modules;
CheckModuleDeps(Set<String> mods)967         CheckModuleDeps(Set<String> mods) {
968             super(CommandOption.CHECK_MODULES);
969             this.modules = mods;
970         }
971 
972         @Override
checkOptions()973         boolean checkOptions() {
974             if (!inputArgs.isEmpty()) {
975                 reportError("err.invalid.options", inputArgs, "--check");
976                 return false;
977             }
978             return true;
979         }
980 
981         @Override
run(JdepsConfiguration config)982         boolean run(JdepsConfiguration config) throws IOException {
983             if (!config.initialArchives().isEmpty()) {
984                 String list = config.initialArchives().stream()
985                                     .map(Archive::getPathName).collect(joining(" "));
986                 throw new UncheckedBadArgs(new BadArgs("err.invalid.options",
987                                                        list, "--check"));
988             }
989             return new ModuleAnalyzer(config, log, modules).run(options.ignoreMissingDeps);
990         }
991 
992         /*
993          * Returns true to analyze all modules
994          */
addModules()995         Set<String> addModules() {
996             return Set.of("ALL-SYSTEM", "ALL-MODULE-PATH");
997         }
998     }
999 
1000     class ListModuleDeps extends Command {
1001         final boolean jdkinternals;
1002         final boolean reduced;
1003         final String separator;
ListModuleDeps(CommandOption option, boolean jdkinternals, boolean reduced)1004         ListModuleDeps(CommandOption option, boolean jdkinternals, boolean reduced) {
1005             this(option, jdkinternals, reduced, System.getProperty("line.separator"));
1006         }
ListModuleDeps(CommandOption option, boolean jdkinternals, boolean reduced, String sep)1007         ListModuleDeps(CommandOption option, boolean jdkinternals, boolean reduced, String sep) {
1008             super(option);
1009             this.jdkinternals = jdkinternals;
1010             this.reduced = reduced;
1011             this.separator = sep;
1012         }
1013 
1014         @Override
checkOptions()1015         boolean checkOptions() {
1016             if (options.showSummary || options.verbose != null) {
1017                 reportError("err.invalid.options", "-summary or -verbose", option);
1018                 return false;
1019             }
1020             if (options.findJDKInternals) {
1021                 reportError("err.invalid.options", "-jdkinternals", option);
1022                 return false;
1023             }
1024             if (options.findMissingDeps) {
1025                 reportError("err.invalid.options", "--missing-deps", option);
1026                 return false;
1027             }
1028 
1029             if (!inputArgs.isEmpty() && !options.rootModules.isEmpty()) {
1030                 reportError("err.invalid.arg.for.option", "-m");
1031             }
1032             if (inputArgs.isEmpty() && !options.hasSourcePath()) {
1033                 showHelp();
1034                 return false;
1035             }
1036             return true;
1037         }
1038 
1039         @Override
run(JdepsConfiguration config)1040         boolean run(JdepsConfiguration config) throws IOException {
1041             ModuleExportsAnalyzer analyzer = new ModuleExportsAnalyzer(config,
1042                                                                        dependencyFilter(config),
1043                                                                        jdkinternals,
1044                                                                        reduced,
1045                                                                        log,
1046                                                                        separator);
1047             boolean ok = analyzer.run(options.depth(), options.ignoreMissingDeps);
1048             if (!ok) {
1049                 reportError("err.missing.dependences");
1050                 log.println();
1051                 analyzer.visitMissingDeps(new SimpleDepVisitor());
1052             }
1053             return ok;
1054         }
1055     }
1056 
1057     class GenDotFile extends AnalyzeDeps {
1058         final Path dotOutputDir;
GenDotFile(Path dotOutputDir)1059         GenDotFile(Path dotOutputDir) {
1060             super(CommandOption.GENERATE_DOT_FILE);
1061 
1062             this.dotOutputDir = dotOutputDir;
1063         }
1064 
1065         @Override
run(JdepsConfiguration config)1066         boolean run(JdepsConfiguration config) throws IOException {
1067             if ((options.showSummary || options.verbose == MODULE) &&
1068                 !options.addmods.isEmpty() && inputArgs.isEmpty()) {
1069                 // generate dot graph from the resolved graph from module
1070                 // resolution.  No class dependency analysis is performed.
1071                 return new ModuleDotGraph(config, options.apiOnly)
1072                         .genDotFiles(dotOutputDir);
1073             }
1074 
1075             Type type = getAnalyzerType();
1076             JdepsWriter writer = new DotFileWriter(dotOutputDir,
1077                                                    type,
1078                                                    options.showProfile,
1079                                                    options.showModule,
1080                                                    options.showLabel);
1081             return run(config, writer, type);
1082         }
1083     }
1084 
1085     class SimpleDepVisitor implements Analyzer.Visitor {
1086         private Archive source;
1087         @Override
visitDependence(String origin, Archive originArchive, String target, Archive targetArchive)1088         public void visitDependence(String origin, Archive originArchive, String target, Archive targetArchive) {
1089             if (source != originArchive) {
1090                 source = originArchive;
1091                 log.format("%s%n", originArchive);
1092             }
1093             log.format("   %-50s -> %-50s %s%n", origin, target, targetArchive.getName());
1094         }
1095     }
1096 
1097     /**
1098      * Returns a filter used during dependency analysis
1099      */
dependencyFilter(JdepsConfiguration config)1100     private JdepsFilter dependencyFilter(JdepsConfiguration config) {
1101         // Filter specified by -filter, -package, -regex, and --require options
1102         JdepsFilter.Builder builder = new JdepsFilter.Builder();
1103 
1104         // source filters
1105         builder.includePattern(options.includePattern);
1106 
1107         // target filters
1108         builder.filter(options.filterSamePackage, options.filterSameArchive);
1109         builder.findJDKInternals(options.findJDKInternals);
1110         builder.findMissingDeps(options.findMissingDeps);
1111 
1112         // --require
1113         if (!options.requires.isEmpty()) {
1114             options.requires.stream()
1115                 .forEach(mn -> {
1116                     Module m = config.findModule(mn).get();
1117                     builder.requires(mn, m.packages());
1118                 });
1119         }
1120         // -regex
1121         if (options.regex != null)
1122             builder.regex(options.regex);
1123         // -package
1124         if (!options.packageNames.isEmpty())
1125             builder.packages(options.packageNames);
1126         // -filter
1127         if (options.filterRegex != null)
1128             builder.filter(options.filterRegex);
1129 
1130         return builder.build();
1131     }
1132 
handleOptions(String[] args)1133     public void handleOptions(String[] args) throws BadArgs {
1134         // process options
1135         for (int i=0; i < args.length; i++) {
1136             if (args[i].charAt(0) == '-') {
1137                 String name = args[i];
1138                 Option option = getOption(name);
1139                 String param = null;
1140                 if (option.hasArg) {
1141                     if (name.startsWith("-") && name.indexOf('=') > 0) {
1142                         param = name.substring(name.indexOf('=') + 1, name.length());
1143                     } else if (i + 1 < args.length) {
1144                         param = args[++i];
1145                     }
1146                     if (param == null || param.isEmpty() || param.charAt(0) == '-') {
1147                         throw new BadArgs("err.missing.arg", name).showUsage(true);
1148                     }
1149                 }
1150                 option.process(this, name, param);
1151                 if (option.ignoreRest()) {
1152                     i = args.length;
1153                 }
1154             } else {
1155                 // process rest of the input arguments
1156                 for (; i < args.length; i++) {
1157                     String name = args[i];
1158                     if (name.charAt(0) == '-') {
1159                         throw new BadArgs("err.option.after.class", name).showUsage(true);
1160                     }
1161                     inputArgs.add(name);
1162                 }
1163             }
1164         }
1165     }
1166 
getOption(String name)1167     private Option getOption(String name) throws BadArgs {
1168         for (Option o : recognizedOptions) {
1169             if (o.matches(name)) {
1170                 return o;
1171             }
1172         }
1173         throw new BadArgs("err.unknown.option", name).showUsage(true);
1174     }
1175 
reportError(String key, Object... args)1176     private void reportError(String key, Object... args) {
1177         log.println(getMessage("error.prefix") + " " + getMessage(key, args));
1178     }
1179 
warning(String key, Object... args)1180     void warning(String key, Object... args) {
1181         log.println(getMessage("warn.prefix") + " " + getMessage(key, args));
1182     }
1183 
showHelp()1184     private void showHelp() {
1185         log.println(getMessage("main.usage", PROGNAME));
1186         for (Option o : recognizedOptions) {
1187             String name = o.aliases[0].substring(1); // there must always be at least one name
1188             name = name.charAt(0) == '-' ? name.substring(1) : name;
1189             if (o.isHidden() || name.startsWith("filter:")) {
1190                 continue;
1191             }
1192             log.println(getMessage("main.opt." + name));
1193         }
1194     }
1195 
showVersion(boolean full)1196     private void showVersion(boolean full) {
1197         log.println(version(full ? "full" : "release"));
1198     }
1199 
version(String key)1200     private String version(String key) {
1201         // key=version:  mm.nn.oo[-milestone]
1202         // key=full:     mm.mm.oo[-milestone]-build
1203         try {
1204             return ResourceBundleHelper.getVersion(key);
1205         } catch (MissingResourceException e) {
1206             return getMessage("version.unknown", System.getProperty("java.version"));
1207         }
1208     }
1209 
getMessage(String key, Object... args)1210     static String getMessage(String key, Object... args) {
1211         try {
1212             return MessageFormat.format(ResourceBundleHelper.getMessage(key), args);
1213         } catch (MissingResourceException e) {
1214             throw new InternalError("Missing message: " + key);
1215         }
1216     }
1217 
1218     private static class Options {
1219         static final int NO_RECURSIVE = 0;
1220         static final int RECURSIVE = 1;
1221         boolean help;
1222         boolean version;
1223         boolean fullVersion;
1224         boolean showProfile;
1225         boolean showModule = true;
1226         boolean showSummary;
1227         boolean apiOnly;
1228         boolean showLabel;
1229         boolean findJDKInternals;
1230         boolean findMissingDeps;
1231         boolean ignoreMissingDeps;
1232         boolean nowarning = false;
1233         Analyzer.Type verbose;
1234         // default filter references from same package
1235         boolean filterSamePackage = true;
1236         boolean filterSameArchive = false;
1237         Pattern filterRegex;
1238         String classpath;
1239         int recursive = -1;     // 0: --no-recursive, 1: --recursive
1240         int depth = -1;
1241         Set<String> requires = new HashSet<>();
1242         Set<String> packageNames = new HashSet<>();
1243         Pattern regex;             // apply to the dependences
1244         Pattern includePattern;
1245         boolean inverse = false;
1246         boolean compileTimeView = false;
1247         String systemModulePath = System.getProperty("java.home");
1248         String upgradeModulePath;
1249         String modulePath;
1250         Set<String> rootModules = new HashSet<>();
1251         Set<String> addmods = new HashSet<>();
1252         Runtime.Version multiRelease;
1253 
hasSourcePath()1254         boolean hasSourcePath() {
1255             return !addmods.isEmpty() || includePattern != null;
1256         }
1257 
hasFilter()1258         boolean hasFilter() {
1259             return numFilters() > 0;
1260         }
1261 
numFilters()1262         int numFilters() {
1263             int count = 0;
1264             if (requires.size() > 0) count++;
1265             if (regex != null) count++;
1266             if (packageNames.size() > 0) count++;
1267             return count;
1268         }
1269 
depth()1270         int depth() {
1271             // ignore -depth if --no-recursive is set
1272             if (recursive == NO_RECURSIVE)
1273                 return 1;
1274 
1275             // depth == 0 if recursive
1276             if (recursive == RECURSIVE && depth == -1)
1277                 return 0;
1278 
1279             // default depth is 1 unless specified via -depth option
1280             return depth == -1 ? 1 : depth;
1281         }
1282     }
1283 
1284     private static class ResourceBundleHelper {
1285         static final String LS = System.lineSeparator();
1286         static final ResourceBundle versionRB;
1287         static final ResourceBundle bundle;
1288         static final ResourceBundle jdkinternals;
1289 
1290         static {
1291             Locale locale = Locale.getDefault();
1292             try {
1293                 bundle = ResourceBundle.getBundle("com.sun.tools.jdeps.resources.jdeps", locale);
1294             } catch (MissingResourceException e) {
1295                 throw new InternalError("Cannot find jdeps resource bundle for locale " + locale);
1296             }
1297             try {
1298                 versionRB = ResourceBundle.getBundle("com.sun.tools.jdeps.resources.version");
1299             } catch (MissingResourceException e) {
1300                 throw new InternalError("version.resource.missing");
1301             }
1302             try {
1303                 jdkinternals = ResourceBundle.getBundle("com.sun.tools.jdeps.resources.jdkinternals");
1304             } catch (MissingResourceException e) {
1305                 throw new InternalError("Cannot find jdkinternals resource bundle");
1306             }
1307         }
1308 
getMessage(String key)1309         static String getMessage(String key) {
1310             return bundle.getString(key).replace("\n", LS);
1311         }
1312 
getVersion(String key)1313         static String getVersion(String key) {
1314             if (ResourceBundleHelper.versionRB == null) {
1315                 return System.getProperty("java.version");
1316             }
1317             return versionRB.getString(key).replace("\n", LS);
1318         }
1319 
getSuggestedReplacement(String key)1320         static String getSuggestedReplacement(String key) {
1321             return ResourceBundleHelper.jdkinternals.getString(key).replace("\n", LS);
1322         }
1323     }
1324 
1325     /**
1326      * Returns the recommended replacement API for the given classname;
1327      * or return null if replacement API is not known.
1328      */
replacementFor(String cn)1329     private Optional<String> replacementFor(String cn) {
1330         String name = cn;
1331         String value = null;
1332         while (value == null && name != null) {
1333             try {
1334                 value = ResourceBundleHelper.getSuggestedReplacement(name);
1335             } catch (MissingResourceException e) {
1336                 // go up one subpackage level
1337                 int i = name.lastIndexOf('.');
1338                 name = i > 0 ? name.substring(0, i) : null;
1339             }
1340         }
1341         return Optional.ofNullable(value);
1342     }
1343 }
1344