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