aoqi@0: /* mchung@2538: * Copyright (c) 2012, 2014, 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; mchung@2538: import com.sun.tools.classfile.Dependency.Location; aoqi@0: import com.sun.tools.jdeps.PlatformClassPath.JDKArchive; mchung@2538: import static com.sun.tools.jdeps.Analyzer.Type.*; 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))) { mchung@2538: throw new BadArgs("err.invalid.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; mchung@2538: task.options.verbose = SUMMARY; aoqi@0: } aoqi@0: }, aoqi@0: new Option(false, "-v", "-verbose", aoqi@0: "-verbose:package", mchung@2538: "-verbose:class") { aoqi@0: void process(JdepsTask task, String opt, String arg) throws BadArgs { aoqi@0: switch (opt) { aoqi@0: case "-v": aoqi@0: case "-verbose": mchung@2538: task.options.verbose = VERBOSE; mchung@2538: task.options.filterSameArchive = false; mchung@2538: task.options.filterSamePackage = false; aoqi@0: break; aoqi@0: case "-verbose:package": mchung@2538: task.options.verbose = PACKAGE; mchung@2538: break; aoqi@0: case "-verbose:class": mchung@2538: task.options.verbose = CLASS; mchung@2538: 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: }, mchung@2538: mchung@2538: new Option(true, "-f", "-filter") { mchung@2538: void process(JdepsTask task, String opt, String arg) { mchung@2538: task.options.filterRegex = arg; mchung@2538: } mchung@2538: }, mchung@2538: new Option(false, "-filter:package", mchung@2538: "-filter:archive", mchung@2538: "-filter:none") { mchung@2538: void process(JdepsTask task, String opt, String arg) { mchung@2538: switch (opt) { mchung@2538: case "-filter:package": mchung@2538: task.options.filterSamePackage = true; mchung@2538: task.options.filterSameArchive = false; mchung@2538: break; mchung@2538: case "-filter:archive": mchung@2538: task.options.filterSameArchive = true; mchung@2538: task.options.filterSamePackage = false; mchung@2538: break; mchung@2538: case "-filter:none": mchung@2538: task.options.filterSameArchive = false; mchung@2538: task.options.filterSamePackage = false; mchung@2538: break; mchung@2538: } mchung@2538: } mchung@2538: }, 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; mchung@2538: // turn off filtering mchung@2538: task.options.filterSameArchive = false; mchung@2538: task.options.filterSamePackage = false; 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; mchung@2538: task.options.verbose = 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: }, mchung@2539: new HiddenOption(false, "-q", "-quiet") { mchung@2539: void process(JdepsTask task, String opt, String arg) { mchung@2539: task.options.nowarning = true; mchung@2539: } mchung@2539: }, 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(); mchung@2539: 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: } mchung@2538: if (options.showSummary && options.verbose != 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 { mchung@2538: // parse classfiles and find all dependencies aoqi@0: findDependencies(); mchung@2538: mchung@2538: Analyzer analyzer = new Analyzer(options.verbose, new Analyzer.Filter() { mchung@2538: @Override mchung@2539: public boolean accepts(Location origin, Archive originArchive, mchung@2539: Location target, Archive targetArchive) mchung@2539: { mchung@2538: if (options.findJDKInternals) { mchung@2538: // accepts target that is JDK class but not exported mchung@2538: return isJDKArchive(targetArchive) && mchung@2538: !((JDKArchive) targetArchive).isExported(target.getClassName()); mchung@2538: } else if (options.filterSameArchive) { mchung@2538: // accepts origin and target that from different archive mchung@2538: return originArchive != targetArchive; mchung@2538: } mchung@2538: return true; mchung@2538: } mchung@2538: }); mchung@2538: mchung@2538: // analyze the dependencies aoqi@0: analyzer.run(sourceLocations); mchung@2538: mchung@2538: // output result 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: } mchung@2539: mchung@2539: if (options.findJDKInternals && !options.nowarning) { mchung@2539: showReplacements(analyzer); mchung@2539: } aoqi@0: return true; aoqi@0: } aoqi@0: mchung@2538: private void generateSummaryDotFile(Path dir, Analyzer analyzer) throws IOException { mchung@2538: // If verbose mode (-v or -verbose option), mchung@2538: // the summary.dot file shows package-level dependencies. mchung@2538: Analyzer.Type summaryType = mchung@2538: (options.verbose == PACKAGE || options.verbose == SUMMARY) ? SUMMARY : PACKAGE; aoqi@0: Path summary = dir.resolve("summary.dot"); mchung@2538: try (PrintWriter sw = new PrintWriter(Files.newOutputStream(summary)); mchung@2538: SummaryDotFile dotfile = new SummaryDotFile(sw, summaryType)) { mchung@2538: for (Archive archive : sourceLocations) { mchung@2538: if (!archive.isEmpty()) { mchung@2538: if (options.verbose == PACKAGE || options.verbose == SUMMARY) { mchung@2538: if (options.showLabel) { mchung@2538: // build labels listing package-level dependencies mchung@2538: analyzer.visitDependences(archive, dotfile.labelBuilder(), PACKAGE); mchung@2538: } mchung@2538: } mchung@2538: analyzer.visitDependences(archive, dotfile, summaryType); mchung@2538: } aoqi@0: } aoqi@0: } mchung@2538: } mchung@2538: mchung@2538: private void generateDotFiles(Path dir, Analyzer analyzer) throws IOException { aoqi@0: // output individual .dot file for each archive mchung@2538: if (options.verbose != SUMMARY) { aoqi@0: for (Archive archive : sourceLocations) { aoqi@0: if (analyzer.hasDependences(archive)) { mchung@2538: Path dotfile = dir.resolve(archive.getName() + ".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: } mchung@2538: // generate summary dot file mchung@2538: generateSummaryDotFile(dir, analyzer); aoqi@0: } aoqi@0: aoqi@0: private void printRawOutput(PrintWriter writer, Analyzer analyzer) { mchung@2538: RawOutputFormatter depFormatter = new RawOutputFormatter(writer); mchung@2538: RawSummaryFormatter summaryFormatter = new RawSummaryFormatter(writer); aoqi@0: for (Archive archive : sourceLocations) { mchung@2538: if (!archive.isEmpty()) { mchung@2538: analyzer.visitDependences(archive, summaryFormatter, SUMMARY); mchung@2538: if (analyzer.hasDependences(archive) && options.verbose != SUMMARY) { mchung@2538: analyzer.visitDependences(archive, depFormatter); mchung@2538: } aoqi@0: } aoqi@0: } aoqi@0: } mchung@2538: 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: mchung@2538: /* mchung@2538: * Dep Filter configured based on the input jdeps option mchung@2538: * 1. -p and -regex to match target dependencies mchung@2538: * 2. -filter:package to filter out same-package dependencies mchung@2538: * mchung@2538: * This filter is applied when jdeps parses the class files mchung@2538: * and filtered dependencies are not stored in the Analyzer. mchung@2538: * mchung@2538: * -filter:archive is applied later in the Analyzer as the mchung@2538: * containing archive of a target class may not be known until mchung@2538: * the entire archive mchung@2538: */ mchung@2538: class DependencyFilter implements Dependency.Filter { mchung@2538: final Dependency.Filter filter; mchung@2538: final Pattern filterPattern; mchung@2538: DependencyFilter() { mchung@2538: if (options.regex != null) { mchung@2538: this.filter = Dependencies.getRegexFilter(Pattern.compile(options.regex)); mchung@2538: } else if (options.packageNames.size() > 0) { mchung@2538: this.filter = Dependencies.getPackageFilter(options.packageNames, false); mchung@2538: } else { mchung@2538: this.filter = null; mchung@2538: } mchung@2538: mchung@2538: this.filterPattern = mchung@2538: options.filterRegex != null ? Pattern.compile(options.filterRegex) : null; mchung@2538: } mchung@2538: @Override mchung@2538: public boolean accepts(Dependency d) { mchung@2538: if (d.getOrigin().equals(d.getTarget())) { mchung@2538: return false; mchung@2538: } mchung@2538: String pn = d.getTarget().getPackageName(); mchung@2538: if (options.filterSamePackage && d.getOrigin().getPackageName().equals(pn)) { mchung@2538: return false; mchung@2538: } mchung@2538: mchung@2538: if (filterPattern != null && filterPattern.matcher(pn).matches()) { mchung@2538: return false; mchung@2538: } mchung@2538: return filter != null ? filter.accepts(d) : true; aoqi@0: } aoqi@0: } aoqi@0: mchung@2538: /** mchung@2538: * Tests if the given class matches the pattern given in the -include option mchung@2538: * or if it's a public class if -apionly option is specified mchung@2538: */ 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(); mchung@2538: Dependency.Filter filter = new DependencyFilter(); 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)) { mchung@2538: archives.add(Archive.getInstance(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: mchung@2538: // tests if this class matches the -include or -apiOnly option if specified mchung@2538: if (!matches(classFileName, cf.access_flags)) { mchung@2538: continue; mchung@2538: } mchung@2538: mchung@2538: if (!doneClasses.contains(classFileName)) { mchung@2538: doneClasses.add(classFileName); mchung@2538: } mchung@2538: mchung@2538: for (Dependency d : finder.findDependencies(cf)) { mchung@2538: if (filter.accepts(d)) { mchung@2538: String cn = d.getTarget().getName(); mchung@2538: if (!doneClasses.contains(cn) && !deque.contains(cn)) { mchung@2538: deque.add(cn); mchung@2538: } mchung@2538: a.addClass(d.getOrigin(), d.getTarget()); aoqi@0: } mchung@2538: } mchung@2538: for (String name : a.reader().skippedEntries()) { mchung@2538: warning("warn.skipped.entry", name, a.getPathName()); 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); mchung@2538: // process @jdk.Exported for JDK classes mchung@2538: if (isJDKArchive(a)) { mchung@2538: ((JDKArchive)a).processJdkExported(cf); mchung@2538: } 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; mchung@2538: if (o.isHidden() || name.equals("h") || name.startsWith("filter:")) { 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 apiOnly; aoqi@0: boolean showLabel; aoqi@0: boolean findJDKInternals; mchung@2539: boolean nowarning; mchung@2538: // default is to show package-level dependencies mchung@2538: // and filter references from same package mchung@2538: Analyzer.Type verbose = PACKAGE; mchung@2538: boolean filterSamePackage = true; mchung@2538: boolean filterSameArchive = false; mchung@2538: String filterRegex; aoqi@0: String dotOutputDir; aoqi@0: String classpath = ""; aoqi@0: int depth = 1; 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; mchung@2539: static final ResourceBundle jdkinternals; 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: } mchung@2539: try { mchung@2539: jdkinternals = ResourceBundle.getBundle("com.sun.tools.jdeps.resources.jdkinternals"); mchung@2539: } catch (MissingResourceException e) { mchung@2539: throw new InternalError("Cannot find jdkinternals resource bundle"); aoqi@0: } aoqi@0: } 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)) { mchung@2538: result.add(Archive.getInstance(f)); aoqi@0: } aoqi@0: } aoqi@0: } aoqi@0: } aoqi@0: return result; aoqi@0: } aoqi@0: aoqi@0: class RawOutputFormatter implements Analyzer.Visitor { aoqi@0: private final PrintWriter writer; mchung@2538: private String pkg = ""; aoqi@0: RawOutputFormatter(PrintWriter writer) { aoqi@0: this.writer = writer; aoqi@0: } aoqi@0: @Override mchung@2538: public void visitDependence(String origin, Archive originArchive, mchung@2538: String target, Archive targetArchive) { mchung@2538: String tag = toTag(target, targetArchive); mchung@2538: if (options.verbose == VERBOSE) { mchung@2538: writer.format(" %-50s -> %-50s %s%n", origin, target, tag); aoqi@0: } else { aoqi@0: if (!origin.equals(pkg)) { aoqi@0: pkg = origin; mchung@2538: writer.format(" %s (%s)%n", origin, originArchive.getName()); aoqi@0: } mchung@2538: writer.format(" -> %-50s %s%n", target, tag); aoqi@0: } aoqi@0: } aoqi@0: } aoqi@0: mchung@2538: class RawSummaryFormatter implements Analyzer.Visitor { mchung@2538: private final PrintWriter writer; mchung@2538: RawSummaryFormatter(PrintWriter writer) { mchung@2538: this.writer = writer; mchung@2538: } mchung@2538: @Override mchung@2538: public void visitDependence(String origin, Archive originArchive, mchung@2538: String target, Archive targetArchive) { mchung@2538: writer.format("%s -> %s", originArchive.getName(), targetArchive.getPathName()); mchung@2538: if (options.showProfile && JDKArchive.isProfileArchive(targetArchive)) { mchung@2538: writer.format(" (%s)", target); mchung@2538: } mchung@2538: writer.format("%n"); mchung@2538: } mchung@2538: } mchung@2538: mchung@2538: class DotFileFormatter implements Analyzer.Visitor, 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; mchung@2538: this.name = archive.getName(); 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 mchung@2538: public void visitDependence(String origin, Archive originArchive, mchung@2538: String target, Archive targetArchive) { mchung@2538: String tag = toTag(target, targetArchive); mchung@2538: writer.format(" %-50s -> \"%s\";%n", mchung@2538: String.format("\"%s\"", origin), mchung@2538: tag.isEmpty() ? target mchung@2538: : String.format("%s (%s)", target, tag)); aoqi@0: } aoqi@0: } aoqi@0: mchung@2538: class SummaryDotFile implements Analyzer.Visitor, AutoCloseable { mchung@2538: private final PrintWriter writer; mchung@2538: private final Analyzer.Type type; mchung@2538: private final Map> edges = new HashMap<>(); mchung@2538: SummaryDotFile(PrintWriter writer, Analyzer.Type type) { mchung@2538: this.writer = writer; mchung@2538: this.type = type; mchung@2538: writer.format("digraph \"summary\" {%n"); mchung@2538: } mchung@2538: aoqi@0: @Override mchung@2538: public void close() { mchung@2538: writer.println("}"); mchung@2538: } mchung@2538: mchung@2538: @Override mchung@2538: public void visitDependence(String origin, Archive originArchive, mchung@2538: String target, Archive targetArchive) { mchung@2538: String targetName = type == PACKAGE ? target : targetArchive.getName(); mchung@2538: if (type == PACKAGE) { mchung@2538: String tag = toTag(target, targetArchive, type); mchung@2538: if (!tag.isEmpty()) mchung@2538: targetName += " (" + tag + ")"; mchung@2538: } else if (options.showProfile && JDKArchive.isProfileArchive(targetArchive)) { mchung@2538: targetName += " (" + target + ")"; aoqi@0: } mchung@2538: String label = getLabel(originArchive, targetArchive); mchung@2538: writer.format(" %-50s -> \"%s\"%s;%n", mchung@2538: String.format("\"%s\"", origin), targetName, label); aoqi@0: } mchung@2538: mchung@2538: String getLabel(Archive origin, Archive target) { mchung@2538: if (edges.isEmpty()) mchung@2538: return ""; mchung@2538: mchung@2538: StringBuilder label = edges.get(origin).get(target); mchung@2538: return label == null ? "" : String.format(" [label=\"%s\",fontsize=9]", label.toString()); mchung@2538: } mchung@2538: mchung@2538: Analyzer.Visitor labelBuilder() { mchung@2538: // show the package-level dependencies as labels in the dot graph mchung@2538: return new Analyzer.Visitor() { mchung@2538: @Override mchung@2538: public void visitDependence(String origin, Archive originArchive, mchung@2538: String target, Archive targetArchive) mchung@2538: { mchung@2538: Map labels = edges.get(originArchive); mchung@2538: if (!edges.containsKey(originArchive)) { mchung@2538: edges.put(originArchive, labels = new HashMap<>()); mchung@2538: } mchung@2538: StringBuilder sb = labels.get(targetArchive); mchung@2538: if (sb == null) { mchung@2538: labels.put(targetArchive, sb = new StringBuilder()); mchung@2538: } mchung@2538: String tag = toTag(target, targetArchive, PACKAGE); mchung@2538: addLabel(sb, origin, target, tag); mchung@2214: } mchung@2538: mchung@2538: void addLabel(StringBuilder label, String origin, String target, String tag) { mchung@2538: label.append(origin).append(" -> ").append(target); mchung@2538: if (!tag.isEmpty()) { mchung@2538: label.append(" (" + tag + ")"); mchung@2538: } mchung@2538: label.append("\\n"); mchung@2538: } mchung@2538: }; mchung@2214: } mchung@2214: } mchung@2214: mchung@2538: /** mchung@2538: * Test if the given archive is part of the JDK mchung@2538: */ mchung@2538: private boolean isJDKArchive(Archive archive) { mchung@2538: return JDKArchive.class.isInstance(archive); mchung@2538: } mchung@2538: mchung@2538: /** mchung@2538: * If the given archive is JDK archive, this method returns the profile name mchung@2538: * only if -profile option is specified; it accesses a private JDK API and mchung@2538: * the returned value will have "JDK internal API" prefix mchung@2538: * mchung@2538: * For non-JDK archives, this method returns the file name of the archive. mchung@2538: */ mchung@2538: private String toTag(String name, Archive source, Analyzer.Type type) { mchung@2538: if (!isJDKArchive(source)) { mchung@2538: return source.getName(); mchung@2214: } mchung@2214: mchung@2538: JDKArchive jdk = (JDKArchive)source; mchung@2538: boolean isExported = false; mchung@2538: if (type == CLASS || type == VERBOSE) { mchung@2538: isExported = jdk.isExported(name); mchung@2538: } else { mchung@2538: isExported = jdk.isExportedPackage(name); mchung@2214: } mchung@2538: Profile p = getProfile(name, type); mchung@2538: if (isExported) { mchung@2538: // exported API mchung@2538: return options.showProfile && p != null ? p.profileName() : ""; mchung@2538: } else { mchung@2538: return "JDK internal API (" + source.getName() + ")"; mchung@2214: } mchung@2214: } mchung@2538: mchung@2538: private String toTag(String name, Archive source) { mchung@2538: return toTag(name, source, options.verbose); mchung@2538: } mchung@2538: mchung@2538: private Profile getProfile(String name, Analyzer.Type type) { mchung@2538: String pn = name; mchung@2538: if (type == CLASS || type == VERBOSE) { mchung@2538: int i = name.lastIndexOf('.'); mchung@2538: pn = i > 0 ? name.substring(0, i) : ""; mchung@2214: } mchung@2538: return Profile.getProfile(pn); mchung@2214: } mchung@2539: mchung@2539: /** mchung@2539: * Returns the recommended replacement API for the given classname; mchung@2539: * or return null if replacement API is not known. mchung@2539: */ mchung@2539: private String replacementFor(String cn) { mchung@2539: String name = cn; mchung@2539: String value = null; mchung@2539: while (value == null && name != null) { mchung@2539: try { mchung@2539: value = ResourceBundleHelper.jdkinternals.getString(name); mchung@2539: } catch (MissingResourceException e) { mchung@2539: // go up one subpackage level mchung@2539: int i = name.lastIndexOf('.'); mchung@2539: name = i > 0 ? name.substring(0, i) : null; mchung@2539: } mchung@2539: } mchung@2539: return value; mchung@2539: }; mchung@2539: mchung@2539: private void showReplacements(Analyzer analyzer) { mchung@2539: Map jdkinternals = new TreeMap<>(); mchung@2539: boolean useInternals = false; mchung@2539: for (Archive source : sourceLocations) { mchung@2539: useInternals = useInternals || analyzer.hasDependences(source); mchung@2539: for (String cn : analyzer.dependences(source)) { mchung@2539: String repl = replacementFor(cn); mchung@2539: if (repl != null && !jdkinternals.containsKey(cn)) { mchung@2539: jdkinternals.put(cn, repl); aoqi@0: } aoqi@0: } aoqi@0: } mchung@2539: if (useInternals) { mchung@2539: log.println(); mchung@2539: warning("warn.replace.useJDKInternals", getMessage("jdeps.wiki.url")); aoqi@0: } mchung@2539: if (!jdkinternals.isEmpty()) { mchung@2539: log.println(); mchung@2539: log.format("%-40s %s%n", "JDK Internal API", "Suggested Replacement"); mchung@2539: log.format("%-40s %s%n", "----------------", "---------------------"); mchung@2539: for (Map.Entry e : jdkinternals.entrySet()) { mchung@2539: log.format("%-40s %s%n", e.getKey(), e.getValue()); aoqi@0: } aoqi@0: } aoqi@0: aoqi@0: } aoqi@0: }