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 }