/* * Copyright (c) 2012, 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.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 java.io.*; import java.text.MessageFormat; import java.util.*; import java.util.regex.Pattern; /** * Implementation for the jdeps tool for static class dependency analysis. */ class JdepsTask { class BadArgs extends Exception { static final long serialVersionUID = 8765093759964640721L; BadArgs(String key, Object... args) { super(JdepsTask.this.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; } } return false; } boolean ignoreRest() { return false; } abstract void process(JdepsTask task, String opt, String arg) throws BadArgs; final boolean hasArg; final String[] aliases; } static Option[] recognizedOptions = { new Option(false, "-h", "--help") { void process(JdepsTask task, String opt, String arg) { task.options.help = true; } }, new Option(false, "-version") { void process(JdepsTask task, String opt, String arg) { task.options.version = true; } }, new Option(false, "-fullversion") { void process(JdepsTask task, String opt, String arg) { task.options.fullVersion = true; } boolean isHidden() { return true; } }, new Option(true, "-classpath") { void process(JdepsTask task, String opt, String arg) { task.options.classpath = arg; } }, new Option(false, "-summary") { void process(JdepsTask task, String opt, String arg) { task.options.showSummary = true; task.options.verbose = Options.Verbose.SUMMARY; } }, new Option(false, "-v:class") { void process(JdepsTask task, String opt, String arg) { task.options.verbose = Options.Verbose.CLASS; } }, new Option(false, "-v:package") { void process(JdepsTask task, String opt, String arg) { task.options.verbose = Options.Verbose.PACKAGE; } }, 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(false, "-P", "--profile") { void process(JdepsTask task, String opt, String arg) { task.options.showProfile = true; } }, new Option(false, "-R", "--recursive") { void process(JdepsTask task, String opt, String arg) { task.options.depth = 0; } }, new Option(true, "-d", "--depth") { void process(JdepsTask task, String opt, String arg) throws BadArgs { try { task.options.depth = Integer.parseInt(arg); } catch (NumberFormatException e) { throw task.new BadArgs("err.invalid.arg.for.option", opt); } } boolean isHidden() { return true; } }, new Option(false, "-all") { void process(JdepsTask task, String opt, String arg) { task.options.all = true; } }}; private static final String PROGNAME = "jdeps"; private final Options options = new Options(); private final List filenames = 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(Arrays.asList(args)); if (filenames.isEmpty() && (!options.all || options.classpath.isEmpty())) { if (options.help || options.version || options.fullVersion) { return EXIT_OK; } else { return EXIT_CMDERR; } } if (options.regex != null && options.packageNames.size() > 0) { showHelp(); return EXIT_CMDERR; } if (options.showSummary && options.verbose != 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 boolean run() throws IOException { Dependency.Finder finder = Dependencies.getClassDependencyFinder(); Dependency.Filter filter; if (options.regex != null) { filter = Dependencies.getRegexFilter(Pattern.compile(options.regex)); } else if (options.packageNames.size() > 0) { filter = Dependencies.getPackageFilter(options.packageNames, false); } else { filter = new Dependency.Filter() { public boolean accepts(Dependency dependency) { return !dependency.getOrigin().equals(dependency.getTarget()); } }; } findDependencies(finder, filter); switch (options.verbose) { case CLASS: printClassDeps(log); break; case PACKAGE: printPackageDeps(log); break; case SUMMARY: for (Archive origin : sourceLocations) { for (Archive target : origin.getRequiredArchives()) { log.format("%-30s -> %s%n", origin, target); } } break; default: throw new InternalError("Should not reach here"); } return true; } private final List sourceLocations = new ArrayList<>(); private final Archive NOT_FOUND = new Archive(null, null); private void findDependencies(Dependency.Finder finder, Dependency.Filter filter) throws IOException { // 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<>(); List archives = new ArrayList<>(getArchives(filenames)); List classpaths = classPathArchives(options.classpath); List platformClassPath = PlatformClassPath.getArchives(); sourceLocations.addAll(archives); sourceLocations.addAll(classpaths); sourceLocations.addAll(platformClassPath); if (options.all) { archives.addAll(classpaths); } // get the immediate dependencies of the input files for (Archive a : archives) { for (ClassFile cf : a.reader().getClassFiles()) { String className; try { className = cf.getName().replace('/', '.'); } catch (ConstantPoolException e) { throw new ClassFileError(e); } a.addClass(className); if (!doneClasses.contains(className)) { doneClasses.add(className); } for (Dependency d : finder.findDependencies(cf)) { if (filter.accepts(d)) { String cn = d.getTarget().getClassName(); if (!doneClasses.contains(cn) && !deque.contains(cn)) { deque.add(cn); } a.addDependency(d); } } } } // add Archive for looking up classes from the classpath // for transitive dependency analysis int depths = options.depth == 0 ? Integer.MAX_VALUE : options.depth; List cpathReaders = new ArrayList<>(); if (!options.all) { cpathReaders.addAll(classpaths); } cpathReaders.addAll(platformClassPath); while (!deque.isEmpty() && depths-- >= 0) { Deque unresolved = deque; deque = new LinkedList<>(); String className; while ((className = unresolved.poll()) != null) { if (doneClasses.contains(className)) { continue; } ClassFile cf = null; for (Archive a : cpathReaders) { cf = a.reader().getClassFile(className); if (cf != null) { a.addClass(className); doneClasses.add(className); if (depths > 0) { // do dependency analysis if depths > 0 for (Dependency d : finder.findDependencies(cf)) { if (filter.accepts(d)) { String cn = d.getTarget().getClassName(); if (!doneClasses.contains(cn) && !deque.contains(cn)) { deque.add(cn); } a.addDependency(d); } } } break; } } if (cf == null) { NOT_FOUND.addClass(className); } } } } private void printPackageDeps(PrintWriter out) { for (Archive source : sourceLocations) { SortedMap> deps = source.getDependencies(); if (deps.isEmpty()) continue; for (Archive target : source.getRequiredArchives()) { out.format("%s -> %s%n", source, target); } Map pkgs = new TreeMap<>(); SortedMap targets = new TreeMap<>(); String pkg = ""; for (Map.Entry> e : deps.entrySet()) { Location o = e.getKey(); String p = packageOf(o); Archive origin = Archive.find(o.getClassName()); if (!pkgs.containsKey(p)) { pkgs.put(p, origin); } else if (pkgs.get(p) != origin) { warning("warn.split.package", p, origin, pkgs.get(p)); } if (!p.equals(pkg)) { printTargets(out, targets); pkg = p; targets.clear(); out.format(" %s (%s)%n", p, Archive.find(o.getClassName()).getFileName()); } for (Location t : e.getValue()) { p = packageOf(t); Archive target = Archive.find(t.getClassName()); if (!targets.containsKey(p)) { targets.put(p, target); } } } printTargets(out, targets); out.println(); } } private void printTargets(PrintWriter out, Map targets) { for (Map.Entry t : targets.entrySet()) { String pn = t.getKey(); out.format(" -> %-40s %s%n", pn, getPackageInfo(pn, t.getValue())); } } private String getPackageInfo(String pn, Archive source) { if (PlatformClassPath.contains(source)) { String name = PlatformClassPath.getProfileName(pn); if (name.isEmpty()) { return "JDK internal API (" + source.getFileName() + ")"; } return options.showProfile ? name : ""; } return source.getFileName(); } private static String packageOf(Location loc) { String cn = loc.getClassName(); int i = cn.lastIndexOf('.'); return i > 0 ? cn.substring(0, i) : ""; } private void printClassDeps(PrintWriter out) { for (Archive source : sourceLocations) { SortedMap> deps = source.getDependencies(); if (deps.isEmpty()) continue; for (Archive target : source.getRequiredArchives()) { out.format("%s -> %s%n", source, target); } out.format("%s%n", source); for (Map.Entry> e : deps.entrySet()) { Location o = e.getKey(); String cn = o.getClassName(); Archive origin = Archive.find(cn); out.format(" %s (%s)%n", cn, origin.getFileName()); for (Location t : e.getValue()) { cn = t.getClassName(); Archive target = Archive.find(cn); out.format(" -> %-60s %s%n", cn, getPackageInfo(packageOf(t), target)); } } out.println(); } } public void handleOptions(Iterable args) throws BadArgs { Iterator iter = args.iterator(); boolean noArgs = !iter.hasNext(); while (iter.hasNext()) { String arg = iter.next(); if (arg.startsWith("-")) { handleOption(arg, iter); } else { filenames.add(arg); while (iter.hasNext()) { filenames.add(iter.next()); } } } if (noArgs || options.help || (filenames.isEmpty() && (!options.all || options.classpath.isEmpty()))) { showHelp(); } if (options.version || options.fullVersion) { showVersion(options.fullVersion); } } private void handleOption(String name, Iterator rest) throws BadArgs { for (Option o : recognizedOptions) { if (o.matches(name)) { if (o.hasArg) { String arg = rest.hasNext() ? rest.next() : "-"; if (arg.startsWith("-")) { throw new BadArgs("err.missing.arg", name).showUsage(true); } else { o.process(this, name, arg); } } else { o.process(this, name, null); } if (o.ignoreRest()) { while (rest.hasNext()) { rest.next(); } } return; } } 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).replace(':', '.'); // there must always be at least one name if (o.isHidden() || name.equals("h")) { continue; } log.println(getMessage("main.opt." + name)); } } private void showVersion(boolean full) { log.println(version(full ? "full" : "release")); } private String version(String key) { // key=version: mm.nn.oo[-milestone] // key=full: mm.mm.oo[-milestone]-build if (ResourceBundleHelper.versionRB == null) { return System.getProperty("java.version"); } try { return ResourceBundleHelper.versionRB.getString(key); } catch (MissingResourceException e) { return getMessage("version.unknown", System.getProperty("java.version")); } } public 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 { enum Verbose { CLASS, PACKAGE, SUMMARY }; public boolean help; public boolean version; public boolean fullVersion; public boolean showFlags; public boolean reverse; public boolean all; public boolean showProfile; public boolean showSummary; public String regex; public String classpath = ""; public int depth = 1; public Verbose verbose = Verbose.PACKAGE; public Set packageNames = new HashSet<>(); } private static class ResourceBundleHelper { static final ResourceBundle versionRB; static final ResourceBundle bundle; static { Locale locale = Locale.getDefault(); try { bundle = ResourceBundle.getBundle("com.sun.tools.jdeps.resources.jdeps", locale); } catch (MissingResourceException e) { throw new InternalError("Cannot find jdeps resource bundle for locale " + locale); } try { versionRB = ResourceBundle.getBundle("com.sun.tools.jdeps.resources.version"); } catch (MissingResourceException e) { throw new InternalError("version.resource.missing"); } } } private List getArchives(List filenames) throws IOException { List result = new ArrayList<>(); for (String s : filenames) { File f = new File(s); if (f.exists()) { ClassFileReader reader = ClassFileReader.newInstance(f); Archive archive = new Archive(f, reader); result.add(archive); } else { warning("warn.file.not.exist", s); } } return result; } private List classPathArchives(String classpath) throws IOException { List result = new ArrayList<>(); if (classpath.isEmpty()) { return result; } for (String p : classpath.split(File.pathSeparator)) { if (p.length() > 0) { File f = new File(p); if (f.exists()) { ClassFileReader reader = ClassFileReader.newInstance(f); Archive archive = new Archive(f, reader); result.add(archive); } } } return result; } }