JdepsTask.java revision 2942:08092deced3f
1/*
2 * Copyright (c) 2012, 2014, 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 */
25package com.sun.tools.jdeps;
26
27import com.sun.tools.classfile.AccessFlags;
28import com.sun.tools.classfile.ClassFile;
29import com.sun.tools.classfile.ConstantPoolException;
30import com.sun.tools.classfile.Dependencies;
31import com.sun.tools.classfile.Dependencies.ClassFileError;
32import com.sun.tools.classfile.Dependency;
33import com.sun.tools.classfile.Dependency.Location;
34import static com.sun.tools.jdeps.Analyzer.Type.*;
35import java.io.*;
36import java.nio.file.DirectoryStream;
37import java.nio.file.Files;
38import java.nio.file.Path;
39import java.nio.file.Paths;
40import java.text.MessageFormat;
41import java.util.*;
42import java.util.regex.Pattern;
43
44/**
45 * Implementation for the jdeps tool for static class dependency analysis.
46 */
47class JdepsTask {
48    static class BadArgs extends Exception {
49        static final long serialVersionUID = 8765093759964640721L;
50        BadArgs(String key, Object... args) {
51            super(JdepsTask.getMessage(key, args));
52            this.key = key;
53            this.args = args;
54        }
55
56        BadArgs showUsage(boolean b) {
57            showUsage = b;
58            return this;
59        }
60        final String key;
61        final Object[] args;
62        boolean showUsage;
63    }
64
65    static abstract class Option {
66        Option(boolean hasArg, String... aliases) {
67            this.hasArg = hasArg;
68            this.aliases = aliases;
69        }
70
71        boolean isHidden() {
72            return false;
73        }
74
75        boolean matches(String opt) {
76            for (String a : aliases) {
77                if (a.equals(opt))
78                    return true;
79                if (hasArg && opt.startsWith(a + "="))
80                    return true;
81            }
82            return false;
83        }
84
85        boolean ignoreRest() {
86            return false;
87        }
88
89        abstract void process(JdepsTask task, String opt, String arg) throws BadArgs;
90        final boolean hasArg;
91        final String[] aliases;
92    }
93
94    static abstract class HiddenOption extends Option {
95        HiddenOption(boolean hasArg, String... aliases) {
96            super(hasArg, aliases);
97        }
98
99        boolean isHidden() {
100            return true;
101        }
102    }
103
104    static Option[] recognizedOptions = {
105        new Option(false, "-h", "-?", "-help") {
106            void process(JdepsTask task, String opt, String arg) {
107                task.options.help = true;
108            }
109        },
110        new Option(true, "-dotoutput") {
111            void process(JdepsTask task, String opt, String arg) throws BadArgs {
112                Path p = Paths.get(arg);
113                if (Files.exists(p) && (!Files.isDirectory(p) || !Files.isWritable(p))) {
114                    throw new BadArgs("err.invalid.path", arg);
115                }
116                task.options.dotOutputDir = arg;
117            }
118        },
119        new Option(false, "-s", "-summary") {
120            void process(JdepsTask task, String opt, String arg) {
121                task.options.showSummary = true;
122                task.options.verbose = SUMMARY;
123            }
124        },
125        new Option(false, "-v", "-verbose",
126                          "-verbose:package",
127                          "-verbose:class") {
128            void process(JdepsTask task, String opt, String arg) throws BadArgs {
129                switch (opt) {
130                    case "-v":
131                    case "-verbose":
132                        task.options.verbose = VERBOSE;
133                        task.options.filterSameArchive = false;
134                        task.options.filterSamePackage = false;
135                        break;
136                    case "-verbose:package":
137                        task.options.verbose = PACKAGE;
138                        break;
139                    case "-verbose:class":
140                        task.options.verbose = CLASS;
141                        break;
142                    default:
143                        throw new BadArgs("err.invalid.arg.for.option", opt);
144                }
145            }
146        },
147        new Option(true, "-cp", "-classpath") {
148            void process(JdepsTask task, String opt, String arg) {
149                task.options.classpath = arg;
150            }
151        },
152        new Option(true, "-p", "-package") {
153            void process(JdepsTask task, String opt, String arg) {
154                task.options.packageNames.add(arg);
155            }
156        },
157        new Option(true, "-e", "-regex") {
158            void process(JdepsTask task, String opt, String arg) {
159                task.options.regex = arg;
160            }
161        },
162
163        new Option(true, "-f", "-filter") {
164            void process(JdepsTask task, String opt, String arg) {
165                task.options.filterRegex = arg;
166            }
167        },
168        new Option(false, "-filter:package",
169                          "-filter:archive",
170                          "-filter:none") {
171            void process(JdepsTask task, String opt, String arg) {
172                switch (opt) {
173                    case "-filter:package":
174                        task.options.filterSamePackage = true;
175                        task.options.filterSameArchive = false;
176                        break;
177                    case "-filter:archive":
178                        task.options.filterSameArchive = true;
179                        task.options.filterSamePackage = false;
180                        break;
181                    case "-filter:none":
182                        task.options.filterSameArchive = false;
183                        task.options.filterSamePackage = false;
184                        break;
185                }
186            }
187        },
188        new Option(true, "-include") {
189            void process(JdepsTask task, String opt, String arg) throws BadArgs {
190                task.options.includePattern = Pattern.compile(arg);
191            }
192        },
193        new Option(false, "-P", "-profile") {
194            void process(JdepsTask task, String opt, String arg) throws BadArgs {
195                task.options.showProfile = true;
196                task.options.showModule = false;
197            }
198        },
199        new Option(false, "-M", "-module") {
200            void process(JdepsTask task, String opt, String arg) throws BadArgs {
201                task.options.showModule = true;
202                task.options.showProfile = false;
203            }
204        },
205        new Option(false, "-apionly") {
206            void process(JdepsTask task, String opt, String arg) {
207                task.options.apiOnly = true;
208            }
209        },
210        new Option(false, "-R", "-recursive") {
211            void process(JdepsTask task, String opt, String arg) {
212                task.options.depth = 0;
213                // turn off filtering
214                task.options.filterSameArchive = false;
215                task.options.filterSamePackage = false;
216            }
217        },
218        new Option(false, "-jdkinternals") {
219            void process(JdepsTask task, String opt, String arg) {
220                task.options.findJDKInternals = true;
221                task.options.verbose = CLASS;
222                if (task.options.includePattern == null) {
223                    task.options.includePattern = Pattern.compile(".*");
224                }
225            }
226        },
227            new HiddenOption(false, "-verify:access") {
228                void process(JdepsTask task, String opt, String arg) {
229                    task.options.verifyAccess = true;
230                    task.options.verbose = VERBOSE;
231                    task.options.filterSameArchive = false;
232                    task.options.filterSamePackage = false;
233                }
234            },
235            new HiddenOption(true, "-mp") {
236                void process(JdepsTask task, String opt, String arg) throws BadArgs {
237                    task.options.mpath = Paths.get(arg);
238                    if (!Files.isDirectory(task.options.mpath)) {
239                        throw new BadArgs("err.invalid.path", arg);
240                    }
241                    if (task.options.includePattern == null) {
242                        task.options.includePattern = Pattern.compile(".*");
243                    }
244                }
245            },
246        new Option(false, "-version") {
247            void process(JdepsTask task, String opt, String arg) {
248                task.options.version = true;
249            }
250        },
251        new HiddenOption(false, "-fullversion") {
252            void process(JdepsTask task, String opt, String arg) {
253                task.options.fullVersion = true;
254            }
255        },
256        new HiddenOption(false, "-showlabel") {
257            void process(JdepsTask task, String opt, String arg) {
258                task.options.showLabel = true;
259            }
260        },
261        new HiddenOption(false, "-q", "-quiet") {
262            void process(JdepsTask task, String opt, String arg) {
263                task.options.nowarning = true;
264            }
265        },
266        new HiddenOption(true, "-depth") {
267            void process(JdepsTask task, String opt, String arg) throws BadArgs {
268                try {
269                    task.options.depth = Integer.parseInt(arg);
270                } catch (NumberFormatException e) {
271                    throw new BadArgs("err.invalid.arg.for.option", opt);
272                }
273            }
274        },
275    };
276
277    private static final String PROGNAME = "jdeps";
278    private final Options options = new Options();
279    private final List<String> classes = new ArrayList<>();
280
281    private PrintWriter log;
282    void setLog(PrintWriter out) {
283        log = out;
284    }
285
286    /**
287     * Result codes.
288     */
289    static final int EXIT_OK = 0, // Completed with no errors.
290                     EXIT_ERROR = 1, // Completed but reported errors.
291                     EXIT_CMDERR = 2, // Bad command-line arguments
292                     EXIT_SYSERR = 3, // System error or resource exhaustion.
293                     EXIT_ABNORMAL = 4;// terminated abnormally
294
295    int run(String[] args) {
296        if (log == null) {
297            log = new PrintWriter(System.out);
298        }
299        try {
300            handleOptions(args);
301            if (options.help) {
302                showHelp();
303            }
304            if (options.version || options.fullVersion) {
305                showVersion(options.fullVersion);
306            }
307            if (classes.isEmpty() && options.includePattern == null) {
308                if (options.help || options.version || options.fullVersion) {
309                    return EXIT_OK;
310                } else {
311                    showHelp();
312                    return EXIT_CMDERR;
313                }
314            }
315            if (options.regex != null && options.packageNames.size() > 0) {
316                showHelp();
317                return EXIT_CMDERR;
318            }
319            if ((options.findJDKInternals || options.verifyAccess) &&
320                   (options.regex != null || options.packageNames.size() > 0 || options.showSummary)) {
321                showHelp();
322                return EXIT_CMDERR;
323            }
324            if (options.showSummary && options.verbose != SUMMARY) {
325                showHelp();
326                return EXIT_CMDERR;
327            }
328
329            boolean ok = run();
330            return ok ? EXIT_OK : EXIT_ERROR;
331        } catch (BadArgs e) {
332            reportError(e.key, e.args);
333            if (e.showUsage) {
334                log.println(getMessage("main.usage.summary", PROGNAME));
335            }
336            return EXIT_CMDERR;
337        } catch (IOException e) {
338            return EXIT_ABNORMAL;
339        } finally {
340            log.flush();
341        }
342    }
343
344    private final List<Archive> sourceLocations = new ArrayList<>();
345    private final List<Archive> classpaths = new ArrayList<>();
346    private final List<Archive> initialArchives = new ArrayList<>();
347    private boolean run() throws IOException {
348        buildArchives();
349
350        if (options.verifyAccess) {
351            return verifyModuleAccess();
352        } else {
353            return analyzeDeps();
354        }
355    }
356
357    private boolean analyzeDeps() throws IOException {
358        Analyzer analyzer = new Analyzer(options.verbose, new Analyzer.Filter() {
359            @Override
360            public boolean accepts(Location origin, Archive originArchive,
361                                   Location target, Archive targetArchive)
362            {
363                if (options.findJDKInternals) {
364                    // accepts target that is JDK class but not exported
365                    return isJDKModule(targetArchive) &&
366                              !((Module) targetArchive).isExported(target.getClassName());
367                } else if (options.filterSameArchive) {
368                    // accepts origin and target that from different archive
369                    return originArchive != targetArchive;
370                }
371                return true;
372            }
373        });
374
375        // parse classfiles and find all dependencies
376        findDependencies(options.apiOnly);
377
378        // analyze the dependencies
379        analyzer.run(sourceLocations);
380
381        // output result
382        if (options.dotOutputDir != null) {
383            Path dir = Paths.get(options.dotOutputDir);
384            Files.createDirectories(dir);
385            generateDotFiles(dir, analyzer);
386        } else {
387            printRawOutput(log, analyzer);
388        }
389
390        if (options.findJDKInternals && !options.nowarning) {
391            showReplacements(analyzer);
392        }
393        return true;
394    }
395
396    private boolean verifyModuleAccess() throws IOException {
397        // two passes
398        // 1. check API dependences where the types of dependences must be re-exported
399        // 2. check all dependences where types must be accessible
400
401        // pass 1
402        findDependencies(true /* api only */);
403        Analyzer analyzer = Analyzer.getExportedAPIsAnalyzer();
404        boolean pass1 = analyzer.run(sourceLocations);
405        if (!pass1) {
406            System.out.println("ERROR: Failed API access verification");
407        }
408        // pass 2
409        findDependencies(false);
410        analyzer =  Analyzer.getModuleAccessAnalyzer();
411        boolean pass2 = analyzer.run(sourceLocations);
412        if (!pass2) {
413            System.out.println("ERROR: Failed module access verification");
414        }
415        if (pass1 & pass2) {
416            System.out.println("Access verification succeeded.");
417        }
418        return pass1 & pass2;
419    }
420
421    private void generateSummaryDotFile(Path dir, Analyzer analyzer) throws IOException {
422        // If verbose mode (-v or -verbose option),
423        // the summary.dot file shows package-level dependencies.
424        Analyzer.Type summaryType =
425            (options.verbose == PACKAGE || options.verbose == SUMMARY) ? SUMMARY : PACKAGE;
426        Path summary = dir.resolve("summary.dot");
427        try (PrintWriter sw = new PrintWriter(Files.newOutputStream(summary));
428             SummaryDotFile dotfile = new SummaryDotFile(sw, summaryType)) {
429            for (Archive archive : sourceLocations) {
430                if (!archive.isEmpty()) {
431                    if (options.verbose == PACKAGE || options.verbose == SUMMARY) {
432                        if (options.showLabel) {
433                            // build labels listing package-level dependencies
434                            analyzer.visitDependences(archive, dotfile.labelBuilder(), PACKAGE);
435                        }
436                    }
437                    analyzer.visitDependences(archive, dotfile, summaryType);
438                }
439            }
440        }
441    }
442
443    private void generateDotFiles(Path dir, Analyzer analyzer) throws IOException {
444        // output individual .dot file for each archive
445        if (options.verbose != SUMMARY) {
446            for (Archive archive : sourceLocations) {
447                if (analyzer.hasDependences(archive)) {
448                    Path dotfile = dir.resolve(archive.getName() + ".dot");
449                    try (PrintWriter pw = new PrintWriter(Files.newOutputStream(dotfile));
450                         DotFileFormatter formatter = new DotFileFormatter(pw, archive)) {
451                        analyzer.visitDependences(archive, formatter);
452                    }
453                }
454            }
455        }
456        // generate summary dot file
457        generateSummaryDotFile(dir, analyzer);
458    }
459
460    private void printRawOutput(PrintWriter writer, Analyzer analyzer) {
461        RawOutputFormatter depFormatter = new RawOutputFormatter(writer);
462        RawSummaryFormatter summaryFormatter = new RawSummaryFormatter(writer);
463        for (Archive archive : sourceLocations) {
464            if (!archive.isEmpty()) {
465                analyzer.visitDependences(archive, summaryFormatter, SUMMARY);
466                if (analyzer.hasDependences(archive) && options.verbose != SUMMARY) {
467                    analyzer.visitDependences(archive, depFormatter);
468                }
469            }
470        }
471    }
472
473    private boolean isValidClassName(String name) {
474        if (!Character.isJavaIdentifierStart(name.charAt(0))) {
475            return false;
476        }
477        for (int i=1; i < name.length(); i++) {
478            char c = name.charAt(i);
479            if (c != '.'  && !Character.isJavaIdentifierPart(c)) {
480                return false;
481            }
482        }
483        return true;
484    }
485
486    /*
487     * Dep Filter configured based on the input jdeps option
488     * 1. -p and -regex to match target dependencies
489     * 2. -filter:package to filter out same-package dependencies
490     *
491     * This filter is applied when jdeps parses the class files
492     * and filtered dependencies are not stored in the Analyzer.
493     *
494     * -filter:archive is applied later in the Analyzer as the
495     * containing archive of a target class may not be known until
496     * the entire archive
497     */
498    class DependencyFilter implements Dependency.Filter {
499        final Dependency.Filter filter;
500        final Pattern filterPattern;
501        DependencyFilter() {
502            if (options.regex != null) {
503                this.filter = Dependencies.getRegexFilter(Pattern.compile(options.regex));
504            } else if (options.packageNames.size() > 0) {
505                this.filter = Dependencies.getPackageFilter(options.packageNames, false);
506            } else {
507                this.filter = null;
508            }
509
510            this.filterPattern =
511                options.filterRegex != null ? Pattern.compile(options.filterRegex) : null;
512        }
513        @Override
514        public boolean accepts(Dependency d) {
515            if (d.getOrigin().equals(d.getTarget())) {
516                return false;
517            }
518            String pn = d.getTarget().getPackageName();
519            if (options.filterSamePackage && d.getOrigin().getPackageName().equals(pn)) {
520                return false;
521            }
522
523            if (filterPattern != null && filterPattern.matcher(pn).matches()) {
524                return false;
525            }
526            return filter != null ? filter.accepts(d) : true;
527        }
528    }
529
530    /**
531     * Tests if the given class matches the pattern given in the -include option
532     */
533    private boolean matches(String classname) {
534        if (options.includePattern != null) {
535            return options.includePattern.matcher(classname.replace('/', '.')).matches();
536        } else {
537            return true;
538        }
539    }
540
541    private void buildArchives() throws IOException {
542        for (String s : classes) {
543            Path p = Paths.get(s);
544            if (Files.exists(p)) {
545                initialArchives.add(Archive.getInstance(p));
546            }
547        }
548        sourceLocations.addAll(initialArchives);
549
550        classpaths.addAll(getClassPathArchives(options.classpath));
551        if (options.includePattern != null) {
552            initialArchives.addAll(classpaths);
553        }
554        classpaths.addAll(PlatformClassPath.getModules(options.mpath));
555        if (options.mpath != null) {
556            initialArchives.addAll(PlatformClassPath.getModules(options.mpath));
557        } else {
558            classpaths.addAll(PlatformClassPath.getJarFiles());
559        }
560        // add all classpath archives to the source locations for reporting
561        sourceLocations.addAll(classpaths);
562    }
563
564    private void findDependencies(boolean apiOnly) throws IOException {
565        Dependency.Finder finder =
566            apiOnly ? Dependencies.getAPIFinder(AccessFlags.ACC_PROTECTED)
567                    : Dependencies.getClassDependencyFinder();
568        Dependency.Filter filter = new DependencyFilter();
569
570        Deque<String> roots = new LinkedList<>();
571        for (String s : classes) {
572            Path p = Paths.get(s);
573            if (!Files.exists(p)) {
574                if (isValidClassName(s)) {
575                    roots.add(s);
576                } else {
577                    warning("warn.invalid.arg", s);
578                }
579            }
580        }
581
582        // Work queue of names of classfiles to be searched.
583        // Entries will be unique, and for classes that do not yet have
584        // dependencies in the results map.
585        Deque<String> deque = new LinkedList<>();
586        Set<String> doneClasses = new HashSet<>();
587
588        // get the immediate dependencies of the input files
589        for (Archive a : initialArchives) {
590            for (ClassFile cf : a.reader().getClassFiles()) {
591                String classFileName;
592                try {
593                    classFileName = cf.getName();
594                } catch (ConstantPoolException e) {
595                    throw new ClassFileError(e);
596                }
597
598                // tests if this class matches the -include or -apiOnly option if specified
599                if (!matches(classFileName) || (apiOnly && !cf.access_flags.is(AccessFlags.ACC_PUBLIC))) {
600                    continue;
601                }
602
603                if (!doneClasses.contains(classFileName)) {
604                    doneClasses.add(classFileName);
605                }
606
607                for (Dependency d : finder.findDependencies(cf)) {
608                    if (filter.accepts(d)) {
609                        String cn = d.getTarget().getName();
610                        if (!doneClasses.contains(cn) && !deque.contains(cn)) {
611                            deque.add(cn);
612                        }
613                        a.addClass(d.getOrigin(), d.getTarget());
614                    } else {
615                        // ensure that the parsed class is added the archive
616                        a.addClass(d.getOrigin());
617                    }
618                }
619                for (String name : a.reader().skippedEntries()) {
620                    warning("warn.skipped.entry", name, a.getPathName());
621                }
622            }
623        }
624
625        // add Archive for looking up classes from the classpath
626        // for transitive dependency analysis
627        Deque<String> unresolved = roots;
628        int depth = options.depth > 0 ? options.depth : Integer.MAX_VALUE;
629        do {
630            String name;
631            while ((name = unresolved.poll()) != null) {
632                if (doneClasses.contains(name)) {
633                    continue;
634                }
635                ClassFile cf = null;
636                for (Archive a : classpaths) {
637                    cf = a.reader().getClassFile(name);
638                    if (cf != null) {
639                        String classFileName;
640                        try {
641                            classFileName = cf.getName();
642                        } catch (ConstantPoolException e) {
643                            throw new ClassFileError(e);
644                        }
645                        if (!doneClasses.contains(classFileName)) {
646                            // if name is a fully-qualified class name specified
647                            // from command-line, this class might already be parsed
648                            doneClasses.add(classFileName);
649
650                            for (Dependency d : finder.findDependencies(cf)) {
651                                if (depth == 0) {
652                                    // ignore the dependency
653                                    a.addClass(d.getOrigin());
654                                    break;
655                                } else if (filter.accepts(d)) {
656                                    a.addClass(d.getOrigin(), d.getTarget());
657                                    String cn = d.getTarget().getName();
658                                    if (!doneClasses.contains(cn) && !deque.contains(cn)) {
659                                        deque.add(cn);
660                                    }
661                                } else {
662                                    // ensure that the parsed class is added the archive
663                                    a.addClass(d.getOrigin());
664                                }
665                            }
666                        }
667                        break;
668                    }
669                }
670                if (cf == null) {
671                    doneClasses.add(name);
672                }
673            }
674            unresolved = deque;
675            deque = new LinkedList<>();
676        } while (!unresolved.isEmpty() && depth-- > 0);
677    }
678
679    public void handleOptions(String[] args) throws BadArgs {
680        // process options
681        for (int i=0; i < args.length; i++) {
682            if (args[i].charAt(0) == '-') {
683                String name = args[i];
684                Option option = getOption(name);
685                String param = null;
686                if (option.hasArg) {
687                    if (name.startsWith("-") && name.indexOf('=') > 0) {
688                        param = name.substring(name.indexOf('=') + 1, name.length());
689                    } else if (i + 1 < args.length) {
690                        param = args[++i];
691                    }
692                    if (param == null || param.isEmpty() || param.charAt(0) == '-') {
693                        throw new BadArgs("err.missing.arg", name).showUsage(true);
694                    }
695                }
696                option.process(this, name, param);
697                if (option.ignoreRest()) {
698                    i = args.length;
699                }
700            } else {
701                // process rest of the input arguments
702                for (; i < args.length; i++) {
703                    String name = args[i];
704                    if (name.charAt(0) == '-') {
705                        throw new BadArgs("err.option.after.class", name).showUsage(true);
706                    }
707                    classes.add(name);
708                }
709            }
710        }
711    }
712
713    private Option getOption(String name) throws BadArgs {
714        for (Option o : recognizedOptions) {
715            if (o.matches(name)) {
716                return o;
717            }
718        }
719        throw new BadArgs("err.unknown.option", name).showUsage(true);
720    }
721
722    private void reportError(String key, Object... args) {
723        log.println(getMessage("error.prefix") + " " + getMessage(key, args));
724    }
725
726    private void warning(String key, Object... args) {
727        log.println(getMessage("warn.prefix") + " " + getMessage(key, args));
728    }
729
730    private void showHelp() {
731        log.println(getMessage("main.usage", PROGNAME));
732        for (Option o : recognizedOptions) {
733            String name = o.aliases[0].substring(1); // there must always be at least one name
734            name = name.charAt(0) == '-' ? name.substring(1) : name;
735            if (o.isHidden() || name.equals("h") || name.startsWith("filter:")) {
736                continue;
737            }
738            log.println(getMessage("main.opt." + name));
739        }
740    }
741
742    private void showVersion(boolean full) {
743        log.println(version(full ? "full" : "release"));
744    }
745
746    private String version(String key) {
747        // key=version:  mm.nn.oo[-milestone]
748        // key=full:     mm.mm.oo[-milestone]-build
749        if (ResourceBundleHelper.versionRB == null) {
750            return System.getProperty("java.version");
751        }
752        try {
753            return ResourceBundleHelper.versionRB.getString(key);
754        } catch (MissingResourceException e) {
755            return getMessage("version.unknown", System.getProperty("java.version"));
756        }
757    }
758
759    static String getMessage(String key, Object... args) {
760        try {
761            return MessageFormat.format(ResourceBundleHelper.bundle.getString(key), args);
762        } catch (MissingResourceException e) {
763            throw new InternalError("Missing message: " + key);
764        }
765    }
766
767    private static class Options {
768        boolean help;
769        boolean version;
770        boolean fullVersion;
771        boolean showProfile;
772        boolean showModule;
773        boolean showSummary;
774        boolean apiOnly;
775        boolean showLabel;
776        boolean findJDKInternals;
777        boolean nowarning;
778        // default is to show package-level dependencies
779        // and filter references from same package
780        Analyzer.Type verbose = PACKAGE;
781        boolean filterSamePackage = true;
782        boolean filterSameArchive = false;
783        String filterRegex;
784        String dotOutputDir;
785        String classpath = "";
786        int depth = 1;
787        Set<String> packageNames = new HashSet<>();
788        String regex;             // apply to the dependences
789        Pattern includePattern;   // apply to classes
790        // module boundary access check
791        boolean verifyAccess;
792        Path mpath;
793    }
794    private static class ResourceBundleHelper {
795        static final ResourceBundle versionRB;
796        static final ResourceBundle bundle;
797        static final ResourceBundle jdkinternals;
798
799        static {
800            Locale locale = Locale.getDefault();
801            try {
802                bundle = ResourceBundle.getBundle("com.sun.tools.jdeps.resources.jdeps", locale);
803            } catch (MissingResourceException e) {
804                throw new InternalError("Cannot find jdeps resource bundle for locale " + locale);
805            }
806            try {
807                versionRB = ResourceBundle.getBundle("com.sun.tools.jdeps.resources.version");
808            } catch (MissingResourceException e) {
809                throw new InternalError("version.resource.missing");
810            }
811            try {
812                jdkinternals = ResourceBundle.getBundle("com.sun.tools.jdeps.resources.jdkinternals");
813            } catch (MissingResourceException e) {
814                throw new InternalError("Cannot find jdkinternals resource bundle");
815            }
816        }
817    }
818
819    /*
820     * Returns the list of Archive specified in cpaths and not included
821     * initialArchives
822     */
823    private List<Archive> getClassPathArchives(String cpaths)
824            throws IOException
825    {
826        List<Archive> result = new ArrayList<>();
827        if (cpaths.isEmpty()) {
828            return result;
829        }
830        List<Path> paths = new ArrayList<>();
831        for (String p : cpaths.split(File.pathSeparator)) {
832            if (p.length() > 0) {
833                // wildcard to parse all JAR files e.g. -classpath dir/*
834                int i = p.lastIndexOf(".*");
835                if (i > 0) {
836                    Path dir = Paths.get(p.substring(0, i));
837                    try (DirectoryStream<Path> stream = Files.newDirectoryStream(dir, "*.jar")) {
838                        for (Path entry : stream) {
839                            paths.add(entry);
840                        }
841                    }
842                } else {
843                    paths.add(Paths.get(p));
844                }
845            }
846        }
847        for (Path path : paths) {
848            boolean found = initialArchives.stream()
849                                           .map(Archive::path)
850                                           .anyMatch(p -> isSameFile(path, p));
851            if (!found && Files.exists(path)) {
852                result.add(Archive.getInstance(path));
853            }
854        }
855        return result;
856    }
857
858    private boolean isSameFile(Path p1, Path p2) {
859        try {
860            return Files.isSameFile(p1, p2);
861        } catch (IOException e) {
862            throw new UncheckedIOException(e);
863        }
864    }
865
866    class RawOutputFormatter implements Analyzer.Visitor {
867        private final PrintWriter writer;
868        private String pkg = "";
869        RawOutputFormatter(PrintWriter writer) {
870            this.writer = writer;
871        }
872        @Override
873        public void visitDependence(String origin, Archive originArchive,
874                                    String target, Archive targetArchive) {
875            String tag = toTag(target, targetArchive);
876            if (options.verbose == VERBOSE) {
877                writer.format("   %-50s -> %-50s %s%n", origin, target, tag);
878            } else {
879                if (!origin.equals(pkg)) {
880                    pkg = origin;
881                    writer.format("   %s (%s)%n", origin, originArchive.getName());
882                }
883                writer.format("      -> %-50s %s%n", target, tag);
884            }
885        }
886    }
887
888    class RawSummaryFormatter implements Analyzer.Visitor {
889        private final PrintWriter writer;
890        RawSummaryFormatter(PrintWriter writer) {
891            this.writer = writer;
892        }
893        @Override
894        public void visitDependence(String origin, Archive originArchive,
895                                    String target, Archive targetArchive) {
896            String targetName =  targetArchive.getPathName();
897            if (options.showModule && isJDKModule(targetArchive)) {
898                targetName = ((Module)targetArchive).name();
899            }
900            writer.format("%s -> %s", originArchive.getName(), targetName);
901            if (options.showProfile && isJDKModule(targetArchive)) {
902                writer.format(" (%s)", target);
903            }
904            writer.format("%n");
905        }
906    }
907
908    class DotFileFormatter implements Analyzer.Visitor, AutoCloseable {
909        private final PrintWriter writer;
910        private final String name;
911        DotFileFormatter(PrintWriter writer, Archive archive) {
912            this.writer = writer;
913            this.name = archive.getName();
914            writer.format("digraph \"%s\" {%n", name);
915            writer.format("    // Path: %s%n", archive.getPathName());
916        }
917
918        @Override
919        public void close() {
920            writer.println("}");
921        }
922
923        @Override
924        public void visitDependence(String origin, Archive originArchive,
925                                    String target, Archive targetArchive) {
926            String tag = toTag(target, targetArchive);
927            writer.format("   %-50s -> \"%s\";%n",
928                          String.format("\"%s\"", origin),
929                          tag.isEmpty() ? target
930                                        : String.format("%s (%s)", target, tag));
931        }
932    }
933
934    class SummaryDotFile implements Analyzer.Visitor, AutoCloseable {
935        private final PrintWriter writer;
936        private final Analyzer.Type type;
937        private final Map<Archive, Map<Archive,StringBuilder>> edges = new HashMap<>();
938        SummaryDotFile(PrintWriter writer, Analyzer.Type type) {
939            this.writer = writer;
940            this.type = type;
941            writer.format("digraph \"summary\" {%n");
942        }
943
944        @Override
945        public void close() {
946            writer.println("}");
947        }
948
949        @Override
950        public void visitDependence(String origin, Archive originArchive,
951                                    String target, Archive targetArchive) {
952            String targetName = type == PACKAGE ? target : targetArchive.getName();
953            if (isJDKModule(targetArchive)) {
954                Module m = (Module)targetArchive;
955                String n = showProfileOrModule(m);
956                if (!n.isEmpty()) {
957                    targetName += " (" + n + ")";
958                }
959            } else if (type == PACKAGE) {
960                targetName += " (" + targetArchive.getName() + ")";
961            }
962            String label = getLabel(originArchive, targetArchive);
963            writer.format("  %-50s -> \"%s\"%s;%n",
964                          String.format("\"%s\"", origin), targetName, label);
965        }
966
967        String getLabel(Archive origin, Archive target) {
968            if (edges.isEmpty())
969                return "";
970
971            StringBuilder label = edges.get(origin).get(target);
972            return label == null ? "" : String.format(" [label=\"%s\",fontsize=9]", label.toString());
973        }
974
975        Analyzer.Visitor labelBuilder() {
976            // show the package-level dependencies as labels in the dot graph
977            return new Analyzer.Visitor() {
978                @Override
979                public void visitDependence(String origin, Archive originArchive, String target, Archive targetArchive) {
980                    edges.putIfAbsent(originArchive, new HashMap<>());
981                    edges.get(originArchive).putIfAbsent(targetArchive, new StringBuilder());
982                    StringBuilder sb = edges.get(originArchive).get(targetArchive);
983                    String tag = toTag(target, targetArchive);
984                    addLabel(sb, origin, target, tag);
985                }
986
987                void addLabel(StringBuilder label, String origin, String target, String tag) {
988                    label.append(origin).append(" -> ").append(target);
989                    if (!tag.isEmpty()) {
990                        label.append(" (" + tag + ")");
991                    }
992                    label.append("\\n");
993                }
994            };
995        }
996    }
997
998    /**
999     * Test if the given archive is part of the JDK
1000     */
1001    private boolean isJDKModule(Archive archive) {
1002        return Module.class.isInstance(archive);
1003    }
1004
1005    /**
1006     * If the given archive is JDK archive, this method returns the profile name
1007     * only if -profile option is specified; it accesses a private JDK API and
1008     * the returned value will have "JDK internal API" prefix
1009     *
1010     * For non-JDK archives, this method returns the file name of the archive.
1011     */
1012    private String toTag(String name, Archive source) {
1013        if (!isJDKModule(source)) {
1014            return source.getName();
1015        }
1016
1017        Module module = (Module)source;
1018        boolean isExported = false;
1019        if (options.verbose == CLASS || options.verbose == VERBOSE) {
1020            isExported = module.isExported(name);
1021        } else {
1022            isExported = module.isExportedPackage(name);
1023        }
1024        if (isExported) {
1025            // exported API
1026            return showProfileOrModule(module);
1027        } else {
1028            return "JDK internal API (" + source.getName() + ")";
1029        }
1030    }
1031
1032    private String showProfileOrModule(Module m) {
1033        String tag = "";
1034        if (options.showProfile) {
1035            Profile p = Profile.getProfile(m);
1036            if (p != null) {
1037                tag = p.profileName();
1038            }
1039        } else if (options.showModule) {
1040            tag = m.name();
1041        }
1042        return tag;
1043    }
1044
1045    private Profile getProfile(String name) {
1046        String pn = name;
1047        if (options.verbose == CLASS || options.verbose == VERBOSE) {
1048            int i = name.lastIndexOf('.');
1049            pn = i > 0 ? name.substring(0, i) : "";
1050        }
1051        return Profile.getProfile(pn);
1052    }
1053
1054    /**
1055     * Returns the recommended replacement API for the given classname;
1056     * or return null if replacement API is not known.
1057     */
1058    private String replacementFor(String cn) {
1059        String name = cn;
1060        String value = null;
1061        while (value == null && name != null) {
1062            try {
1063                value = ResourceBundleHelper.jdkinternals.getString(name);
1064            } catch (MissingResourceException e) {
1065                // go up one subpackage level
1066                int i = name.lastIndexOf('.');
1067                name = i > 0 ? name.substring(0, i) : null;
1068            }
1069        }
1070        return value;
1071    };
1072
1073    private void showReplacements(Analyzer analyzer) {
1074        Map<String,String> jdkinternals = new TreeMap<>();
1075        boolean useInternals = false;
1076        for (Archive source : sourceLocations) {
1077            useInternals = useInternals || analyzer.hasDependences(source);
1078            for (String cn : analyzer.dependences(source)) {
1079                String repl = replacementFor(cn);
1080                if (repl != null) {
1081                    jdkinternals.putIfAbsent(cn, repl);
1082                }
1083            }
1084        }
1085        if (useInternals) {
1086            log.println();
1087            warning("warn.replace.useJDKInternals", getMessage("jdeps.wiki.url"));
1088        }
1089        if (!jdkinternals.isEmpty()) {
1090            log.println();
1091            log.format("%-40s %s%n", "JDK Internal API", "Suggested Replacement");
1092            log.format("%-40s %s%n", "----------------", "---------------------");
1093            for (Map.Entry<String,String> e : jdkinternals.entrySet()) {
1094                log.format("%-40s %s%n", e.getKey(), e.getValue());
1095            }
1096        }
1097
1098    }
1099}
1100