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