/* * Copyright (c) 2012, 2014, 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.classfile.Dependency.Location; import static com.sun.tools.jdeps.Analyzer.Type.*; 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.invalid.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 = 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 = VERBOSE; task.options.filterSameArchive = false; task.options.filterSamePackage = false; break; case "-verbose:package": task.options.verbose = PACKAGE; break; case "-verbose:class": task.options.verbose = 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, "-f", "-filter") { void process(JdepsTask task, String opt, String arg) { task.options.filterRegex = arg; } }, new Option(false, "-filter:package", "-filter:archive", "-filter:none") { void process(JdepsTask task, String opt, String arg) { switch (opt) { case "-filter:package": task.options.filterSamePackage = true; task.options.filterSameArchive = false; break; case "-filter:archive": task.options.filterSameArchive = true; task.options.filterSamePackage = false; break; case "-filter:none": task.options.filterSameArchive = false; task.options.filterSamePackage = false; break; } } }, 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; task.options.showModule = false; } }, new Option(false, "-M", "-module") { void process(JdepsTask task, String opt, String arg) throws BadArgs { task.options.showModule = true; task.options.showProfile = false; } }, 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; // turn off filtering task.options.filterSameArchive = false; task.options.filterSamePackage = false; } }, new Option(false, "-jdkinternals") { void process(JdepsTask task, String opt, String arg) { task.options.findJDKInternals = true; task.options.verbose = CLASS; if (task.options.includePattern == null) { task.options.includePattern = Pattern.compile(".*"); } } }, new HiddenOption(false, "-verify:access") { void process(JdepsTask task, String opt, String arg) { task.options.verifyAccess = true; task.options.verbose = VERBOSE; task.options.filterSameArchive = false; task.options.filterSamePackage = false; } }, new HiddenOption(true, "-mp") { void process(JdepsTask task, String opt, String arg) throws BadArgs { task.options.mpath = Paths.get(arg); if (!Files.isDirectory(task.options.mpath)) { throw new BadArgs("err.invalid.path", arg); } 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(false, "-q", "-quiet") { void process(JdepsTask task, String opt, String arg) { task.options.nowarning = 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 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.verifyAccess) && (options.regex != null || options.packageNames.size() > 0 || options.showSummary)) { showHelp(); return EXIT_CMDERR; } if (options.showSummary && options.verbose != 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 sourceLocations = new ArrayList<>(); private final List classpaths = new ArrayList<>(); private final List initialArchives = new ArrayList<>(); private boolean run() throws IOException { buildArchives(); if (options.verifyAccess) { return verifyModuleAccess(); } else { return analyzeDeps(); } } private boolean analyzeDeps() throws IOException { Analyzer analyzer = new Analyzer(options.verbose, new Analyzer.Filter() { @Override public boolean accepts(Location origin, Archive originArchive, Location target, Archive targetArchive) { if (options.findJDKInternals) { // accepts target that is JDK class but not exported return isJDKModule(targetArchive) && !((Module) targetArchive).isExported(target.getClassName()); } else if (options.filterSameArchive) { // accepts origin and target that from different archive return originArchive != targetArchive; } return true; } }); // parse classfiles and find all dependencies findDependencies(options.apiOnly); // analyze the dependencies analyzer.run(sourceLocations); // output result if (options.dotOutputDir != null) { Path dir = Paths.get(options.dotOutputDir); Files.createDirectories(dir); generateDotFiles(dir, analyzer); } else { printRawOutput(log, analyzer); } if (options.findJDKInternals && !options.nowarning) { showReplacements(analyzer); } return true; } private boolean verifyModuleAccess() throws IOException { // two passes // 1. check API dependences where the types of dependences must be re-exported // 2. check all dependences where types must be accessible // pass 1 findDependencies(true /* api only */); Analyzer analyzer = Analyzer.getExportedAPIsAnalyzer(); boolean pass1 = analyzer.run(sourceLocations); if (!pass1) { System.out.println("ERROR: Failed API access verification"); } // pass 2 findDependencies(false); analyzer = Analyzer.getModuleAccessAnalyzer(); boolean pass2 = analyzer.run(sourceLocations); if (!pass2) { System.out.println("ERROR: Failed module access verification"); } if (pass1 & pass2) { System.out.println("Access verification succeeded."); } return pass1 & pass2; } private void generateSummaryDotFile(Path dir, Analyzer analyzer) throws IOException { // If verbose mode (-v or -verbose option), // the summary.dot file shows package-level dependencies. Analyzer.Type summaryType = (options.verbose == PACKAGE || options.verbose == SUMMARY) ? SUMMARY : PACKAGE; Path summary = dir.resolve("summary.dot"); try (PrintWriter sw = new PrintWriter(Files.newOutputStream(summary)); SummaryDotFile dotfile = new SummaryDotFile(sw, summaryType)) { for (Archive archive : sourceLocations) { if (!archive.isEmpty()) { if (options.verbose == PACKAGE || options.verbose == SUMMARY) { if (options.showLabel) { // build labels listing package-level dependencies analyzer.visitDependences(archive, dotfile.labelBuilder(), PACKAGE); } } analyzer.visitDependences(archive, dotfile, summaryType); } } } } private void generateDotFiles(Path dir, Analyzer analyzer) throws IOException { // output individual .dot file for each archive if (options.verbose != SUMMARY) { for (Archive archive : sourceLocations) { if (analyzer.hasDependences(archive)) { Path dotfile = dir.resolve(archive.getName() + ".dot"); try (PrintWriter pw = new PrintWriter(Files.newOutputStream(dotfile)); DotFileFormatter formatter = new DotFileFormatter(pw, archive)) { analyzer.visitDependences(archive, formatter); } } } } // generate summary dot file generateSummaryDotFile(dir, analyzer); } private void printRawOutput(PrintWriter writer, Analyzer analyzer) { RawOutputFormatter depFormatter = new RawOutputFormatter(writer); RawSummaryFormatter summaryFormatter = new RawSummaryFormatter(writer); for (Archive archive : sourceLocations) { if (!archive.isEmpty()) { analyzer.visitDependences(archive, summaryFormatter, SUMMARY); if (analyzer.hasDependences(archive) && options.verbose != SUMMARY) { analyzer.visitDependences(archive, depFormatter); } } } } 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; } /* * Dep Filter configured based on the input jdeps option * 1. -p and -regex to match target dependencies * 2. -filter:package to filter out same-package dependencies * * This filter is applied when jdeps parses the class files * and filtered dependencies are not stored in the Analyzer. * * -filter:archive is applied later in the Analyzer as the * containing archive of a target class may not be known until * the entire archive */ class DependencyFilter implements Dependency.Filter { final Dependency.Filter filter; final Pattern filterPattern; DependencyFilter() { if (options.regex != null) { this.filter = Dependencies.getRegexFilter(Pattern.compile(options.regex)); } else if (options.packageNames.size() > 0) { this.filter = Dependencies.getPackageFilter(options.packageNames, false); } else { this.filter = null; } this.filterPattern = options.filterRegex != null ? Pattern.compile(options.filterRegex) : null; } @Override public boolean accepts(Dependency d) { if (d.getOrigin().equals(d.getTarget())) { return false; } String pn = d.getTarget().getPackageName(); if (options.filterSamePackage && d.getOrigin().getPackageName().equals(pn)) { return false; } if (filterPattern != null && filterPattern.matcher(pn).matches()) { return false; } return filter != null ? filter.accepts(d) : true; } } /** * Tests if the given class matches the pattern given in the -include option */ private boolean matches(String classname) { if (options.includePattern != null) { return options.includePattern.matcher(classname.replace('/', '.')).matches(); } else { return true; } } private void buildArchives() throws IOException { for (String s : classes) { Path p = Paths.get(s); if (Files.exists(p)) { initialArchives.add(Archive.getInstance(p)); } } sourceLocations.addAll(initialArchives); classpaths.addAll(getClassPathArchives(options.classpath)); if (options.includePattern != null) { initialArchives.addAll(classpaths); } classpaths.addAll(PlatformClassPath.getModules(options.mpath)); if (options.mpath != null) { initialArchives.addAll(PlatformClassPath.getModules(options.mpath)); } else { classpaths.addAll(PlatformClassPath.getJarFiles()); } // add all classpath archives to the source locations for reporting sourceLocations.addAll(classpaths); } private void findDependencies(boolean apiOnly) throws IOException { Dependency.Finder finder = apiOnly ? Dependencies.getAPIFinder(AccessFlags.ACC_PROTECTED) : Dependencies.getClassDependencyFinder(); Dependency.Filter filter = new DependencyFilter(); Deque roots = new LinkedList<>(); for (String s : classes) { Path p = Paths.get(s); if (!Files.exists(p)) { if (isValidClassName(s)) { roots.add(s); } else { warning("warn.invalid.arg", s); } } } // 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 deque = new LinkedList<>(); Set doneClasses = new HashSet<>(); // get the immediate dependencies of the input files for (Archive a : initialArchives) { for (ClassFile cf : a.reader().getClassFiles()) { String classFileName; try { classFileName = cf.getName(); } catch (ConstantPoolException e) { throw new ClassFileError(e); } // tests if this class matches the -include or -apiOnly option if specified if (!matches(classFileName) || (apiOnly && !cf.access_flags.is(AccessFlags.ACC_PUBLIC))) { continue; } 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()); } } for (String name : a.reader().skippedEntries()) { warning("warn.skipped.entry", name, a.getPathName()); } } } // add Archive for looking up classes from the classpath // for transitive dependency analysis Deque 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") || name.startsWith("filter:")) { 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 showModule; boolean showSummary; boolean apiOnly; boolean showLabel; boolean findJDKInternals; boolean nowarning; // default is to show package-level dependencies // and filter references from same package Analyzer.Type verbose = PACKAGE; boolean filterSamePackage = true; boolean filterSameArchive = false; String filterRegex; String dotOutputDir; String classpath = ""; int depth = 1; Set packageNames = new HashSet<>(); String regex; // apply to the dependences Pattern includePattern; // apply to classes // module boundary access check boolean verifyAccess; Path mpath; } private static class ResourceBundleHelper { static final ResourceBundle versionRB; static final ResourceBundle bundle; static final ResourceBundle jdkinternals; 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"); } try { jdkinternals = ResourceBundle.getBundle("com.sun.tools.jdeps.resources.jdkinternals"); } catch (MissingResourceException e) { throw new InternalError("Cannot find jdkinternals resource bundle"); } } } private List getClassPathArchives(String paths) throws IOException { List result = new ArrayList<>(); if (paths.isEmpty()) { return result; } for (String p : paths.split(File.pathSeparator)) { if (p.length() > 0) { List 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 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(Archive.getInstance(f)); } } } } return result; } class RawOutputFormatter implements Analyzer.Visitor { private final PrintWriter writer; private String pkg = ""; RawOutputFormatter(PrintWriter writer) { this.writer = writer; } @Override public void visitDependence(String origin, Archive originArchive, String target, Archive targetArchive) { String tag = toTag(target, targetArchive); if (options.verbose == VERBOSE) { writer.format(" %-50s -> %-50s %s%n", origin, target, tag); } else { if (!origin.equals(pkg)) { pkg = origin; writer.format(" %s (%s)%n", origin, originArchive.getName()); } writer.format(" -> %-50s %s%n", target, tag); } } } class RawSummaryFormatter implements Analyzer.Visitor { private final PrintWriter writer; RawSummaryFormatter(PrintWriter writer) { this.writer = writer; } @Override public void visitDependence(String origin, Archive originArchive, String target, Archive targetArchive) { String targetName = targetArchive.getPathName(); if (options.showModule && isJDKModule(targetArchive)) { targetName = ((Module)targetArchive).name(); } writer.format("%s -> %s", originArchive.getName(), targetName); if (options.showProfile && isJDKModule(targetArchive)) { writer.format(" (%s)", target); } writer.format("%n"); } } class DotFileFormatter implements Analyzer.Visitor, AutoCloseable { private final PrintWriter writer; private final String name; DotFileFormatter(PrintWriter writer, Archive archive) { this.writer = writer; this.name = archive.getName(); 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 originArchive, String target, Archive targetArchive) { String tag = toTag(target, targetArchive); writer.format(" %-50s -> \"%s\";%n", String.format("\"%s\"", origin), tag.isEmpty() ? target : String.format("%s (%s)", target, tag)); } } class SummaryDotFile implements Analyzer.Visitor, AutoCloseable { private final PrintWriter writer; private final Analyzer.Type type; private final Map> edges = new HashMap<>(); SummaryDotFile(PrintWriter writer, Analyzer.Type type) { this.writer = writer; this.type = type; writer.format("digraph \"summary\" {%n"); } @Override public void close() { writer.println("}"); } @Override public void visitDependence(String origin, Archive originArchive, String target, Archive targetArchive) { String targetName = type == PACKAGE ? target : targetArchive.getName(); if (isJDKModule(targetArchive)) { Module m = (Module)targetArchive; String n = showProfileOrModule(m); if (!n.isEmpty()) { targetName += " (" + n + ")"; } } else if (type == PACKAGE) { targetName += " (" + targetArchive.getName() + ")"; } String label = getLabel(originArchive, targetArchive); writer.format(" %-50s -> \"%s\"%s;%n", String.format("\"%s\"", origin), targetName, label); } String getLabel(Archive origin, Archive target) { if (edges.isEmpty()) return ""; StringBuilder label = edges.get(origin).get(target); return label == null ? "" : String.format(" [label=\"%s\",fontsize=9]", label.toString()); } Analyzer.Visitor labelBuilder() { // show the package-level dependencies as labels in the dot graph return new Analyzer.Visitor() { @Override public void visitDependence(String origin, Archive originArchive, String target, Archive targetArchive) { edges.putIfAbsent(originArchive, new HashMap<>()); edges.get(originArchive).putIfAbsent(targetArchive, new StringBuilder()); StringBuilder sb = edges.get(originArchive).get(targetArchive); String tag = toTag(target, targetArchive); addLabel(sb, origin, target, tag); } void addLabel(StringBuilder label, String origin, String target, String tag) { label.append(origin).append(" -> ").append(target); if (!tag.isEmpty()) { label.append(" (" + tag + ")"); } label.append("\\n"); } }; } } /** * Test if the given archive is part of the JDK */ private boolean isJDKModule(Archive archive) { return Module.class.isInstance(archive); } /** * If the given archive is JDK archive, this method returns the profile name * only if -profile option is specified; it accesses a private JDK API and * the returned value will have "JDK internal API" prefix * * For non-JDK archives, this method returns the file name of the archive. */ private String toTag(String name, Archive source) { if (!isJDKModule(source)) { return source.getName(); } Module module = (Module)source; boolean isExported = false; if (options.verbose == CLASS || options.verbose == VERBOSE) { isExported = module.isExported(name); } else { isExported = module.isExportedPackage(name); } if (isExported) { // exported API return showProfileOrModule(module); } else { return "JDK internal API (" + source.getName() + ")"; } } private String showProfileOrModule(Module m) { String tag = ""; if (options.showProfile) { Profile p = Profile.getProfile(m); if (p != null) { tag = p.profileName(); } } else if (options.showModule) { tag = m.name(); } return tag; } private Profile getProfile(String name) { String pn = name; if (options.verbose == CLASS || options.verbose == VERBOSE) { int i = name.lastIndexOf('.'); pn = i > 0 ? name.substring(0, i) : ""; } return Profile.getProfile(pn); } /** * Returns the recommended replacement API for the given classname; * or return null if replacement API is not known. */ private String replacementFor(String cn) { String name = cn; String value = null; while (value == null && name != null) { try { value = ResourceBundleHelper.jdkinternals.getString(name); } catch (MissingResourceException e) { // go up one subpackage level int i = name.lastIndexOf('.'); name = i > 0 ? name.substring(0, i) : null; } } return value; }; private void showReplacements(Analyzer analyzer) { Map jdkinternals = new TreeMap<>(); boolean useInternals = false; for (Archive source : sourceLocations) { useInternals = useInternals || analyzer.hasDependences(source); for (String cn : analyzer.dependences(source)) { String repl = replacementFor(cn); if (repl != null) { jdkinternals.putIfAbsent(cn, repl); } } } if (useInternals) { log.println(); warning("warn.replace.useJDKInternals", getMessage("jdeps.wiki.url")); } if (!jdkinternals.isEmpty()) { log.println(); log.format("%-40s %s%n", "JDK Internal API", "Suggested Replacement"); log.format("%-40s %s%n", "----------------", "---------------------"); for (Map.Entry e : jdkinternals.entrySet()) { log.format("%-40s %s%n", e.getKey(), e.getValue()); } } } }