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