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