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