1 /*
   2  * Copyright (c) 2012, 2014, 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.AccessFlags;
  28 import com.sun.tools.classfile.ClassFile;
  29 import com.sun.tools.classfile.ConstantPoolException;
  30 import com.sun.tools.classfile.Dependencies;
  31 import com.sun.tools.classfile.Dependencies.ClassFileError;
  32 import com.sun.tools.classfile.Dependency;
  33 import com.sun.tools.classfile.Dependency.Location;
  34 import com.sun.tools.jdeps.PlatformClassPath.JDKArchive;
  35 import static com.sun.tools.jdeps.Analyzer.Type.*;
  36 import java.io.*;
  37 import java.nio.file.DirectoryStream;
  38 import java.nio.file.Files;
  39 import java.nio.file.Path;
  40 import java.nio.file.Paths;
  41 import java.text.MessageFormat;
  42 import java.util.*;
  43 import java.util.regex.Pattern;
  44 
  45 /**
  46  * Implementation for the jdeps tool for static class dependency analysis.
  47  */
  48 class JdepsTask {
  49     static class BadArgs extends Exception {
  50         static final long serialVersionUID = 8765093759964640721L;
  51         BadArgs(String key, Object... args) {
  52             super(JdepsTask.getMessage(key, args));
  53             this.key = key;
  54             this.args = args;
  55         }
  56 
  57         BadArgs showUsage(boolean b) {
  58             showUsage = b;
  59             return this;
  60         }
  61         final String key;
  62         final Object[] args;
  63         boolean showUsage;
  64     }
  65 
  66     static abstract class Option {
  67         Option(boolean hasArg, String... aliases) {
  68             this.hasArg = hasArg;
  69             this.aliases = aliases;
  70         }
  71 
  72         boolean isHidden() {
  73             return false;
  74         }
  75 
  76         boolean matches(String opt) {
  77             for (String a : aliases) {
  78                 if (a.equals(opt))
  79                     return true;
  80                 if (hasArg && opt.startsWith(a + "="))
  81                     return true;
  82             }
  83             return false;
  84         }
  85 
  86         boolean ignoreRest() {
  87             return false;
  88         }
  89 
  90         abstract void process(JdepsTask task, String opt, String arg) throws BadArgs;
  91         final boolean hasArg;
  92         final String[] aliases;
  93     }
  94 
  95     static abstract class HiddenOption extends Option {
  96         HiddenOption(boolean hasArg, String... aliases) {
  97             super(hasArg, aliases);
  98         }
  99 
 100         boolean isHidden() {
 101             return true;
 102         }
 103     }
 104 
 105     static Option[] recognizedOptions = {
 106         new Option(false, "-h", "-?", "-help") {
 107             void process(JdepsTask task, String opt, String arg) {
 108                 task.options.help = true;
 109             }
 110         },
 111         new Option(true, "-dotoutput") {
 112             void process(JdepsTask task, String opt, String arg) throws BadArgs {
 113                 Path p = Paths.get(arg);
 114                 if (Files.exists(p) && (!Files.isDirectory(p) || !Files.isWritable(p))) {
 115                     throw new BadArgs("err.invalid.path", arg);
 116                 }
 117                 task.options.dotOutputDir = arg;
 118             }
 119         },
 120         new Option(false, "-s", "-summary") {
 121             void process(JdepsTask task, String opt, String arg) {
 122                 task.options.showSummary = true;
 123                 task.options.verbose = SUMMARY;
 124             }
 125         },
 126         new Option(false, "-v", "-verbose",
 127                           "-verbose:package",
 128                           "-verbose:class") {
 129             void process(JdepsTask task, String opt, String arg) throws BadArgs {
 130                 switch (opt) {
 131                     case "-v":
 132                     case "-verbose":
 133                         task.options.verbose = VERBOSE;
 134                         task.options.filterSameArchive = false;
 135                         task.options.filterSamePackage = false;
 136                         break;
 137                     case "-verbose:package":
 138                         task.options.verbose = PACKAGE;
 139                         break;
 140                     case "-verbose:class":
 141                         task.options.verbose = CLASS;
 142                         break;
 143                     default:
 144                         throw new BadArgs("err.invalid.arg.for.option", opt);
 145                 }
 146             }
 147         },
 148         new Option(true, "-cp", "-classpath") {
 149             void process(JdepsTask task, String opt, String arg) {
 150                 task.options.classpath = arg;
 151             }
 152         },
 153         new Option(true, "-p", "-package") {
 154             void process(JdepsTask task, String opt, String arg) {
 155                 task.options.packageNames.add(arg);
 156             }
 157         },
 158         new Option(true, "-e", "-regex") {
 159             void process(JdepsTask task, String opt, String arg) {
 160                 task.options.regex = arg;
 161             }
 162         },
 163 
 164         new Option(true, "-f", "-filter") {
 165             void process(JdepsTask task, String opt, String arg) {
 166                 task.options.filterRegex = arg;
 167             }
 168         },
 169         new Option(false, "-filter:package",
 170                           "-filter:archive",
 171                           "-filter:none") {
 172             void process(JdepsTask task, String opt, String arg) {
 173                 switch (opt) {
 174                     case "-filter:package":
 175                         task.options.filterSamePackage = true;
 176                         task.options.filterSameArchive = false;
 177                         break;
 178                     case "-filter:archive":
 179                         task.options.filterSameArchive = true;
 180                         task.options.filterSamePackage = false;
 181                         break;
 182                     case "-filter:none":
 183                         task.options.filterSameArchive = false;
 184                         task.options.filterSamePackage = false;
 185                         break;
 186                 }
 187             }
 188         },
 189         new Option(true, "-include") {
 190             void process(JdepsTask task, String opt, String arg) throws BadArgs {
 191                 task.options.includePattern = Pattern.compile(arg);
 192             }
 193         },
 194         new Option(false, "-P", "-profile") {
 195             void process(JdepsTask task, String opt, String arg) throws BadArgs {
 196                 task.options.showProfile = true;
 197                 if (Profile.getProfileCount() == 0) {
 198                     throw new BadArgs("err.option.unsupported", opt, getMessage("err.profiles.msg"));
 199                 }
 200             }
 201         },
 202         new Option(false, "-apionly") {
 203             void process(JdepsTask task, String opt, String arg) {
 204                 task.options.apiOnly = true;
 205             }
 206         },
 207         new Option(false, "-R", "-recursive") {
 208             void process(JdepsTask task, String opt, String arg) {
 209                 task.options.depth = 0;
 210                 // turn off filtering
 211                 task.options.filterSameArchive = false;
 212                 task.options.filterSamePackage = false;
 213             }
 214         },
 215         new Option(false, "-jdkinternals") {
 216             void process(JdepsTask task, String opt, String arg) {
 217                 task.options.findJDKInternals = true;
 218                 task.options.verbose = CLASS;
 219                 if (task.options.includePattern == null) {
 220                     task.options.includePattern = Pattern.compile(".*");
 221                 }
 222             }
 223         },
 224         new Option(false, "-version") {
 225             void process(JdepsTask task, String opt, String arg) {
 226                 task.options.version = true;
 227             }
 228         },
 229         new HiddenOption(false, "-fullversion") {
 230             void process(JdepsTask task, String opt, String arg) {
 231                 task.options.fullVersion = true;
 232             }
 233         },
 234         new HiddenOption(false, "-showlabel") {
 235             void process(JdepsTask task, String opt, String arg) {
 236                 task.options.showLabel = true;
 237             }
 238         },
 239         new HiddenOption(false, "-q", "-quiet") {
 240             void process(JdepsTask task, String opt, String arg) {
 241                 task.options.nowarning = true;
 242             }
 243         },
 244         new HiddenOption(true, "-depth") {
 245             void process(JdepsTask task, String opt, String arg) throws BadArgs {
 246                 try {
 247                     task.options.depth = Integer.parseInt(arg);
 248                 } catch (NumberFormatException e) {
 249                     throw new BadArgs("err.invalid.arg.for.option", opt);
 250                 }
 251             }
 252         },
 253     };
 254 
 255     private static final String PROGNAME = "jdeps";
 256     private final Options options = new Options();
 257     private final List<String> classes = new ArrayList<>();
 258 
 259     private PrintWriter log;
 260     void setLog(PrintWriter out) {
 261         log = out;
 262     }
 263 
 264     /**
 265      * Result codes.
 266      */
 267     static final int EXIT_OK = 0, // Completed with no errors.
 268                      EXIT_ERROR = 1, // Completed but reported errors.
 269                      EXIT_CMDERR = 2, // Bad command-line arguments
 270                      EXIT_SYSERR = 3, // System error or resource exhaustion.
 271                      EXIT_ABNORMAL = 4;// terminated abnormally
 272 
 273     int run(String[] args) {
 274         if (log == null) {
 275             log = new PrintWriter(System.out);
 276         }
 277         try {
 278             handleOptions(args);
 279             if (options.help) {
 280                 showHelp();
 281             }
 282             if (options.version || options.fullVersion) {
 283                 showVersion(options.fullVersion);
 284             }
 285             if (classes.isEmpty() && options.includePattern == null) {
 286                 if (options.help || options.version || options.fullVersion) {
 287                     return EXIT_OK;
 288                 } else {
 289                     showHelp();
 290                     return EXIT_CMDERR;
 291                 }
 292             }
 293             if (options.regex != null && options.packageNames.size() > 0) {
 294                 showHelp();
 295                 return EXIT_CMDERR;
 296             }
 297             if (options.findJDKInternals &&
 298                    (options.regex != null || options.packageNames.size() > 0 || options.showSummary)) {
 299                 showHelp();
 300                 return EXIT_CMDERR;
 301             }
 302             if (options.showSummary && options.verbose != SUMMARY) {
 303                 showHelp();
 304                 return EXIT_CMDERR;
 305             }
 306             boolean ok = run();
 307             return ok ? EXIT_OK : EXIT_ERROR;
 308         } catch (BadArgs e) {
 309             reportError(e.key, e.args);
 310             if (e.showUsage) {
 311                 log.println(getMessage("main.usage.summary", PROGNAME));
 312             }
 313             return EXIT_CMDERR;
 314         } catch (IOException e) {
 315             return EXIT_ABNORMAL;
 316         } finally {
 317             log.flush();
 318         }
 319     }
 320 
 321     private final List<Archive> sourceLocations = new ArrayList<>();
 322     private boolean run() throws IOException {
 323         // parse classfiles and find all dependencies
 324         findDependencies();
 325 
 326         Analyzer analyzer = new Analyzer(options.verbose, new Analyzer.Filter() {
 327             @Override
 328             public boolean accepts(Location origin, Archive originArchive,
 329                                    Location target, Archive targetArchive)
 330             {
 331                 if (options.findJDKInternals) {
 332                     // accepts target that is JDK class but not exported
 333                     return isJDKArchive(targetArchive) &&
 334                               !((JDKArchive) targetArchive).isExported(target.getClassName());
 335                 } else if (options.filterSameArchive) {
 336                     // accepts origin and target that from different archive
 337                     return originArchive != targetArchive;
 338                 }
 339                 return true;
 340             }
 341         });
 342 
 343         // analyze the dependencies
 344         analyzer.run(sourceLocations);
 345 
 346         // output result
 347         if (options.dotOutputDir != null) {
 348             Path dir = Paths.get(options.dotOutputDir);
 349             Files.createDirectories(dir);
 350             generateDotFiles(dir, analyzer);
 351         } else {
 352             printRawOutput(log, analyzer);
 353         }
 354 
 355         if (options.findJDKInternals && !options.nowarning) {
 356             showReplacements(analyzer);
 357         }
 358         return true;
 359     }
 360 
 361     private void generateSummaryDotFile(Path dir, Analyzer analyzer) throws IOException {
 362         // If verbose mode (-v or -verbose option),
 363         // the summary.dot file shows package-level dependencies.
 364         Analyzer.Type summaryType =
 365             (options.verbose == PACKAGE || options.verbose == SUMMARY) ? SUMMARY : PACKAGE;
 366         Path summary = dir.resolve("summary.dot");
 367         try (PrintWriter sw = new PrintWriter(Files.newOutputStream(summary));
 368              SummaryDotFile dotfile = new SummaryDotFile(sw, summaryType)) {
 369             for (Archive archive : sourceLocations) {
 370                 if (!archive.isEmpty()) {
 371                     if (options.verbose == PACKAGE || options.verbose == SUMMARY) {
 372                         if (options.showLabel) {
 373                             // build labels listing package-level dependencies
 374                             analyzer.visitDependences(archive, dotfile.labelBuilder(), PACKAGE);
 375                         }
 376                     }
 377                     analyzer.visitDependences(archive, dotfile, summaryType);
 378                 }
 379             }
 380         }
 381     }
 382 
 383     private void generateDotFiles(Path dir, Analyzer analyzer) throws IOException {
 384         // output individual .dot file for each archive
 385         if (options.verbose != SUMMARY) {
 386             for (Archive archive : sourceLocations) {
 387                 if (analyzer.hasDependences(archive)) {
 388                     Path dotfile = dir.resolve(archive.getName() + ".dot");
 389                     try (PrintWriter pw = new PrintWriter(Files.newOutputStream(dotfile));
 390                          DotFileFormatter formatter = new DotFileFormatter(pw, archive)) {
 391                         analyzer.visitDependences(archive, formatter);
 392                     }
 393                 }
 394             }
 395         }
 396         // generate summary dot file
 397         generateSummaryDotFile(dir, analyzer);
 398     }
 399 
 400     private void printRawOutput(PrintWriter writer, Analyzer analyzer) {
 401         RawOutputFormatter depFormatter = new RawOutputFormatter(writer);
 402         RawSummaryFormatter summaryFormatter = new RawSummaryFormatter(writer);
 403         for (Archive archive : sourceLocations) {
 404             if (!archive.isEmpty()) {
 405                 analyzer.visitDependences(archive, summaryFormatter, SUMMARY);
 406                 if (analyzer.hasDependences(archive) && options.verbose != SUMMARY) {
 407                     analyzer.visitDependences(archive, depFormatter);
 408                 }
 409             }
 410         }
 411     }
 412 
 413     private boolean isValidClassName(String name) {
 414         if (!Character.isJavaIdentifierStart(name.charAt(0))) {
 415             return false;
 416         }
 417         for (int i=1; i < name.length(); i++) {
 418             char c = name.charAt(i);
 419             if (c != '.'  && !Character.isJavaIdentifierPart(c)) {
 420                 return false;
 421             }
 422         }
 423         return true;
 424     }
 425 
 426     /*
 427      * Dep Filter configured based on the input jdeps option
 428      * 1. -p and -regex to match target dependencies
 429      * 2. -filter:package to filter out same-package dependencies
 430      *
 431      * This filter is applied when jdeps parses the class files
 432      * and filtered dependencies are not stored in the Analyzer.
 433      *
 434      * -filter:archive is applied later in the Analyzer as the
 435      * containing archive of a target class may not be known until
 436      * the entire archive
 437      */
 438     class DependencyFilter implements Dependency.Filter {
 439         final Dependency.Filter filter;
 440         final Pattern filterPattern;
 441         DependencyFilter() {
 442             if (options.regex != null) {
 443                 this.filter = Dependencies.getRegexFilter(Pattern.compile(options.regex));
 444             } else if (options.packageNames.size() > 0) {
 445                 this.filter = Dependencies.getPackageFilter(options.packageNames, false);
 446             } else {
 447                 this.filter = null;
 448             }
 449 
 450             this.filterPattern =
 451                 options.filterRegex != null ? Pattern.compile(options.filterRegex) : null;
 452         }
 453         @Override
 454         public boolean accepts(Dependency d) {
 455             if (d.getOrigin().equals(d.getTarget())) {
 456                 return false;
 457             }
 458             String pn = d.getTarget().getPackageName();
 459             if (options.filterSamePackage && d.getOrigin().getPackageName().equals(pn)) {
 460                 return false;
 461             }
 462 
 463             if (filterPattern != null && filterPattern.matcher(pn).matches()) {
 464                 return false;
 465             }
 466             return filter != null ? filter.accepts(d) : true;
 467         }
 468     }
 469 
 470     /**
 471      * Tests if the given class matches the pattern given in the -include option
 472      * or if it's a public class if -apionly option is specified
 473      */
 474     private boolean matches(String classname, AccessFlags flags) {
 475         if (options.apiOnly && !flags.is(AccessFlags.ACC_PUBLIC)) {
 476             return false;
 477         } else if (options.includePattern != null) {
 478             return options.includePattern.matcher(classname.replace('/', '.')).matches();
 479         } else {
 480             return true;
 481         }
 482     }
 483 
 484     private void findDependencies() throws IOException {
 485         Dependency.Finder finder =
 486             options.apiOnly ? Dependencies.getAPIFinder(AccessFlags.ACC_PROTECTED)
 487                             : Dependencies.getClassDependencyFinder();
 488         Dependency.Filter filter = new DependencyFilter();
 489 
 490         List<Archive> archives = new ArrayList<>();
 491         Deque<String> roots = new LinkedList<>();
 492         List<Path> paths = new ArrayList<>();
 493         for (String s : classes) {
 494             Path p = Paths.get(s);
 495             if (Files.exists(p)) {
 496                 paths.add(p);
 497                 archives.add(Archive.getInstance(p));
 498             } else {
 499                 if (isValidClassName(s)) {
 500                     roots.add(s);
 501                 } else {
 502                     warning("warn.invalid.arg", s);
 503                 }
 504             }
 505         }
 506         sourceLocations.addAll(archives);
 507 
 508         List<Archive> classpaths = new ArrayList<>(); // for class file lookup
 509         classpaths.addAll(getClassPathArchives(options.classpath, paths));
 510         if (options.includePattern != null) {
 511             archives.addAll(classpaths);
 512         }
 513         classpaths.addAll(PlatformClassPath.getArchives());
 514 
 515         // add all classpath archives to the source locations for reporting
 516         sourceLocations.addAll(classpaths);
 517 
 518         // Work queue of names of classfiles to be searched.
 519         // Entries will be unique, and for classes that do not yet have
 520         // dependencies in the results map.
 521         Deque<String> deque = new LinkedList<>();
 522         Set<String> doneClasses = new HashSet<>();
 523 
 524         // get the immediate dependencies of the input files
 525         for (Archive a : archives) {
 526             for (ClassFile cf : a.reader().getClassFiles()) {
 527                 String classFileName;
 528                 try {
 529                     classFileName = cf.getName();
 530                 } catch (ConstantPoolException e) {
 531                     throw new ClassFileError(e);
 532                 }
 533 
 534                 // tests if this class matches the -include or -apiOnly option if specified
 535                 if (!matches(classFileName, cf.access_flags)) {
 536                     continue;
 537                 }
 538 
 539                 if (!doneClasses.contains(classFileName)) {
 540                     doneClasses.add(classFileName);
 541                 }
 542 
 543                 for (Dependency d : finder.findDependencies(cf)) {
 544                     if (filter.accepts(d)) {
 545                         String cn = d.getTarget().getName();
 546                         if (!doneClasses.contains(cn) && !deque.contains(cn)) {
 547                             deque.add(cn);
 548                         }
 549                         a.addClass(d.getOrigin(), d.getTarget());
 550                     } else {
 551                         // ensure that the parsed class is added the archive
 552                         a.addClass(d.getOrigin());
 553                     }
 554                 }
 555                 for (String name : a.reader().skippedEntries()) {
 556                     warning("warn.skipped.entry", name, a.getPathName());
 557                 }
 558             }
 559         }
 560 
 561         // add Archive for looking up classes from the classpath
 562         // for transitive dependency analysis
 563         Deque<String> unresolved = roots;
 564         int depth = options.depth > 0 ? options.depth : Integer.MAX_VALUE;
 565         do {
 566             String name;
 567             while ((name = unresolved.poll()) != null) {
 568                 if (doneClasses.contains(name)) {
 569                     continue;
 570                 }
 571                 ClassFile cf = null;
 572                 for (Archive a : classpaths) {
 573                     cf = a.reader().getClassFile(name);
 574                     if (cf != null) {
 575                         String classFileName;
 576                         try {
 577                             classFileName = cf.getName();
 578                         } catch (ConstantPoolException e) {
 579                             throw new ClassFileError(e);
 580                         }
 581                         if (!doneClasses.contains(classFileName)) {
 582                             // if name is a fully-qualified class name specified
 583                             // from command-line, this class might already be parsed
 584                             doneClasses.add(classFileName);
 585                             // process @jdk.Exported for JDK classes
 586                             if (isJDKArchive(a)) {
 587                                 ((JDKArchive)a).processJdkExported(cf);
 588                             }
 589                             for (Dependency d : finder.findDependencies(cf)) {
 590                                 if (depth == 0) {
 591                                     // ignore the dependency
 592                                     a.addClass(d.getOrigin());
 593                                     break;
 594                                 } else if (filter.accepts(d)) {
 595                                     a.addClass(d.getOrigin(), d.getTarget());
 596                                     String cn = d.getTarget().getName();
 597                                     if (!doneClasses.contains(cn) && !deque.contains(cn)) {
 598                                         deque.add(cn);
 599                                     }
 600                                 } else {
 601                                     // ensure that the parsed class is added the archive
 602                                     a.addClass(d.getOrigin());
 603                                 }
 604                             }
 605                         }
 606                         break;
 607                     }
 608                 }
 609                 if (cf == null) {
 610                     doneClasses.add(name);
 611                 }
 612             }
 613             unresolved = deque;
 614             deque = new LinkedList<>();
 615         } while (!unresolved.isEmpty() && depth-- > 0);
 616     }
 617 
 618     public void handleOptions(String[] args) throws BadArgs {
 619         // process options
 620         for (int i=0; i < args.length; i++) {
 621             if (args[i].charAt(0) == '-') {
 622                 String name = args[i];
 623                 Option option = getOption(name);
 624                 String param = null;
 625                 if (option.hasArg) {
 626                     if (name.startsWith("-") && name.indexOf('=') > 0) {
 627                         param = name.substring(name.indexOf('=') + 1, name.length());
 628                     } else if (i + 1 < args.length) {
 629                         param = args[++i];
 630                     }
 631                     if (param == null || param.isEmpty() || param.charAt(0) == '-') {
 632                         throw new BadArgs("err.missing.arg", name).showUsage(true);
 633                     }
 634                 }
 635                 option.process(this, name, param);
 636                 if (option.ignoreRest()) {
 637                     i = args.length;
 638                 }
 639             } else {
 640                 // process rest of the input arguments
 641                 for (; i < args.length; i++) {
 642                     String name = args[i];
 643                     if (name.charAt(0) == '-') {
 644                         throw new BadArgs("err.option.after.class", name).showUsage(true);
 645                     }
 646                     classes.add(name);
 647                 }
 648             }
 649         }
 650     }
 651 
 652     private Option getOption(String name) throws BadArgs {
 653         for (Option o : recognizedOptions) {
 654             if (o.matches(name)) {
 655                 return o;
 656             }
 657         }
 658         throw new BadArgs("err.unknown.option", name).showUsage(true);
 659     }
 660 
 661     private void reportError(String key, Object... args) {
 662         log.println(getMessage("error.prefix") + " " + getMessage(key, args));
 663     }
 664 
 665     private void warning(String key, Object... args) {
 666         log.println(getMessage("warn.prefix") + " " + getMessage(key, args));
 667     }
 668 
 669     private void showHelp() {
 670         log.println(getMessage("main.usage", PROGNAME));
 671         for (Option o : recognizedOptions) {
 672             String name = o.aliases[0].substring(1); // there must always be at least one name
 673             name = name.charAt(0) == '-' ? name.substring(1) : name;
 674             if (o.isHidden() || name.equals("h") || name.startsWith("filter:")) {
 675                 continue;
 676             }
 677             log.println(getMessage("main.opt." + name));
 678         }
 679     }
 680 
 681     private void showVersion(boolean full) {
 682         log.println(version(full ? "full" : "release"));
 683     }
 684 
 685     private String version(String key) {
 686         // key=version:  mm.nn.oo[-milestone]
 687         // key=full:     mm.mm.oo[-milestone]-build
 688         if (ResourceBundleHelper.versionRB == null) {
 689             return System.getProperty("java.version");
 690         }
 691         try {
 692             return ResourceBundleHelper.versionRB.getString(key);
 693         } catch (MissingResourceException e) {
 694             return getMessage("version.unknown", System.getProperty("java.version"));
 695         }
 696     }
 697 
 698     static String getMessage(String key, Object... args) {
 699         try {
 700             return MessageFormat.format(ResourceBundleHelper.bundle.getString(key), args);
 701         } catch (MissingResourceException e) {
 702             throw new InternalError("Missing message: " + key);
 703         }
 704     }
 705 
 706     private static class Options {
 707         boolean help;
 708         boolean version;
 709         boolean fullVersion;
 710         boolean showProfile;
 711         boolean showSummary;
 712         boolean apiOnly;
 713         boolean showLabel;
 714         boolean findJDKInternals;
 715         boolean nowarning;
 716         // default is to show package-level dependencies
 717         // and filter references from same package
 718         Analyzer.Type verbose = PACKAGE;
 719         boolean filterSamePackage = true;
 720         boolean filterSameArchive = false;
 721         String filterRegex;
 722         String dotOutputDir;
 723         String classpath = "";
 724         int depth = 1;
 725         Set<String> packageNames = new HashSet<>();
 726         String regex;             // apply to the dependences
 727         Pattern includePattern;   // apply to classes
 728     }
 729     private static class ResourceBundleHelper {
 730         static final ResourceBundle versionRB;
 731         static final ResourceBundle bundle;
 732         static final ResourceBundle jdkinternals;
 733 
 734         static {
 735             Locale locale = Locale.getDefault();
 736             try {
 737                 bundle = ResourceBundle.getBundle("com.sun.tools.jdeps.resources.jdeps", locale);
 738             } catch (MissingResourceException e) {
 739                 throw new InternalError("Cannot find jdeps resource bundle for locale " + locale);
 740             }
 741             try {
 742                 versionRB = ResourceBundle.getBundle("com.sun.tools.jdeps.resources.version");
 743             } catch (MissingResourceException e) {
 744                 throw new InternalError("version.resource.missing");
 745             }
 746             try {
 747                 jdkinternals = ResourceBundle.getBundle("com.sun.tools.jdeps.resources.jdkinternals");
 748             } catch (MissingResourceException e) {
 749                 throw new InternalError("Cannot find jdkinternals resource bundle");
 750             }
 751         }
 752     }
 753 
 754     /*
 755      * Returns the list of Archive specified in cpaths and not included
 756      * initialArchives
 757      */
 758     private List<Archive> getClassPathArchives(String cpaths, List<Path> initialArchives)
 759             throws IOException
 760     {
 761         List<Archive> result = new ArrayList<>();
 762         if (cpaths.isEmpty()) {
 763             return result;
 764         }
 765 
 766         List<Path> paths = new ArrayList<>();
 767         for (String p : cpaths.split(File.pathSeparator)) {
 768             if (p.length() > 0) {
 769                 // wildcard to parse all JAR files e.g. -classpath dir/*
 770                 int i = p.lastIndexOf(".*");
 771                 if (i > 0) {
 772                     Path dir = Paths.get(p.substring(0, i));
 773                     try (DirectoryStream<Path> stream = Files.newDirectoryStream(dir, "*.jar")) {
 774                         for (Path entry : stream) {
 775                             paths.add(entry);
 776                         }
 777                     }
 778                 } else {
 779                     paths.add(Paths.get(p));
 780                 }
 781             }
 782         }
 783         for (Path p : paths) {
 784             if (Files.exists(p) && !hasSameFile(initialArchives, p)) {
 785                 result.add(Archive.getInstance(p));
 786             }
 787         }
 788         return result;
 789     }
 790 
 791     private boolean hasSameFile(List<Path> paths, Path p2) throws IOException {
 792         for (Path p1 : paths) {
 793             if (Files.isSameFile(p1, p2)) {
 794                 return true;
 795             }
 796         }
 797         return false;
 798     }
 799 
 800     class RawOutputFormatter implements Analyzer.Visitor {
 801         private final PrintWriter writer;
 802         private String pkg = "";
 803         RawOutputFormatter(PrintWriter writer) {
 804             this.writer = writer;
 805         }
 806         @Override
 807         public void visitDependence(String origin, Archive originArchive,
 808                                     String target, Archive targetArchive) {
 809             String tag = toTag(target, targetArchive);
 810             if (options.verbose == VERBOSE) {
 811                 writer.format("   %-50s -> %-50s %s%n", origin, target, tag);
 812             } else {
 813                 if (!origin.equals(pkg)) {
 814                     pkg = origin;
 815                     writer.format("   %s (%s)%n", origin, originArchive.getName());
 816                 }
 817                 writer.format("      -> %-50s %s%n", target, tag);
 818             }
 819         }
 820     }
 821 
 822     class RawSummaryFormatter implements Analyzer.Visitor {
 823         private final PrintWriter writer;
 824         RawSummaryFormatter(PrintWriter writer) {
 825             this.writer = writer;
 826         }
 827         @Override
 828         public void visitDependence(String origin, Archive originArchive,
 829                                     String target, Archive targetArchive) {
 830             writer.format("%s -> %s", originArchive.getName(), targetArchive.getPathName());
 831             if (options.showProfile && JDKArchive.isProfileArchive(targetArchive)) {
 832                 writer.format(" (%s)", target);
 833             }
 834             writer.format("%n");
 835         }
 836     }
 837 
 838     class DotFileFormatter implements Analyzer.Visitor, AutoCloseable {
 839         private final PrintWriter writer;
 840         private final String name;
 841         DotFileFormatter(PrintWriter writer, Archive archive) {
 842             this.writer = writer;
 843             this.name = archive.getName();
 844             writer.format("digraph \"%s\" {%n", name);
 845             writer.format("    // Path: %s%n", archive.getPathName());
 846         }
 847 
 848         @Override
 849         public void close() {
 850             writer.println("}");
 851         }
 852 
 853         @Override
 854         public void visitDependence(String origin, Archive originArchive,
 855                                     String target, Archive targetArchive) {
 856             String tag = toTag(target, targetArchive);
 857             writer.format("   %-50s -> \"%s\";%n",
 858                           String.format("\"%s\"", origin),
 859                           tag.isEmpty() ? target
 860                                         : String.format("%s (%s)", target, tag));
 861         }
 862     }
 863 
 864     class SummaryDotFile implements Analyzer.Visitor, AutoCloseable {
 865         private final PrintWriter writer;
 866         private final Analyzer.Type type;
 867         private final Map<Archive, Map<Archive,StringBuilder>> edges = new HashMap<>();
 868         SummaryDotFile(PrintWriter writer, Analyzer.Type type) {
 869             this.writer = writer;
 870             this.type = type;
 871             writer.format("digraph \"summary\" {%n");
 872         }
 873 
 874         @Override
 875         public void close() {
 876             writer.println("}");
 877         }
 878 
 879         @Override
 880         public void visitDependence(String origin, Archive originArchive,
 881                                     String target, Archive targetArchive) {
 882             String targetName = type == PACKAGE ? target : targetArchive.getName();
 883             if (type == PACKAGE) {
 884                 String tag = toTag(target, targetArchive, type);
 885                 if (!tag.isEmpty())
 886                     targetName += " (" + tag + ")";
 887             } else if (options.showProfile && JDKArchive.isProfileArchive(targetArchive)) {
 888                 targetName += " (" + target + ")";
 889             }
 890             String label = getLabel(originArchive, targetArchive);
 891             writer.format("  %-50s -> \"%s\"%s;%n",
 892                           String.format("\"%s\"", origin), targetName, label);
 893         }
 894 
 895         String getLabel(Archive origin, Archive target) {
 896             if (edges.isEmpty())
 897                 return "";
 898 
 899             StringBuilder label = edges.get(origin).get(target);
 900             return label == null ? "" : String.format(" [label=\"%s\",fontsize=9]", label.toString());
 901         }
 902 
 903         Analyzer.Visitor labelBuilder() {
 904             // show the package-level dependencies as labels in the dot graph
 905             return new Analyzer.Visitor() {
 906                 @Override
 907                 public void visitDependence(String origin, Archive originArchive,
 908                                             String target, Archive targetArchive)
 909                 {
 910                     Map<Archive,StringBuilder> labels = edges.get(originArchive);
 911                     if (!edges.containsKey(originArchive)) {
 912                         edges.put(originArchive, labels = new HashMap<>());
 913                     }
 914                     StringBuilder sb = labels.get(targetArchive);
 915                     if (sb == null) {
 916                         labels.put(targetArchive, sb = new StringBuilder());
 917                     }
 918                     String tag = toTag(target, targetArchive, PACKAGE);
 919                     addLabel(sb, origin, target, tag);
 920                 }
 921 
 922                 void addLabel(StringBuilder label, String origin, String target, String tag) {
 923                     label.append(origin).append(" -> ").append(target);
 924                     if (!tag.isEmpty()) {
 925                         label.append(" (" + tag + ")");
 926                     }
 927                     label.append("\\n");
 928                 }
 929             };
 930         }
 931     }
 932 
 933     /**
 934      * Test if the given archive is part of the JDK
 935      */
 936     private boolean isJDKArchive(Archive archive) {
 937         return JDKArchive.class.isInstance(archive);
 938     }
 939 
 940     /**
 941      * If the given archive is JDK archive, this method returns the profile name
 942      * only if -profile option is specified; it accesses a private JDK API and
 943      * the returned value will have "JDK internal API" prefix
 944      *
 945      * For non-JDK archives, this method returns the file name of the archive.
 946      */
 947     private String toTag(String name, Archive source, Analyzer.Type type) {
 948         if (!isJDKArchive(source)) {
 949             return source.getName();
 950         }
 951 
 952         JDKArchive jdk = (JDKArchive)source;
 953         boolean isExported = false;
 954         if (type == CLASS || type == VERBOSE) {
 955             isExported = jdk.isExported(name);
 956         } else {
 957             isExported = jdk.isExportedPackage(name);
 958         }
 959         Profile p = getProfile(name, type);
 960         if (isExported) {
 961             // exported API
 962             return options.showProfile && p != null ? p.profileName() : "";
 963         } else {
 964             return "JDK internal API (" + source.getName() + ")";
 965         }
 966     }
 967 
 968     private String toTag(String name, Archive source) {
 969         return toTag(name, source, options.verbose);
 970     }
 971 
 972     private Profile getProfile(String name, Analyzer.Type type) {
 973         String pn = name;
 974         if (type == CLASS || type == VERBOSE) {
 975             int i = name.lastIndexOf('.');
 976             pn = i > 0 ? name.substring(0, i) : "";
 977         }
 978         return Profile.getProfile(pn);
 979     }
 980 
 981     /**
 982      * Returns the recommended replacement API for the given classname;
 983      * or return null if replacement API is not known.
 984      */
 985     private String replacementFor(String cn) {
 986         String name = cn;
 987         String value = null;
 988         while (value == null && name != null) {
 989             try {
 990                 value = ResourceBundleHelper.jdkinternals.getString(name);
 991             } catch (MissingResourceException e) {
 992                 // go up one subpackage level
 993                 int i = name.lastIndexOf('.');
 994                 name = i > 0 ? name.substring(0, i) : null;
 995             }
 996         }
 997         return value;
 998     };
 999 
1000     private void showReplacements(Analyzer analyzer) {
1001         Map<String,String> jdkinternals = new TreeMap<>();
1002         boolean useInternals = false;
1003         for (Archive source : sourceLocations) {
1004             useInternals = useInternals || analyzer.hasDependences(source);
1005             for (String cn : analyzer.dependences(source)) {
1006                 String repl = replacementFor(cn);
1007                 if (repl != null && !jdkinternals.containsKey(cn)) {
1008                     jdkinternals.put(cn, repl);
1009                 }
1010             }
1011         }
1012         if (useInternals) {
1013             log.println();
1014             warning("warn.replace.useJDKInternals", getMessage("jdeps.wiki.url"));
1015         }
1016         if (!jdkinternals.isEmpty()) {
1017             log.println();
1018             log.format("%-40s %s%n", "JDK Internal API", "Suggested Replacement");
1019             log.format("%-40s %s%n", "----------------", "---------------------");
1020             for (Map.Entry<String,String> e : jdkinternals.entrySet()) {
1021                 log.format("%-40s %s%n", e.getKey(), e.getValue());
1022             }
1023         }
1024 
1025     }
1026 }