Thu, 27 Apr 2017 16:18:18 -0700
8176329: jdeps to detect MR jar file and output a warning
Reviewed-by: mchung
1 /*
2 * Copyright (c) 2012, 2017, 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 package com.sun.tools.jdeps;
27 import com.sun.tools.classfile.AccessFlags;
28 import com.sun.tools.classfile.ClassFile;
29 import com.sun.tools.classfile.ConstantPoolException;
30 import com.sun.tools.classfile.Dependencies;
31 import com.sun.tools.classfile.Dependencies.ClassFileError;
32 import com.sun.tools.classfile.Dependency;
33 import com.sun.tools.classfile.Dependency.Location;
34 import com.sun.tools.jdeps.PlatformClassPath.JDKArchive;
35 import static com.sun.tools.jdeps.Analyzer.Type.*;
36 import java.io.*;
37 import java.nio.file.DirectoryStream;
38 import java.nio.file.Files;
39 import java.nio.file.Path;
40 import java.nio.file.Paths;
41 import java.text.MessageFormat;
42 import java.util.*;
43 import java.util.regex.Pattern;
45 /**
46 * Implementation for the jdeps tool for static class dependency analysis.
47 */
48 class 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 }
57 BadArgs showUsage(boolean b) {
58 showUsage = b;
59 return this;
60 }
61 final String key;
62 final Object[] args;
63 boolean showUsage;
64 }
66 static abstract class Option {
67 Option(boolean hasArg, String... aliases) {
68 this.hasArg = hasArg;
69 this.aliases = aliases;
70 }
72 boolean isHidden() {
73 return false;
74 }
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 }
86 boolean ignoreRest() {
87 return false;
88 }
90 abstract void process(JdepsTask task, String opt, String arg) throws BadArgs;
91 final boolean hasArg;
92 final String[] aliases;
93 }
95 static abstract class HiddenOption extends Option {
96 HiddenOption(boolean hasArg, String... aliases) {
97 super(hasArg, aliases);
98 }
100 boolean isHidden() {
101 return true;
102 }
103 }
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 },
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 if (Profile.getProfileCount() == 0) {
198 throw new BadArgs("err.option.unsupported", opt, getMessage("err.profiles.msg"));
199 }
200 }
201 },
202 new Option(false, "-apionly") {
203 void process(JdepsTask task, String opt, String arg) {
204 task.options.apiOnly = true;
205 }
206 },
207 new Option(false, "-R", "-recursive") {
208 void process(JdepsTask task, String opt, String arg) {
209 task.options.depth = 0;
210 // turn off filtering
211 task.options.filterSameArchive = false;
212 task.options.filterSamePackage = false;
213 }
214 },
215 new Option(false, "-jdkinternals") {
216 void process(JdepsTask task, String opt, String arg) {
217 task.options.findJDKInternals = true;
218 task.options.verbose = CLASS;
219 if (task.options.includePattern == null) {
220 task.options.includePattern = Pattern.compile(".*");
221 }
222 }
223 },
224 new Option(false, "-version") {
225 void process(JdepsTask task, String opt, String arg) {
226 task.options.version = true;
227 }
228 },
229 new HiddenOption(false, "-fullversion") {
230 void process(JdepsTask task, String opt, String arg) {
231 task.options.fullVersion = true;
232 }
233 },
234 new HiddenOption(false, "-showlabel") {
235 void process(JdepsTask task, String opt, String arg) {
236 task.options.showLabel = true;
237 }
238 },
239 new HiddenOption(false, "-q", "-quiet") {
240 void process(JdepsTask task, String opt, String arg) {
241 task.options.nowarning = true;
242 }
243 },
244 new HiddenOption(true, "-depth") {
245 void process(JdepsTask task, String opt, String arg) throws BadArgs {
246 try {
247 task.options.depth = Integer.parseInt(arg);
248 } catch (NumberFormatException e) {
249 throw new BadArgs("err.invalid.arg.for.option", opt);
250 }
251 }
252 },
253 };
255 private static final String PROGNAME = "jdeps";
256 private final Options options = new Options();
257 private final List<String> classes = new ArrayList<>();
259 private PrintWriter log;
260 void setLog(PrintWriter out) {
261 log = out;
262 }
264 /**
265 * Result codes.
266 */
267 static final int EXIT_OK = 0, // Completed with no errors.
268 EXIT_ERROR = 1, // Completed but reported errors.
269 EXIT_CMDERR = 2, // Bad command-line arguments
270 EXIT_SYSERR = 3, // System error or resource exhaustion.
271 EXIT_ABNORMAL = 4;// terminated abnormally
273 int run(String[] args) {
274 if (log == null) {
275 log = new PrintWriter(System.out);
276 }
277 try {
278 handleOptions(args);
279 if (options.help) {
280 showHelp();
281 }
282 if (options.version || options.fullVersion) {
283 showVersion(options.fullVersion);
284 }
285 if (classes.isEmpty() && options.includePattern == null) {
286 if (options.help || options.version || options.fullVersion) {
287 return EXIT_OK;
288 } else {
289 showHelp();
290 return EXIT_CMDERR;
291 }
292 }
293 if (options.regex != null && options.packageNames.size() > 0) {
294 showHelp();
295 return EXIT_CMDERR;
296 }
297 if (options.findJDKInternals &&
298 (options.regex != null || options.packageNames.size() > 0 || options.showSummary)) {
299 showHelp();
300 return EXIT_CMDERR;
301 }
302 if (options.showSummary && options.verbose != SUMMARY) {
303 showHelp();
304 return EXIT_CMDERR;
305 }
306 boolean ok = run();
307 return ok ? EXIT_OK : EXIT_ERROR;
308 } catch (BadArgs e) {
309 reportError(e.key, e.args);
310 if (e.showUsage) {
311 log.println(getMessage("main.usage.summary", PROGNAME));
312 }
313 return EXIT_CMDERR;
314 } catch (IOException e) {
315 return EXIT_ABNORMAL;
316 } finally {
317 log.flush();
318 }
319 }
321 private final List<Archive> sourceLocations = new ArrayList<>();
322 private boolean run() throws IOException {
323 // parse classfiles and find all dependencies
324 findDependencies();
326 Analyzer analyzer = new Analyzer(options.verbose, new Analyzer.Filter() {
327 @Override
328 public boolean accepts(Location origin, Archive originArchive,
329 Location target, Archive targetArchive)
330 {
331 if (options.findJDKInternals) {
332 // accepts target that is JDK class but not exported
333 return isJDKArchive(targetArchive) &&
334 !((JDKArchive) targetArchive).isExported(target.getClassName());
335 } else if (options.filterSameArchive) {
336 // accepts origin and target that from different archive
337 return originArchive != targetArchive;
338 }
339 return true;
340 }
341 });
343 // analyze the dependencies
344 analyzer.run(sourceLocations);
346 // output result
347 if (options.dotOutputDir != null) {
348 Path dir = Paths.get(options.dotOutputDir);
349 Files.createDirectories(dir);
350 generateDotFiles(dir, analyzer);
351 } else {
352 printRawOutput(log, analyzer);
353 }
355 if (options.findJDKInternals && !options.nowarning) {
356 showReplacements(analyzer);
357 }
358 return true;
359 }
361 private void generateSummaryDotFile(Path dir, Analyzer analyzer) throws IOException {
362 // If verbose mode (-v or -verbose option),
363 // the summary.dot file shows package-level dependencies.
364 Analyzer.Type summaryType =
365 (options.verbose == PACKAGE || options.verbose == SUMMARY) ? SUMMARY : PACKAGE;
366 Path summary = dir.resolve("summary.dot");
367 try (PrintWriter sw = new PrintWriter(Files.newOutputStream(summary));
368 SummaryDotFile dotfile = new SummaryDotFile(sw, summaryType)) {
369 for (Archive archive : sourceLocations) {
370 if (!archive.isEmpty()) {
371 if (options.verbose == PACKAGE || options.verbose == SUMMARY) {
372 if (options.showLabel) {
373 // build labels listing package-level dependencies
374 analyzer.visitDependences(archive, dotfile.labelBuilder(), PACKAGE);
375 }
376 }
377 analyzer.visitDependences(archive, dotfile, summaryType);
378 }
379 }
380 }
381 }
383 private void generateDotFiles(Path dir, Analyzer analyzer) throws IOException {
384 // output individual .dot file for each archive
385 if (options.verbose != SUMMARY) {
386 for (Archive archive : sourceLocations) {
387 if (analyzer.hasDependences(archive)) {
388 Path dotfile = dir.resolve(archive.getName() + ".dot");
389 try (PrintWriter pw = new PrintWriter(Files.newOutputStream(dotfile));
390 DotFileFormatter formatter = new DotFileFormatter(pw, archive)) {
391 analyzer.visitDependences(archive, formatter);
392 }
393 }
394 }
395 }
396 // generate summary dot file
397 generateSummaryDotFile(dir, analyzer);
398 }
400 private void printRawOutput(PrintWriter writer, Analyzer analyzer) {
401 RawOutputFormatter depFormatter = new RawOutputFormatter(writer);
402 RawSummaryFormatter summaryFormatter = new RawSummaryFormatter(writer);
403 for (Archive archive : sourceLocations) {
404 if (!archive.isEmpty()) {
405 analyzer.visitDependences(archive, summaryFormatter, SUMMARY);
406 if (analyzer.hasDependences(archive) && options.verbose != SUMMARY) {
407 analyzer.visitDependences(archive, depFormatter);
408 }
409 }
410 }
411 }
413 private boolean isValidClassName(String name) {
414 if (!Character.isJavaIdentifierStart(name.charAt(0))) {
415 return false;
416 }
417 for (int i=1; i < name.length(); i++) {
418 char c = name.charAt(i);
419 if (c != '.' && !Character.isJavaIdentifierPart(c)) {
420 return false;
421 }
422 }
423 return true;
424 }
426 /*
427 * Dep Filter configured based on the input jdeps option
428 * 1. -p and -regex to match target dependencies
429 * 2. -filter:package to filter out same-package dependencies
430 *
431 * This filter is applied when jdeps parses the class files
432 * and filtered dependencies are not stored in the Analyzer.
433 *
434 * -filter:archive is applied later in the Analyzer as the
435 * containing archive of a target class may not be known until
436 * the entire archive
437 */
438 class DependencyFilter implements Dependency.Filter {
439 final Dependency.Filter filter;
440 final Pattern filterPattern;
441 DependencyFilter() {
442 if (options.regex != null) {
443 this.filter = Dependencies.getRegexFilter(Pattern.compile(options.regex));
444 } else if (options.packageNames.size() > 0) {
445 this.filter = Dependencies.getPackageFilter(options.packageNames, false);
446 } else {
447 this.filter = null;
448 }
450 this.filterPattern =
451 options.filterRegex != null ? Pattern.compile(options.filterRegex) : null;
452 }
453 @Override
454 public boolean accepts(Dependency d) {
455 if (d.getOrigin().equals(d.getTarget())) {
456 return false;
457 }
458 String pn = d.getTarget().getPackageName();
459 if (options.filterSamePackage && d.getOrigin().getPackageName().equals(pn)) {
460 return false;
461 }
463 if (filterPattern != null && filterPattern.matcher(pn).matches()) {
464 return false;
465 }
466 return filter != null ? filter.accepts(d) : true;
467 }
468 }
470 /**
471 * Tests if the given class matches the pattern given in the -include option
472 * or if it's a public class if -apionly option is specified
473 */
474 private boolean matches(String classname, AccessFlags flags) {
475 if (options.apiOnly && !flags.is(AccessFlags.ACC_PUBLIC)) {
476 return false;
477 } else if (options.includePattern != null) {
478 return options.includePattern.matcher(classname.replace('/', '.')).matches();
479 } else {
480 return true;
481 }
482 }
484 private void findDependencies() throws IOException {
485 Dependency.Finder finder =
486 options.apiOnly ? Dependencies.getAPIFinder(AccessFlags.ACC_PROTECTED)
487 : Dependencies.getClassDependencyFinder();
488 Dependency.Filter filter = new DependencyFilter();
490 List<Archive> archives = new ArrayList<>();
491 Deque<String> roots = new LinkedList<>();
492 List<Path> paths = new ArrayList<>();
493 for (String s : classes) {
494 Path p = Paths.get(s);
495 if (Files.exists(p)) {
496 paths.add(p);
497 archives.add(Archive.getInstance(p));
498 } else {
499 if (isValidClassName(s)) {
500 roots.add(s);
501 } else {
502 warning("warn.invalid.arg", s);
503 }
504 }
505 }
506 sourceLocations.addAll(archives);
508 List<Archive> classpaths = new ArrayList<>(); // for class file lookup
509 classpaths.addAll(getClassPathArchives(options.classpath, paths));
510 if (options.includePattern != null) {
511 archives.addAll(classpaths);
512 }
513 classpaths.addAll(PlatformClassPath.getArchives());
515 // add all classpath archives to the source locations for reporting
516 sourceLocations.addAll(classpaths);
518 // warn about Multi-Release jars
519 for (Archive a : sourceLocations) {
520 if (a.reader().isMultiReleaseJar()) {
521 warning("warn.mrjar.usejdk9", a.getPathName());
522 }
523 }
525 // Work queue of names of classfiles to be searched.
526 // Entries will be unique, and for classes that do not yet have
527 // dependencies in the results map.
528 Deque<String> deque = new LinkedList<>();
529 Set<String> doneClasses = new HashSet<>();
531 // get the immediate dependencies of the input files
532 for (Archive a : archives) {
533 for (ClassFile cf : a.reader().getClassFiles()) {
534 String classFileName;
535 try {
536 classFileName = cf.getName();
537 } catch (ConstantPoolException e) {
538 throw new ClassFileError(e);
539 }
541 // tests if this class matches the -include or -apiOnly option if specified
542 if (!matches(classFileName, cf.access_flags)) {
543 continue;
544 }
546 if (!doneClasses.contains(classFileName)) {
547 doneClasses.add(classFileName);
548 }
550 for (Dependency d : finder.findDependencies(cf)) {
551 if (filter.accepts(d)) {
552 String cn = d.getTarget().getName();
553 if (!doneClasses.contains(cn) && !deque.contains(cn)) {
554 deque.add(cn);
555 }
556 a.addClass(d.getOrigin(), d.getTarget());
557 } else {
558 // ensure that the parsed class is added the archive
559 a.addClass(d.getOrigin());
560 }
561 }
562 for (String name : a.reader().skippedEntries()) {
563 warning("warn.skipped.entry", name, a.getPathName());
564 }
565 }
566 }
568 // add Archive for looking up classes from the classpath
569 // for transitive dependency analysis
570 Deque<String> unresolved = roots;
571 int depth = options.depth > 0 ? options.depth : Integer.MAX_VALUE;
572 do {
573 String name;
574 while ((name = unresolved.poll()) != null) {
575 if (doneClasses.contains(name)) {
576 continue;
577 }
578 ClassFile cf = null;
579 for (Archive a : classpaths) {
580 cf = a.reader().getClassFile(name);
581 if (cf != null) {
582 String classFileName;
583 try {
584 classFileName = cf.getName();
585 } catch (ConstantPoolException e) {
586 throw new ClassFileError(e);
587 }
588 if (!doneClasses.contains(classFileName)) {
589 // if name is a fully-qualified class name specified
590 // from command-line, this class might already be parsed
591 doneClasses.add(classFileName);
592 // process @jdk.Exported for JDK classes
593 if (isJDKArchive(a)) {
594 ((JDKArchive)a).processJdkExported(cf);
595 }
596 for (Dependency d : finder.findDependencies(cf)) {
597 if (depth == 0) {
598 // ignore the dependency
599 a.addClass(d.getOrigin());
600 break;
601 } else if (filter.accepts(d)) {
602 a.addClass(d.getOrigin(), d.getTarget());
603 String cn = d.getTarget().getName();
604 if (!doneClasses.contains(cn) && !deque.contains(cn)) {
605 deque.add(cn);
606 }
607 } else {
608 // ensure that the parsed class is added the archive
609 a.addClass(d.getOrigin());
610 }
611 }
612 }
613 break;
614 }
615 }
616 if (cf == null) {
617 doneClasses.add(name);
618 }
619 }
620 unresolved = deque;
621 deque = new LinkedList<>();
622 } while (!unresolved.isEmpty() && depth-- > 0);
623 }
625 public void handleOptions(String[] args) throws BadArgs {
626 // process options
627 for (int i=0; i < args.length; i++) {
628 if (args[i].charAt(0) == '-') {
629 String name = args[i];
630 Option option = getOption(name);
631 String param = null;
632 if (option.hasArg) {
633 if (name.startsWith("-") && name.indexOf('=') > 0) {
634 param = name.substring(name.indexOf('=') + 1, name.length());
635 } else if (i + 1 < args.length) {
636 param = args[++i];
637 }
638 if (param == null || param.isEmpty() || param.charAt(0) == '-') {
639 throw new BadArgs("err.missing.arg", name).showUsage(true);
640 }
641 }
642 option.process(this, name, param);
643 if (option.ignoreRest()) {
644 i = args.length;
645 }
646 } else {
647 // process rest of the input arguments
648 for (; i < args.length; i++) {
649 String name = args[i];
650 if (name.charAt(0) == '-') {
651 throw new BadArgs("err.option.after.class", name).showUsage(true);
652 }
653 classes.add(name);
654 }
655 }
656 }
657 }
659 private Option getOption(String name) throws BadArgs {
660 for (Option o : recognizedOptions) {
661 if (o.matches(name)) {
662 return o;
663 }
664 }
665 throw new BadArgs("err.unknown.option", name).showUsage(true);
666 }
668 private void reportError(String key, Object... args) {
669 log.println(getMessage("error.prefix") + " " + getMessage(key, args));
670 }
672 private void warning(String key, Object... args) {
673 log.println(getMessage("warn.prefix") + " " + getMessage(key, args));
674 }
676 private void showHelp() {
677 log.println(getMessage("main.usage", PROGNAME));
678 for (Option o : recognizedOptions) {
679 String name = o.aliases[0].substring(1); // there must always be at least one name
680 name = name.charAt(0) == '-' ? name.substring(1) : name;
681 if (o.isHidden() || name.equals("h") || name.startsWith("filter:")) {
682 continue;
683 }
684 log.println(getMessage("main.opt." + name));
685 }
686 }
688 private void showVersion(boolean full) {
689 log.println(version(full ? "full" : "release"));
690 }
692 private String version(String key) {
693 // key=version: mm.nn.oo[-milestone]
694 // key=full: mm.mm.oo[-milestone]-build
695 if (ResourceBundleHelper.versionRB == null) {
696 return System.getProperty("java.version");
697 }
698 try {
699 return ResourceBundleHelper.versionRB.getString(key);
700 } catch (MissingResourceException e) {
701 return getMessage("version.unknown", System.getProperty("java.version"));
702 }
703 }
705 static String getMessage(String key, Object... args) {
706 try {
707 return MessageFormat.format(ResourceBundleHelper.bundle.getString(key), args);
708 } catch (MissingResourceException e) {
709 throw new InternalError("Missing message: " + key);
710 }
711 }
713 private static class Options {
714 boolean help;
715 boolean version;
716 boolean fullVersion;
717 boolean showProfile;
718 boolean showSummary;
719 boolean apiOnly;
720 boolean showLabel;
721 boolean findJDKInternals;
722 boolean nowarning;
723 // default is to show package-level dependencies
724 // and filter references from same package
725 Analyzer.Type verbose = PACKAGE;
726 boolean filterSamePackage = true;
727 boolean filterSameArchive = false;
728 String filterRegex;
729 String dotOutputDir;
730 String classpath = "";
731 int depth = 1;
732 Set<String> packageNames = new HashSet<>();
733 String regex; // apply to the dependences
734 Pattern includePattern; // apply to classes
735 }
736 private static class ResourceBundleHelper {
737 static final ResourceBundle versionRB;
738 static final ResourceBundle bundle;
739 static final ResourceBundle jdkinternals;
741 static {
742 Locale locale = Locale.getDefault();
743 try {
744 bundle = ResourceBundle.getBundle("com.sun.tools.jdeps.resources.jdeps", locale);
745 } catch (MissingResourceException e) {
746 throw new InternalError("Cannot find jdeps resource bundle for locale " + locale);
747 }
748 try {
749 versionRB = ResourceBundle.getBundle("com.sun.tools.jdeps.resources.version");
750 } catch (MissingResourceException e) {
751 throw new InternalError("version.resource.missing");
752 }
753 try {
754 jdkinternals = ResourceBundle.getBundle("com.sun.tools.jdeps.resources.jdkinternals");
755 } catch (MissingResourceException e) {
756 throw new InternalError("Cannot find jdkinternals resource bundle");
757 }
758 }
759 }
761 /*
762 * Returns the list of Archive specified in cpaths and not included
763 * initialArchives
764 */
765 private List<Archive> getClassPathArchives(String cpaths, List<Path> initialArchives)
766 throws IOException
767 {
768 List<Archive> result = new ArrayList<>();
769 if (cpaths.isEmpty()) {
770 return result;
771 }
773 List<Path> paths = new ArrayList<>();
774 for (String p : cpaths.split(File.pathSeparator)) {
775 if (p.length() > 0) {
776 // wildcard to parse all JAR files e.g. -classpath dir/*
777 int i = p.lastIndexOf(".*");
778 if (i > 0) {
779 Path dir = Paths.get(p.substring(0, i));
780 try (DirectoryStream<Path> stream = Files.newDirectoryStream(dir, "*.jar")) {
781 for (Path entry : stream) {
782 paths.add(entry);
783 }
784 }
785 } else {
786 paths.add(Paths.get(p));
787 }
788 }
789 }
790 for (Path p : paths) {
791 if (Files.exists(p) && !hasSameFile(initialArchives, p)) {
792 result.add(Archive.getInstance(p));
793 }
794 }
795 return result;
796 }
798 private boolean hasSameFile(List<Path> paths, Path p2) throws IOException {
799 for (Path p1 : paths) {
800 if (Files.isSameFile(p1, p2)) {
801 return true;
802 }
803 }
804 return false;
805 }
807 class RawOutputFormatter implements Analyzer.Visitor {
808 private final PrintWriter writer;
809 private String pkg = "";
810 RawOutputFormatter(PrintWriter writer) {
811 this.writer = writer;
812 }
813 @Override
814 public void visitDependence(String origin, Archive originArchive,
815 String target, Archive targetArchive) {
816 String tag = toTag(target, targetArchive);
817 if (options.verbose == VERBOSE) {
818 writer.format(" %-50s -> %-50s %s%n", origin, target, tag);
819 } else {
820 if (!origin.equals(pkg)) {
821 pkg = origin;
822 writer.format(" %s (%s)%n", origin, originArchive.getName());
823 }
824 writer.format(" -> %-50s %s%n", target, tag);
825 }
826 }
827 }
829 class RawSummaryFormatter implements Analyzer.Visitor {
830 private final PrintWriter writer;
831 RawSummaryFormatter(PrintWriter writer) {
832 this.writer = writer;
833 }
834 @Override
835 public void visitDependence(String origin, Archive originArchive,
836 String target, Archive targetArchive) {
837 writer.format("%s -> %s", originArchive.getName(), targetArchive.getPathName());
838 if (options.showProfile && JDKArchive.isProfileArchive(targetArchive)) {
839 writer.format(" (%s)", target);
840 }
841 writer.format("%n");
842 }
843 }
845 class DotFileFormatter implements Analyzer.Visitor, AutoCloseable {
846 private final PrintWriter writer;
847 private final String name;
848 DotFileFormatter(PrintWriter writer, Archive archive) {
849 this.writer = writer;
850 this.name = archive.getName();
851 writer.format("digraph \"%s\" {%n", name);
852 writer.format(" // Path: %s%n", archive.getPathName());
853 }
855 @Override
856 public void close() {
857 writer.println("}");
858 }
860 @Override
861 public void visitDependence(String origin, Archive originArchive,
862 String target, Archive targetArchive) {
863 String tag = toTag(target, targetArchive);
864 writer.format(" %-50s -> \"%s\";%n",
865 String.format("\"%s\"", origin),
866 tag.isEmpty() ? target
867 : String.format("%s (%s)", target, tag));
868 }
869 }
871 class SummaryDotFile implements Analyzer.Visitor, AutoCloseable {
872 private final PrintWriter writer;
873 private final Analyzer.Type type;
874 private final Map<Archive, Map<Archive,StringBuilder>> edges = new HashMap<>();
875 SummaryDotFile(PrintWriter writer, Analyzer.Type type) {
876 this.writer = writer;
877 this.type = type;
878 writer.format("digraph \"summary\" {%n");
879 }
881 @Override
882 public void close() {
883 writer.println("}");
884 }
886 @Override
887 public void visitDependence(String origin, Archive originArchive,
888 String target, Archive targetArchive) {
889 String targetName = type == PACKAGE ? target : targetArchive.getName();
890 if (type == PACKAGE) {
891 String tag = toTag(target, targetArchive, type);
892 if (!tag.isEmpty())
893 targetName += " (" + tag + ")";
894 } else if (options.showProfile && JDKArchive.isProfileArchive(targetArchive)) {
895 targetName += " (" + target + ")";
896 }
897 String label = getLabel(originArchive, targetArchive);
898 writer.format(" %-50s -> \"%s\"%s;%n",
899 String.format("\"%s\"", origin), targetName, label);
900 }
902 String getLabel(Archive origin, Archive target) {
903 if (edges.isEmpty())
904 return "";
906 StringBuilder label = edges.get(origin).get(target);
907 return label == null ? "" : String.format(" [label=\"%s\",fontsize=9]", label.toString());
908 }
910 Analyzer.Visitor labelBuilder() {
911 // show the package-level dependencies as labels in the dot graph
912 return new Analyzer.Visitor() {
913 @Override
914 public void visitDependence(String origin, Archive originArchive,
915 String target, Archive targetArchive)
916 {
917 Map<Archive,StringBuilder> labels = edges.get(originArchive);
918 if (!edges.containsKey(originArchive)) {
919 edges.put(originArchive, labels = new HashMap<>());
920 }
921 StringBuilder sb = labels.get(targetArchive);
922 if (sb == null) {
923 labels.put(targetArchive, sb = new StringBuilder());
924 }
925 String tag = toTag(target, targetArchive, PACKAGE);
926 addLabel(sb, origin, target, tag);
927 }
929 void addLabel(StringBuilder label, String origin, String target, String tag) {
930 label.append(origin).append(" -> ").append(target);
931 if (!tag.isEmpty()) {
932 label.append(" (" + tag + ")");
933 }
934 label.append("\\n");
935 }
936 };
937 }
938 }
940 /**
941 * Test if the given archive is part of the JDK
942 */
943 private boolean isJDKArchive(Archive archive) {
944 return JDKArchive.class.isInstance(archive);
945 }
947 /**
948 * If the given archive is JDK archive, this method returns the profile name
949 * only if -profile option is specified; it accesses a private JDK API and
950 * the returned value will have "JDK internal API" prefix
951 *
952 * For non-JDK archives, this method returns the file name of the archive.
953 */
954 private String toTag(String name, Archive source, Analyzer.Type type) {
955 if (!isJDKArchive(source)) {
956 return source.getName();
957 }
959 JDKArchive jdk = (JDKArchive)source;
960 boolean isExported = false;
961 if (type == CLASS || type == VERBOSE) {
962 isExported = jdk.isExported(name);
963 } else {
964 isExported = jdk.isExportedPackage(name);
965 }
966 Profile p = getProfile(name, type);
967 if (isExported) {
968 // exported API
969 return options.showProfile && p != null ? p.profileName() : "";
970 } else {
971 return "JDK internal API (" + source.getName() + ")";
972 }
973 }
975 private String toTag(String name, Archive source) {
976 return toTag(name, source, options.verbose);
977 }
979 private Profile getProfile(String name, Analyzer.Type type) {
980 String pn = name;
981 if (type == CLASS || type == VERBOSE) {
982 int i = name.lastIndexOf('.');
983 pn = i > 0 ? name.substring(0, i) : "";
984 }
985 return Profile.getProfile(pn);
986 }
988 /**
989 * Returns the recommended replacement API for the given classname;
990 * or return null if replacement API is not known.
991 */
992 private String replacementFor(String cn) {
993 String name = cn;
994 String value = null;
995 while (value == null && name != null) {
996 try {
997 value = ResourceBundleHelper.jdkinternals.getString(name);
998 } catch (MissingResourceException e) {
999 // go up one subpackage level
1000 int i = name.lastIndexOf('.');
1001 name = i > 0 ? name.substring(0, i) : null;
1002 }
1003 }
1004 return value;
1005 };
1007 private void showReplacements(Analyzer analyzer) {
1008 Map<String,String> jdkinternals = new TreeMap<>();
1009 boolean useInternals = false;
1010 for (Archive source : sourceLocations) {
1011 useInternals = useInternals || analyzer.hasDependences(source);
1012 for (String cn : analyzer.dependences(source)) {
1013 String repl = replacementFor(cn);
1014 if (repl != null && !jdkinternals.containsKey(cn)) {
1015 jdkinternals.put(cn, repl);
1016 }
1017 }
1018 }
1019 if (useInternals) {
1020 log.println();
1021 warning("warn.replace.useJDKInternals", getMessage("jdeps.wiki.url"));
1022 }
1023 if (!jdkinternals.isEmpty()) {
1024 log.println();
1025 log.format("%-40s %s%n", "JDK Internal API", "Suggested Replacement");
1026 log.format("%-40s %s%n", "----------------", "---------------------");
1027 for (Map.Entry<String,String> e : jdkinternals.entrySet()) {
1028 log.format("%-40s %s%n", e.getKey(), e.getValue());
1029 }
1030 }
1032 }
1033 }