Thu, 17 Oct 2013 13:19:48 -0700
8015912: jdeps support to output in dot file format
8026255: Switch jdeps to follow traditional Java option style
Reviewed-by: alanb
1 /*
2 * Copyright (c) 2012, 2013, Oracle and/or its affiliates. All rights reserved.
3 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
4 *
5 * This code is free software; you can redistribute it and/or modify it
6 * under the terms of the GNU General Public License version 2 only, as
7 * published by the Free Software Foundation. Oracle designates this
8 * particular file as subject to the "Classpath" exception as provided
9 * by Oracle in the LICENSE file that accompanied this code.
10 *
11 * This code is distributed in the hope that it will be useful, but WITHOUT
12 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
13 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
14 * version 2 for more details (a copy is included in the LICENSE file that
15 * accompanied this code).
16 *
17 * You should have received a copy of the GNU General Public License version
18 * 2 along with this work; if not, write to the Free Software Foundation,
19 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
20 *
21 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
22 * or visit www.oracle.com if you need additional information or have any
23 * questions.
24 */
25 package com.sun.tools.jdeps;
27 import com.sun.tools.classfile.AccessFlags;
28 import com.sun.tools.classfile.ClassFile;
29 import com.sun.tools.classfile.ConstantPoolException;
30 import com.sun.tools.classfile.Dependencies;
31 import com.sun.tools.classfile.Dependencies.ClassFileError;
32 import com.sun.tools.classfile.Dependency;
33 import com.sun.tools.jdeps.PlatformClassPath.JDKArchive;
34 import java.io.*;
35 import java.nio.file.DirectoryStream;
36 import java.nio.file.Files;
37 import java.nio.file.Path;
38 import java.nio.file.Paths;
39 import java.text.MessageFormat;
40 import java.util.*;
41 import java.util.regex.Pattern;
43 /**
44 * Implementation for the jdeps tool for static class dependency analysis.
45 */
46 class JdepsTask {
47 static class BadArgs extends Exception {
48 static final long serialVersionUID = 8765093759964640721L;
49 BadArgs(String key, Object... args) {
50 super(JdepsTask.getMessage(key, args));
51 this.key = key;
52 this.args = args;
53 }
55 BadArgs showUsage(boolean b) {
56 showUsage = b;
57 return this;
58 }
59 final String key;
60 final Object[] args;
61 boolean showUsage;
62 }
64 static abstract class Option {
65 Option(boolean hasArg, String... aliases) {
66 this.hasArg = hasArg;
67 this.aliases = aliases;
68 }
70 boolean isHidden() {
71 return false;
72 }
74 boolean matches(String opt) {
75 for (String a : aliases) {
76 if (a.equals(opt))
77 return true;
78 if (hasArg && opt.startsWith(a + "="))
79 return true;
80 }
81 return false;
82 }
84 boolean ignoreRest() {
85 return false;
86 }
88 abstract void process(JdepsTask task, String opt, String arg) throws BadArgs;
89 final boolean hasArg;
90 final String[] aliases;
91 }
93 static abstract class HiddenOption extends Option {
94 HiddenOption(boolean hasArg, String... aliases) {
95 super(hasArg, aliases);
96 }
98 boolean isHidden() {
99 return true;
100 }
101 }
103 static Option[] recognizedOptions = {
104 new Option(false, "-h", "-?", "-help") {
105 void process(JdepsTask task, String opt, String arg) {
106 task.options.help = true;
107 }
108 },
109 new Option(true, "-dotoutput") {
110 void process(JdepsTask task, String opt, String arg) throws BadArgs {
111 Path p = Paths.get(arg);
112 if (Files.exists(p) && (!Files.isDirectory(p) || !Files.isWritable(p))) {
113 throw new BadArgs("err.dot.output.path", arg);
114 }
115 task.options.dotOutputDir = arg;
116 }
117 },
118 new Option(false, "-s", "-summary") {
119 void process(JdepsTask task, String opt, String arg) {
120 task.options.showSummary = true;
121 task.options.verbose = Analyzer.Type.SUMMARY;
122 }
123 },
124 new Option(false, "-v", "-verbose",
125 "-verbose:package",
126 "-verbose:class")
127 {
128 void process(JdepsTask task, String opt, String arg) throws BadArgs {
129 switch (opt) {
130 case "-v":
131 case "-verbose":
132 task.options.verbose = Analyzer.Type.VERBOSE;
133 break;
134 case "-verbose:package":
135 task.options.verbose = Analyzer.Type.PACKAGE;
136 break;
137 case "-verbose:class":
138 task.options.verbose = Analyzer.Type.CLASS;
139 break;
140 default:
141 throw new BadArgs("err.invalid.arg.for.option", opt);
142 }
143 }
144 },
145 new Option(true, "-cp", "-classpath") {
146 void process(JdepsTask task, String opt, String arg) {
147 task.options.classpath = arg;
148 }
149 },
150 new Option(true, "-p", "-package") {
151 void process(JdepsTask task, String opt, String arg) {
152 task.options.packageNames.add(arg);
153 }
154 },
155 new Option(true, "-e", "-regex") {
156 void process(JdepsTask task, String opt, String arg) {
157 task.options.regex = arg;
158 }
159 },
160 new Option(true, "-include") {
161 void process(JdepsTask task, String opt, String arg) throws BadArgs {
162 task.options.includePattern = Pattern.compile(arg);
163 }
164 },
165 new Option(false, "-P", "-profile") {
166 void process(JdepsTask task, String opt, String arg) throws BadArgs {
167 task.options.showProfile = true;
168 if (Profile.getProfileCount() == 0) {
169 throw new BadArgs("err.option.unsupported", opt, getMessage("err.profiles.msg"));
170 }
171 }
172 },
173 new Option(false, "-apionly") {
174 void process(JdepsTask task, String opt, String arg) {
175 task.options.apiOnly = true;
176 }
177 },
178 new Option(false, "-R", "-recursive") {
179 void process(JdepsTask task, String opt, String arg) {
180 task.options.depth = 0;
181 }
182 },
183 new Option(false, "-version") {
184 void process(JdepsTask task, String opt, String arg) {
185 task.options.version = true;
186 }
187 },
188 new HiddenOption(false, "-fullversion") {
189 void process(JdepsTask task, String opt, String arg) {
190 task.options.fullVersion = true;
191 }
192 },
193 new HiddenOption(true, "-depth") {
194 void process(JdepsTask task, String opt, String arg) throws BadArgs {
195 try {
196 task.options.depth = Integer.parseInt(arg);
197 } catch (NumberFormatException e) {
198 throw new BadArgs("err.invalid.arg.for.option", opt);
199 }
200 }
201 },
202 };
204 private static final String PROGNAME = "jdeps";
205 private final Options options = new Options();
206 private final List<String> classes = new ArrayList<String>();
208 private PrintWriter log;
209 void setLog(PrintWriter out) {
210 log = out;
211 }
213 /**
214 * Result codes.
215 */
216 static final int EXIT_OK = 0, // Completed with no errors.
217 EXIT_ERROR = 1, // Completed but reported errors.
218 EXIT_CMDERR = 2, // Bad command-line arguments
219 EXIT_SYSERR = 3, // System error or resource exhaustion.
220 EXIT_ABNORMAL = 4;// terminated abnormally
222 int run(String[] args) {
223 if (log == null) {
224 log = new PrintWriter(System.out);
225 }
226 try {
227 handleOptions(args);
228 if (options.help) {
229 showHelp();
230 }
231 if (options.version || options.fullVersion) {
232 showVersion(options.fullVersion);
233 }
234 if (classes.isEmpty() && options.includePattern == null) {
235 if (options.help || options.version || options.fullVersion) {
236 return EXIT_OK;
237 } else {
238 showHelp();
239 return EXIT_CMDERR;
240 }
241 }
242 if (options.regex != null && options.packageNames.size() > 0) {
243 showHelp();
244 return EXIT_CMDERR;
245 }
246 if (options.showSummary && options.verbose != Analyzer.Type.SUMMARY) {
247 showHelp();
248 return EXIT_CMDERR;
249 }
250 boolean ok = run();
251 return ok ? EXIT_OK : EXIT_ERROR;
252 } catch (BadArgs e) {
253 reportError(e.key, e.args);
254 if (e.showUsage) {
255 log.println(getMessage("main.usage.summary", PROGNAME));
256 }
257 return EXIT_CMDERR;
258 } catch (IOException e) {
259 return EXIT_ABNORMAL;
260 } finally {
261 log.flush();
262 }
263 }
265 private final List<Archive> sourceLocations = new ArrayList<>();
266 private boolean run() throws IOException {
267 findDependencies();
268 Analyzer analyzer = new Analyzer(options.verbose);
269 analyzer.run(sourceLocations);
270 if (options.dotOutputDir != null) {
271 Path dir = Paths.get(options.dotOutputDir);
272 Files.createDirectories(dir);
273 generateDotFiles(dir, analyzer);
274 } else {
275 printRawOutput(log, analyzer);
276 }
277 return true;
278 }
280 private void generateDotFiles(Path dir, Analyzer analyzer) throws IOException {
281 Path summary = dir.resolve("summary.dot");
282 try (PrintWriter sw = new PrintWriter(Files.newOutputStream(summary));
283 DotFileFormatter formatter = new DotFileFormatter(sw, "summary")) {
284 for (Archive archive : sourceLocations) {
285 analyzer.visitArchiveDependences(archive, formatter);
286 }
287 }
288 if (options.verbose != Analyzer.Type.SUMMARY) {
289 for (Archive archive : sourceLocations) {
290 if (analyzer.hasDependences(archive)) {
291 Path dotfile = dir.resolve(archive.getFileName() + ".dot");
292 try (PrintWriter pw = new PrintWriter(Files.newOutputStream(dotfile));
293 DotFileFormatter formatter = new DotFileFormatter(pw, archive)) {
294 analyzer.visitDependences(archive, formatter);
295 }
296 }
297 }
298 }
299 }
301 private void printRawOutput(PrintWriter writer, Analyzer analyzer) {
302 for (Archive archive : sourceLocations) {
303 RawOutputFormatter formatter = new RawOutputFormatter(writer);
304 analyzer.visitArchiveDependences(archive, formatter);
305 if (options.verbose != Analyzer.Type.SUMMARY) {
306 analyzer.visitDependences(archive, formatter);
307 }
308 }
309 }
310 private boolean isValidClassName(String name) {
311 if (!Character.isJavaIdentifierStart(name.charAt(0))) {
312 return false;
313 }
314 for (int i=1; i < name.length(); i++) {
315 char c = name.charAt(i);
316 if (c != '.' && !Character.isJavaIdentifierPart(c)) {
317 return false;
318 }
319 }
320 return true;
321 }
323 private Dependency.Filter getDependencyFilter() {
324 if (options.regex != null) {
325 return Dependencies.getRegexFilter(Pattern.compile(options.regex));
326 } else if (options.packageNames.size() > 0) {
327 return Dependencies.getPackageFilter(options.packageNames, false);
328 } else {
329 return new Dependency.Filter() {
330 @Override
331 public boolean accepts(Dependency dependency) {
332 return !dependency.getOrigin().equals(dependency.getTarget());
333 }
334 };
335 }
336 }
338 private boolean matches(String classname, AccessFlags flags) {
339 if (options.apiOnly && !flags.is(AccessFlags.ACC_PUBLIC)) {
340 return false;
341 } else if (options.includePattern != null) {
342 return options.includePattern.matcher(classname.replace('/', '.')).matches();
343 } else {
344 return true;
345 }
346 }
348 private void findDependencies() throws IOException {
349 Dependency.Finder finder =
350 options.apiOnly ? Dependencies.getAPIFinder(AccessFlags.ACC_PROTECTED)
351 : Dependencies.getClassDependencyFinder();
352 Dependency.Filter filter = getDependencyFilter();
354 List<Archive> archives = new ArrayList<>();
355 Deque<String> roots = new LinkedList<>();
356 for (String s : classes) {
357 Path p = Paths.get(s);
358 if (Files.exists(p)) {
359 archives.add(new Archive(p, ClassFileReader.newInstance(p)));
360 } else {
361 if (isValidClassName(s)) {
362 roots.add(s);
363 } else {
364 warning("warn.invalid.arg", s);
365 }
366 }
367 }
369 List<Archive> classpaths = new ArrayList<>(); // for class file lookup
370 if (options.includePattern != null) {
371 archives.addAll(getClassPathArchives(options.classpath));
372 } else {
373 classpaths.addAll(getClassPathArchives(options.classpath));
374 }
375 classpaths.addAll(PlatformClassPath.getArchives());
377 // add all archives to the source locations for reporting
378 sourceLocations.addAll(archives);
379 sourceLocations.addAll(classpaths);
381 // Work queue of names of classfiles to be searched.
382 // Entries will be unique, and for classes that do not yet have
383 // dependencies in the results map.
384 Deque<String> deque = new LinkedList<>();
385 Set<String> doneClasses = new HashSet<>();
387 // get the immediate dependencies of the input files
388 for (Archive a : archives) {
389 for (ClassFile cf : a.reader().getClassFiles()) {
390 String classFileName;
391 try {
392 classFileName = cf.getName();
393 } catch (ConstantPoolException e) {
394 throw new ClassFileError(e);
395 }
397 if (matches(classFileName, cf.access_flags)) {
398 if (!doneClasses.contains(classFileName)) {
399 doneClasses.add(classFileName);
400 }
401 for (Dependency d : finder.findDependencies(cf)) {
402 if (filter.accepts(d)) {
403 String cn = d.getTarget().getName();
404 if (!doneClasses.contains(cn) && !deque.contains(cn)) {
405 deque.add(cn);
406 }
407 a.addClass(d.getOrigin(), d.getTarget());
408 }
409 }
410 }
411 }
412 }
414 // add Archive for looking up classes from the classpath
415 // for transitive dependency analysis
416 Deque<String> unresolved = roots;
417 int depth = options.depth > 0 ? options.depth : Integer.MAX_VALUE;
418 do {
419 String name;
420 while ((name = unresolved.poll()) != null) {
421 if (doneClasses.contains(name)) {
422 continue;
423 }
424 ClassFile cf = null;
425 for (Archive a : classpaths) {
426 cf = a.reader().getClassFile(name);
427 if (cf != null) {
428 String classFileName;
429 try {
430 classFileName = cf.getName();
431 } catch (ConstantPoolException e) {
432 throw new ClassFileError(e);
433 }
434 if (!doneClasses.contains(classFileName)) {
435 // if name is a fully-qualified class name specified
436 // from command-line, this class might already be parsed
437 doneClasses.add(classFileName);
438 for (Dependency d : finder.findDependencies(cf)) {
439 if (depth == 0) {
440 // ignore the dependency
441 a.addClass(d.getOrigin());
442 break;
443 } else if (filter.accepts(d)) {
444 a.addClass(d.getOrigin(), d.getTarget());
445 String cn = d.getTarget().getName();
446 if (!doneClasses.contains(cn) && !deque.contains(cn)) {
447 deque.add(cn);
448 }
449 }
450 }
451 }
452 break;
453 }
454 }
455 if (cf == null) {
456 doneClasses.add(name);
457 }
458 }
459 unresolved = deque;
460 deque = new LinkedList<>();
461 } while (!unresolved.isEmpty() && depth-- > 0);
462 }
464 public void handleOptions(String[] args) throws BadArgs {
465 // process options
466 for (int i=0; i < args.length; i++) {
467 if (args[i].charAt(0) == '-') {
468 String name = args[i];
469 Option option = getOption(name);
470 String param = null;
471 if (option.hasArg) {
472 if (name.startsWith("-") && name.indexOf('=') > 0) {
473 param = name.substring(name.indexOf('=') + 1, name.length());
474 } else if (i + 1 < args.length) {
475 param = args[++i];
476 }
477 if (param == null || param.isEmpty() || param.charAt(0) == '-') {
478 throw new BadArgs("err.missing.arg", name).showUsage(true);
479 }
480 }
481 option.process(this, name, param);
482 if (option.ignoreRest()) {
483 i = args.length;
484 }
485 } else {
486 // process rest of the input arguments
487 for (; i < args.length; i++) {
488 String name = args[i];
489 if (name.charAt(0) == '-') {
490 throw new BadArgs("err.option.after.class", name).showUsage(true);
491 }
492 classes.add(name);
493 }
494 }
495 }
496 }
498 private Option getOption(String name) throws BadArgs {
499 for (Option o : recognizedOptions) {
500 if (o.matches(name)) {
501 return o;
502 }
503 }
504 throw new BadArgs("err.unknown.option", name).showUsage(true);
505 }
507 private void reportError(String key, Object... args) {
508 log.println(getMessage("error.prefix") + " " + getMessage(key, args));
509 }
511 private void warning(String key, Object... args) {
512 log.println(getMessage("warn.prefix") + " " + getMessage(key, args));
513 }
515 private void showHelp() {
516 log.println(getMessage("main.usage", PROGNAME));
517 for (Option o : recognizedOptions) {
518 String name = o.aliases[0].substring(1); // there must always be at least one name
519 name = name.charAt(0) == '-' ? name.substring(1) : name;
520 if (o.isHidden() || name.equals("h")) {
521 continue;
522 }
523 log.println(getMessage("main.opt." + name));
524 }
525 }
527 private void showVersion(boolean full) {
528 log.println(version(full ? "full" : "release"));
529 }
531 private String version(String key) {
532 // key=version: mm.nn.oo[-milestone]
533 // key=full: mm.mm.oo[-milestone]-build
534 if (ResourceBundleHelper.versionRB == null) {
535 return System.getProperty("java.version");
536 }
537 try {
538 return ResourceBundleHelper.versionRB.getString(key);
539 } catch (MissingResourceException e) {
540 return getMessage("version.unknown", System.getProperty("java.version"));
541 }
542 }
544 static String getMessage(String key, Object... args) {
545 try {
546 return MessageFormat.format(ResourceBundleHelper.bundle.getString(key), args);
547 } catch (MissingResourceException e) {
548 throw new InternalError("Missing message: " + key);
549 }
550 }
552 private static class Options {
553 boolean help;
554 boolean version;
555 boolean fullVersion;
556 boolean showProfile;
557 boolean showSummary;
558 boolean wildcard;
559 boolean apiOnly;
560 String dotOutputDir;
561 String classpath = "";
562 int depth = 1;
563 Analyzer.Type verbose = Analyzer.Type.PACKAGE;
564 Set<String> packageNames = new HashSet<>();
565 String regex; // apply to the dependences
566 Pattern includePattern; // apply to classes
567 }
568 private static class ResourceBundleHelper {
569 static final ResourceBundle versionRB;
570 static final ResourceBundle bundle;
572 static {
573 Locale locale = Locale.getDefault();
574 try {
575 bundle = ResourceBundle.getBundle("com.sun.tools.jdeps.resources.jdeps", locale);
576 } catch (MissingResourceException e) {
577 throw new InternalError("Cannot find jdeps resource bundle for locale " + locale);
578 }
579 try {
580 versionRB = ResourceBundle.getBundle("com.sun.tools.jdeps.resources.version");
581 } catch (MissingResourceException e) {
582 throw new InternalError("version.resource.missing");
583 }
584 }
585 }
587 private List<Archive> getArchives(List<String> filenames) throws IOException {
588 List<Archive> result = new ArrayList<Archive>();
589 for (String s : filenames) {
590 Path p = Paths.get(s);
591 if (Files.exists(p)) {
592 result.add(new Archive(p, ClassFileReader.newInstance(p)));
593 } else {
594 warning("warn.file.not.exist", s);
595 }
596 }
597 return result;
598 }
600 private List<Archive> getClassPathArchives(String paths) throws IOException {
601 List<Archive> result = new ArrayList<>();
602 if (paths.isEmpty()) {
603 return result;
604 }
605 for (String p : paths.split(File.pathSeparator)) {
606 if (p.length() > 0) {
607 List<Path> files = new ArrayList<>();
608 // wildcard to parse all JAR files e.g. -classpath dir/*
609 int i = p.lastIndexOf(".*");
610 if (i > 0) {
611 Path dir = Paths.get(p.substring(0, i));
612 try (DirectoryStream<Path> stream = Files.newDirectoryStream(dir, "*.jar")) {
613 for (Path entry : stream) {
614 files.add(entry);
615 }
616 }
617 } else {
618 files.add(Paths.get(p));
619 }
620 for (Path f : files) {
621 if (Files.exists(f)) {
622 result.add(new Archive(f, ClassFileReader.newInstance(f)));
623 }
624 }
625 }
626 }
627 return result;
628 }
631 /**
632 * Returns the file name of the archive for non-JRE class or
633 * internal JRE classes. It returns empty string for SE API.
634 */
635 private static String getArchiveName(Archive source, String profile) {
636 String name = source.getFileName();
637 if (source instanceof JDKArchive)
638 return profile.isEmpty() ? "JDK internal API (" + name + ")" : "";
639 return name;
640 }
642 class RawOutputFormatter implements Analyzer.Visitor {
643 private final PrintWriter writer;
644 RawOutputFormatter(PrintWriter writer) {
645 this.writer = writer;
646 }
648 private String pkg = "";
649 @Override
650 public void visitDependence(String origin, Archive source,
651 String target, Archive archive, String profile) {
652 if (!origin.equals(pkg)) {
653 pkg = origin;
654 writer.format(" %s (%s)%n", origin, source.getFileName());
655 }
656 String name = (options.showProfile && !profile.isEmpty())
657 ? profile
658 : getArchiveName(archive, profile);
659 writer.format(" -> %-50s %s%n", target, name);
660 }
662 @Override
663 public void visitArchiveDependence(Archive origin, Archive target, String profile) {
664 writer.format("%s -> %s", origin, target);
665 if (options.showProfile && !profile.isEmpty()) {
666 writer.format(" (%s)%n", profile);
667 } else {
668 writer.format("%n");
669 }
670 }
671 }
673 class DotFileFormatter implements Analyzer.Visitor, AutoCloseable {
674 private final PrintWriter writer;
675 private final String name;
676 DotFileFormatter(PrintWriter writer, String name) {
677 this.writer = writer;
678 this.name = name;
679 writer.format("digraph \"%s\" {%n", name);
680 }
681 DotFileFormatter(PrintWriter writer, Archive archive) {
682 this.writer = writer;
683 this.name = archive.getFileName();
684 writer.format("digraph \"%s\" {%n", name);
685 writer.format(" // Path: %s%n", archive.toString());
686 }
688 @Override
689 public void close() {
690 writer.println("}");
691 }
693 private final Set<String> edges = new HashSet<>();
694 private String node = "";
695 @Override
696 public void visitDependence(String origin, Archive source,
697 String target, Archive archive, String profile) {
698 if (!node.equals(origin)) {
699 edges.clear();
700 node = origin;
701 }
702 // if -P option is specified, package name -> profile will
703 // be shown and filter out multiple same edges.
704 if (!edges.contains(target)) {
705 StringBuilder sb = new StringBuilder();
706 String name = options.showProfile && !profile.isEmpty()
707 ? profile
708 : getArchiveName(archive, profile);
709 writer.format(" %-50s -> %s;%n",
710 String.format("\"%s\"", origin),
711 name.isEmpty() ? String.format("\"%s\"", target)
712 : String.format("\"%s (%s)\"", target, name));
713 edges.add(target);
714 }
715 }
717 @Override
718 public void visitArchiveDependence(Archive origin, Archive target, String profile) {
719 String name = options.showProfile && !profile.isEmpty()
720 ? profile : "";
721 writer.format(" %-30s -> \"%s\";%n",
722 String.format("\"%s\"", origin.getFileName()),
723 name.isEmpty()
724 ? target.getFileName()
725 : String.format("%s (%s)", target.getFileName(), name));
726 }
727 }
728 }