1 /* 2 * Copyright (c) 2012, 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.ClassFile; 28 import com.sun.tools.classfile.ConstantPoolException; 29 import com.sun.tools.classfile.Dependencies; 30 import com.sun.tools.classfile.Dependencies.ClassFileError; 31 import com.sun.tools.classfile.Dependency; 32 import com.sun.tools.classfile.Dependency.Finder; 33 import com.sun.tools.classfile.Dependency.Location; 34 import java.io.*; 35 import java.nio.file.FileVisitResult; 36 import java.nio.file.Files; 37 import java.nio.file.Path; 38 import java.nio.file.SimpleFileVisitor; 39 import java.nio.file.attribute.BasicFileAttributes; 40 import java.text.MessageFormat; 41 import java.util.*; 42 import java.util.regex.Pattern; 43 44 /** 45 * Implementation for the jdeps tool for static class dependency analysis. 46 */ 47 class JdepsTask { 48 class BadArgs extends Exception { 49 static final long serialVersionUID = 8765093759964640721L; 50 BadArgs(String key, Object... args) { 51 super(JdepsTask.this.getMessage(key, args)); 52 this.key = key; 53 this.args = args; 54 } 55 56 BadArgs showUsage(boolean b) { 57 showUsage = b; 58 return this; 59 } 60 final String key; 61 final Object[] args; 62 boolean showUsage; 63 } 64 65 static abstract class Option { 66 Option(boolean hasArg, String... aliases) { 67 this.hasArg = hasArg; 68 this.aliases = aliases; 69 } 70 71 boolean isHidden() { 72 return false; 73 } 74 75 boolean matches(String opt) { 76 for (String a : aliases) { 77 if (a.equals(opt)) { 78 return true; 79 } 80 } 81 return false; 82 } 83 84 boolean ignoreRest() { 85 return false; 86 } 87 88 abstract void process(JdepsTask task, String opt, String arg) throws BadArgs; 89 final boolean hasArg; 90 final String[] aliases; 91 } 92 93 static Option[] recognizedOptions = { 94 new Option(false, "-h", "--help") { 95 void process(JdepsTask task, String opt, String arg) { 96 task.options.help = true; 97 } 98 }, 99 new Option(false, "-version") { 100 void process(JdepsTask task, String opt, String arg) { 101 task.options.version = true; 102 } 103 }, 104 new Option(false, "-fullversion") { 105 void process(JdepsTask task, String opt, String arg) { 106 task.options.fullVersion = true; 107 } 108 }, 109 new Option(true, "-classpath") { 110 void process(JdepsTask task, String opt, String arg) { 111 task.options.classpath = arg; 112 } 113 }, 114 new Option(false, "-v", "--verbose") { 115 void process(JdepsTask task, String opt, String arg) { 116 task.options.verbose = true; 117 } 118 }, 119 new Option(false, "-r", "--reverse") { 120 void process(JdepsTask task, String opt, String arg) { 121 task.options.reverse = true; 122 } 123 }, 124 new Option(true, "-p") { 125 void process(JdepsTask task, String opt, String arg) { 126 task.options.packageNames.add(arg); 127 } 128 }, 129 new Option(true, "-e", "--regex") { 130 void process(JdepsTask task, String opt, String arg) { 131 task.options.regex = arg; 132 } 133 }, 134 new Option(false, "-P", "--profile") { 135 void process(JdepsTask task, String opt, String arg) { 136 task.options.showProfile = true; 137 } 138 }, 139 new Option(true, "-d", "--depth") { 140 void process(JdepsTask task, String opt, String arg) throws BadArgs { 141 try { 142 task.options.depth = Integer.parseInt(arg); 143 } catch (NumberFormatException e) { 144 throw task.new BadArgs("err.invalid.arg.for.option", opt); 145 } 146 } 147 }, 148 new Option(false, "-all") { 149 void process(JdepsTask task, String opt, String arg) { 150 task.options.all = true; 151 } 152 },}; 153 private static final String PROGNAME = "jdeps"; 154 private final Options options = new Options(); 155 private final List<String> filenames = new ArrayList<>(); 156 157 private PrintWriter log; 158 void setLog(PrintWriter out) { 159 log = out; 160 } 161 /** 162 * Result codes. 163 */ 164 static final int EXIT_OK = 0, // Completed with no errors. 165 EXIT_ERROR = 1, // Completed but reported errors. 166 EXIT_CMDERR = 2, // Bad command-line arguments 167 EXIT_SYSERR = 3, // System error or resource exhaustion. 168 EXIT_ABNORMAL = 4;// terminated abnormally 169 170 int run(String[] args) { 171 if (log == null) { 172 log = new PrintWriter(System.out); 173 } 174 try { 175 handleOptions(Arrays.asList(args)); 176 if (filenames.isEmpty()) { 177 if (options.help || options.version || options.fullVersion) { 178 return EXIT_OK; 179 } else { 180 return EXIT_CMDERR; 181 } 182 } 183 if (options.regex != null && options.packageNames.size() > 0) { 184 showHelp(); 185 return EXIT_CMDERR; 186 } 187 boolean ok = run(); 188 return ok ? EXIT_OK : EXIT_ERROR; 189 } catch (BadArgs e) { 190 reportError(e.key, e.args); 191 if (e.showUsage) { 192 log.println(getMessage("main.usage.summary", PROGNAME)); 193 } 194 return EXIT_CMDERR; 195 } catch (IOException e) { 196 return EXIT_ABNORMAL; 197 } finally { 198 log.flush(); 199 } 200 } 201 202 private boolean run() throws IOException { 203 Dependency.Filter filter; 204 if (options.regex != null) { 205 filter = Dependencies.getRegexFilter(Pattern.compile(options.regex)); 206 } else if (options.packageNames.size() > 0) { 207 filter = Dependencies.getPackageFilter(options.packageNames, false); 208 } else { 209 filter = new Dependency.Filter() { 210 public boolean accepts(Dependency dependency) { 211 return !dependency.getOrigin().equals(dependency.getTarget()); 212 } 213 }; 214 } 215 216 Finder finder = Dependencies.getClassDependencyFinder(); 217 DependencyRecorder recorder = new DependencyRecorder(options.reverse); 218 findDependencies(finder, filter, recorder); 219 220 if (options.verbose) { 221 printClassDeps(log, recorder); 222 } else { 223 printPackageDeps(log, recorder); 224 } 225 return true; 226 } 227 228 private Map<String, ClassFileReader> classToReaderMap = new HashMap<>(); 229 private void findDependencies(Dependency.Finder finder, 230 Dependency.Filter filter, 231 DependencyRecorder recorder) 232 throws IOException 233 { 234 // Work queue of names of classfiles to be searched. 235 // Entries will be unique, and for classes that do not yet have 236 // dependencies in the results map. 237 Deque<String> deque = new LinkedList<>(); 238 239 // get the immediate dependencies of the input files 240 for (ClassFileReader r : getClassFileReaders(filenames)) { 241 for (ClassFile cf : r.getClassFiles()) { 242 String className; 243 try { 244 className = cf.getName().replace('/', '.'); 245 } catch (ConstantPoolException e) { 246 throw new ClassFileError(e); 247 } 248 if (!classToReaderMap.containsKey(className)) { 249 classToReaderMap.put(className, r); 250 } 251 for (Dependency d : finder.findDependencies(cf)) { 252 if (filter.accepts(d)) { 253 String cn = d.getTarget().getClassName(); 254 if (!classToReaderMap.containsKey(cn) && !deque.contains(cn)) { 255 deque.add(cn); 256 } 257 recorder.addDependency(d); 258 } 259 } 260 } 261 } 262 263 // add ClassFileReader for looking up classes from the classpath 264 // for transitive dependency analysis 265 int depths = options.depth == 0 ? Integer.MAX_VALUE : options.depth; 266 initJavaHomeReaders(); 267 List<ClassFileReader> readers = new ArrayList<>(classPathReaders(options.classpath)); 268 readers.addAll(javaHomeReaders); 269 while (!deque.isEmpty() && depths-- >= 0) { 270 Deque<String> unresolved = deque; 271 deque = new LinkedList<>(); 272 String className; 273 while ((className = unresolved.poll()) != null) { 274 if (classToReaderMap.containsKey(className)) { 275 continue; 276 } 277 278 ClassFile cf = null; 279 for (ClassFileReader r : readers) { 280 cf = r.getClassFile(className); 281 if (cf != null) { 282 classToReaderMap.put(className, r); 283 if (depths > 0) { 284 // do dependency analysis if depths > 0 285 for (Dependency d : finder.findDependencies(cf)) { 286 if (filter.accepts(d)) { 287 String cn = d.getTarget().getClassName(); 288 if (!classToReaderMap.containsKey(cn) && !deque.contains(cn)) { 289 deque.add(cn); 290 } 291 recorder.addDependency(d); 292 } 293 } 294 } 295 break; 296 } 297 } 298 299 if (cf == null) { 300 warning("warn.class.not.found", className); 301 classToReaderMap.put(className, null); 302 } 303 } 304 } 305 } 306 307 private void printPackageDeps(PrintWriter out, DependencyRecorder r) { 308 if (options.all) { 309 SortedMap<String, ClassFileReader> pkgs = new TreeMap<>(); 310 for (Location loc : r.getAllClasses()) { 311 String pn = packageOf(loc); 312 ClassFileReader reader = classToReaderMap.get(loc.getClassName()); 313 if (!pkgs.containsKey(pn)) { 314 pkgs.put(pn, reader); 315 } else if (pkgs.get(pn) != reader) { 316 warning("warn.split.package", pn, reader, pkgs.get(pn)); 317 } 318 } 319 for (Map.Entry<String, ClassFileReader> e : pkgs.entrySet()) { 320 String pn = e.getKey(); 321 ClassFileReader reader = e.getValue(); 322 out.format(" %-60s %s%n", pn, getPackageInfo(pn, reader)); 323 } 324 } else { 325 String pkg = ""; 326 SortedMap<Location, SortedSet<Location>> deps = getDependencies(r); 327 SortedMap<String, ClassFileReader> targets = new TreeMap<>(); 328 for (Map.Entry<Location, SortedSet<Location>> e : getDependencies(r).entrySet()) { 329 Location o = e.getKey(); 330 String p = packageOf(o); 331 if (!p.equals(pkg)) { 332 printTargets(out, targets); 333 pkg = p; 334 targets.clear(); 335 out.format("%s (%s)%n", p, classToReaderMap.get(o.getClassName())); 336 } 337 338 for (Location t : e.getValue()) { 339 p = packageOf(t); 340 ClassFileReader reader = classToReaderMap.get(t.getClassName()); 341 if (!targets.containsKey(p)) { 342 targets.put(p, reader); 343 } 344 } 345 } 346 printTargets(out, targets); 347 } 348 } 349 350 private void printTargets(PrintWriter out, Map<String, ClassFileReader> targets) { 351 String arrow = options.reverse ? "<-" : "->"; 352 for (Map.Entry<String, ClassFileReader> t : targets.entrySet()) { 353 String pn = t.getKey(); 354 out.format(" %s %-40s %s%n", arrow, pn, getPackageInfo(pn, t.getValue())); 355 } 356 } 357 358 private String getPackageInfo(String pn, ClassFileReader reader) { 359 Profile profile = Profile.getProfile(pn); 360 if (profile == null && javaHomeReaders.contains(reader)) { 361 return "JDK internal API"; 362 } 363 if (options.showProfile) { 364 String s = reader != null ? reader.toString() : null; 365 return profile != null ? profile.toString() : s; 366 } else { 367 // default no package information 368 return ""; 369 } 370 } 371 372 private static String packageOf(Location loc) { 373 String cn = loc.getClassName(); 374 int i = cn.lastIndexOf('.'); 375 return i > 0 ? cn.substring(0, i) : "<unnamed>"; 376 } 377 378 private void printClassDeps(PrintWriter out, DependencyRecorder r) { 379 if (options.all) { 380 SortedSet<Location> classes = r.getAllClasses(); 381 for (Location t : classes) { 382 out.format(" %s%n", t.getClassName()); 383 } 384 } else { 385 String arrow = options.reverse ? "<-" : "->"; 386 SortedMap<Location, SortedSet<Location>> deps = getDependencies(r); 387 for (Map.Entry<Location, SortedSet<Location>> e : deps.entrySet()) { 388 Location o = e.getKey(); 389 String cn = o.getClassName(); 390 ClassFileReader reader = classToReaderMap.get(cn); 391 out.format("%s (%s)%n", cn, reader); 392 for (Location t : e.getValue()) { 393 cn = t.getClassName(); 394 reader = classToReaderMap.get(cn); 395 out.format(" %s %-60s %s%n", arrow, cn, getPackageInfo(packageOf(t), reader)); 396 } 397 } 398 } 399 } 400 401 private SortedMap<Location, SortedSet<Location>> getDependencies(DependencyRecorder r) { 402 return r.getDependencies(new DependencyRecorder.Filter() { 403 @Override 404 public boolean accept(Location origin, Location target) { 405 String o = origin.getClassName(); 406 String t = target.getClassName(); 407 return (options.all || classToReaderMap.get(o) != classToReaderMap.get(t)); 408 }}); 409 } 410 411 public void handleOptions(Iterable<String> args) throws BadArgs { 412 Iterator<String> iter = args.iterator(); 413 boolean noArgs = !iter.hasNext(); 414 while (iter.hasNext()) { 415 String arg = iter.next(); 416 if (arg.startsWith("-")) { 417 handleOption(arg, iter); 418 } else { 419 filenames.add(arg); 420 while (iter.hasNext()) { 421 filenames.add(iter.next()); 422 } 423 } 424 } 425 426 if (noArgs || filenames.isEmpty() || options.help) { 427 showHelp(); 428 } 429 430 if (options.version || options.fullVersion) { 431 showVersion(options.fullVersion); 432 } 433 } 434 435 private void handleOption(String name, Iterator<String> rest) throws BadArgs { 436 for (Option o : recognizedOptions) { 437 if (o.matches(name)) { 438 if (o.hasArg) { 439 String arg = rest.hasNext() ? rest.next() : "-"; 440 if (arg.startsWith("-")) { 441 throw new BadArgs("err.missing.arg", name).showUsage(true); 442 } else { 443 o.process(this, name, arg); 444 } 445 } else { 446 o.process(this, name, null); 447 } 448 449 if (o.ignoreRest()) { 450 while (rest.hasNext()) { 451 rest.next(); 452 } 453 } 454 return; 455 } 456 } 457 throw new BadArgs("err.unknown.option", name).showUsage(true); 458 } 459 460 private void reportError(String key, Object... args) { 461 log.println(getMessage("error.prefix") + " " + getMessage(key, args)); 462 } 463 464 private void warning(String key, Object... args) { 465 log.println(getMessage("warn.prefix") + " " + getMessage(key, args)); 466 } 467 468 private void showHelp() { 469 log.println(getMessage("main.usage", PROGNAME)); 470 for (Option o : recognizedOptions) { 471 String name = o.aliases[0].substring(1); // there must always be at least one name 472 if (name.equals("fullversion") || name.equals("h")) { 473 continue; 474 } 475 log.println(getMessage("main.opt." + name)); 476 } 477 } 478 479 private void showVersion(boolean full) { 480 log.println(version(full ? "full" : "release")); 481 } 482 483 private String version(String key) { 484 // key=version: mm.nn.oo[-milestone] 485 // key=full: mm.mm.oo[-milestone]-build 486 if (ResourceBundleHelper.versionRB == null) { 487 return System.getProperty("java.version"); 488 } 489 try { 490 return ResourceBundleHelper.versionRB.getString(key); 491 } catch (MissingResourceException e) { 492 return getMessage("version.unknown", System.getProperty("java.version")); 493 } 494 } 495 496 public String getMessage(String key, Object... args) { 497 try { 498 return MessageFormat.format(ResourceBundleHelper.bundle.getString(key), args); 499 } catch (MissingResourceException e) { 500 throw new InternalError("Missing message: " + key); 501 } 502 } 503 504 private static class Options { 505 public boolean help; 506 public boolean verbose; 507 public boolean version; 508 public boolean fullVersion; 509 public boolean showFlags; 510 public boolean reverse; 511 public boolean all; 512 public boolean showProfile; 513 public String regex; 514 public String classpath; 515 public int depth = 1; 516 public Set<String> packageNames = new HashSet<>(); 517 } 518 519 private static class ResourceBundleHelper { 520 static final ResourceBundle versionRB; 521 static final ResourceBundle bundle; 522 523 static { 524 Locale locale = Locale.getDefault(); 525 try { 526 bundle = ResourceBundle.getBundle("com.sun.tools.jdeps.resources.jdeps", locale); 527 } catch (MissingResourceException e) { 528 throw new InternalError("Cannot find jdeps resource bundle for locale " + locale); 529 } 530 try { 531 versionRB = ResourceBundle.getBundle("com.sun.tools.jdeps.resources.version"); 532 } catch (MissingResourceException e) { 533 throw new InternalError("version.resource.missing"); 534 } 535 } 536 } 537 538 static class DependencyRecorder { 539 static interface Filter { 540 boolean accept(Location origin, Location target); 541 } 542 public DependencyRecorder(boolean reverse) { 543 this.reverse = reverse; 544 } 545 546 public void addDependency(Dependency d) { 547 Location origin = reverse ? d.getTarget() : d.getOrigin(); 548 Location target = reverse ? d.getOrigin() : d.getTarget(); 549 SortedSet<Location> odeps = map.get(origin); 550 if (odeps == null) { 551 map.put(origin, odeps = new TreeSet<>(locationComparator)); 552 } 553 odeps.add(target); 554 } 555 556 public SortedMap<Location, SortedSet<Location>> getClassMap() { 557 return map; 558 } 559 560 public SortedSet<Location> getAllClasses() { 561 SortedSet<Location> classes = new TreeSet<>(locationComparator); 562 for (SortedSet<Location> set : map.values()) { 563 classes.addAll(set); 564 } 565 return classes; 566 } 567 568 public SortedMap<Location, SortedSet<Location>> getDependencies(Filter filter) { 569 SortedMap<Location, SortedSet<Location>> result = new TreeMap<>(locationComparator); 570 for (Map.Entry<Location, SortedSet<Location>> e : map.entrySet()) { 571 Location o = e.getKey(); 572 for (Location t : e.getValue()) { 573 if (filter.accept(o, t)) { 574 SortedSet<Location> odeps = result.get(o); 575 if (odeps == null) { 576 result.put(o, odeps = new TreeSet<>(locationComparator)); 577 } 578 odeps.add(t); 579 } 580 } 581 } 582 return result; 583 } 584 585 private Comparator<Location> locationComparator = 586 new Comparator<Location>() { 587 public int compare(Location o1, Location o2) { 588 return o1.toString().compareTo(o2.toString()); 589 } 590 }; 591 private final SortedMap<Location, SortedSet<Location>> map = 592 new TreeMap<>(locationComparator); 593 private final boolean reverse; 594 } 595 596 private List<ClassFileReader> getClassFileReaders(List<String> filenames) throws IOException { 597 List<ClassFileReader> readers = new ArrayList<>(); 598 for (String s : filenames) { 599 File f = new File(s); 600 if (f.exists()) { 601 readers.add(ClassFileReader.newInstance(f)); 602 } else { 603 System.err.println("WARNING: " + s + " not exist"); 604 } 605 } 606 return readers; 607 } 608 609 private List<ClassFileReader> classPathReaders(String classpath) throws IOException { 610 List<ClassFileReader> result = new ArrayList<>(); 611 if (classpath == null || classpath.isEmpty()) { 612 return result; 613 } 614 for (String p : classpath.split(File.pathSeparator)) { 615 if (p.length() > 0) { 616 File f = new File(p); 617 if (f.exists()) { 618 result.add(ClassFileReader.newInstance(f)); 619 } 620 } 621 } 622 return result; 623 } 624 625 private final List<ClassFileReader> javaHomeReaders = new ArrayList<>(); 626 private void initJavaHomeReaders() throws IOException { 627 String javaHome = System.getProperty("java.home"); 628 List<File> files = new ArrayList<>(); 629 File jre = new File(javaHome, "jre"); 630 File lib = new File(javaHome, "lib"); 631 632 if (jre.exists() && jre.isDirectory()) { 633 javaHomeReaders.addAll(addJarFiles(new File(jre, "lib"))); 634 javaHomeReaders.addAll(addJarFiles(lib)); 635 } else if (lib.exists() && lib.isDirectory()) { 636 // either a JRE or a jdk build image 637 File classes = new File(javaHome, "classes"); 638 if (classes.exists() && classes.isDirectory()) { 639 // jdk build outputdir 640 javaHomeReaders.add(ClassFileReader.newInstance(classes)); 641 } 642 // add other JAR files 643 javaHomeReaders.addAll(addJarFiles(lib)); 644 } else { 645 throw new RuntimeException("\"" + javaHome + "\" not a JDK home"); 646 } 647 } 648 649 private List<ClassFileReader> addJarFiles(File f) throws IOException { 650 final List<ClassFileReader> result = new ArrayList<>(); 651 Files.walkFileTree(f.toPath(), new SimpleFileVisitor<Path>() { 652 @Override 653 public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) 654 throws IOException { 655 String fn = file.toFile().getName(); 656 if (fn.endsWith(".jar") && !fn.equals("alt-rt.jar")) { 657 result.add(ClassFileReader.newInstance(file.toFile())); 658 } 659 return FileVisitResult.CONTINUE; 660 } 661 662 @Override 663 public FileVisitResult postVisitDirectory(Path dir, IOException e) 664 throws IOException { 665 if (e == null) { 666 return FileVisitResult.CONTINUE; 667 } else { 668 // directory iteration failed 669 throw e; 670 } 671 } 672 }); 673 return result; 674 } 675 }