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