blob: a7d0ecf9b6f3705a9c670ffc503b932f6f96bffd [file] [log] [blame]
/*
* 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 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.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 = Analyzer.Type.SUMMARY;
}
},
new Option(false, "-v", "--verbose") {
void process(JdepsTask task, String opt, String arg) {
task.options.verbose = Analyzer.Type.VERBOSE;
}
},
new Option(true, "-V", "--verbose-level") {
void process(JdepsTask task, String opt, String arg) throws BadArgs {
if ("package".equals(arg)) {
task.options.verbose = Analyzer.Type.PACKAGE;
} else if ("class".equals(arg)) {
task.options.verbose = Analyzer.Type.CLASS;
} else {
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) throws BadArgs {
task.options.showProfile = true;
if (Profiles.getProfileCount() == 0) {
throw task.new BadArgs("err.option.unsupported", opt, getMessage("err.profiles.msg"));
}
}
},
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 != Analyzer.Type.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 boolean run() throws IOException {
findDependencies();
Analyzer analyzer = new Analyzer(options.verbose);
analyzer.run(sourceLocations);
if (options.verbose == Analyzer.Type.SUMMARY) {
printSummary(log, analyzer);
} else {
printDependencies(log, analyzer);
}
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);
}
if (!doneClasses.contains(classFileName)) {
doneClasses.add(classFileName);
}
for (Dependency d : finder.findDependencies(cf)) {
if (filter.accepts(d)) {
String cn = d.getTarget().getName();
if (!doneClasses.contains(cn) && !deque.contains(cn)) {
deque.add(cn);
}
a.addClass(d.getOrigin(), d.getTarget());
}
}
}
}
// 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);
}
if (!doneClasses.contains(classFileName)) {
// if name is a fully-qualified class name specified
// from command-line, this class might already be parsed
doneClasses.add(classFileName);
for (Dependency d : finder.findDependencies(cf)) {
if (depth == 0) {
// ignore the dependency
a.addClass(d.getOrigin());
break;
} else if (filter.accepts(d)) {
a.addClass(d.getOrigin(), d.getTarget());
String cn = d.getTarget().getName();
if (!doneClasses.contains(cn) && !deque.contains(cn)) {
deque.add(cn);
}
}
}
}
break;
}
}
if (cf == null) {
doneClasses.add(name);
}
}
unresolved = deque;
deque = new LinkedList<String>();
} while (!unresolved.isEmpty() && depth-- > 0);
}
private void printSummary(final PrintWriter out, final Analyzer analyzer) {
Analyzer.Visitor visitor = new Analyzer.Visitor() {
public void visit(String origin, String target, String profile) {
if (options.showProfile) {
out.format("%-30s -> %s%n", origin, target);
}
}
public void visit(Archive origin, Archive target) {
if (!options.showProfile) {
out.format("%-30s -> %s%n", origin, target);
}
}
};
analyzer.visitSummary(visitor);
}
private void printDependencies(final PrintWriter out, final Analyzer analyzer) {
Analyzer.Visitor visitor = new Analyzer.Visitor() {
private String pkg = "";
public void visit(String origin, String target, String profile) {
if (!origin.equals(pkg)) {
pkg = origin;
out.format(" %s (%s)%n", origin, analyzer.getArchive(origin).getFileName());
}
out.format(" -> %-50s %s%n", target,
(options.showProfile && !profile.isEmpty())
? profile
: analyzer.getArchiveName(target, profile));
}
public void visit(Archive origin, Archive target) {
out.format("%s -> %s%n", origin, target);
}
};
analyzer.visit(visitor);
}
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"));
}
}
static String getMessage(String key, Object... args) {
try {
return MessageFormat.format(ResourceBundleHelper.bundle.getString(key), args);
} catch (MissingResourceException e) {
throw new InternalError("Missing message: " + key);
}
}
private static class Options {
boolean help;
boolean version;
boolean fullVersion;
boolean showFlags;
boolean showProfile;
boolean showSummary;
boolean wildcard;
String regex;
String classpath = "";
int depth = 1;
Analyzer.Type verbose = Analyzer.Type.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;
}
}