1 /* 2 * Copyright (c) 2012, 2014, 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; 26 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.classfile.Dependency.Location; 34 import com.sun.tools.jdeps.PlatformClassPath.JDKArchive; 35 import static com.sun.tools.jdeps.Analyzer.Type.*; 36 import java.io.*; 37 import java.nio.file.DirectoryStream; 38 import java.nio.file.Files; 39 import java.nio.file.Path; 40 import java.nio.file.Paths; 41 import java.text.MessageFormat; 42 import java.util.*; 43 import java.util.regex.Pattern; 44 45 /** 46 * Implementation for the jdeps tool for static class dependency analysis. 47 */ 48 class JdepsTask { 49 static class BadArgs extends Exception { 50 static final long serialVersionUID = 8765093759964640721L; 51 BadArgs(String key, Object... args) { 52 super(JdepsTask.getMessage(key, args)); 53 this.key = key; 54 this.args = args; 55 } 56 57 BadArgs showUsage(boolean b) { 58 showUsage = b; 59 return this; 60 } 61 final String key; 62 final Object[] args; 63 boolean showUsage; 64 } 65 66 static abstract class Option { 67 Option(boolean hasArg, String... aliases) { 68 this.hasArg = hasArg; 69 this.aliases = aliases; 70 } 71 72 boolean isHidden() { 73 return false; 74 } 75 76 boolean matches(String opt) { 77 for (String a : aliases) { 78 if (a.equals(opt)) 79 return true; 80 if (hasArg && opt.startsWith(a + "=")) 81 return true; 82 } 83 return false; 84 } 85 86 boolean ignoreRest() { 87 return false; 88 } 89 90 abstract void process(JdepsTask task, String opt, String arg) throws BadArgs; 91 final boolean hasArg; 92 final String[] aliases; 93 } 94 95 static abstract class HiddenOption extends Option { 96 HiddenOption(boolean hasArg, String... aliases) { 97 super(hasArg, aliases); 98 } 99 100 boolean isHidden() { 101 return true; 102 } 103 } 104 105 static Option[] recognizedOptions = { 106 new Option(false, "-h", "-?", "-help") { 107 void process(JdepsTask task, String opt, String arg) { 108 task.options.help = true; 109 } 110 }, 111 new Option(true, "-dotoutput") { 112 void process(JdepsTask task, String opt, String arg) throws BadArgs { 113 Path p = Paths.get(arg); 114 if (Files.exists(p) && (!Files.isDirectory(p) || !Files.isWritable(p))) { 115 throw new BadArgs("err.invalid.path", arg); 116 } 117 task.options.dotOutputDir = arg; 118 } 119 }, 120 new Option(false, "-s", "-summary") { 121 void process(JdepsTask task, String opt, String arg) { 122 task.options.showSummary = true; 123 task.options.verbose = SUMMARY; 124 } 125 }, 126 new Option(false, "-v", "-verbose", 127 "-verbose:package", 128 "-verbose:class") { 129 void process(JdepsTask task, String opt, String arg) throws BadArgs { 130 switch (opt) { 131 case "-v": 132 case "-verbose": 133 task.options.verbose = VERBOSE; 134 task.options.filterSameArchive = false; 135 task.options.filterSamePackage = false; 136 break; 137 case "-verbose:package": 138 task.options.verbose = PACKAGE; 139 break; 140 case "-verbose:class": 141 task.options.verbose = CLASS; 142 break; 143 default: 144 throw new BadArgs("err.invalid.arg.for.option", opt); 145 } 146 } 147 }, 148 new Option(true, "-cp", "-classpath") { 149 void process(JdepsTask task, String opt, String arg) { 150 task.options.classpath = arg; 151 } 152 }, 153 new Option(true, "-p", "-package") { 154 void process(JdepsTask task, String opt, String arg) { 155 task.options.packageNames.add(arg); 156 } 157 }, 158 new Option(true, "-e", "-regex") { 159 void process(JdepsTask task, String opt, String arg) { 160 task.options.regex = arg; 161 } 162 }, 163 164 new Option(true, "-f", "-filter") { 165 void process(JdepsTask task, String opt, String arg) { 166 task.options.filterRegex = arg; 167 } 168 }, 169 new Option(false, "-filter:package", 170 "-filter:archive", 171 "-filter:none") { 172 void process(JdepsTask task, String opt, String arg) { 173 switch (opt) { 174 case "-filter:package": 175 task.options.filterSamePackage = true; 176 task.options.filterSameArchive = false; 177 break; 178 case "-filter:archive": 179 task.options.filterSameArchive = true; 180 task.options.filterSamePackage = false; 181 break; 182 case "-filter:none": 183 task.options.filterSameArchive = false; 184 task.options.filterSamePackage = false; 185 break; 186 } 187 } 188 }, 189 new Option(true, "-include") { 190 void process(JdepsTask task, String opt, String arg) throws BadArgs { 191 task.options.includePattern = Pattern.compile(arg); 192 } 193 }, 194 new Option(false, "-P", "-profile") { 195 void process(JdepsTask task, String opt, String arg) throws BadArgs { 196 task.options.showProfile = true; 197 if (Profile.getProfileCount() == 0) { 198 throw new BadArgs("err.option.unsupported", opt, getMessage("err.profiles.msg")); 199 } 200 } 201 }, 202 new Option(false, "-apionly") { 203 void process(JdepsTask task, String opt, String arg) { 204 task.options.apiOnly = true; 205 } 206 }, 207 new Option(false, "-R", "-recursive") { 208 void process(JdepsTask task, String opt, String arg) { 209 task.options.depth = 0; 210 // turn off filtering 211 task.options.filterSameArchive = false; 212 task.options.filterSamePackage = false; 213 } 214 }, 215 new Option(false, "-jdkinternals") { 216 void process(JdepsTask task, String opt, String arg) { 217 task.options.findJDKInternals = true; 218 task.options.verbose = CLASS; 219 if (task.options.includePattern == null) { 220 task.options.includePattern = Pattern.compile(".*"); 221 } 222 } 223 }, 224 new Option(false, "-version") { 225 void process(JdepsTask task, String opt, String arg) { 226 task.options.version = true; 227 } 228 }, 229 new HiddenOption(false, "-fullversion") { 230 void process(JdepsTask task, String opt, String arg) { 231 task.options.fullVersion = true; 232 } 233 }, 234 new HiddenOption(false, "-showlabel") { 235 void process(JdepsTask task, String opt, String arg) { 236 task.options.showLabel = true; 237 } 238 }, 239 new HiddenOption(true, "-depth") { 240 void process(JdepsTask task, String opt, String arg) throws BadArgs { 241 try { 242 task.options.depth = Integer.parseInt(arg); 243 } catch (NumberFormatException e) { 244 throw new BadArgs("err.invalid.arg.for.option", opt); 245 } 246 } 247 }, 248 }; 249 250 private static final String PROGNAME = "jdeps"; 251 private final Options options = new Options(); 252 private final List<String> classes = new ArrayList<>(); 253 254 private PrintWriter log; 255 void setLog(PrintWriter out) { 256 log = out; 257 } 258 259 /** 260 * Result codes. 261 */ 262 static final int EXIT_OK = 0, // Completed with no errors. 263 EXIT_ERROR = 1, // Completed but reported errors. 264 EXIT_CMDERR = 2, // Bad command-line arguments 265 EXIT_SYSERR = 3, // System error or resource exhaustion. 266 EXIT_ABNORMAL = 4;// terminated abnormally 267 268 int run(String[] args) { 269 if (log == null) { 270 log = new PrintWriter(System.out); 271 } 272 try { 273 handleOptions(args); 274 if (options.help) { 275 showHelp(); 276 } 277 if (options.version || options.fullVersion) { 278 showVersion(options.fullVersion); 279 } 280 if (classes.isEmpty() && options.includePattern == null) { 281 if (options.help || options.version || options.fullVersion) { 282 return EXIT_OK; 283 } else { 284 showHelp(); 285 return EXIT_CMDERR; 286 } 287 } 288 if (options.regex != null && options.packageNames.size() > 0) { 289 showHelp(); 290 return EXIT_CMDERR; 291 } 292 if (options.findJDKInternals && 293 (options.regex != null || options.packageNames.size() > 0 || options.showSummary)) { 294 showHelp(); 295 return EXIT_CMDERR; 296 } 297 if (options.showSummary && options.verbose != SUMMARY) { 298 showHelp(); 299 return EXIT_CMDERR; 300 } 301 boolean ok = run(); 302 return ok ? EXIT_OK : EXIT_ERROR; 303 } catch (BadArgs e) { 304 reportError(e.key, e.args); 305 if (e.showUsage) { 306 log.println(getMessage("main.usage.summary", PROGNAME)); 307 } 308 return EXIT_CMDERR; 309 } catch (IOException e) { 310 return EXIT_ABNORMAL; 311 } finally { 312 log.flush(); 313 } 314 } 315 316 private final List<Archive> sourceLocations = new ArrayList<>(); 317 private boolean run() throws IOException { 318 // parse classfiles and find all dependencies 319 findDependencies(); 320 321 Analyzer analyzer = new Analyzer(options.verbose, new Analyzer.Filter() { 322 @Override 323 public boolean accepts(Location origin, Archive originArchive, Location target, Archive targetArchive) { 324 if (options.findJDKInternals) { 325 // accepts target that is JDK class but not exported 326 return isJDKArchive(targetArchive) && 327 !((JDKArchive) targetArchive).isExported(target.getClassName()); 328 } else if (options.filterSameArchive) { 329 // accepts origin and target that from different archive 330 return originArchive != targetArchive; 331 } 332 return true; 333 } 334 }); 335 336 // analyze the dependencies 337 analyzer.run(sourceLocations); 338 339 // output result 340 if (options.dotOutputDir != null) { 341 Path dir = Paths.get(options.dotOutputDir); 342 Files.createDirectories(dir); 343 generateDotFiles(dir, analyzer); 344 } else { 345 printRawOutput(log, analyzer); 346 } 347 return true; 348 } 349 350 private void generateSummaryDotFile(Path dir, Analyzer analyzer) throws IOException { 351 // If verbose mode (-v or -verbose option), 352 // the summary.dot file shows package-level dependencies. 353 Analyzer.Type summaryType = 354 (options.verbose == PACKAGE || options.verbose == SUMMARY) ? SUMMARY : PACKAGE; 355 Path summary = dir.resolve("summary.dot"); 356 try (PrintWriter sw = new PrintWriter(Files.newOutputStream(summary)); 357 SummaryDotFile dotfile = new SummaryDotFile(sw, summaryType)) { 358 for (Archive archive : sourceLocations) { 359 if (!archive.isEmpty()) { 360 if (options.verbose == PACKAGE || options.verbose == SUMMARY) { 361 if (options.showLabel) { 362 // build labels listing package-level dependencies 363 analyzer.visitDependences(archive, dotfile.labelBuilder(), PACKAGE); 364 } 365 } 366 analyzer.visitDependences(archive, dotfile, summaryType); 367 } 368 } 369 } 370 } 371 372 private void generateDotFiles(Path dir, Analyzer analyzer) throws IOException { 373 // output individual .dot file for each archive 374 if (options.verbose != SUMMARY) { 375 for (Archive archive : sourceLocations) { 376 if (analyzer.hasDependences(archive)) { 377 Path dotfile = dir.resolve(archive.getName() + ".dot"); 378 try (PrintWriter pw = new PrintWriter(Files.newOutputStream(dotfile)); 379 DotFileFormatter formatter = new DotFileFormatter(pw, archive)) { 380 analyzer.visitDependences(archive, formatter); 381 } 382 } 383 } 384 } 385 // generate summary dot file 386 generateSummaryDotFile(dir, analyzer); 387 } 388 389 private void printRawOutput(PrintWriter writer, Analyzer analyzer) { 390 RawOutputFormatter depFormatter = new RawOutputFormatter(writer); 391 RawSummaryFormatter summaryFormatter = new RawSummaryFormatter(writer); 392 for (Archive archive : sourceLocations) { 393 if (!archive.isEmpty()) { 394 analyzer.visitDependences(archive, summaryFormatter, SUMMARY); 395 if (analyzer.hasDependences(archive) && options.verbose != SUMMARY) { 396 analyzer.visitDependences(archive, depFormatter); 397 } 398 } 399 } 400 } 401 402 private boolean isValidClassName(String name) { 403 if (!Character.isJavaIdentifierStart(name.charAt(0))) { 404 return false; 405 } 406 for (int i=1; i < name.length(); i++) { 407 char c = name.charAt(i); 408 if (c != '.' && !Character.isJavaIdentifierPart(c)) { 409 return false; 410 } 411 } 412 return true; 413 } 414 415 /* 416 * Dep Filter configured based on the input jdeps option 417 * 1. -p and -regex to match target dependencies 418 * 2. -filter:package to filter out same-package dependencies 419 * 420 * This filter is applied when jdeps parses the class files 421 * and filtered dependencies are not stored in the Analyzer. 422 * 423 * -filter:archive is applied later in the Analyzer as the 424 * containing archive of a target class may not be known until 425 * the entire archive 426 */ 427 class DependencyFilter implements Dependency.Filter { 428 final Dependency.Filter filter; 429 final Pattern filterPattern; 430 DependencyFilter() { 431 if (options.regex != null) { 432 this.filter = Dependencies.getRegexFilter(Pattern.compile(options.regex)); 433 } else if (options.packageNames.size() > 0) { 434 this.filter = Dependencies.getPackageFilter(options.packageNames, false); 435 } else { 436 this.filter = null; 437 } 438 439 this.filterPattern = 440 options.filterRegex != null ? Pattern.compile(options.filterRegex) : null; 441 } 442 @Override 443 public boolean accepts(Dependency d) { 444 if (d.getOrigin().equals(d.getTarget())) { 445 return false; 446 } 447 String pn = d.getTarget().getPackageName(); 448 if (options.filterSamePackage && d.getOrigin().getPackageName().equals(pn)) { 449 return false; 450 } 451 452 if (filterPattern != null && filterPattern.matcher(pn).matches()) { 453 return false; 454 } 455 return filter != null ? filter.accepts(d) : true; 456 } 457 } 458 459 /** 460 * Tests if the given class matches the pattern given in the -include option 461 * or if it's a public class if -apionly option is specified 462 */ 463 private boolean matches(String classname, AccessFlags flags) { 464 if (options.apiOnly && !flags.is(AccessFlags.ACC_PUBLIC)) { 465 return false; 466 } else if (options.includePattern != null) { 467 return options.includePattern.matcher(classname.replace('/', '.')).matches(); 468 } else { 469 return true; 470 } 471 } 472 473 private void findDependencies() throws IOException { 474 Dependency.Finder finder = 475 options.apiOnly ? Dependencies.getAPIFinder(AccessFlags.ACC_PROTECTED) 476 : Dependencies.getClassDependencyFinder(); 477 Dependency.Filter filter = new DependencyFilter(); 478 479 List<Archive> archives = new ArrayList<>(); 480 Deque<String> roots = new LinkedList<>(); 481 for (String s : classes) { 482 Path p = Paths.get(s); 483 if (Files.exists(p)) { 484 archives.add(Archive.getInstance(p)); 485 } else { 486 if (isValidClassName(s)) { 487 roots.add(s); 488 } else { 489 warning("warn.invalid.arg", s); 490 } 491 } 492 } 493 sourceLocations.addAll(archives); 494 495 List<Archive> classpaths = new ArrayList<>(); // for class file lookup 496 classpaths.addAll(getClassPathArchives(options.classpath)); 497 if (options.includePattern != null) { 498 archives.addAll(classpaths); 499 } 500 classpaths.addAll(PlatformClassPath.getArchives()); 501 502 // add all classpath archives to the source locations for reporting 503 sourceLocations.addAll(classpaths); 504 505 // Work queue of names of classfiles to be searched. 506 // Entries will be unique, and for classes that do not yet have 507 // dependencies in the results map. 508 Deque<String> deque = new LinkedList<>(); 509 Set<String> doneClasses = new HashSet<>(); 510 511 // get the immediate dependencies of the input files 512 for (Archive a : archives) { 513 for (ClassFile cf : a.reader().getClassFiles()) { 514 String classFileName; 515 try { 516 classFileName = cf.getName(); 517 } catch (ConstantPoolException e) { 518 throw new ClassFileError(e); 519 } 520 521 // tests if this class matches the -include or -apiOnly option if specified 522 if (!matches(classFileName, cf.access_flags)) { 523 continue; 524 } 525 526 if (!doneClasses.contains(classFileName)) { 527 doneClasses.add(classFileName); 528 } 529 530 for (Dependency d : finder.findDependencies(cf)) { 531 if (filter.accepts(d)) { 532 String cn = d.getTarget().getName(); 533 if (!doneClasses.contains(cn) && !deque.contains(cn)) { 534 deque.add(cn); 535 } 536 a.addClass(d.getOrigin(), d.getTarget()); 537 } 538 } 539 for (String name : a.reader().skippedEntries()) { 540 warning("warn.skipped.entry", name, a.getPathName()); 541 } 542 } 543 } 544 545 // add Archive for looking up classes from the classpath 546 // for transitive dependency analysis 547 Deque<String> unresolved = roots; 548 int depth = options.depth > 0 ? options.depth : Integer.MAX_VALUE; 549 do { 550 String name; 551 while ((name = unresolved.poll()) != null) { 552 if (doneClasses.contains(name)) { 553 continue; 554 } 555 ClassFile cf = null; 556 for (Archive a : classpaths) { 557 cf = a.reader().getClassFile(name); 558 if (cf != null) { 559 String classFileName; 560 try { 561 classFileName = cf.getName(); 562 } catch (ConstantPoolException e) { 563 throw new ClassFileError(e); 564 } 565 if (!doneClasses.contains(classFileName)) { 566 // if name is a fully-qualified class name specified 567 // from command-line, this class might already be parsed 568 doneClasses.add(classFileName); 569 // process @jdk.Exported for JDK classes 570 if (isJDKArchive(a)) { 571 ((JDKArchive)a).processJdkExported(cf); 572 } 573 for (Dependency d : finder.findDependencies(cf)) { 574 if (depth == 0) { 575 // ignore the dependency 576 a.addClass(d.getOrigin()); 577 break; 578 } else if (filter.accepts(d)) { 579 a.addClass(d.getOrigin(), d.getTarget()); 580 String cn = d.getTarget().getName(); 581 if (!doneClasses.contains(cn) && !deque.contains(cn)) { 582 deque.add(cn); 583 } 584 } 585 } 586 } 587 break; 588 } 589 } 590 if (cf == null) { 591 doneClasses.add(name); 592 } 593 } 594 unresolved = deque; 595 deque = new LinkedList<>(); 596 } while (!unresolved.isEmpty() && depth-- > 0); 597 } 598 599 public void handleOptions(String[] args) throws BadArgs { 600 // process options 601 for (int i=0; i < args.length; i++) { 602 if (args[i].charAt(0) == '-') { 603 String name = args[i]; 604 Option option = getOption(name); 605 String param = null; 606 if (option.hasArg) { 607 if (name.startsWith("-") && name.indexOf('=') > 0) { 608 param = name.substring(name.indexOf('=') + 1, name.length()); 609 } else if (i + 1 < args.length) { 610 param = args[++i]; 611 } 612 if (param == null || param.isEmpty() || param.charAt(0) == '-') { 613 throw new BadArgs("err.missing.arg", name).showUsage(true); 614 } 615 } 616 option.process(this, name, param); 617 if (option.ignoreRest()) { 618 i = args.length; 619 } 620 } else { 621 // process rest of the input arguments 622 for (; i < args.length; i++) { 623 String name = args[i]; 624 if (name.charAt(0) == '-') { 625 throw new BadArgs("err.option.after.class", name).showUsage(true); 626 } 627 classes.add(name); 628 } 629 } 630 } 631 } 632 633 private Option getOption(String name) throws BadArgs { 634 for (Option o : recognizedOptions) { 635 if (o.matches(name)) { 636 return o; 637 } 638 } 639 throw new BadArgs("err.unknown.option", name).showUsage(true); 640 } 641 642 private void reportError(String key, Object... args) { 643 log.println(getMessage("error.prefix") + " " + getMessage(key, args)); 644 } 645 646 private void warning(String key, Object... args) { 647 log.println(getMessage("warn.prefix") + " " + getMessage(key, args)); 648 } 649 650 private void showHelp() { 651 log.println(getMessage("main.usage", PROGNAME)); 652 for (Option o : recognizedOptions) { 653 String name = o.aliases[0].substring(1); // there must always be at least one name 654 name = name.charAt(0) == '-' ? name.substring(1) : name; 655 if (o.isHidden() || name.equals("h") || name.startsWith("filter:")) { 656 continue; 657 } 658 log.println(getMessage("main.opt." + name)); 659 } 660 } 661 662 private void showVersion(boolean full) { 663 log.println(version(full ? "full" : "release")); 664 } 665 666 private String version(String key) { 667 // key=version: mm.nn.oo[-milestone] 668 // key=full: mm.mm.oo[-milestone]-build 669 if (ResourceBundleHelper.versionRB == null) { 670 return System.getProperty("java.version"); 671 } 672 try { 673 return ResourceBundleHelper.versionRB.getString(key); 674 } catch (MissingResourceException e) { 675 return getMessage("version.unknown", System.getProperty("java.version")); 676 } 677 } 678 679 static String getMessage(String key, Object... args) { 680 try { 681 return MessageFormat.format(ResourceBundleHelper.bundle.getString(key), args); 682 } catch (MissingResourceException e) { 683 throw new InternalError("Missing message: " + key); 684 } 685 } 686 687 private static class Options { 688 boolean help; 689 boolean version; 690 boolean fullVersion; 691 boolean showProfile; 692 boolean showSummary; 693 boolean apiOnly; 694 boolean showLabel; 695 boolean findJDKInternals; 696 // default is to show package-level dependencies 697 // and filter references from same package 698 Analyzer.Type verbose = PACKAGE; 699 boolean filterSamePackage = true; 700 boolean filterSameArchive = false; 701 String filterRegex; 702 String dotOutputDir; 703 String classpath = ""; 704 int depth = 1; 705 Set<String> packageNames = new HashSet<>(); 706 String regex; // apply to the dependences 707 Pattern includePattern; // apply to classes 708 } 709 private static class ResourceBundleHelper { 710 static final ResourceBundle versionRB; 711 static final ResourceBundle bundle; 712 713 static { 714 Locale locale = Locale.getDefault(); 715 try { 716 bundle = ResourceBundle.getBundle("com.sun.tools.jdeps.resources.jdeps", locale); 717 } catch (MissingResourceException e) { 718 throw new InternalError("Cannot find jdeps resource bundle for locale " + locale); 719 } 720 try { 721 versionRB = ResourceBundle.getBundle("com.sun.tools.jdeps.resources.version"); 722 } catch (MissingResourceException e) { 723 throw new InternalError("version.resource.missing"); 724 } 725 } 726 } 727 728 private List<Archive> getClassPathArchives(String paths) throws IOException { 729 List<Archive> result = new ArrayList<>(); 730 if (paths.isEmpty()) { 731 return result; 732 } 733 for (String p : paths.split(File.pathSeparator)) { 734 if (p.length() > 0) { 735 List<Path> files = new ArrayList<>(); 736 // wildcard to parse all JAR files e.g. -classpath dir/* 737 int i = p.lastIndexOf(".*"); 738 if (i > 0) { 739 Path dir = Paths.get(p.substring(0, i)); 740 try (DirectoryStream<Path> stream = Files.newDirectoryStream(dir, "*.jar")) { 741 for (Path entry : stream) { 742 files.add(entry); 743 } 744 } 745 } else { 746 files.add(Paths.get(p)); 747 } 748 for (Path f : files) { 749 if (Files.exists(f)) { 750 result.add(Archive.getInstance(f)); 751 } 752 } 753 } 754 } 755 return result; 756 } 757 758 class RawOutputFormatter implements Analyzer.Visitor { 759 private final PrintWriter writer; 760 private String pkg = ""; 761 RawOutputFormatter(PrintWriter writer) { 762 this.writer = writer; 763 } 764 @Override 765 public void visitDependence(String origin, Archive originArchive, 766 String target, Archive targetArchive) { 767 String tag = toTag(target, targetArchive); 768 if (options.verbose == VERBOSE) { 769 writer.format(" %-50s -> %-50s %s%n", origin, target, tag); 770 } else { 771 if (!origin.equals(pkg)) { 772 pkg = origin; 773 writer.format(" %s (%s)%n", origin, originArchive.getName()); 774 } 775 writer.format(" -> %-50s %s%n", target, tag); 776 } 777 } 778 } 779 780 class RawSummaryFormatter implements Analyzer.Visitor { 781 private final PrintWriter writer; 782 RawSummaryFormatter(PrintWriter writer) { 783 this.writer = writer; 784 } 785 @Override 786 public void visitDependence(String origin, Archive originArchive, 787 String target, Archive targetArchive) { 788 writer.format("%s -> %s", originArchive.getName(), targetArchive.getPathName()); 789 if (options.showProfile && JDKArchive.isProfileArchive(targetArchive)) { 790 writer.format(" (%s)", target); 791 } 792 writer.format("%n"); 793 } 794 } 795 796 class DotFileFormatter implements Analyzer.Visitor, AutoCloseable { 797 private final PrintWriter writer; 798 private final String name; 799 DotFileFormatter(PrintWriter writer, Archive archive) { 800 this.writer = writer; 801 this.name = archive.getName(); 802 writer.format("digraph \"%s\" {%n", name); 803 writer.format(" // Path: %s%n", archive.getPathName()); 804 } 805 806 @Override 807 public void close() { 808 writer.println("}"); 809 } 810 811 @Override 812 public void visitDependence(String origin, Archive originArchive, 813 String target, Archive targetArchive) { 814 String tag = toTag(target, targetArchive); 815 writer.format(" %-50s -> \"%s\";%n", 816 String.format("\"%s\"", origin), 817 tag.isEmpty() ? target 818 : String.format("%s (%s)", target, tag)); 819 } 820 } 821 822 class SummaryDotFile implements Analyzer.Visitor, AutoCloseable { 823 private final PrintWriter writer; 824 private final Analyzer.Type type; 825 private final Map<Archive, Map<Archive,StringBuilder>> edges = new HashMap<>(); 826 SummaryDotFile(PrintWriter writer, Analyzer.Type type) { 827 this.writer = writer; 828 this.type = type; 829 writer.format("digraph \"summary\" {%n"); 830 } 831 832 @Override 833 public void close() { 834 writer.println("}"); 835 } 836 837 @Override 838 public void visitDependence(String origin, Archive originArchive, 839 String target, Archive targetArchive) { 840 String targetName = type == PACKAGE ? target : targetArchive.getName(); 841 if (type == PACKAGE) { 842 String tag = toTag(target, targetArchive, type); 843 if (!tag.isEmpty()) 844 targetName += " (" + tag + ")"; 845 } else if (options.showProfile && JDKArchive.isProfileArchive(targetArchive)) { 846 targetName += " (" + target + ")"; 847 } 848 String label = getLabel(originArchive, targetArchive); 849 writer.format(" %-50s -> \"%s\"%s;%n", 850 String.format("\"%s\"", origin), targetName, label); 851 } 852 853 String getLabel(Archive origin, Archive target) { 854 if (edges.isEmpty()) 855 return ""; 856 857 StringBuilder label = edges.get(origin).get(target); 858 return label == null ? "" : String.format(" [label=\"%s\",fontsize=9]", label.toString()); 859 } 860 861 Analyzer.Visitor labelBuilder() { 862 // show the package-level dependencies as labels in the dot graph 863 return new Analyzer.Visitor() { 864 @Override 865 public void visitDependence(String origin, Archive originArchive, String target, Archive targetArchive) { 866 edges.putIfAbsent(originArchive, new HashMap<>()); 867 edges.get(originArchive).putIfAbsent(targetArchive, new StringBuilder()); 868 StringBuilder sb = edges.get(originArchive).get(targetArchive); 869 String tag = toTag(target, targetArchive, PACKAGE); 870 addLabel(sb, origin, target, tag); 871 } 872 873 void addLabel(StringBuilder label, String origin, String target, String tag) { 874 label.append(origin).append(" -> ").append(target); 875 if (!tag.isEmpty()) { 876 label.append(" (" + tag + ")"); 877 } 878 label.append("\\n"); 879 } 880 }; 881 } 882 } 883 884 /** 885 * Test if the given archive is part of the JDK 886 */ 887 private boolean isJDKArchive(Archive archive) { 888 return JDKArchive.class.isInstance(archive); 889 } 890 891 /** 892 * If the given archive is JDK archive, this method returns the profile name 893 * only if -profile option is specified; it accesses a private JDK API and 894 * the returned value will have "JDK internal API" prefix 895 * 896 * For non-JDK archives, this method returns the file name of the archive. 897 */ 898 private String toTag(String name, Archive source, Analyzer.Type type) { 899 if (!isJDKArchive(source)) { 900 return source.getName(); 901 } 902 903 JDKArchive jdk = (JDKArchive)source; 904 boolean isExported = false; 905 if (type == CLASS || type == VERBOSE) { 906 isExported = jdk.isExported(name); 907 } else { 908 isExported = jdk.isExportedPackage(name); 909 } 910 Profile p = getProfile(name, type); 911 if (isExported) { 912 // exported API 913 return options.showProfile && p != null ? p.profileName() : ""; 914 } else { 915 return "JDK internal API (" + source.getName() + ")"; 916 } 917 } 918 919 private String toTag(String name, Archive source) { 920 return toTag(name, source, options.verbose); 921 } 922 923 private Profile getProfile(String name, Analyzer.Type type) { 924 String pn = name; 925 if (type == CLASS || type == VERBOSE) { 926 int i = name.lastIndexOf('.'); 927 pn = i > 0 ? name.substring(0, i) : ""; 928 } 929 return Profile.getProfile(pn); 930 } 931 }