| /* |
| * Copyright (c) 2012, 2013, Oracle and/or its affiliates. All rights reserved. |
| * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. |
| * |
| * This code is free software; you can redistribute it and/or modify it |
| * under the terms of the GNU General Public License version 2 only, as |
| * published by the Free Software Foundation. Oracle designates this |
| * particular file as subject to the "Classpath" exception as provided |
| * by Oracle in the LICENSE file that accompanied this code. |
| * |
| * This code is distributed in the hope that it will be useful, but WITHOUT |
| * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or |
| * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License |
| * version 2 for more details (a copy is included in the LICENSE file that |
| * accompanied this code). |
| * |
| * You should have received a copy of the GNU General Public License version |
| * 2 along with this work; if not, write to the Free Software Foundation, |
| * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. |
| * |
| * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA |
| * or visit www.oracle.com if you need additional information or have any |
| * questions. |
| */ |
| package com.sun.tools.jdeps; |
| |
| import com.sun.tools.classfile.AccessFlags; |
| import com.sun.tools.classfile.ClassFile; |
| import com.sun.tools.classfile.ConstantPoolException; |
| import com.sun.tools.classfile.Dependencies; |
| import com.sun.tools.classfile.Dependencies.ClassFileError; |
| import com.sun.tools.classfile.Dependency; |
| import com.sun.tools.jdeps.PlatformClassPath.JDKArchive; |
| import java.io.*; |
| import java.nio.file.DirectoryStream; |
| import java.nio.file.Files; |
| import java.nio.file.Path; |
| import java.nio.file.Paths; |
| import java.text.MessageFormat; |
| import java.util.*; |
| import java.util.regex.Pattern; |
| |
| /** |
| * Implementation for the jdeps tool for static class dependency analysis. |
| */ |
| class JdepsTask { |
| static class BadArgs extends Exception { |
| static final long serialVersionUID = 8765093759964640721L; |
| BadArgs(String key, Object... args) { |
| super(JdepsTask.getMessage(key, args)); |
| this.key = key; |
| this.args = args; |
| } |
| |
| BadArgs showUsage(boolean b) { |
| showUsage = b; |
| return this; |
| } |
| final String key; |
| final Object[] args; |
| boolean showUsage; |
| } |
| |
| static abstract class Option { |
| Option(boolean hasArg, String... aliases) { |
| this.hasArg = hasArg; |
| this.aliases = aliases; |
| } |
| |
| boolean isHidden() { |
| return false; |
| } |
| |
| boolean matches(String opt) { |
| for (String a : aliases) { |
| if (a.equals(opt)) |
| return true; |
| if (hasArg && opt.startsWith(a + "=")) |
| return true; |
| } |
| return false; |
| } |
| |
| boolean ignoreRest() { |
| return false; |
| } |
| |
| abstract void process(JdepsTask task, String opt, String arg) throws BadArgs; |
| final boolean hasArg; |
| final String[] aliases; |
| } |
| |
| static abstract class HiddenOption extends Option { |
| HiddenOption(boolean hasArg, String... aliases) { |
| super(hasArg, aliases); |
| } |
| |
| boolean isHidden() { |
| return true; |
| } |
| } |
| |
| static Option[] recognizedOptions = { |
| new Option(false, "-h", "-?", "-help") { |
| void process(JdepsTask task, String opt, String arg) { |
| task.options.help = true; |
| } |
| }, |
| new Option(true, "-dotoutput") { |
| void process(JdepsTask task, String opt, String arg) throws BadArgs { |
| Path p = Paths.get(arg); |
| if (Files.exists(p) && (!Files.isDirectory(p) || !Files.isWritable(p))) { |
| throw new BadArgs("err.dot.output.path", arg); |
| } |
| task.options.dotOutputDir = arg; |
| } |
| }, |
| new Option(false, "-s", "-summary") { |
| void process(JdepsTask task, String opt, String arg) { |
| task.options.showSummary = true; |
| task.options.verbose = Analyzer.Type.SUMMARY; |
| } |
| }, |
| new Option(false, "-v", "-verbose", |
| "-verbose:package", |
| "-verbose:class") |
| { |
| void process(JdepsTask task, String opt, String arg) throws BadArgs { |
| switch (opt) { |
| case "-v": |
| case "-verbose": |
| task.options.verbose = Analyzer.Type.VERBOSE; |
| break; |
| case "-verbose:package": |
| task.options.verbose = Analyzer.Type.PACKAGE; |
| break; |
| case "-verbose:class": |
| task.options.verbose = Analyzer.Type.CLASS; |
| break; |
| default: |
| throw new BadArgs("err.invalid.arg.for.option", opt); |
| } |
| } |
| }, |
| new Option(true, "-cp", "-classpath") { |
| void process(JdepsTask task, String opt, String arg) { |
| task.options.classpath = arg; |
| } |
| }, |
| new Option(true, "-p", "-package") { |
| void process(JdepsTask task, String opt, String arg) { |
| task.options.packageNames.add(arg); |
| } |
| }, |
| new Option(true, "-e", "-regex") { |
| void process(JdepsTask task, String opt, String arg) { |
| task.options.regex = arg; |
| } |
| }, |
| new Option(true, "-include") { |
| void process(JdepsTask task, String opt, String arg) throws BadArgs { |
| task.options.includePattern = Pattern.compile(arg); |
| } |
| }, |
| new Option(false, "-P", "-profile") { |
| void process(JdepsTask task, String opt, String arg) throws BadArgs { |
| task.options.showProfile = true; |
| if (Profile.getProfileCount() == 0) { |
| throw new BadArgs("err.option.unsupported", opt, getMessage("err.profiles.msg")); |
| } |
| } |
| }, |
| new Option(false, "-apionly") { |
| void process(JdepsTask task, String opt, String arg) { |
| task.options.apiOnly = true; |
| } |
| }, |
| new Option(false, "-R", "-recursive") { |
| void process(JdepsTask task, String opt, String arg) { |
| task.options.depth = 0; |
| } |
| }, |
| new Option(false, "-jdkinternals") { |
| void process(JdepsTask task, String opt, String arg) { |
| task.options.findJDKInternals = true; |
| task.options.verbose = Analyzer.Type.CLASS; |
| if (task.options.includePattern == null) { |
| task.options.includePattern = Pattern.compile(".*"); |
| } |
| } |
| }, |
| new Option(false, "-version") { |
| void process(JdepsTask task, String opt, String arg) { |
| task.options.version = true; |
| } |
| }, |
| new HiddenOption(false, "-fullversion") { |
| void process(JdepsTask task, String opt, String arg) { |
| task.options.fullVersion = true; |
| } |
| }, |
| new HiddenOption(false, "-showlabel") { |
| void process(JdepsTask task, String opt, String arg) { |
| task.options.showLabel = true; |
| } |
| }, |
| new HiddenOption(true, "-depth") { |
| void process(JdepsTask task, String opt, String arg) throws BadArgs { |
| try { |
| task.options.depth = Integer.parseInt(arg); |
| } catch (NumberFormatException e) { |
| throw new BadArgs("err.invalid.arg.for.option", opt); |
| } |
| } |
| }, |
| }; |
| |
| private static final String PROGNAME = "jdeps"; |
| private final Options options = new Options(); |
| private final List<String> classes = new ArrayList<>(); |
| |
| private PrintWriter log; |
| void setLog(PrintWriter out) { |
| log = out; |
| } |
| |
| /** |
| * Result codes. |
| */ |
| static final int EXIT_OK = 0, // Completed with no errors. |
| EXIT_ERROR = 1, // Completed but reported errors. |
| EXIT_CMDERR = 2, // Bad command-line arguments |
| EXIT_SYSERR = 3, // System error or resource exhaustion. |
| EXIT_ABNORMAL = 4;// terminated abnormally |
| |
| int run(String[] args) { |
| if (log == null) { |
| log = new PrintWriter(System.out); |
| } |
| try { |
| handleOptions(args); |
| if (options.help) { |
| showHelp(); |
| } |
| if (options.version || options.fullVersion) { |
| showVersion(options.fullVersion); |
| } |
| if (classes.isEmpty() && options.includePattern == null) { |
| if (options.help || options.version || options.fullVersion) { |
| return EXIT_OK; |
| } else { |
| showHelp(); |
| return EXIT_CMDERR; |
| } |
| } |
| if (options.regex != null && options.packageNames.size() > 0) { |
| showHelp(); |
| return EXIT_CMDERR; |
| } |
| if (options.findJDKInternals && |
| (options.regex != null || options.packageNames.size() > 0 || options.showSummary)) { |
| showHelp(); |
| return EXIT_CMDERR; |
| } |
| if (options.showSummary && options.verbose != Analyzer.Type.SUMMARY) { |
| showHelp(); |
| return EXIT_CMDERR; |
| } |
| boolean ok = run(); |
| return ok ? EXIT_OK : EXIT_ERROR; |
| } catch (BadArgs e) { |
| reportError(e.key, e.args); |
| if (e.showUsage) { |
| log.println(getMessage("main.usage.summary", PROGNAME)); |
| } |
| return EXIT_CMDERR; |
| } catch (IOException e) { |
| return EXIT_ABNORMAL; |
| } finally { |
| log.flush(); |
| } |
| } |
| |
| private final List<Archive> sourceLocations = new ArrayList<>(); |
| private boolean run() throws IOException { |
| findDependencies(); |
| Analyzer analyzer = new Analyzer(options.verbose); |
| analyzer.run(sourceLocations); |
| if (options.dotOutputDir != null) { |
| Path dir = Paths.get(options.dotOutputDir); |
| Files.createDirectories(dir); |
| generateDotFiles(dir, analyzer); |
| } else { |
| printRawOutput(log, analyzer); |
| } |
| return true; |
| } |
| |
| private void generateDotFiles(Path dir, Analyzer analyzer) throws IOException { |
| Path summary = dir.resolve("summary.dot"); |
| boolean verbose = options.verbose == Analyzer.Type.VERBOSE; |
| DotGraph<?> graph = verbose ? new DotSummaryForPackage() |
| : new DotSummaryForArchive(); |
| for (Archive archive : sourceLocations) { |
| analyzer.visitArchiveDependences(archive, graph); |
| if (verbose || options.showLabel) { |
| // traverse detailed dependences to generate package-level |
| // summary or build labels for edges |
| analyzer.visitDependences(archive, graph); |
| } |
| } |
| try (PrintWriter sw = new PrintWriter(Files.newOutputStream(summary))) { |
| graph.writeTo(sw); |
| } |
| // output individual .dot file for each archive |
| if (options.verbose != Analyzer.Type.SUMMARY) { |
| for (Archive archive : sourceLocations) { |
| if (analyzer.hasDependences(archive)) { |
| Path dotfile = dir.resolve(archive.getFileName() + ".dot"); |
| try (PrintWriter pw = new PrintWriter(Files.newOutputStream(dotfile)); |
| DotFileFormatter formatter = new DotFileFormatter(pw, archive)) { |
| analyzer.visitDependences(archive, formatter); |
| } |
| } |
| } |
| } |
| } |
| |
| private void printRawOutput(PrintWriter writer, Analyzer analyzer) { |
| for (Archive archive : sourceLocations) { |
| RawOutputFormatter formatter = new RawOutputFormatter(writer); |
| analyzer.visitArchiveDependences(archive, formatter); |
| if (options.verbose != Analyzer.Type.SUMMARY) { |
| analyzer.visitDependences(archive, formatter); |
| } |
| } |
| } |
| private boolean isValidClassName(String name) { |
| if (!Character.isJavaIdentifierStart(name.charAt(0))) { |
| return false; |
| } |
| for (int i=1; i < name.length(); i++) { |
| char c = name.charAt(i); |
| if (c != '.' && !Character.isJavaIdentifierPart(c)) { |
| return false; |
| } |
| } |
| return true; |
| } |
| |
| private Dependency.Filter getDependencyFilter() { |
| if (options.regex != null) { |
| return Dependencies.getRegexFilter(Pattern.compile(options.regex)); |
| } else if (options.packageNames.size() > 0) { |
| return Dependencies.getPackageFilter(options.packageNames, false); |
| } else { |
| return new Dependency.Filter() { |
| @Override |
| public boolean accepts(Dependency dependency) { |
| return !dependency.getOrigin().equals(dependency.getTarget()); |
| } |
| }; |
| } |
| } |
| |
| private boolean matches(String classname, AccessFlags flags) { |
| if (options.apiOnly && !flags.is(AccessFlags.ACC_PUBLIC)) { |
| return false; |
| } else if (options.includePattern != null) { |
| return options.includePattern.matcher(classname.replace('/', '.')).matches(); |
| } else { |
| return true; |
| } |
| } |
| |
| private void findDependencies() throws IOException { |
| Dependency.Finder finder = |
| options.apiOnly ? Dependencies.getAPIFinder(AccessFlags.ACC_PROTECTED) |
| : Dependencies.getClassDependencyFinder(); |
| Dependency.Filter filter = getDependencyFilter(); |
| |
| List<Archive> archives = new ArrayList<>(); |
| Deque<String> roots = new LinkedList<>(); |
| for (String s : classes) { |
| Path p = Paths.get(s); |
| if (Files.exists(p)) { |
| archives.add(new Archive(p, ClassFileReader.newInstance(p))); |
| } else { |
| if (isValidClassName(s)) { |
| roots.add(s); |
| } else { |
| warning("warn.invalid.arg", s); |
| } |
| } |
| } |
| sourceLocations.addAll(archives); |
| |
| List<Archive> classpaths = new ArrayList<>(); // for class file lookup |
| classpaths.addAll(getClassPathArchives(options.classpath)); |
| if (options.includePattern != null) { |
| archives.addAll(classpaths); |
| } |
| classpaths.addAll(PlatformClassPath.getArchives()); |
| |
| // add all classpath archives to the source locations for reporting |
| sourceLocations.addAll(classpaths); |
| |
| // Work queue of names of classfiles to be searched. |
| // Entries will be unique, and for classes that do not yet have |
| // dependencies in the results map. |
| Deque<String> deque = new LinkedList<>(); |
| Set<String> doneClasses = new HashSet<>(); |
| |
| // get the immediate dependencies of the input files |
| for (Archive a : archives) { |
| for (ClassFile cf : a.reader().getClassFiles()) { |
| String classFileName; |
| try { |
| classFileName = cf.getName(); |
| } catch (ConstantPoolException e) { |
| throw new ClassFileError(e); |
| } |
| |
| if (matches(classFileName, cf.access_flags)) { |
| if (!doneClasses.contains(classFileName)) { |
| doneClasses.add(classFileName); |
| } |
| for (Dependency d : finder.findDependencies(cf)) { |
| if (filter.accepts(d)) { |
| String cn = d.getTarget().getName(); |
| if (!doneClasses.contains(cn) && !deque.contains(cn)) { |
| deque.add(cn); |
| } |
| a.addClass(d.getOrigin(), d.getTarget()); |
| } |
| } |
| } |
| } |
| } |
| |
| // add Archive for looking up classes from the classpath |
| // for transitive dependency analysis |
| Deque<String> unresolved = roots; |
| int depth = options.depth > 0 ? options.depth : Integer.MAX_VALUE; |
| do { |
| String name; |
| while ((name = unresolved.poll()) != null) { |
| if (doneClasses.contains(name)) { |
| continue; |
| } |
| ClassFile cf = null; |
| for (Archive a : classpaths) { |
| cf = a.reader().getClassFile(name); |
| if (cf != null) { |
| String classFileName; |
| try { |
| classFileName = cf.getName(); |
| } catch (ConstantPoolException e) { |
| throw new ClassFileError(e); |
| } |
| if (!doneClasses.contains(classFileName)) { |
| // if name is a fully-qualified class name specified |
| // from command-line, this class might already be parsed |
| doneClasses.add(classFileName); |
| for (Dependency d : finder.findDependencies(cf)) { |
| if (depth == 0) { |
| // ignore the dependency |
| a.addClass(d.getOrigin()); |
| break; |
| } else if (filter.accepts(d)) { |
| a.addClass(d.getOrigin(), d.getTarget()); |
| String cn = d.getTarget().getName(); |
| if (!doneClasses.contains(cn) && !deque.contains(cn)) { |
| deque.add(cn); |
| } |
| } |
| } |
| } |
| break; |
| } |
| } |
| if (cf == null) { |
| doneClasses.add(name); |
| } |
| } |
| unresolved = deque; |
| deque = new LinkedList<>(); |
| } while (!unresolved.isEmpty() && depth-- > 0); |
| } |
| |
| public void handleOptions(String[] args) throws BadArgs { |
| // process options |
| for (int i=0; i < args.length; i++) { |
| if (args[i].charAt(0) == '-') { |
| String name = args[i]; |
| Option option = getOption(name); |
| String param = null; |
| if (option.hasArg) { |
| if (name.startsWith("-") && name.indexOf('=') > 0) { |
| param = name.substring(name.indexOf('=') + 1, name.length()); |
| } else if (i + 1 < args.length) { |
| param = args[++i]; |
| } |
| if (param == null || param.isEmpty() || param.charAt(0) == '-') { |
| throw new BadArgs("err.missing.arg", name).showUsage(true); |
| } |
| } |
| option.process(this, name, param); |
| if (option.ignoreRest()) { |
| i = args.length; |
| } |
| } else { |
| // process rest of the input arguments |
| for (; i < args.length; i++) { |
| String name = args[i]; |
| if (name.charAt(0) == '-') { |
| throw new BadArgs("err.option.after.class", name).showUsage(true); |
| } |
| classes.add(name); |
| } |
| } |
| } |
| } |
| |
| private Option getOption(String name) throws BadArgs { |
| for (Option o : recognizedOptions) { |
| if (o.matches(name)) { |
| return o; |
| } |
| } |
| throw new BadArgs("err.unknown.option", name).showUsage(true); |
| } |
| |
| private void reportError(String key, Object... args) { |
| log.println(getMessage("error.prefix") + " " + getMessage(key, args)); |
| } |
| |
| private void warning(String key, Object... args) { |
| log.println(getMessage("warn.prefix") + " " + getMessage(key, args)); |
| } |
| |
| private void showHelp() { |
| log.println(getMessage("main.usage", PROGNAME)); |
| for (Option o : recognizedOptions) { |
| String name = o.aliases[0].substring(1); // there must always be at least one name |
| name = name.charAt(0) == '-' ? name.substring(1) : name; |
| if (o.isHidden() || name.equals("h")) { |
| continue; |
| } |
| log.println(getMessage("main.opt." + name)); |
| } |
| } |
| |
| private void showVersion(boolean full) { |
| log.println(version(full ? "full" : "release")); |
| } |
| |
| private String version(String key) { |
| // key=version: mm.nn.oo[-milestone] |
| // key=full: mm.mm.oo[-milestone]-build |
| if (ResourceBundleHelper.versionRB == null) { |
| return System.getProperty("java.version"); |
| } |
| try { |
| return ResourceBundleHelper.versionRB.getString(key); |
| } catch (MissingResourceException e) { |
| return getMessage("version.unknown", System.getProperty("java.version")); |
| } |
| } |
| |
| static String getMessage(String key, Object... args) { |
| try { |
| return MessageFormat.format(ResourceBundleHelper.bundle.getString(key), args); |
| } catch (MissingResourceException e) { |
| throw new InternalError("Missing message: " + key); |
| } |
| } |
| |
| private static class Options { |
| boolean help; |
| boolean version; |
| boolean fullVersion; |
| boolean showProfile; |
| boolean showSummary; |
| boolean wildcard; |
| boolean apiOnly; |
| boolean showLabel; |
| boolean findJDKInternals; |
| String dotOutputDir; |
| String classpath = ""; |
| int depth = 1; |
| Analyzer.Type verbose = Analyzer.Type.PACKAGE; |
| Set<String> packageNames = new HashSet<>(); |
| String regex; // apply to the dependences |
| Pattern includePattern; // apply to classes |
| } |
| private static class ResourceBundleHelper { |
| static final ResourceBundle versionRB; |
| static final ResourceBundle bundle; |
| |
| static { |
| Locale locale = Locale.getDefault(); |
| try { |
| bundle = ResourceBundle.getBundle("com.sun.tools.jdeps.resources.jdeps", locale); |
| } catch (MissingResourceException e) { |
| throw new InternalError("Cannot find jdeps resource bundle for locale " + locale); |
| } |
| try { |
| versionRB = ResourceBundle.getBundle("com.sun.tools.jdeps.resources.version"); |
| } catch (MissingResourceException e) { |
| throw new InternalError("version.resource.missing"); |
| } |
| } |
| } |
| |
| private List<Archive> getArchives(List<String> filenames) throws IOException { |
| List<Archive> result = new ArrayList<>(); |
| for (String s : filenames) { |
| Path p = Paths.get(s); |
| if (Files.exists(p)) { |
| result.add(new Archive(p, ClassFileReader.newInstance(p))); |
| } else { |
| warning("warn.file.not.exist", s); |
| } |
| } |
| return result; |
| } |
| |
| private List<Archive> getClassPathArchives(String paths) throws IOException { |
| List<Archive> result = new ArrayList<>(); |
| if (paths.isEmpty()) { |
| return result; |
| } |
| for (String p : paths.split(File.pathSeparator)) { |
| if (p.length() > 0) { |
| List<Path> files = new ArrayList<>(); |
| // wildcard to parse all JAR files e.g. -classpath dir/* |
| int i = p.lastIndexOf(".*"); |
| if (i > 0) { |
| Path dir = Paths.get(p.substring(0, i)); |
| try (DirectoryStream<Path> stream = Files.newDirectoryStream(dir, "*.jar")) { |
| for (Path entry : stream) { |
| files.add(entry); |
| } |
| } |
| } else { |
| files.add(Paths.get(p)); |
| } |
| for (Path f : files) { |
| if (Files.exists(f)) { |
| result.add(new Archive(f, ClassFileReader.newInstance(f))); |
| } |
| } |
| } |
| } |
| return result; |
| } |
| |
| /** |
| * If the given archive is JDK archive and non-null Profile, |
| * this method returns the profile name only if -profile option is specified; |
| * a null profile indicates it accesses a private JDK API and this method |
| * will return "JDK internal API". |
| * |
| * For non-JDK archives, this method returns the file name of the archive. |
| */ |
| private String getProfileArchiveInfo(Archive source, Profile profile) { |
| if (options.showProfile && profile != null) |
| return profile.toString(); |
| |
| if (source instanceof JDKArchive) { |
| return profile == null ? "JDK internal API (" + source.getFileName() + ")" : ""; |
| } |
| return source.getFileName(); |
| } |
| |
| /** |
| * Returns the profile name or "JDK internal API" for JDK archive; |
| * otherwise empty string. |
| */ |
| private String profileName(Archive archive, Profile profile) { |
| if (archive instanceof JDKArchive) { |
| return Objects.toString(profile, "JDK internal API"); |
| } else { |
| return ""; |
| } |
| } |
| |
| class RawOutputFormatter implements Analyzer.Visitor { |
| private final PrintWriter writer; |
| RawOutputFormatter(PrintWriter writer) { |
| this.writer = writer; |
| } |
| |
| private String pkg = ""; |
| @Override |
| public void visitDependence(String origin, Archive source, |
| String target, Archive archive, Profile profile) { |
| if (options.findJDKInternals && |
| !(archive instanceof JDKArchive && profile == null)) { |
| // filter dependences other than JDK internal APIs |
| return; |
| } |
| if (options.verbose == Analyzer.Type.VERBOSE) { |
| writer.format(" %-50s -> %-50s %s%n", |
| origin, target, getProfileArchiveInfo(archive, profile)); |
| } else { |
| if (!origin.equals(pkg)) { |
| pkg = origin; |
| writer.format(" %s (%s)%n", origin, source.getFileName()); |
| } |
| writer.format(" -> %-50s %s%n", |
| target, getProfileArchiveInfo(archive, profile)); |
| } |
| } |
| |
| @Override |
| public void visitArchiveDependence(Archive origin, Archive target, Profile profile) { |
| writer.format("%s -> %s", origin.getPathName(), target.getPathName()); |
| if (options.showProfile && profile != null) { |
| writer.format(" (%s)%n", profile); |
| } else { |
| writer.format("%n"); |
| } |
| } |
| } |
| |
| class DotFileFormatter extends DotGraph<String> implements AutoCloseable { |
| private final PrintWriter writer; |
| private final String name; |
| DotFileFormatter(PrintWriter writer, Archive archive) { |
| this.writer = writer; |
| this.name = archive.getFileName(); |
| writer.format("digraph \"%s\" {%n", name); |
| writer.format(" // Path: %s%n", archive.getPathName()); |
| } |
| |
| @Override |
| public void close() { |
| writer.println("}"); |
| } |
| |
| @Override |
| public void visitDependence(String origin, Archive source, |
| String target, Archive archive, Profile profile) { |
| if (options.findJDKInternals && |
| !(archive instanceof JDKArchive && profile == null)) { |
| // filter dependences other than JDK internal APIs |
| return; |
| } |
| // if -P option is specified, package name -> profile will |
| // be shown and filter out multiple same edges. |
| String name = getProfileArchiveInfo(archive, profile); |
| writeEdge(writer, new Edge(origin, target, getProfileArchiveInfo(archive, profile))); |
| } |
| @Override |
| public void visitArchiveDependence(Archive origin, Archive target, Profile profile) { |
| throw new UnsupportedOperationException(); |
| } |
| } |
| |
| class DotSummaryForArchive extends DotGraph<Archive> { |
| @Override |
| public void visitDependence(String origin, Archive source, |
| String target, Archive archive, Profile profile) { |
| Edge e = findEdge(source, archive); |
| assert e != null; |
| // add the dependency to the label if enabled and not compact1 |
| if (profile == Profile.COMPACT1) { |
| return; |
| } |
| e.addLabel(origin, target, profileName(archive, profile)); |
| } |
| @Override |
| public void visitArchiveDependence(Archive origin, Archive target, Profile profile) { |
| // add an edge with the archive's name with no tag |
| // so that there is only one node for each JDK archive |
| // while there may be edges to different profiles |
| Edge e = addEdge(origin, target, ""); |
| if (target instanceof JDKArchive) { |
| // add a label to print the profile |
| if (profile == null) { |
| e.addLabel("JDK internal API"); |
| } else if (options.showProfile && !options.showLabel) { |
| e.addLabel(profile.toString()); |
| } |
| } |
| } |
| } |
| |
| // DotSummaryForPackage generates the summary.dot file for verbose mode |
| // (-v or -verbose option) that includes all class dependencies. |
| // The summary.dot file shows package-level dependencies. |
| class DotSummaryForPackage extends DotGraph<String> { |
| private String packageOf(String cn) { |
| int i = cn.lastIndexOf('.'); |
| return i > 0 ? cn.substring(0, i) : "<unnamed>"; |
| } |
| @Override |
| public void visitDependence(String origin, Archive source, |
| String target, Archive archive, Profile profile) { |
| // add a package dependency edge |
| String from = packageOf(origin); |
| String to = packageOf(target); |
| Edge e = addEdge(from, to, getProfileArchiveInfo(archive, profile)); |
| |
| // add the dependency to the label if enabled and not compact1 |
| if (!options.showLabel || profile == Profile.COMPACT1) { |
| return; |
| } |
| |
| // trim the package name of origin to shorten the label |
| int i = origin.lastIndexOf('.'); |
| String n1 = i < 0 ? origin : origin.substring(i+1); |
| e.addLabel(n1, target, profileName(archive, profile)); |
| } |
| @Override |
| public void visitArchiveDependence(Archive origin, Archive target, Profile profile) { |
| // nop |
| } |
| } |
| abstract class DotGraph<T> implements Analyzer.Visitor { |
| private final Set<Edge> edges = new LinkedHashSet<>(); |
| private Edge curEdge; |
| public void writeTo(PrintWriter writer) { |
| writer.format("digraph \"summary\" {%n"); |
| for (Edge e: edges) { |
| writeEdge(writer, e); |
| } |
| writer.println("}"); |
| } |
| |
| void writeEdge(PrintWriter writer, Edge e) { |
| writer.format(" %-50s -> \"%s\"%s;%n", |
| String.format("\"%s\"", e.from.toString()), |
| e.tag.isEmpty() ? e.to |
| : String.format("%s (%s)", e.to, e.tag), |
| getLabel(e)); |
| } |
| |
| Edge addEdge(T origin, T target, String tag) { |
| Edge e = new Edge(origin, target, tag); |
| if (e.equals(curEdge)) { |
| return curEdge; |
| } |
| |
| if (edges.contains(e)) { |
| for (Edge e1 : edges) { |
| if (e.equals(e1)) { |
| curEdge = e1; |
| } |
| } |
| } else { |
| edges.add(e); |
| curEdge = e; |
| } |
| return curEdge; |
| } |
| |
| Edge findEdge(T origin, T target) { |
| for (Edge e : edges) { |
| if (e.from.equals(origin) && e.to.equals(target)) { |
| return e; |
| } |
| } |
| return null; |
| } |
| |
| String getLabel(Edge e) { |
| String label = e.label.toString(); |
| return label.isEmpty() ? "" : String.format("[label=\"%s\",fontsize=9]", label); |
| } |
| |
| class Edge { |
| final T from; |
| final T to; |
| final String tag; // optional tag |
| final StringBuilder label = new StringBuilder(); |
| Edge(T from, T to, String tag) { |
| this.from = from; |
| this.to = to; |
| this.tag = tag; |
| } |
| void addLabel(String s) { |
| label.append(s).append("\\n"); |
| } |
| void addLabel(String origin, String target, String profile) { |
| label.append(origin).append(" -> ").append(target); |
| if (!profile.isEmpty()) { |
| label.append(" (" + profile + ")"); |
| } |
| label.append("\\n"); |
| } |
| @Override @SuppressWarnings("unchecked") |
| public boolean equals(Object o) { |
| if (o instanceof DotGraph<?>.Edge) { |
| DotGraph<?>.Edge e = (DotGraph<?>.Edge)o; |
| return this.from.equals(e.from) && |
| this.to.equals(e.to) && |
| this.tag.equals(e.tag); |
| } |
| return false; |
| } |
| @Override |
| public int hashCode() { |
| int hash = 7; |
| hash = 67 * hash + Objects.hashCode(this.from) + |
| Objects.hashCode(this.to) + Objects.hashCode(this.tag); |
| return hash; |
| } |
| } |
| } |
| } |