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