Wed, 30 Oct 2013 08:35:52 -0700
8027481: jdeps to handle classes with the same package name and correct profile for javax.crypto.*
Reviewed-by: alanb, dfuchs
1 /*
2 * Copyright (c) 2012, 2013, 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.jdeps.PlatformClassPath.JDKArchive;
34 import java.io.*;
35 import java.nio.file.DirectoryStream;
36 import java.nio.file.Files;
37 import java.nio.file.Path;
38 import java.nio.file.Paths;
39 import java.text.MessageFormat;
40 import java.util.*;
41 import java.util.regex.Pattern;
43 /**
44 * Implementation for the jdeps tool for static class dependency analysis.
45 */
46 class JdepsTask {
47 static class BadArgs extends Exception {
48 static final long serialVersionUID = 8765093759964640721L;
49 BadArgs(String key, Object... args) {
50 super(JdepsTask.getMessage(key, args));
51 this.key = key;
52 this.args = args;
53 }
55 BadArgs showUsage(boolean b) {
56 showUsage = b;
57 return this;
58 }
59 final String key;
60 final Object[] args;
61 boolean showUsage;
62 }
64 static abstract class Option {
65 Option(boolean hasArg, String... aliases) {
66 this.hasArg = hasArg;
67 this.aliases = aliases;
68 }
70 boolean isHidden() {
71 return false;
72 }
74 boolean matches(String opt) {
75 for (String a : aliases) {
76 if (a.equals(opt))
77 return true;
78 if (hasArg && opt.startsWith(a + "="))
79 return true;
80 }
81 return false;
82 }
84 boolean ignoreRest() {
85 return false;
86 }
88 abstract void process(JdepsTask task, String opt, String arg) throws BadArgs;
89 final boolean hasArg;
90 final String[] aliases;
91 }
93 static abstract class HiddenOption extends Option {
94 HiddenOption(boolean hasArg, String... aliases) {
95 super(hasArg, aliases);
96 }
98 boolean isHidden() {
99 return true;
100 }
101 }
103 static Option[] recognizedOptions = {
104 new Option(false, "-h", "-?", "-help") {
105 void process(JdepsTask task, String opt, String arg) {
106 task.options.help = true;
107 }
108 },
109 new Option(true, "-dotoutput") {
110 void process(JdepsTask task, String opt, String arg) throws BadArgs {
111 Path p = Paths.get(arg);
112 if (Files.exists(p) && (!Files.isDirectory(p) || !Files.isWritable(p))) {
113 throw new BadArgs("err.dot.output.path", arg);
114 }
115 task.options.dotOutputDir = arg;
116 }
117 },
118 new Option(false, "-s", "-summary") {
119 void process(JdepsTask task, String opt, String arg) {
120 task.options.showSummary = true;
121 task.options.verbose = Analyzer.Type.SUMMARY;
122 }
123 },
124 new Option(false, "-v", "-verbose",
125 "-verbose:package",
126 "-verbose:class")
127 {
128 void process(JdepsTask task, String opt, String arg) throws BadArgs {
129 switch (opt) {
130 case "-v":
131 case "-verbose":
132 task.options.verbose = Analyzer.Type.VERBOSE;
133 break;
134 case "-verbose:package":
135 task.options.verbose = Analyzer.Type.PACKAGE;
136 break;
137 case "-verbose:class":
138 task.options.verbose = Analyzer.Type.CLASS;
139 break;
140 default:
141 throw new BadArgs("err.invalid.arg.for.option", opt);
142 }
143 }
144 },
145 new Option(true, "-cp", "-classpath") {
146 void process(JdepsTask task, String opt, String arg) {
147 task.options.classpath = arg;
148 }
149 },
150 new Option(true, "-p", "-package") {
151 void process(JdepsTask task, String opt, String arg) {
152 task.options.packageNames.add(arg);
153 }
154 },
155 new Option(true, "-e", "-regex") {
156 void process(JdepsTask task, String opt, String arg) {
157 task.options.regex = arg;
158 }
159 },
160 new Option(true, "-include") {
161 void process(JdepsTask task, String opt, String arg) throws BadArgs {
162 task.options.includePattern = Pattern.compile(arg);
163 }
164 },
165 new Option(false, "-P", "-profile") {
166 void process(JdepsTask task, String opt, String arg) throws BadArgs {
167 task.options.showProfile = true;
168 if (Profile.getProfileCount() == 0) {
169 throw new BadArgs("err.option.unsupported", opt, getMessage("err.profiles.msg"));
170 }
171 }
172 },
173 new Option(false, "-apionly") {
174 void process(JdepsTask task, String opt, String arg) {
175 task.options.apiOnly = true;
176 }
177 },
178 new Option(false, "-R", "-recursive") {
179 void process(JdepsTask task, String opt, String arg) {
180 task.options.depth = 0;
181 }
182 },
183 new Option(false, "-version") {
184 void process(JdepsTask task, String opt, String arg) {
185 task.options.version = true;
186 }
187 },
188 new HiddenOption(false, "-fullversion") {
189 void process(JdepsTask task, String opt, String arg) {
190 task.options.fullVersion = true;
191 }
192 },
193 new HiddenOption(false, "-showlabel") {
194 void process(JdepsTask task, String opt, String arg) {
195 task.options.showLabel = true;
196 }
197 },
198 new HiddenOption(true, "-depth") {
199 void process(JdepsTask task, String opt, String arg) throws BadArgs {
200 try {
201 task.options.depth = Integer.parseInt(arg);
202 } catch (NumberFormatException e) {
203 throw new BadArgs("err.invalid.arg.for.option", opt);
204 }
205 }
206 },
207 };
209 private static final String PROGNAME = "jdeps";
210 private final Options options = new Options();
211 private final List<String> classes = new ArrayList<String>();
213 private PrintWriter log;
214 void setLog(PrintWriter out) {
215 log = out;
216 }
218 /**
219 * Result codes.
220 */
221 static final int EXIT_OK = 0, // Completed with no errors.
222 EXIT_ERROR = 1, // Completed but reported errors.
223 EXIT_CMDERR = 2, // Bad command-line arguments
224 EXIT_SYSERR = 3, // System error or resource exhaustion.
225 EXIT_ABNORMAL = 4;// terminated abnormally
227 int run(String[] args) {
228 if (log == null) {
229 log = new PrintWriter(System.out);
230 }
231 try {
232 handleOptions(args);
233 if (options.help) {
234 showHelp();
235 }
236 if (options.version || options.fullVersion) {
237 showVersion(options.fullVersion);
238 }
239 if (classes.isEmpty() && options.includePattern == null) {
240 if (options.help || options.version || options.fullVersion) {
241 return EXIT_OK;
242 } else {
243 showHelp();
244 return EXIT_CMDERR;
245 }
246 }
247 if (options.regex != null && options.packageNames.size() > 0) {
248 showHelp();
249 return EXIT_CMDERR;
250 }
251 if (options.showSummary && options.verbose != Analyzer.Type.SUMMARY) {
252 showHelp();
253 return EXIT_CMDERR;
254 }
255 boolean ok = run();
256 return ok ? EXIT_OK : EXIT_ERROR;
257 } catch (BadArgs e) {
258 reportError(e.key, e.args);
259 if (e.showUsage) {
260 log.println(getMessage("main.usage.summary", PROGNAME));
261 }
262 return EXIT_CMDERR;
263 } catch (IOException e) {
264 return EXIT_ABNORMAL;
265 } finally {
266 log.flush();
267 }
268 }
270 private final List<Archive> sourceLocations = new ArrayList<>();
271 private boolean run() throws IOException {
272 findDependencies();
273 Analyzer analyzer = new Analyzer(options.verbose);
274 analyzer.run(sourceLocations);
275 if (options.dotOutputDir != null) {
276 Path dir = Paths.get(options.dotOutputDir);
277 Files.createDirectories(dir);
278 generateDotFiles(dir, analyzer);
279 } else {
280 printRawOutput(log, analyzer);
281 }
282 return true;
283 }
285 private void generateDotFiles(Path dir, Analyzer analyzer) throws IOException {
286 Path summary = dir.resolve("summary.dot");
287 boolean verbose = options.verbose == Analyzer.Type.VERBOSE;
288 DotGraph<?> graph = verbose ? new DotSummaryForPackage()
289 : new DotSummaryForArchive();
290 for (Archive archive : sourceLocations) {
291 analyzer.visitArchiveDependences(archive, graph);
292 if (verbose || options.showLabel) {
293 // traverse detailed dependences to generate package-level
294 // summary or build labels for edges
295 analyzer.visitDependences(archive, graph);
296 }
297 }
298 try (PrintWriter sw = new PrintWriter(Files.newOutputStream(summary))) {
299 graph.writeTo(sw);
300 }
301 // output individual .dot file for each archive
302 if (options.verbose != Analyzer.Type.SUMMARY) {
303 for (Archive archive : sourceLocations) {
304 if (analyzer.hasDependences(archive)) {
305 Path dotfile = dir.resolve(archive.getFileName() + ".dot");
306 try (PrintWriter pw = new PrintWriter(Files.newOutputStream(dotfile));
307 DotFileFormatter formatter = new DotFileFormatter(pw, archive)) {
308 analyzer.visitDependences(archive, formatter);
309 }
310 }
311 }
312 }
313 }
315 private void printRawOutput(PrintWriter writer, Analyzer analyzer) {
316 for (Archive archive : sourceLocations) {
317 RawOutputFormatter formatter = new RawOutputFormatter(writer);
318 analyzer.visitArchiveDependences(archive, formatter);
319 if (options.verbose != Analyzer.Type.SUMMARY) {
320 analyzer.visitDependences(archive, formatter);
321 }
322 }
323 }
324 private boolean isValidClassName(String name) {
325 if (!Character.isJavaIdentifierStart(name.charAt(0))) {
326 return false;
327 }
328 for (int i=1; i < name.length(); i++) {
329 char c = name.charAt(i);
330 if (c != '.' && !Character.isJavaIdentifierPart(c)) {
331 return false;
332 }
333 }
334 return true;
335 }
337 private Dependency.Filter getDependencyFilter() {
338 if (options.regex != null) {
339 return Dependencies.getRegexFilter(Pattern.compile(options.regex));
340 } else if (options.packageNames.size() > 0) {
341 return Dependencies.getPackageFilter(options.packageNames, false);
342 } else {
343 return new Dependency.Filter() {
344 @Override
345 public boolean accepts(Dependency dependency) {
346 return !dependency.getOrigin().equals(dependency.getTarget());
347 }
348 };
349 }
350 }
352 private boolean matches(String classname, AccessFlags flags) {
353 if (options.apiOnly && !flags.is(AccessFlags.ACC_PUBLIC)) {
354 return false;
355 } else if (options.includePattern != null) {
356 return options.includePattern.matcher(classname.replace('/', '.')).matches();
357 } else {
358 return true;
359 }
360 }
362 private void findDependencies() throws IOException {
363 Dependency.Finder finder =
364 options.apiOnly ? Dependencies.getAPIFinder(AccessFlags.ACC_PROTECTED)
365 : Dependencies.getClassDependencyFinder();
366 Dependency.Filter filter = getDependencyFilter();
368 List<Archive> archives = new ArrayList<>();
369 Deque<String> roots = new LinkedList<>();
370 for (String s : classes) {
371 Path p = Paths.get(s);
372 if (Files.exists(p)) {
373 archives.add(new Archive(p, ClassFileReader.newInstance(p)));
374 } else {
375 if (isValidClassName(s)) {
376 roots.add(s);
377 } else {
378 warning("warn.invalid.arg", s);
379 }
380 }
381 }
382 sourceLocations.addAll(archives);
384 List<Archive> classpaths = new ArrayList<>(); // for class file lookup
385 classpaths.addAll(getClassPathArchives(options.classpath));
386 if (options.includePattern != null) {
387 archives.addAll(classpaths);
388 }
389 classpaths.addAll(PlatformClassPath.getArchives());
391 // add all classpath archives to the source locations for reporting
392 sourceLocations.addAll(classpaths);
394 // Work queue of names of classfiles to be searched.
395 // Entries will be unique, and for classes that do not yet have
396 // dependencies in the results map.
397 Deque<String> deque = new LinkedList<>();
398 Set<String> doneClasses = new HashSet<>();
400 // get the immediate dependencies of the input files
401 for (Archive a : archives) {
402 for (ClassFile cf : a.reader().getClassFiles()) {
403 String classFileName;
404 try {
405 classFileName = cf.getName();
406 } catch (ConstantPoolException e) {
407 throw new ClassFileError(e);
408 }
410 if (matches(classFileName, cf.access_flags)) {
411 if (!doneClasses.contains(classFileName)) {
412 doneClasses.add(classFileName);
413 }
414 for (Dependency d : finder.findDependencies(cf)) {
415 if (filter.accepts(d)) {
416 String cn = d.getTarget().getName();
417 if (!doneClasses.contains(cn) && !deque.contains(cn)) {
418 deque.add(cn);
419 }
420 a.addClass(d.getOrigin(), d.getTarget());
421 }
422 }
423 }
424 }
425 }
427 // add Archive for looking up classes from the classpath
428 // for transitive dependency analysis
429 Deque<String> unresolved = roots;
430 int depth = options.depth > 0 ? options.depth : Integer.MAX_VALUE;
431 do {
432 String name;
433 while ((name = unresolved.poll()) != null) {
434 if (doneClasses.contains(name)) {
435 continue;
436 }
437 ClassFile cf = null;
438 for (Archive a : classpaths) {
439 cf = a.reader().getClassFile(name);
440 if (cf != null) {
441 String classFileName;
442 try {
443 classFileName = cf.getName();
444 } catch (ConstantPoolException e) {
445 throw new ClassFileError(e);
446 }
447 if (!doneClasses.contains(classFileName)) {
448 // if name is a fully-qualified class name specified
449 // from command-line, this class might already be parsed
450 doneClasses.add(classFileName);
451 for (Dependency d : finder.findDependencies(cf)) {
452 if (depth == 0) {
453 // ignore the dependency
454 a.addClass(d.getOrigin());
455 break;
456 } else if (filter.accepts(d)) {
457 a.addClass(d.getOrigin(), d.getTarget());
458 String cn = d.getTarget().getName();
459 if (!doneClasses.contains(cn) && !deque.contains(cn)) {
460 deque.add(cn);
461 }
462 }
463 }
464 }
465 break;
466 }
467 }
468 if (cf == null) {
469 doneClasses.add(name);
470 }
471 }
472 unresolved = deque;
473 deque = new LinkedList<>();
474 } while (!unresolved.isEmpty() && depth-- > 0);
475 }
477 public void handleOptions(String[] args) throws BadArgs {
478 // process options
479 for (int i=0; i < args.length; i++) {
480 if (args[i].charAt(0) == '-') {
481 String name = args[i];
482 Option option = getOption(name);
483 String param = null;
484 if (option.hasArg) {
485 if (name.startsWith("-") && name.indexOf('=') > 0) {
486 param = name.substring(name.indexOf('=') + 1, name.length());
487 } else if (i + 1 < args.length) {
488 param = args[++i];
489 }
490 if (param == null || param.isEmpty() || param.charAt(0) == '-') {
491 throw new BadArgs("err.missing.arg", name).showUsage(true);
492 }
493 }
494 option.process(this, name, param);
495 if (option.ignoreRest()) {
496 i = args.length;
497 }
498 } else {
499 // process rest of the input arguments
500 for (; i < args.length; i++) {
501 String name = args[i];
502 if (name.charAt(0) == '-') {
503 throw new BadArgs("err.option.after.class", name).showUsage(true);
504 }
505 classes.add(name);
506 }
507 }
508 }
509 }
511 private Option getOption(String name) throws BadArgs {
512 for (Option o : recognizedOptions) {
513 if (o.matches(name)) {
514 return o;
515 }
516 }
517 throw new BadArgs("err.unknown.option", name).showUsage(true);
518 }
520 private void reportError(String key, Object... args) {
521 log.println(getMessage("error.prefix") + " " + getMessage(key, args));
522 }
524 private void warning(String key, Object... args) {
525 log.println(getMessage("warn.prefix") + " " + getMessage(key, args));
526 }
528 private void showHelp() {
529 log.println(getMessage("main.usage", PROGNAME));
530 for (Option o : recognizedOptions) {
531 String name = o.aliases[0].substring(1); // there must always be at least one name
532 name = name.charAt(0) == '-' ? name.substring(1) : name;
533 if (o.isHidden() || name.equals("h")) {
534 continue;
535 }
536 log.println(getMessage("main.opt." + name));
537 }
538 }
540 private void showVersion(boolean full) {
541 log.println(version(full ? "full" : "release"));
542 }
544 private String version(String key) {
545 // key=version: mm.nn.oo[-milestone]
546 // key=full: mm.mm.oo[-milestone]-build
547 if (ResourceBundleHelper.versionRB == null) {
548 return System.getProperty("java.version");
549 }
550 try {
551 return ResourceBundleHelper.versionRB.getString(key);
552 } catch (MissingResourceException e) {
553 return getMessage("version.unknown", System.getProperty("java.version"));
554 }
555 }
557 static String getMessage(String key, Object... args) {
558 try {
559 return MessageFormat.format(ResourceBundleHelper.bundle.getString(key), args);
560 } catch (MissingResourceException e) {
561 throw new InternalError("Missing message: " + key);
562 }
563 }
565 private static class Options {
566 boolean help;
567 boolean version;
568 boolean fullVersion;
569 boolean showProfile;
570 boolean showSummary;
571 boolean wildcard;
572 boolean apiOnly;
573 boolean showLabel;
574 String dotOutputDir;
575 String classpath = "";
576 int depth = 1;
577 Analyzer.Type verbose = Analyzer.Type.PACKAGE;
578 Set<String> packageNames = new HashSet<>();
579 String regex; // apply to the dependences
580 Pattern includePattern; // apply to classes
581 }
582 private static class ResourceBundleHelper {
583 static final ResourceBundle versionRB;
584 static final ResourceBundle bundle;
586 static {
587 Locale locale = Locale.getDefault();
588 try {
589 bundle = ResourceBundle.getBundle("com.sun.tools.jdeps.resources.jdeps", locale);
590 } catch (MissingResourceException e) {
591 throw new InternalError("Cannot find jdeps resource bundle for locale " + locale);
592 }
593 try {
594 versionRB = ResourceBundle.getBundle("com.sun.tools.jdeps.resources.version");
595 } catch (MissingResourceException e) {
596 throw new InternalError("version.resource.missing");
597 }
598 }
599 }
601 private List<Archive> getArchives(List<String> filenames) throws IOException {
602 List<Archive> result = new ArrayList<Archive>();
603 for (String s : filenames) {
604 Path p = Paths.get(s);
605 if (Files.exists(p)) {
606 result.add(new Archive(p, ClassFileReader.newInstance(p)));
607 } else {
608 warning("warn.file.not.exist", s);
609 }
610 }
611 return result;
612 }
614 private List<Archive> getClassPathArchives(String paths) throws IOException {
615 List<Archive> result = new ArrayList<>();
616 if (paths.isEmpty()) {
617 return result;
618 }
619 for (String p : paths.split(File.pathSeparator)) {
620 if (p.length() > 0) {
621 List<Path> files = new ArrayList<>();
622 // wildcard to parse all JAR files e.g. -classpath dir/*
623 int i = p.lastIndexOf(".*");
624 if (i > 0) {
625 Path dir = Paths.get(p.substring(0, i));
626 try (DirectoryStream<Path> stream = Files.newDirectoryStream(dir, "*.jar")) {
627 for (Path entry : stream) {
628 files.add(entry);
629 }
630 }
631 } else {
632 files.add(Paths.get(p));
633 }
634 for (Path f : files) {
635 if (Files.exists(f)) {
636 result.add(new Archive(f, ClassFileReader.newInstance(f)));
637 }
638 }
639 }
640 }
641 return result;
642 }
644 /**
645 * If the given archive is JDK archive and non-null Profile,
646 * this method returns the profile name only if -profile option is specified;
647 * a null profile indicates it accesses a private JDK API and this method
648 * will return "JDK internal API".
649 *
650 * For non-JDK archives, this method returns the file name of the archive.
651 */
652 private String getProfileArchiveInfo(Archive source, Profile profile) {
653 if (options.showProfile && profile != null)
654 return profile.toString();
656 if (source instanceof JDKArchive) {
657 return profile == null ? "JDK internal API (" + source.getFileName() + ")" : "";
658 }
659 return source.getFileName();
660 }
662 /**
663 * Returns the profile name or "JDK internal API" for JDK archive;
664 * otherwise empty string.
665 */
666 private String profileName(Archive archive, Profile profile) {
667 if (archive instanceof JDKArchive) {
668 return Objects.toString(profile, "JDK internal API");
669 } else {
670 return "";
671 }
672 }
674 class RawOutputFormatter implements Analyzer.Visitor {
675 private final PrintWriter writer;
676 RawOutputFormatter(PrintWriter writer) {
677 this.writer = writer;
678 }
680 private String pkg = "";
681 @Override
682 public void visitDependence(String origin, Archive source,
683 String target, Archive archive, Profile profile) {
684 if (!origin.equals(pkg)) {
685 pkg = origin;
686 writer.format(" %s (%s)%n", origin, source.getFileName());
687 }
688 writer.format(" -> %-50s %s%n", target, getProfileArchiveInfo(archive, profile));
689 }
691 @Override
692 public void visitArchiveDependence(Archive origin, Archive target, Profile profile) {
693 writer.format("%s -> %s", origin.getPathName(), target.getPathName());
694 if (options.showProfile && profile != null) {
695 writer.format(" (%s)%n", profile);
696 } else {
697 writer.format("%n");
698 }
699 }
700 }
702 class DotFileFormatter extends DotGraph<String> implements AutoCloseable {
703 private final PrintWriter writer;
704 private final String name;
705 DotFileFormatter(PrintWriter writer, Archive archive) {
706 this.writer = writer;
707 this.name = archive.getFileName();
708 writer.format("digraph \"%s\" {%n", name);
709 writer.format(" // Path: %s%n", archive.getPathName());
710 }
712 @Override
713 public void close() {
714 writer.println("}");
715 }
717 @Override
718 public void visitDependence(String origin, Archive source,
719 String target, Archive archive, Profile profile) {
720 // if -P option is specified, package name -> profile will
721 // be shown and filter out multiple same edges.
722 String name = getProfileArchiveInfo(archive, profile);
723 writeEdge(writer, new Edge(origin, target, getProfileArchiveInfo(archive, profile)));
724 }
725 @Override
726 public void visitArchiveDependence(Archive origin, Archive target, Profile profile) {
727 throw new UnsupportedOperationException();
728 }
729 }
731 class DotSummaryForArchive extends DotGraph<Archive> {
732 @Override
733 public void visitDependence(String origin, Archive source,
734 String target, Archive archive, Profile profile) {
735 Edge e = findEdge(source, archive);
736 assert e != null;
737 // add the dependency to the label if enabled and not compact1
738 if (profile == Profile.COMPACT1) {
739 return;
740 }
741 e.addLabel(origin, target, profileName(archive, profile));
742 }
743 @Override
744 public void visitArchiveDependence(Archive origin, Archive target, Profile profile) {
745 // add an edge with the archive's name with no tag
746 // so that there is only one node for each JDK archive
747 // while there may be edges to different profiles
748 Edge e = addEdge(origin, target, "");
749 if (target instanceof JDKArchive) {
750 // add a label to print the profile
751 if (profile == null) {
752 e.addLabel("JDK internal API");
753 } else if (options.showProfile && !options.showLabel) {
754 e.addLabel(profile.toString());
755 }
756 }
757 }
758 }
760 // DotSummaryForPackage generates the summary.dot file for verbose mode
761 // (-v or -verbose option) that includes all class dependencies.
762 // The summary.dot file shows package-level dependencies.
763 class DotSummaryForPackage extends DotGraph<String> {
764 private String packageOf(String cn) {
765 int i = cn.lastIndexOf('.');
766 return i > 0 ? cn.substring(0, i) : "<unnamed>";
767 }
768 @Override
769 public void visitDependence(String origin, Archive source,
770 String target, Archive archive, Profile profile) {
771 // add a package dependency edge
772 String from = packageOf(origin);
773 String to = packageOf(target);
774 Edge e = addEdge(from, to, getProfileArchiveInfo(archive, profile));
776 // add the dependency to the label if enabled and not compact1
777 if (!options.showLabel || profile == Profile.COMPACT1) {
778 return;
779 }
781 // trim the package name of origin to shorten the label
782 int i = origin.lastIndexOf('.');
783 String n1 = i < 0 ? origin : origin.substring(i+1);
784 e.addLabel(n1, target, profileName(archive, profile));
785 }
786 @Override
787 public void visitArchiveDependence(Archive origin, Archive target, Profile profile) {
788 // nop
789 }
790 }
791 abstract class DotGraph<T> implements Analyzer.Visitor {
792 private final Set<Edge> edges = new LinkedHashSet<>();
793 private Edge curEdge;
794 public void writeTo(PrintWriter writer) {
795 writer.format("digraph \"summary\" {%n");
796 for (Edge e: edges) {
797 writeEdge(writer, e);
798 }
799 writer.println("}");
800 }
802 void writeEdge(PrintWriter writer, Edge e) {
803 writer.format(" %-50s -> \"%s\"%s;%n",
804 String.format("\"%s\"", e.from.toString()),
805 e.tag.isEmpty() ? e.to
806 : String.format("%s (%s)", e.to, e.tag),
807 getLabel(e));
808 }
810 Edge addEdge(T origin, T target, String tag) {
811 Edge e = new Edge(origin, target, tag);
812 if (e.equals(curEdge)) {
813 return curEdge;
814 }
816 if (edges.contains(e)) {
817 for (Edge e1 : edges) {
818 if (e.equals(e1)) {
819 curEdge = e1;
820 }
821 }
822 } else {
823 edges.add(e);
824 curEdge = e;
825 }
826 return curEdge;
827 }
829 Edge findEdge(T origin, T target) {
830 for (Edge e : edges) {
831 if (e.from.equals(origin) && e.to.equals(target)) {
832 return e;
833 }
834 }
835 return null;
836 }
838 String getLabel(Edge e) {
839 String label = e.label.toString();
840 return label.isEmpty() ? "" : String.format("[label=\"%s\",fontsize=9]", label);
841 }
843 class Edge {
844 final T from;
845 final T to;
846 final String tag; // optional tag
847 final StringBuilder label = new StringBuilder();
848 Edge(T from, T to, String tag) {
849 this.from = from;
850 this.to = to;
851 this.tag = tag;
852 }
853 void addLabel(String s) {
854 label.append(s).append("\\n");
855 }
856 void addLabel(String origin, String target, String profile) {
857 label.append(origin).append(" -> ").append(target);
858 if (!profile.isEmpty()) {
859 label.append(" (" + profile + ")");
860 }
861 label.append("\\n");
862 }
863 @Override @SuppressWarnings("unchecked")
864 public boolean equals(Object o) {
865 if (o instanceof DotGraph<?>.Edge) {
866 DotGraph<?>.Edge e = (DotGraph<?>.Edge)o;
867 return this.from.equals(e.from) &&
868 this.to.equals(e.to) &&
869 this.tag.equals(e.tag);
870 }
871 return false;
872 }
873 @Override
874 public int hashCode() {
875 int hash = 7;
876 hash = 67 * hash + Objects.hashCode(this.from) +
877 Objects.hashCode(this.to) + Objects.hashCode(this.tag);
878 return hash;
879 }
880 }
881 }
882 }