langtools/src/share/classes/com/sun/tools/jdeps/JdepsTask.java
changeset 15030 2d8dec41f029
child 16290 b0b4f52de7ea
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/langtools/src/share/classes/com/sun/tools/jdeps/JdepsTask.java	Fri Dec 28 22:25:21 2012 -0800
@@ -0,0 +1,650 @@
+/*
+ * 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;
+                } else if (opt.startsWith("--") && 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(false, "-s", "--summary") {
+            void process(JdepsTask task, String opt, String arg) {
+                task.options.showSummary = true;
+                task.options.verbose = Options.Verbose.SUMMARY;
+            }
+        },
+        new Option(false, "-v", "--verbose") {
+            void process(JdepsTask task, String opt, String arg) {
+                task.options.verbose = Options.Verbose.VERBOSE;
+            }
+        },
+        new Option(true, "-V", "--verbose-level") {
+            void process(JdepsTask task, String opt, String arg) throws BadArgs {
+                switch (arg) {
+                    case "package":
+                        task.options.verbose = Options.Verbose.PACKAGE;
+                        break;
+                    case "class":
+                        task.options.verbose = Options.Verbose.CLASS;
+                        break;
+                    default:
+                        throw task.new BadArgs("err.invalid.arg.for.option", opt);
+                }
+            }
+        },
+        new Option(true, "-c", "--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(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 HiddenOption(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);
+                }
+            }
+        },
+        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;
+            }
+        },
+
+    };
+
+    private static final String PROGNAME = "jdeps";
+    private final Options options = new Options();
+    private final List<String> classes = new ArrayList<String>();
+
+    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.wildcard) {
+                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.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 final List<Archive> sourceLocations = new ArrayList<Archive>();
+    private final Archive NOT_FOUND = new Archive(getMessage("artifact.not.found"));
+    private boolean run() throws IOException {
+        findDependencies();
+        switch (options.verbose) {
+            case 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 boolean isValidClassName(String name) {
+        if (!Character.isJavaIdentifierStart(name.charAt(0))) {
+            return false;
+        }
+        for (int i=1; i < name.length(); i++) {
+            char c = name.charAt(i);
+            if (c != '.'  && !Character.isJavaIdentifierPart(c)) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    private void findDependencies() 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());
+                }
+            };
+        }
+
+        List<Archive> archives = new ArrayList<Archive>();
+        Deque<String> roots = new LinkedList<String>();
+        for (String s : classes) {
+            File f = new File(s);
+            if (f.exists()) {
+                archives.add(new Archive(f, ClassFileReader.newInstance(f)));
+            } else {
+                if (isValidClassName(s)) {
+                    roots.add(s);
+                } else {
+                    warning("warn.invalid.arg", s);
+                }
+            }
+        }
+
+        List<Archive> classpaths = new ArrayList<Archive>(); // for class file lookup
+        if (options.wildcard) {
+            // include all archives from classpath to the initial list
+            archives.addAll(getClassPathArchives(options.classpath));
+        } else {
+            classpaths.addAll(getClassPathArchives(options.classpath));
+        }
+        classpaths.addAll(PlatformClassPath.getArchives());
+
+        // add all archives to the source locations for reporting
+        sourceLocations.addAll(archives);
+        sourceLocations.addAll(classpaths);
+
+        // Work queue of names of classfiles to be searched.
+        // Entries will be unique, and for classes that do not yet have
+        // dependencies in the results map.
+        Deque<String> deque = new LinkedList<String>();
+        Set<String> doneClasses = new HashSet<String>();
+
+        // get the immediate dependencies of the input files
+        for (Archive a : archives) {
+            for (ClassFile cf : a.reader().getClassFiles()) {
+                String classFileName;
+                try {
+                    classFileName = cf.getName();
+                } catch (ConstantPoolException e) {
+                    throw new ClassFileError(e);
+                }
+                a.addClass(classFileName);
+                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.addDependency(d);
+                    }
+                }
+            }
+        }
+
+        // add Archive for looking up classes from the classpath
+        // for transitive dependency analysis
+        Deque<String> unresolved = roots;
+        int depth = options.depth > 0 ? options.depth : Integer.MAX_VALUE;
+        do {
+            String name;
+            while ((name = unresolved.poll()) != null) {
+                if (doneClasses.contains(name)) {
+                    continue;
+                }
+                ClassFile cf = null;
+                for (Archive a : classpaths) {
+                    cf = a.reader().getClassFile(name);
+                    if (cf != null) {
+                        String classFileName;
+                        try {
+                            classFileName = cf.getName();
+                        } catch (ConstantPoolException e) {
+                            throw new ClassFileError(e);
+                        }
+                        a.addClass(classFileName);
+                        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);
+                            if (depth > 0) {
+                                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.addDependency(d);
+                                    }
+                                }
+                            }
+                        }
+                        break;
+                    }
+                }
+                if (cf == null) {
+                    NOT_FOUND.addClass(name);
+                }
+            }
+            unresolved = deque;
+            deque = new LinkedList<String>();
+        } while (!unresolved.isEmpty() && depth-- > 0);
+    }
+
+    private void printPackageDeps(PrintWriter out) {
+        for (Archive source : sourceLocations) {
+            SortedMap<Location, SortedSet<Location>> deps = source.getDependencies();
+            if (deps.isEmpty())
+                continue;
+
+            for (Archive target : source.getRequiredArchives()) {
+                out.format("%s -> %s%n", source, target);
+            }
+
+            Map<String, Archive> pkgs = new TreeMap<String, Archive>();
+            SortedMap<String, Archive> targets = new TreeMap<String, Archive>();
+            String pkg = "";
+            for (Map.Entry<Location, SortedSet<Location>> e : deps.entrySet()) {
+                String cn = e.getKey().getClassName();
+                String p = packageOf(e.getKey());
+                Archive origin = Archive.find(e.getKey());
+                assert origin != null;
+                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, origin.getFileName());
+                }
+
+                for (Location t : e.getValue()) {
+                    p = packageOf(t);
+                    Archive target = Archive.find(t);
+                    if (!targets.containsKey(p)) {
+                        targets.put(p, target);
+                    }
+                }
+            }
+            printTargets(out, targets);
+            out.println();
+        }
+    }
+
+    private void printTargets(PrintWriter out, Map<String, Archive> targets) {
+        for (Map.Entry<String, Archive> 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 pkg = loc.getPackageName();
+        return pkg.isEmpty() ? "<unnamed>" : pkg;
+    }
+
+    private void printClassDeps(PrintWriter out) {
+        for (Archive source : sourceLocations) {
+            SortedMap<Location, SortedSet<Location>> 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<Location, SortedSet<Location>> e : deps.entrySet()) {
+                String cn = e.getKey().getClassName();
+                Archive origin = Archive.find(e.getKey());
+                out.format("   %s (%s)%n", cn, origin.getFileName());
+                for (Location t : e.getValue()) {
+                    cn = t.getClassName();
+                    Archive target = Archive.find(t);
+                    out.format("      -> %-60s %s%n", cn, getPackageInfo(t.getPackageName(), target));
+                }
+            }
+            out.println();
+        }
+    }
+    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);
+                    }
+                    if (name.equals("*") || name.equals("\"*\"")) {
+                        options.wildcard = true;
+                    } else {
+                        classes.add(name);
+                    }
+                }
+            }
+        }
+    }
+
+    private Option getOption(String name) throws BadArgs {
+        for (Option o : recognizedOptions) {
+            if (o.matches(name)) {
+                return o;
+            }
+        }
+        throw new BadArgs("err.unknown.option", name).showUsage(true);
+    }
+
+    private void reportError(String key, Object... args) {
+        log.println(getMessage("error.prefix") + " " + getMessage(key, args));
+    }
+
+    private void warning(String key, Object... args) {
+        log.println(getMessage("warn.prefix") + " " + getMessage(key, args));
+    }
+
+    private void showHelp() {
+        log.println(getMessage("main.usage", PROGNAME));
+        for (Option o : recognizedOptions) {
+            String name = o.aliases[0].substring(1); // there must always be at least one name
+            name = name.charAt(0) == '-' ? name.substring(1) : name;
+            if (o.isHidden() || name.equals("h")) {
+                continue;
+            }
+            log.println(getMessage("main.opt." + name));
+        }
+    }
+
+    private void showVersion(boolean full) {
+        log.println(version(full ? "full" : "release"));
+    }
+
+    private String version(String key) {
+        // key=version:  mm.nn.oo[-milestone]
+        // key=full:     mm.mm.oo[-milestone]-build
+        if (ResourceBundleHelper.versionRB == null) {
+            return System.getProperty("java.version");
+        }
+        try {
+            return ResourceBundleHelper.versionRB.getString(key);
+        } catch (MissingResourceException e) {
+            return getMessage("version.unknown", System.getProperty("java.version"));
+        }
+    }
+
+    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,
+            VERBOSE
+        };
+
+        boolean help;
+        boolean version;
+        boolean fullVersion;
+        boolean showFlags;
+        boolean showProfile;
+        boolean showSummary;
+        boolean wildcard;
+        String regex;
+        String classpath = "";
+        int depth = 1;
+        Verbose verbose = Verbose.PACKAGE;
+        Set<String> packageNames = new HashSet<String>();
+    }
+
+    private static class ResourceBundleHelper {
+        static final ResourceBundle versionRB;
+        static final ResourceBundle bundle;
+
+        static {
+            Locale locale = Locale.getDefault();
+            try {
+                bundle = ResourceBundle.getBundle("com.sun.tools.jdeps.resources.jdeps", locale);
+            } catch (MissingResourceException e) {
+                throw new InternalError("Cannot find jdeps resource bundle for locale " + locale);
+            }
+            try {
+                versionRB = ResourceBundle.getBundle("com.sun.tools.jdeps.resources.version");
+            } catch (MissingResourceException e) {
+                throw new InternalError("version.resource.missing");
+            }
+        }
+    }
+
+    private List<Archive> getArchives(List<String> filenames) throws IOException {
+        List<Archive> result = new ArrayList<Archive>();
+        for (String s : filenames) {
+            File f = new File(s);
+            if (f.exists()) {
+                result.add(new Archive(f, ClassFileReader.newInstance(f)));
+            } else {
+                warning("warn.file.not.exist", s);
+            }
+        }
+        return result;
+    }
+
+    private List<Archive> getClassPathArchives(String paths) throws IOException {
+        List<Archive> result = new ArrayList<Archive>();
+        if (paths.isEmpty()) {
+            return result;
+        }
+        for (String p : paths.split(File.pathSeparator)) {
+            if (p.length() > 0) {
+                File f = new File(p);
+                if (f.exists()) {
+                    result.add(new Archive(f, ClassFileReader.newInstance(f)));
+                }
+            }
+        }
+        return result;
+    }
+}