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(false, "-q", "-quiet") { 240 void process(JdepsTask task, String opt, String arg) { 241 task.options.nowarning = true; 242 } 243 }, 244 new HiddenOption(true, "-depth") { 245 void process(JdepsTask task, String opt, String arg) throws BadArgs { 246 try { 247 task.options.depth = Integer.parseInt(arg); 248 } catch (NumberFormatException e) { 249 throw new BadArgs("err.invalid.arg.for.option", opt); 250 } 251 } 252 }, 253 }; 254 255 private static final String PROGNAME = "jdeps"; 256 private final Options options = new Options(); 257 private final List<String> classes = new ArrayList<>(); 258 259 private PrintWriter log; 260 void setLog(PrintWriter out) { 261 log = out; 262 } 263 264 /** 265 * Result codes. 266 */ 267 static final int EXIT_OK = 0, // Completed with no errors. 268 EXIT_ERROR = 1, // Completed but reported errors. 269 EXIT_CMDERR = 2, // Bad command-line arguments 270 EXIT_SYSERR = 3, // System error or resource exhaustion. 271 EXIT_ABNORMAL = 4;// terminated abnormally 272 273 int run(String[] args) { 274 if (log == null) { 275 log = new PrintWriter(System.out); 276 } 277 try { 278 handleOptions(args); 279 if (options.help) { 280 showHelp(); 281 } 282 if (options.version || options.fullVersion) { 283 showVersion(options.fullVersion); 284 } 285 if (classes.isEmpty() && options.includePattern == null) { 286 if (options.help || options.version || options.fullVersion) { 287 return EXIT_OK; 288 } else { 289 showHelp(); 290 return EXIT_CMDERR; 291 } 292 } 293 if (options.regex != null && options.packageNames.size() > 0) { 294 showHelp(); 295 return EXIT_CMDERR; 296 } 297 if (options.findJDKInternals && 298 (options.regex != null || options.packageNames.size() > 0 || options.showSummary)) { 299 showHelp(); 300 return EXIT_CMDERR; 301 } 302 if (options.showSummary && options.verbose != SUMMARY) { 303 showHelp(); 304 return EXIT_CMDERR; 305 } 306 boolean ok = run(); 307 return ok ? EXIT_OK : EXIT_ERROR; 308 } catch (BadArgs e) { 309 reportError(e.key, e.args); 310 if (e.showUsage) { 311 log.println(getMessage("main.usage.summary", PROGNAME)); 312 } 313 return EXIT_CMDERR; 314 } catch (IOException e) { 315 return EXIT_ABNORMAL; 316 } finally { 317 log.flush(); 318 } 319 } 320 321 private final List<Archive> sourceLocations = new ArrayList<>(); 322 private boolean run() throws IOException { 323 // parse classfiles and find all dependencies 324 findDependencies(); 325 326 Analyzer analyzer = new Analyzer(options.verbose, new Analyzer.Filter() { 327 @Override 328 public boolean accepts(Location origin, Archive originArchive, 329 Location target, Archive targetArchive) 330 { 331 if (options.findJDKInternals) { 332 // accepts target that is JDK class but not exported 333 return isJDKArchive(targetArchive) && 334 !((JDKArchive) targetArchive).isExported(target.getClassName()); 335 } else if (options.filterSameArchive) { 336 // accepts origin and target that from different archive 337 return originArchive != targetArchive; 338 } 339 return true; 340 } 341 }); 342 343 // analyze the dependencies 344 analyzer.run(sourceLocations); 345 346 // output result 347 if (options.dotOutputDir != null) { 348 Path dir = Paths.get(options.dotOutputDir); 349 Files.createDirectories(dir); 350 generateDotFiles(dir, analyzer); 351 } else { 352 printRawOutput(log, analyzer); 353 } 354 355 if (options.findJDKInternals && !options.nowarning) { 356 showReplacements(analyzer); 357 } 358 return true; 359 } 360 361 private void generateSummaryDotFile(Path dir, Analyzer analyzer) throws IOException { 362 // If verbose mode (-v or -verbose option), 363 // the summary.dot file shows package-level dependencies. 364 Analyzer.Type summaryType = 365 (options.verbose == PACKAGE || options.verbose == SUMMARY) ? SUMMARY : PACKAGE; 366 Path summary = dir.resolve("summary.dot"); 367 try (PrintWriter sw = new PrintWriter(Files.newOutputStream(summary)); 368 SummaryDotFile dotfile = new SummaryDotFile(sw, summaryType)) { 369 for (Archive archive : sourceLocations) { 370 if (!archive.isEmpty()) { 371 if (options.verbose == PACKAGE || options.verbose == SUMMARY) { 372 if (options.showLabel) { 373 // build labels listing package-level dependencies 374 analyzer.visitDependences(archive, dotfile.labelBuilder(), PACKAGE); 375 } 376 } 377 analyzer.visitDependences(archive, dotfile, summaryType); 378 } 379 } 380 } 381 } 382 383 private void generateDotFiles(Path dir, Analyzer analyzer) throws IOException { 384 // output individual .dot file for each archive 385 if (options.verbose != SUMMARY) { 386 for (Archive archive : sourceLocations) { 387 if (analyzer.hasDependences(archive)) { 388 Path dotfile = dir.resolve(archive.getName() + ".dot"); 389 try (PrintWriter pw = new PrintWriter(Files.newOutputStream(dotfile)); 390 DotFileFormatter formatter = new DotFileFormatter(pw, archive)) { 391 analyzer.visitDependences(archive, formatter); 392 } 393 } 394 } 395 } 396 // generate summary dot file 397 generateSummaryDotFile(dir, analyzer); 398 } 399 400 private void printRawOutput(PrintWriter writer, Analyzer analyzer) { 401 RawOutputFormatter depFormatter = new RawOutputFormatter(writer); 402 RawSummaryFormatter summaryFormatter = new RawSummaryFormatter(writer); 403 for (Archive archive : sourceLocations) { 404 if (!archive.isEmpty()) { 405 analyzer.visitDependences(archive, summaryFormatter, SUMMARY); 406 if (analyzer.hasDependences(archive) && options.verbose != SUMMARY) { 407 analyzer.visitDependences(archive, depFormatter); 408 } 409 } 410 } 411 } 412 413 private boolean isValidClassName(String name) { 414 if (!Character.isJavaIdentifierStart(name.charAt(0))) { 415 return false; 416 } 417 for (int i=1; i < name.length(); i++) { 418 char c = name.charAt(i); 419 if (c != '.' && !Character.isJavaIdentifierPart(c)) { 420 return false; 421 } 422 } 423 return true; 424 } 425 426 /* 427 * Dep Filter configured based on the input jdeps option 428 * 1. -p and -regex to match target dependencies 429 * 2. -filter:package to filter out same-package dependencies 430 * 431 * This filter is applied when jdeps parses the class files 432 * and filtered dependencies are not stored in the Analyzer. 433 * 434 * -filter:archive is applied later in the Analyzer as the 435 * containing archive of a target class may not be known until 436 * the entire archive 437 */ 438 class DependencyFilter implements Dependency.Filter { 439 final Dependency.Filter filter; 440 final Pattern filterPattern; 441 DependencyFilter() { 442 if (options.regex != null) { 443 this.filter = Dependencies.getRegexFilter(Pattern.compile(options.regex)); 444 } else if (options.packageNames.size() > 0) { 445 this.filter = Dependencies.getPackageFilter(options.packageNames, false); 446 } else { 447 this.filter = null; 448 } 449 450 this.filterPattern = 451 options.filterRegex != null ? Pattern.compile(options.filterRegex) : null; 452 } 453 @Override 454 public boolean accepts(Dependency d) { 455 if (d.getOrigin().equals(d.getTarget())) { 456 return false; 457 } 458 String pn = d.getTarget().getPackageName(); 459 if (options.filterSamePackage && d.getOrigin().getPackageName().equals(pn)) { 460 return false; 461 } 462 463 if (filterPattern != null && filterPattern.matcher(pn).matches()) { 464 return false; 465 } 466 return filter != null ? filter.accepts(d) : true; 467 } 468 } 469 470 /** 471 * Tests if the given class matches the pattern given in the -include option 472 * or if it's a public class if -apionly option is specified 473 */ 474 private boolean matches(String classname, AccessFlags flags) { 475 if (options.apiOnly && !flags.is(AccessFlags.ACC_PUBLIC)) { 476 return false; 477 } else if (options.includePattern != null) { 478 return options.includePattern.matcher(classname.replace('/', '.')).matches(); 479 } else { 480 return true; 481 } 482 } 483 484 private void findDependencies() throws IOException { 485 Dependency.Finder finder = 486 options.apiOnly ? Dependencies.getAPIFinder(AccessFlags.ACC_PROTECTED) 487 : Dependencies.getClassDependencyFinder(); 488 Dependency.Filter filter = new DependencyFilter(); 489 490 List<Archive> archives = new ArrayList<>(); 491 Deque<String> roots = new LinkedList<>(); 492 List<Path> paths = new ArrayList<>(); 493 for (String s : classes) { 494 Path p = Paths.get(s); 495 if (Files.exists(p)) { 496 paths.add(p); 497 archives.add(Archive.getInstance(p)); 498 } else { 499 if (isValidClassName(s)) { 500 roots.add(s); 501 } else { 502 warning("warn.invalid.arg", s); 503 } 504 } 505 } 506 sourceLocations.addAll(archives); 507 508 List<Archive> classpaths = new ArrayList<>(); // for class file lookup 509 classpaths.addAll(getClassPathArchives(options.classpath, paths)); 510 if (options.includePattern != null) { 511 archives.addAll(classpaths); 512 } 513 classpaths.addAll(PlatformClassPath.getArchives()); 514 515 // add all classpath archives to the source locations for reporting 516 sourceLocations.addAll(classpaths); 517 518 // Work queue of names of classfiles to be searched. 519 // Entries will be unique, and for classes that do not yet have 520 // dependencies in the results map. 521 Deque<String> deque = new LinkedList<>(); 522 Set<String> doneClasses = new HashSet<>(); 523 524 // get the immediate dependencies of the input files 525 for (Archive a : archives) { 526 for (ClassFile cf : a.reader().getClassFiles()) { 527 String classFileName; 528 try { 529 classFileName = cf.getName(); 530 } catch (ConstantPoolException e) { 531 throw new ClassFileError(e); 532 } 533 534 // tests if this class matches the -include or -apiOnly option if specified 535 if (!matches(classFileName, cf.access_flags)) { 536 continue; 537 } 538 539 if (!doneClasses.contains(classFileName)) { 540 doneClasses.add(classFileName); 541 } 542 543 for (Dependency d : finder.findDependencies(cf)) { 544 if (filter.accepts(d)) { 545 String cn = d.getTarget().getName(); 546 if (!doneClasses.contains(cn) && !deque.contains(cn)) { 547 deque.add(cn); 548 } 549 a.addClass(d.getOrigin(), d.getTarget()); 550 } else { 551 // ensure that the parsed class is added the archive 552 a.addClass(d.getOrigin()); 553 } 554 } 555 for (String name : a.reader().skippedEntries()) { 556 warning("warn.skipped.entry", name, a.getPathName()); 557 } 558 } 559 } 560 561 // add Archive for looking up classes from the classpath 562 // for transitive dependency analysis 563 Deque<String> unresolved = roots; 564 int depth = options.depth > 0 ? options.depth : Integer.MAX_VALUE; 565 do { 566 String name; 567 while ((name = unresolved.poll()) != null) { 568 if (doneClasses.contains(name)) { 569 continue; 570 } 571 ClassFile cf = null; 572 for (Archive a : classpaths) { 573 cf = a.reader().getClassFile(name); 574 if (cf != null) { 575 String classFileName; 576 try { 577 classFileName = cf.getName(); 578 } catch (ConstantPoolException e) { 579 throw new ClassFileError(e); 580 } 581 if (!doneClasses.contains(classFileName)) { 582 // if name is a fully-qualified class name specified 583 // from command-line, this class might already be parsed 584 doneClasses.add(classFileName); 585 // process @jdk.Exported for JDK classes 586 if (isJDKArchive(a)) { 587 ((JDKArchive)a).processJdkExported(cf); 588 } 589 for (Dependency d : finder.findDependencies(cf)) { 590 if (depth == 0) { 591 // ignore the dependency 592 a.addClass(d.getOrigin()); 593 break; 594 } else if (filter.accepts(d)) { 595 a.addClass(d.getOrigin(), d.getTarget()); 596 String cn = d.getTarget().getName(); 597 if (!doneClasses.contains(cn) && !deque.contains(cn)) { 598 deque.add(cn); 599 } 600 } else { 601 // ensure that the parsed class is added the archive 602 a.addClass(d.getOrigin()); 603 } 604 } 605 } 606 break; 607 } 608 } 609 if (cf == null) { 610 doneClasses.add(name); 611 } 612 } 613 unresolved = deque; 614 deque = new LinkedList<>(); 615 } while (!unresolved.isEmpty() && depth-- > 0); 616 } 617 618 public void handleOptions(String[] args) throws BadArgs { 619 // process options 620 for (int i=0; i < args.length; i++) { 621 if (args[i].charAt(0) == '-') { 622 String name = args[i]; 623 Option option = getOption(name); 624 String param = null; 625 if (option.hasArg) { 626 if (name.startsWith("-") && name.indexOf('=') > 0) { 627 param = name.substring(name.indexOf('=') + 1, name.length()); 628 } else if (i + 1 < args.length) { 629 param = args[++i]; 630 } 631 if (param == null || param.isEmpty() || param.charAt(0) == '-') { 632 throw new BadArgs("err.missing.arg", name).showUsage(true); 633 } 634 } 635 option.process(this, name, param); 636 if (option.ignoreRest()) { 637 i = args.length; 638 } 639 } else { 640 // process rest of the input arguments 641 for (; i < args.length; i++) { 642 String name = args[i]; 643 if (name.charAt(0) == '-') { 644 throw new BadArgs("err.option.after.class", name).showUsage(true); 645 } 646 classes.add(name); 647 } 648 } 649 } 650 } 651 652 private Option getOption(String name) throws BadArgs { 653 for (Option o : recognizedOptions) { 654 if (o.matches(name)) { 655 return o; 656 } 657 } 658 throw new BadArgs("err.unknown.option", name).showUsage(true); 659 } 660 661 private void reportError(String key, Object... args) { 662 log.println(getMessage("error.prefix") + " " + getMessage(key, args)); 663 } 664 665 private void warning(String key, Object... args) { 666 log.println(getMessage("warn.prefix") + " " + getMessage(key, args)); 667 } 668 669 private void showHelp() { 670 log.println(getMessage("main.usage", PROGNAME)); 671 for (Option o : recognizedOptions) { 672 String name = o.aliases[0].substring(1); // there must always be at least one name 673 name = name.charAt(0) == '-' ? name.substring(1) : name; 674 if (o.isHidden() || name.equals("h") || name.startsWith("filter:")) { 675 continue; 676 } 677 log.println(getMessage("main.opt." + name)); 678 } 679 } 680 681 private void showVersion(boolean full) { 682 log.println(version(full ? "full" : "release")); 683 } 684 685 private String version(String key) { 686 // key=version: mm.nn.oo[-milestone] 687 // key=full: mm.mm.oo[-milestone]-build 688 if (ResourceBundleHelper.versionRB == null) { 689 return System.getProperty("java.version"); 690 } 691 try { 692 return ResourceBundleHelper.versionRB.getString(key); 693 } catch (MissingResourceException e) { 694 return getMessage("version.unknown", System.getProperty("java.version")); 695 } 696 } 697 698 static String getMessage(String key, Object... args) { 699 try { 700 return MessageFormat.format(ResourceBundleHelper.bundle.getString(key), args); 701 } catch (MissingResourceException e) { 702 throw new InternalError("Missing message: " + key); 703 } 704 } 705 706 private static class Options { 707 boolean help; 708 boolean version; 709 boolean fullVersion; 710 boolean showProfile; 711 boolean showSummary; 712 boolean apiOnly; 713 boolean showLabel; 714 boolean findJDKInternals; 715 boolean nowarning; 716 // default is to show package-level dependencies 717 // and filter references from same package 718 Analyzer.Type verbose = PACKAGE; 719 boolean filterSamePackage = true; 720 boolean filterSameArchive = false; 721 String filterRegex; 722 String dotOutputDir; 723 String classpath = ""; 724 int depth = 1; 725 Set<String> packageNames = new HashSet<>(); 726 String regex; // apply to the dependences 727 Pattern includePattern; // apply to classes 728 } 729 private static class ResourceBundleHelper { 730 static final ResourceBundle versionRB; 731 static final ResourceBundle bundle; 732 static final ResourceBundle jdkinternals; 733 734 static { 735 Locale locale = Locale.getDefault(); 736 try { 737 bundle = ResourceBundle.getBundle("com.sun.tools.jdeps.resources.jdeps", locale); 738 } catch (MissingResourceException e) { 739 throw new InternalError("Cannot find jdeps resource bundle for locale " + locale); 740 } 741 try { 742 versionRB = ResourceBundle.getBundle("com.sun.tools.jdeps.resources.version"); 743 } catch (MissingResourceException e) { 744 throw new InternalError("version.resource.missing"); 745 } 746 try { 747 jdkinternals = ResourceBundle.getBundle("com.sun.tools.jdeps.resources.jdkinternals"); 748 } catch (MissingResourceException e) { 749 throw new InternalError("Cannot find jdkinternals resource bundle"); 750 } 751 } 752 } 753 754 /* 755 * Returns the list of Archive specified in cpaths and not included 756 * initialArchives 757 */ 758 private List<Archive> getClassPathArchives(String cpaths, List<Path> initialArchives) 759 throws IOException 760 { 761 List<Archive> result = new ArrayList<>(); 762 if (cpaths.isEmpty()) { 763 return result; 764 } 765 766 List<Path> paths = new ArrayList<>(); 767 for (String p : cpaths.split(File.pathSeparator)) { 768 if (p.length() > 0) { 769 // wildcard to parse all JAR files e.g. -classpath dir/* 770 int i = p.lastIndexOf(".*"); 771 if (i > 0) { 772 Path dir = Paths.get(p.substring(0, i)); 773 try (DirectoryStream<Path> stream = Files.newDirectoryStream(dir, "*.jar")) { 774 for (Path entry : stream) { 775 paths.add(entry); 776 } 777 } 778 } else { 779 paths.add(Paths.get(p)); 780 } 781 } 782 } 783 for (Path p : paths) { 784 if (Files.exists(p) && !hasSameFile(initialArchives, p)) { 785 result.add(Archive.getInstance(p)); 786 } 787 } 788 return result; 789 } 790 791 private boolean hasSameFile(List<Path> paths, Path p2) throws IOException { 792 for (Path p1 : paths) { 793 if (Files.isSameFile(p1, p2)) { 794 return true; 795 } 796 } 797 return false; 798 } 799 800 class RawOutputFormatter implements Analyzer.Visitor { 801 private final PrintWriter writer; 802 private String pkg = ""; 803 RawOutputFormatter(PrintWriter writer) { 804 this.writer = writer; 805 } 806 @Override 807 public void visitDependence(String origin, Archive originArchive, 808 String target, Archive targetArchive) { 809 String tag = toTag(target, targetArchive); 810 if (options.verbose == VERBOSE) { 811 writer.format(" %-50s -> %-50s %s%n", origin, target, tag); 812 } else { 813 if (!origin.equals(pkg)) { 814 pkg = origin; 815 writer.format(" %s (%s)%n", origin, originArchive.getName()); 816 } 817 writer.format(" -> %-50s %s%n", target, tag); 818 } 819 } 820 } 821 822 class RawSummaryFormatter implements Analyzer.Visitor { 823 private final PrintWriter writer; 824 RawSummaryFormatter(PrintWriter writer) { 825 this.writer = writer; 826 } 827 @Override 828 public void visitDependence(String origin, Archive originArchive, 829 String target, Archive targetArchive) { 830 writer.format("%s -> %s", originArchive.getName(), targetArchive.getPathName()); 831 if (options.showProfile && JDKArchive.isProfileArchive(targetArchive)) { 832 writer.format(" (%s)", target); 833 } 834 writer.format("%n"); 835 } 836 } 837 838 class DotFileFormatter implements Analyzer.Visitor, AutoCloseable { 839 private final PrintWriter writer; 840 private final String name; 841 DotFileFormatter(PrintWriter writer, Archive archive) { 842 this.writer = writer; 843 this.name = archive.getName(); 844 writer.format("digraph \"%s\" {%n", name); 845 writer.format(" // Path: %s%n", archive.getPathName()); 846 } 847 848 @Override 849 public void close() { 850 writer.println("}"); 851 } 852 853 @Override 854 public void visitDependence(String origin, Archive originArchive, 855 String target, Archive targetArchive) { 856 String tag = toTag(target, targetArchive); 857 writer.format(" %-50s -> \"%s\";%n", 858 String.format("\"%s\"", origin), 859 tag.isEmpty() ? target 860 : String.format("%s (%s)", target, tag)); 861 } 862 } 863 864 class SummaryDotFile implements Analyzer.Visitor, AutoCloseable { 865 private final PrintWriter writer; 866 private final Analyzer.Type type; 867 private final Map<Archive, Map<Archive,StringBuilder>> edges = new HashMap<>(); 868 SummaryDotFile(PrintWriter writer, Analyzer.Type type) { 869 this.writer = writer; 870 this.type = type; 871 writer.format("digraph \"summary\" {%n"); 872 } 873 874 @Override 875 public void close() { 876 writer.println("}"); 877 } 878 879 @Override 880 public void visitDependence(String origin, Archive originArchive, 881 String target, Archive targetArchive) { 882 String targetName = type == PACKAGE ? target : targetArchive.getName(); 883 if (type == PACKAGE) { 884 String tag = toTag(target, targetArchive, type); 885 if (!tag.isEmpty()) 886 targetName += " (" + tag + ")"; 887 } else if (options.showProfile && JDKArchive.isProfileArchive(targetArchive)) { 888 targetName += " (" + target + ")"; 889 } 890 String label = getLabel(originArchive, targetArchive); 891 writer.format(" %-50s -> \"%s\"%s;%n", 892 String.format("\"%s\"", origin), targetName, label); 893 } 894 895 String getLabel(Archive origin, Archive target) { 896 if (edges.isEmpty()) 897 return ""; 898 899 StringBuilder label = edges.get(origin).get(target); 900 return label == null ? "" : String.format(" [label=\"%s\",fontsize=9]", label.toString()); 901 } 902 903 Analyzer.Visitor labelBuilder() { 904 // show the package-level dependencies as labels in the dot graph 905 return new Analyzer.Visitor() { 906 @Override 907 public void visitDependence(String origin, Archive originArchive, 908 String target, Archive targetArchive) 909 { 910 Map<Archive,StringBuilder> labels = edges.get(originArchive); 911 if (!edges.containsKey(originArchive)) { 912 edges.put(originArchive, labels = new HashMap<>()); 913 } 914 StringBuilder sb = labels.get(targetArchive); 915 if (sb == null) { 916 labels.put(targetArchive, sb = new StringBuilder()); 917 } 918 String tag = toTag(target, targetArchive, PACKAGE); 919 addLabel(sb, origin, target, tag); 920 } 921 922 void addLabel(StringBuilder label, String origin, String target, String tag) { 923 label.append(origin).append(" -> ").append(target); 924 if (!tag.isEmpty()) { 925 label.append(" (" + tag + ")"); 926 } 927 label.append("\\n"); 928 } 929 }; 930 } 931 } 932 933 /** 934 * Test if the given archive is part of the JDK 935 */ 936 private boolean isJDKArchive(Archive archive) { 937 return JDKArchive.class.isInstance(archive); 938 } 939 940 /** 941 * If the given archive is JDK archive, this method returns the profile name 942 * only if -profile option is specified; it accesses a private JDK API and 943 * the returned value will have "JDK internal API" prefix 944 * 945 * For non-JDK archives, this method returns the file name of the archive. 946 */ 947 private String toTag(String name, Archive source, Analyzer.Type type) { 948 if (!isJDKArchive(source)) { 949 return source.getName(); 950 } 951 952 JDKArchive jdk = (JDKArchive)source; 953 boolean isExported = false; 954 if (type == CLASS || type == VERBOSE) { 955 isExported = jdk.isExported(name); 956 } else { 957 isExported = jdk.isExportedPackage(name); 958 } 959 Profile p = getProfile(name, type); 960 if (isExported) { 961 // exported API 962 return options.showProfile && p != null ? p.profileName() : ""; 963 } else { 964 return "JDK internal API (" + source.getName() + ")"; 965 } 966 } 967 968 private String toTag(String name, Archive source) { 969 return toTag(name, source, options.verbose); 970 } 971 972 private Profile getProfile(String name, Analyzer.Type type) { 973 String pn = name; 974 if (type == CLASS || type == VERBOSE) { 975 int i = name.lastIndexOf('.'); 976 pn = i > 0 ? name.substring(0, i) : ""; 977 } 978 return Profile.getProfile(pn); 979 } 980 981 /** 982 * Returns the recommended replacement API for the given classname; 983 * or return null if replacement API is not known. 984 */ 985 private String replacementFor(String cn) { 986 String name = cn; 987 String value = null; 988 while (value == null && name != null) { 989 try { 990 value = ResourceBundleHelper.jdkinternals.getString(name); 991 } catch (MissingResourceException e) { 992 // go up one subpackage level 993 int i = name.lastIndexOf('.'); 994 name = i > 0 ? name.substring(0, i) : null; 995 } 996 } 997 return value; 998 }; 999 1000 private void showReplacements(Analyzer analyzer) { 1001 Map<String,String> jdkinternals = new TreeMap<>(); 1002 boolean useInternals = false; 1003 for (Archive source : sourceLocations) { 1004 useInternals = useInternals || analyzer.hasDependences(source); 1005 for (String cn : analyzer.dependences(source)) { 1006 String repl = replacementFor(cn); 1007 if (repl != null && !jdkinternals.containsKey(cn)) { 1008 jdkinternals.put(cn, repl); 1009 } 1010 } 1011 } 1012 if (useInternals) { 1013 log.println(); 1014 warning("warn.replace.useJDKInternals", getMessage("jdeps.wiki.url")); 1015 } 1016 if (!jdkinternals.isEmpty()) { 1017 log.println(); 1018 log.format("%-40s %s%n", "JDK Internal API", "Suggested Replacement"); 1019 log.format("%-40s %s%n", "----------------", "---------------------"); 1020 for (Map.Entry<String,String> e : jdkinternals.entrySet()) { 1021 log.format("%-40s %s%n", e.getKey(), e.getValue()); 1022 } 1023 } 1024 1025 } 1026 }