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 }