1 /*
   2  * Copyright (c) 2012, 2018, 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 
  26 package com.sun.tools.jdeps;
  27 
  28 import com.sun.tools.jdeps.Analyzer.Type;
  29 import static com.sun.tools.jdeps.Analyzer.Type.*;
  30 import static com.sun.tools.jdeps.JdepsWriter.*;
  31 import static java.util.stream.Collectors.*;
  32 
  33 import java.io.IOException;
  34 import java.io.PrintWriter;
  35 import java.lang.module.ResolutionException;
  36 import java.nio.file.Files;
  37 import java.nio.file.Path;
  38 import java.nio.file.Paths;
  39 import java.text.MessageFormat;
  40 import java.util.*;
  41 import java.util.jar.JarFile;
  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 interface BadArguments {
  49         String getKey();
  50         Object[] getArgs();
  51         boolean showUsage();
  52     }
  53     static class BadArgs extends Exception implements BadArguments {
  54         static final long serialVersionUID = 8765093759964640721L;
  55         BadArgs(String key, Object... args) {
  56             super(JdepsTask.getMessage(key, args));
  57             this.key = key;
  58             this.args = args;
  59         }
  60 
  61         BadArgs showUsage(boolean b) {
  62             showUsage = b;
  63             return this;
  64         }
  65         final String key;
  66         final Object[] args;
  67         boolean showUsage;
  68 
  69         @Override
  70         public String getKey() {
  71             return key;
  72         }
  73 
  74         @Override
  75         public Object[] getArgs() {
  76             return args;
  77         }
  78 
  79         @Override
  80         public boolean showUsage() {
  81             return showUsage;
  82         }
  83     }
  84 
  85     static class UncheckedBadArgs extends RuntimeException implements BadArguments {
  86         static final long serialVersionUID = -1L;
  87         final BadArgs cause;
  88         UncheckedBadArgs(BadArgs cause) {
  89             super(cause);
  90             this.cause = cause;
  91         }
  92         @Override
  93         public String getKey() {
  94             return cause.key;
  95         }
  96 
  97         @Override
  98         public Object[] getArgs() {
  99             return cause.args;
 100         }
 101 
 102         @Override
 103         public boolean showUsage() {
 104             return cause.showUsage;
 105         }
 106     }
 107 
 108     static abstract class Option {
 109         Option(boolean hasArg, String... aliases) {
 110             this.hasArg = hasArg;
 111             this.aliases = aliases;
 112         }
 113 
 114         Option(boolean hasArg, CommandOption cmd) {
 115             this(hasArg, cmd.names());
 116         }
 117 
 118         boolean isHidden() {
 119             return false;
 120         }
 121 
 122         boolean matches(String opt) {
 123             for (String a : aliases) {
 124                 if (a.equals(opt))
 125                     return true;
 126                 if (hasArg && opt.startsWith(a + "="))
 127                     return true;
 128             }
 129             return false;
 130         }
 131 
 132         boolean ignoreRest() {
 133             return false;
 134         }
 135 
 136         abstract void process(JdepsTask task, String opt, String arg) throws BadArgs;
 137         final boolean hasArg;
 138         final String[] aliases;
 139     }
 140 
 141     static abstract class HiddenOption extends Option {
 142         HiddenOption(boolean hasArg, String... aliases) {
 143             super(hasArg, aliases);
 144         }
 145 
 146         boolean isHidden() {
 147             return true;
 148         }
 149     }
 150 
 151     enum CommandOption {
 152         ANALYZE_DEPS(""),
 153         GENERATE_DOT_FILE("-dotoutput", "--dot-output"),
 154         GENERATE_MODULE_INFO("--generate-module-info"),
 155         GENERATE_OPEN_MODULE("--generate-open-module"),
 156         LIST_DEPS("--list-deps"),
 157         LIST_REDUCED_DEPS("--list-reduced-deps"),
 158         PRINT_MODULE_DEPS("--print-module-deps"),
 159         CHECK_MODULES("--check");
 160 
 161         private final String[] names;
 162         CommandOption(String... names) {
 163             this.names = names;
 164         }
 165 
 166         String[] names() {
 167             return names;
 168         }
 169 
 170         @Override
 171         public String toString() {
 172             return names[0];
 173         }
 174     }
 175 
 176     static Option[] recognizedOptions = {
 177         new Option(false, "-h", "-?", "-help", "--help") {
 178             void process(JdepsTask task, String opt, String arg) {
 179                 task.options.help = true;
 180             }
 181         },
 182         new Option(true, CommandOption.GENERATE_DOT_FILE) {
 183             void process(JdepsTask task, String opt, String arg) throws BadArgs {
 184                 if (task.command != null) {
 185                     throw new BadArgs("err.command.set", task.command, opt);
 186                 }
 187                 task.command = task.genDotFile(Paths.get(arg));
 188             }
 189         },
 190         new Option(false, "-s", "-summary") {
 191             void process(JdepsTask task, String opt, String arg) {
 192                 task.options.showSummary = true;
 193             }
 194         },
 195         new Option(false, "-v", "-verbose",
 196                                 "-verbose:module",
 197                                 "-verbose:package",
 198                                 "-verbose:class") {
 199             void process(JdepsTask task, String opt, String arg) throws BadArgs {
 200                 switch (opt) {
 201                     case "-v":
 202                     case "-verbose":
 203                         task.options.verbose = VERBOSE;
 204                         task.options.filterSameArchive = false;
 205                         task.options.filterSamePackage = false;
 206                         break;
 207                     case "-verbose:module":
 208                         task.options.verbose = MODULE;
 209                         break;
 210                     case "-verbose:package":
 211                         task.options.verbose = PACKAGE;
 212                         break;
 213                     case "-verbose:class":
 214                         task.options.verbose = CLASS;
 215                         break;
 216                     default:
 217                         throw new BadArgs("err.invalid.arg.for.option", opt);
 218                 }
 219             }
 220         },
 221         new Option(false, "-apionly", "--api-only") {
 222             void process(JdepsTask task, String opt, String arg) {
 223                 task.options.apiOnly = true;
 224             }
 225         },
 226 
 227         new Option(false, "-jdkinternals", "--jdk-internals") {
 228             void process(JdepsTask task, String opt, String arg) {
 229                 task.options.findJDKInternals = true;
 230                 if (task.options.includePattern == null) {
 231                     task.options.includePattern = Pattern.compile(".*");
 232                 }
 233             }
 234         },
 235 
 236         // ---- paths option ----
 237         new Option(true, "-cp", "-classpath", "--class-path") {
 238             void process(JdepsTask task, String opt, String arg) {
 239                 task.options.classpath = arg;
 240             }
 241         },
 242         new Option(true, "--module-path") {
 243             void process(JdepsTask task, String opt, String arg) throws BadArgs {
 244                 task.options.modulePath = arg;
 245             }
 246         },
 247         new Option(true, "--upgrade-module-path") {
 248             void process(JdepsTask task, String opt, String arg) throws BadArgs {
 249                 task.options.upgradeModulePath = arg;
 250             }
 251         },
 252         new Option(true, "--system") {
 253             void process(JdepsTask task, String opt, String arg) throws BadArgs {
 254                 if (arg.equals("none")) {
 255                     task.options.systemModulePath = null;
 256                 } else {
 257                     Path path = Paths.get(arg);
 258                     if (Files.isRegularFile(path.resolve("lib").resolve("modules")))
 259                         task.options.systemModulePath = arg;
 260                     else
 261                         throw new BadArgs("err.invalid.path", arg);
 262                 }
 263             }
 264         },
 265         new Option(true, "--add-modules") {
 266             void process(JdepsTask task, String opt, String arg) throws BadArgs {
 267                 Set<String> mods = Set.of(arg.split(","));
 268                 task.options.addmods.addAll(mods);
 269             }
 270         },
 271         new Option(true, "--multi-release") {
 272             void process(JdepsTask task, String opt, String arg) throws BadArgs {
 273                 if (arg.equalsIgnoreCase("base")) {
 274                     task.options.multiRelease = JarFile.baseVersion();
 275                 } else {
 276                     try {
 277                         int v = Integer.parseInt(arg);
 278                         if (v < 9) {
 279                             throw new BadArgs("err.invalid.arg.for.option", arg);
 280                         }
 281                     } catch (NumberFormatException x) {
 282                         throw new BadArgs("err.invalid.arg.for.option", arg);
 283                     }
 284                     task.options.multiRelease = Runtime.Version.parse(arg);
 285                 }
 286             }
 287         },
 288         new Option(false, "-q", "-quiet") {
 289             void process(JdepsTask task, String opt, String arg) {
 290                 task.options.nowarning = true;
 291             }
 292         },
 293         new Option(false, "-version", "--version") {
 294             void process(JdepsTask task, String opt, String arg) {
 295                 task.options.version = true;
 296             }
 297         },
 298 
 299         // ---- module-specific options ----
 300 
 301         new Option(true, "-m", "--module") {
 302             void process(JdepsTask task, String opt, String arg) throws BadArgs {
 303                 if (!task.options.rootModules.isEmpty()) {
 304                     throw new BadArgs("err.option.already.specified", opt);
 305                 }
 306                 task.options.rootModules.add(arg);
 307                 task.options.addmods.add(arg);
 308             }
 309         },
 310         new Option(true, CommandOption.GENERATE_MODULE_INFO) {
 311             void process(JdepsTask task, String opt, String arg) throws BadArgs {
 312                 if (task.command != null) {
 313                     throw new BadArgs("err.command.set", task.command, opt);
 314                 }
 315                 task.command = task.genModuleInfo(Paths.get(arg), false);
 316             }
 317         },
 318         new Option(true, CommandOption.GENERATE_OPEN_MODULE) {
 319             void process(JdepsTask task, String opt, String arg) throws BadArgs {
 320                 if (task.command != null) {
 321                     throw new BadArgs("err.command.set", task.command, opt);
 322                 }
 323                 task.command = task.genModuleInfo(Paths.get(arg), true);
 324             }
 325         },
 326         new Option(true, CommandOption.CHECK_MODULES) {
 327             void process(JdepsTask task, String opt, String arg) throws BadArgs {
 328                 if (task.command != null) {
 329                     throw new BadArgs("err.command.set", task.command, opt);
 330                 }
 331                 Set<String> mods =  Set.of(arg.split(","));
 332                 task.options.addmods.addAll(mods);
 333                 task.command = task.checkModuleDeps(mods);
 334             }
 335         },
 336         new Option(false, CommandOption.LIST_DEPS) {
 337             void process(JdepsTask task, String opt, String arg) throws BadArgs {
 338                 if (task.command != null) {
 339                     throw new BadArgs("err.command.set", task.command, opt);
 340                 }
 341                 task.command = task.listModuleDeps(CommandOption.LIST_DEPS);
 342             }
 343         },
 344         new Option(false, CommandOption.LIST_REDUCED_DEPS) {
 345             void process(JdepsTask task, String opt, String arg) throws BadArgs {
 346                 if (task.command != null) {
 347                     throw new BadArgs("err.command.set", task.command, opt);
 348                 }
 349                 task.command = task.listModuleDeps(CommandOption.LIST_REDUCED_DEPS);
 350             }
 351         },
 352         new Option(false, CommandOption.PRINT_MODULE_DEPS) {
 353             void process(JdepsTask task, String opt, String arg) throws BadArgs {
 354                 if (task.command != null) {
 355                     throw new BadArgs("err.command.set", task.command, opt);
 356                 }
 357                 task.command = task.listModuleDeps(CommandOption.PRINT_MODULE_DEPS);
 358             }
 359         },
 360 
 361         // ---- Target filtering options ----
 362         new Option(true, "-p", "-package", "--package") {
 363             void process(JdepsTask task, String opt, String arg) {
 364                 task.options.packageNames.add(arg);
 365             }
 366         },
 367         new Option(true, "-e", "-regex", "--regex") {
 368             void process(JdepsTask task, String opt, String arg) {
 369                 task.options.regex = Pattern.compile(arg);
 370             }
 371         },
 372         new Option(true, "--require") {
 373             void process(JdepsTask task, String opt, String arg) {
 374                 task.options.requires.add(arg);
 375                 task.options.addmods.add(arg);
 376             }
 377         },
 378         new Option(true, "-f", "-filter") {
 379             void process(JdepsTask task, String opt, String arg) {
 380                 task.options.filterRegex = Pattern.compile(arg);
 381             }
 382         },
 383         new Option(false, "-filter:package",
 384                           "-filter:archive", "-filter:module",
 385                           "-filter:none") {
 386             void process(JdepsTask task, String opt, String arg) {
 387                 switch (opt) {
 388                     case "-filter:package":
 389                         task.options.filterSamePackage = true;
 390                         task.options.filterSameArchive = false;
 391                         break;
 392                     case "-filter:archive":
 393                     case "-filter:module":
 394                         task.options.filterSameArchive = true;
 395                         task.options.filterSamePackage = false;
 396                         break;
 397                     case "-filter:none":
 398                         task.options.filterSameArchive = false;
 399                         task.options.filterSamePackage = false;
 400                         break;
 401                 }
 402             }
 403         },
 404 
 405         // ---- Source filtering options ----
 406         new Option(true, "-include") {
 407             void process(JdepsTask task, String opt, String arg) throws BadArgs {
 408                 task.options.includePattern = Pattern.compile(arg);
 409             }
 410         },
 411 
 412         new Option(false, "-P", "-profile") {
 413             void process(JdepsTask task, String opt, String arg) throws BadArgs {
 414                 task.options.showProfile = true;
 415             }
 416         },
 417 
 418         new Option(false, "-R", "-recursive") {
 419             void process(JdepsTask task, String opt, String arg) {
 420                 task.options.depth = 0;
 421                 // turn off filtering
 422                 task.options.filterSameArchive = false;
 423                 task.options.filterSamePackage = false;
 424             }
 425         },
 426 
 427         new Option(false, "-I", "--inverse") {
 428             void process(JdepsTask task, String opt, String arg) {
 429                 task.options.inverse = true;
 430                 // equivalent to the inverse of compile-time view analysis
 431                 task.options.compileTimeView = true;
 432                 task.options.filterSamePackage = true;
 433                 task.options.filterSameArchive = true;
 434             }
 435         },
 436 
 437         new Option(false, "--compile-time") {
 438             void process(JdepsTask task, String opt, String arg) {
 439                 task.options.compileTimeView = true;
 440                 task.options.filterSamePackage = true;
 441                 task.options.filterSameArchive = true;
 442                 task.options.depth = 0;
 443             }
 444         },
 445 
 446         new HiddenOption(false, "-fullversion") {
 447             void process(JdepsTask task, String opt, String arg) {
 448                 task.options.fullVersion = true;
 449             }
 450         },
 451         new HiddenOption(false, "-showlabel") {
 452             void process(JdepsTask task, String opt, String arg) {
 453                 task.options.showLabel = true;
 454             }
 455         },
 456         new HiddenOption(false, "--hide-show-module") {
 457             void process(JdepsTask task, String opt, String arg) {
 458                 task.options.showModule = false;
 459             }
 460         },
 461         new HiddenOption(true, "-depth") {
 462             void process(JdepsTask task, String opt, String arg) throws BadArgs {
 463                 try {
 464                     task.options.depth = Integer.parseInt(arg);
 465                 } catch (NumberFormatException e) {
 466                     throw new BadArgs("err.invalid.arg.for.option", opt);
 467                 }
 468             }
 469         },
 470     };
 471 
 472     private static final String PROGNAME = "jdeps";
 473     private final Options options = new Options();
 474     private final List<String> inputArgs = new ArrayList<>();
 475 
 476     private Command command;
 477     private PrintWriter log;
 478     void setLog(PrintWriter out) {
 479         log = out;
 480     }
 481 
 482     /**
 483      * Result codes.
 484      */
 485     static final int EXIT_OK = 0,       // Completed with no errors.
 486                      EXIT_ERROR = 1,    // Completed but reported errors.
 487                      EXIT_CMDERR = 2,   // Bad command-line arguments
 488                      EXIT_SYSERR = 3,   // System error or resource exhaustion.
 489                      EXIT_ABNORMAL = 4; // terminated abnormally
 490 
 491     int run(String... args) {
 492         if (log == null) {
 493             log = new PrintWriter(System.out);
 494         }
 495         try {
 496             handleOptions(args);
 497             if (options.help) {
 498                 showHelp();
 499             }
 500             if (options.version || options.fullVersion) {
 501                 showVersion(options.fullVersion);
 502             }
 503             if (options.help || options.version || options.fullVersion) {
 504                 return EXIT_OK;
 505             }
 506             if (options.numFilters() > 1) {
 507                 reportError("err.invalid.filters");
 508                 return EXIT_CMDERR;
 509             }
 510 
 511             // default command to analyze dependences
 512             if (command == null) {
 513                 command = analyzeDeps();
 514             }
 515             if (!command.checkOptions()) {
 516                 return EXIT_CMDERR;
 517             }
 518 
 519             boolean ok = run();
 520             return ok ? EXIT_OK : EXIT_ERROR;
 521 
 522         } catch (BadArgs|UncheckedBadArgs e) {
 523             reportError(e.getKey(), e.getArgs());
 524             if (e.showUsage()) {
 525                 log.println(getMessage("main.usage.summary", PROGNAME));
 526             }
 527             return EXIT_CMDERR;
 528         } catch (ResolutionException e) {
 529             reportError("err.exception.message", e.getMessage());
 530             return EXIT_CMDERR;
 531         } catch (IOException e) {
 532             e.printStackTrace();
 533             return EXIT_CMDERR;
 534         } catch (MultiReleaseException e) {
 535             reportError(e.getKey(), e.getParams());
 536             return EXIT_CMDERR;  // could be EXIT_ABNORMAL sometimes
 537         } finally {
 538             log.flush();
 539         }
 540     }
 541 
 542     boolean run() throws IOException {
 543         try (JdepsConfiguration config = buildConfig()) {
 544             if (!options.nowarning) {
 545                 // detect split packages
 546                 config.splitPackages().entrySet()
 547                       .stream()
 548                       .sorted(Map.Entry.comparingByKey())
 549                       .forEach(e -> warning("warn.split.package",
 550                                             e.getKey(),
 551                                             e.getValue().stream().collect(joining(" "))));
 552             }
 553 
 554             // check if any module specified in --add-modules, --require, and -m is missing
 555             options.addmods.stream()
 556                 .filter(mn -> !JdepsConfiguration.isToken(mn))
 557                 .forEach(mn -> config.findModule(mn).orElseThrow(() ->
 558                     new UncheckedBadArgs(new BadArgs("err.module.not.found", mn))));
 559 
 560             return command.run(config);
 561         }
 562     }
 563 
 564     private JdepsConfiguration buildConfig() throws IOException {
 565         JdepsConfiguration.Builder builder =
 566             new JdepsConfiguration.Builder(options.systemModulePath);
 567 
 568         builder.upgradeModulePath(options.upgradeModulePath)
 569                .appModulePath(options.modulePath)
 570                .addmods(options.addmods)
 571                .addmods(command.addModules());
 572 
 573         if (options.classpath != null)
 574             builder.addClassPath(options.classpath);
 575 
 576         if (options.multiRelease != null)
 577             builder.multiRelease(options.multiRelease);
 578 
 579         // build the root set of archives to be analyzed
 580         for (String s : inputArgs) {
 581             Path p = Paths.get(s);
 582             if (Files.exists(p)) {
 583                 builder.addRoot(p);
 584             } else {
 585                 warning("warn.invalid.arg", s);
 586             }
 587         }
 588 
 589         return builder.build();
 590     }
 591 
 592     // ---- factory methods to create a Command
 593 
 594     private AnalyzeDeps analyzeDeps() throws BadArgs {
 595         return options.inverse ? new InverseAnalyzeDeps()
 596                                : new AnalyzeDeps();
 597     }
 598 
 599     private GenDotFile genDotFile(Path dir) throws BadArgs {
 600         if (Files.exists(dir) && (!Files.isDirectory(dir) || !Files.isWritable(dir))) {
 601             throw new BadArgs("err.invalid.path", dir.toString());
 602         }
 603         return new GenDotFile(dir);
 604     }
 605 
 606     private GenModuleInfo genModuleInfo(Path dir, boolean openModule) throws BadArgs {
 607         if (Files.exists(dir) && (!Files.isDirectory(dir) || !Files.isWritable(dir))) {
 608             throw new BadArgs("err.invalid.path", dir.toString());
 609         }
 610         return new GenModuleInfo(dir, openModule);
 611     }
 612 
 613     private ListModuleDeps listModuleDeps(CommandOption option) throws BadArgs {
 614         switch (option) {
 615             case LIST_DEPS:
 616                 return new ListModuleDeps(option, true, false);
 617             case LIST_REDUCED_DEPS:
 618                 return new ListModuleDeps(option, true, true);
 619             case PRINT_MODULE_DEPS:
 620                 return new ListModuleDeps(option, false, true, ",");
 621             default:
 622                 throw new IllegalArgumentException(option.toString());
 623         }
 624     }
 625 
 626     private CheckModuleDeps checkModuleDeps(Set<String> mods) throws BadArgs {
 627         return new CheckModuleDeps(mods);
 628     }
 629 
 630     abstract class Command {
 631         final CommandOption option;
 632         protected Command(CommandOption option) {
 633             this.option = option;
 634         }
 635 
 636         /**
 637          * Returns true if the command-line options are all valid;
 638          * otherwise, returns false.
 639          */
 640         abstract boolean checkOptions();
 641 
 642         /**
 643          * Do analysis
 644          */
 645         abstract boolean run(JdepsConfiguration config) throws IOException;
 646 
 647         /**
 648          * Includes all modules on system module path and application module path
 649          *
 650          * When a named module is analyzed, it will analyze the dependences
 651          * only.  The method should be overridden when this command should
 652          * analyze all modules instead.
 653          */
 654         Set<String> addModules() {
 655             return Set.of();
 656         }
 657 
 658         @Override
 659         public String toString() {
 660             return option.toString();
 661         }
 662     }
 663 
 664 
 665     /**
 666      * Analyze dependences
 667      */
 668     class AnalyzeDeps extends Command {
 669         JdepsWriter writer;
 670         AnalyzeDeps() {
 671             this(CommandOption.ANALYZE_DEPS);
 672         }
 673 
 674         AnalyzeDeps(CommandOption option) {
 675             super(option);
 676         }
 677 
 678         @Override
 679         boolean checkOptions() {
 680             if (options.findJDKInternals) {
 681                 // cannot set any filter, -verbose and -summary option
 682                 if (options.showSummary || options.verbose != null) {
 683                     reportError("err.invalid.options", "-summary or -verbose",
 684                                 "-jdkinternals");
 685                     return false;
 686                 }
 687                 if (options.hasFilter()) {
 688                     reportError("err.invalid.options", "--package, --regex, --require",
 689                                 "-jdkinternals");
 690                     return false;
 691                 }
 692             }
 693             if (options.showSummary) {
 694                 // -summary cannot use with -verbose option
 695                 if (options.verbose != null) {
 696                     reportError("err.invalid.options", "-v, -verbose", "-s, -summary");
 697                     return false;
 698                 }
 699             }
 700 
 701             if (!inputArgs.isEmpty() && !options.rootModules.isEmpty()) {
 702                 reportError("err.invalid.arg.for.option", "-m");
 703             }
 704             if (inputArgs.isEmpty() && !options.hasSourcePath()) {
 705                 showHelp();
 706                 return false;
 707             }
 708             return true;
 709         }
 710 
 711         /*
 712          * Default is to show package-level dependencies
 713          */
 714         Type getAnalyzerType() {
 715             if (options.showSummary)
 716                 return Type.SUMMARY;
 717 
 718             if (options.findJDKInternals)
 719                 return Type.CLASS;
 720 
 721             // default to package-level verbose
 722            return options.verbose != null ? options.verbose : PACKAGE;
 723         }
 724 
 725         @Override
 726         boolean run(JdepsConfiguration config) throws IOException {
 727             Type type = getAnalyzerType();
 728             // default to package-level verbose
 729             JdepsWriter writer = new SimpleWriter(log,
 730                                                   type,
 731                                                   options.showProfile,
 732                                                   options.showModule);
 733 
 734             return run(config, writer, type);
 735         }
 736 
 737         boolean run(JdepsConfiguration config, JdepsWriter writer, Type type)
 738             throws IOException
 739         {
 740             // analyze the dependencies
 741             DepsAnalyzer analyzer = new DepsAnalyzer(config,
 742                                                      dependencyFilter(config),
 743                                                      writer,
 744                                                      type,
 745                                                      options.apiOnly);
 746 
 747             boolean ok = analyzer.run(options.compileTimeView, options.depth);
 748 
 749             // print skipped entries, if any
 750             if (!options.nowarning) {
 751                 analyzer.archives()
 752                     .forEach(archive -> archive.reader()
 753                         .skippedEntries().stream()
 754                         .forEach(name -> warning("warn.skipped.entry", name)));
 755             }
 756 
 757             if (options.findJDKInternals && !options.nowarning) {
 758                 Map<String, String> jdkInternals = new TreeMap<>();
 759                 Set<String> deps = analyzer.dependences();
 760                 // find the ones with replacement
 761                 deps.forEach(cn -> replacementFor(cn).ifPresent(
 762                     repl -> jdkInternals.put(cn, repl))
 763                 );
 764 
 765                 if (!deps.isEmpty()) {
 766                     log.println();
 767                     warning("warn.replace.useJDKInternals", getMessage("jdeps.wiki.url"));
 768                 }
 769 
 770                 if (!jdkInternals.isEmpty()) {
 771                     log.println();
 772                     String internalApiTitle = getMessage("internal.api.column.header");
 773                     String replacementApiTitle = getMessage("public.api.replacement.column.header");
 774                     log.format("%-40s %s%n", internalApiTitle, replacementApiTitle);
 775                     log.format("%-40s %s%n",
 776                                internalApiTitle.replaceAll(".", "-"),
 777                                replacementApiTitle.replaceAll(".", "-"));
 778                     jdkInternals.entrySet().stream()
 779                         .forEach(e -> {
 780                             String key = e.getKey();
 781                             String[] lines = e.getValue().split("\\n");
 782                             for (String s : lines) {
 783                                 log.format("%-40s %s%n", key, s);
 784                                 key = "";
 785                             }
 786                         });
 787                 }
 788             }
 789             return ok;
 790         }
 791     }
 792 
 793 
 794     class InverseAnalyzeDeps extends AnalyzeDeps {
 795         InverseAnalyzeDeps() {
 796         }
 797 
 798         @Override
 799         boolean checkOptions() {
 800             if (options.depth != 1) {
 801                 reportError("err.invalid.options", "-R", "--inverse");
 802                 return false;
 803             }
 804 
 805             if (options.numFilters() == 0) {
 806                 reportError("err.filter.not.specified");
 807                 return false;
 808             }
 809 
 810             if (!super.checkOptions()) {
 811                 return false;
 812             }
 813 
 814             return true;
 815         }
 816 
 817         @Override
 818         boolean run(JdepsConfiguration config) throws IOException {
 819             Type type = getAnalyzerType();
 820 
 821             InverseDepsAnalyzer analyzer =
 822                 new InverseDepsAnalyzer(config,
 823                                         dependencyFilter(config),
 824                                         writer,
 825                                         type,
 826                                         options.apiOnly);
 827             boolean ok = analyzer.run();
 828 
 829             log.println();
 830             if (!options.requires.isEmpty())
 831                 log.println(getMessage("inverse.transitive.dependencies.on",
 832                                        options.requires));
 833             else
 834                 log.println(getMessage("inverse.transitive.dependencies.matching",
 835                                        options.regex != null
 836                                            ? options.regex.toString()
 837                                            : "packages " + options.packageNames));
 838 
 839             analyzer.inverseDependences()
 840                     .stream()
 841                     .sorted(comparator())
 842                     .map(this::toInversePath)
 843                     .forEach(log::println);
 844             return ok;
 845         }
 846 
 847         private String toInversePath(Deque<Archive> path) {
 848             return path.stream()
 849                        .map(Archive::getName)
 850                        .collect(joining(" <- "));
 851         }
 852 
 853         /*
 854          * Returns a comparator for sorting the inversed path, grouped by
 855          * the first module name, then the shortest path and then sort by
 856          * the module names of each path
 857          */
 858         private Comparator<Deque<Archive>> comparator() {
 859             return Comparator.<Deque<Archive>, String>
 860                 comparing(deque -> deque.peekFirst().getName())
 861                     .thenComparingInt(Deque::size)
 862                     .thenComparing(this::toInversePath);
 863         }
 864 
 865         /*
 866          * Returns true if --require is specified so that all modules are
 867          * analyzed to find all modules that depend on the modules specified in the
 868          * --require option directly and indirectly
 869          */
 870         Set<String> addModules() {
 871             return options.requires.size() > 0 ? Set.of("ALL-SYSTEM") : Set.of();
 872         }
 873     }
 874 
 875 
 876     class GenModuleInfo extends Command {
 877         final Path dir;
 878         final boolean openModule;
 879         GenModuleInfo(Path dir, boolean openModule) {
 880             super(CommandOption.GENERATE_MODULE_INFO);
 881             this.dir = dir;
 882             this.openModule = openModule;
 883         }
 884 
 885         @Override
 886         boolean checkOptions() {
 887             if (options.classpath != null) {
 888                 reportError("err.invalid.options", "-classpath",
 889                             option);
 890                 return false;
 891             }
 892             if (options.hasFilter()) {
 893                 reportError("err.invalid.options", "--package, --regex, --require",
 894                             option);
 895                 return false;
 896             }
 897             return true;
 898         }
 899 
 900         @Override
 901         boolean run(JdepsConfiguration config) throws IOException {
 902             // check if any JAR file contains unnamed package
 903             for (String arg : inputArgs) {
 904                 try (ClassFileReader reader = ClassFileReader.newInstance(Paths.get(arg))) {
 905                     Optional<String> classInUnnamedPackage =
 906                         reader.entries().stream()
 907                              .filter(n -> n.endsWith(".class"))
 908                              .filter(cn -> toPackageName(cn).isEmpty())
 909                              .findFirst();
 910 
 911                     if (classInUnnamedPackage.isPresent()) {
 912                         if (classInUnnamedPackage.get().equals("module-info.class")) {
 913                             reportError("err.genmoduleinfo.not.jarfile", arg);
 914                         } else {
 915                             reportError("err.genmoduleinfo.unnamed.package", arg);
 916                         }
 917                         return false;
 918                     }
 919                 }
 920             }
 921 
 922             ModuleInfoBuilder builder
 923                  = new ModuleInfoBuilder(config, inputArgs, dir, openModule);
 924             boolean ok = builder.run();
 925 
 926             if (!ok && !options.nowarning) {
 927                 reportError("err.missing.dependences");
 928                 builder.visitMissingDeps(
 929                         (origin, originArchive, target, targetArchive) -> {
 930                             if (builder.notFound(targetArchive))
 931                                 log.format("   %-50s -> %-50s %s%n",
 932                                     origin, target, targetArchive.getName());
 933                         });
 934             }
 935             return ok;
 936         }
 937 
 938         private String toPackageName(String name) {
 939             int i = name.lastIndexOf('/');
 940             return i > 0 ? name.replace('/', '.').substring(0, i) : "";
 941         }
 942     }
 943 
 944     class CheckModuleDeps extends Command {
 945         final Set<String> modules;
 946         CheckModuleDeps(Set<String> mods) {
 947             super(CommandOption.CHECK_MODULES);
 948             this.modules = mods;
 949         }
 950 
 951         @Override
 952         boolean checkOptions() {
 953             if (!inputArgs.isEmpty()) {
 954                 reportError("err.invalid.options", inputArgs, "--check");
 955                 return false;
 956             }
 957             return true;
 958         }
 959 
 960         @Override
 961         boolean run(JdepsConfiguration config) throws IOException {
 962             if (!config.initialArchives().isEmpty()) {
 963                 String list = config.initialArchives().stream()
 964                                     .map(Archive::getPathName).collect(joining(" "));
 965                 throw new UncheckedBadArgs(new BadArgs("err.invalid.options",
 966                                                        list, "--check"));
 967             }
 968             return new ModuleAnalyzer(config, log, modules).run();
 969         }
 970 
 971         /*
 972          * Returns true to analyze all modules
 973          */
 974         Set<String> addModules() {
 975             return Set.of("ALL-SYSTEM", "ALL-MODULE-PATH");
 976         }
 977     }
 978 
 979     class ListModuleDeps extends Command {
 980         final boolean jdkinternals;
 981         final boolean reduced;
 982         final String separator;
 983         ListModuleDeps(CommandOption option, boolean jdkinternals, boolean reduced) {
 984             this(option, jdkinternals, reduced, System.getProperty("line.separator"));
 985         }
 986         ListModuleDeps(CommandOption option, boolean jdkinternals, boolean reduced, String sep) {
 987             super(option);
 988             this.jdkinternals = jdkinternals;
 989             this.reduced = reduced;
 990             this.separator = sep;
 991         }
 992 
 993         @Override
 994         boolean checkOptions() {
 995             if (options.showSummary || options.verbose != null) {
 996                 reportError("err.invalid.options", "-summary or -verbose",
 997                             option);
 998                 return false;
 999             }
1000             if (options.findJDKInternals) {
1001                 reportError("err.invalid.options", "-jdkinternals",
1002                             option);
1003                 return false;
1004             }
1005 
1006             if (!inputArgs.isEmpty() && !options.rootModules.isEmpty()) {
1007                 reportError("err.invalid.arg.for.option", "-m");
1008             }
1009             if (inputArgs.isEmpty() && !options.hasSourcePath()) {
1010                 showHelp();
1011                 return false;
1012             }
1013             return true;
1014         }
1015 
1016         @Override
1017         boolean run(JdepsConfiguration config) throws IOException {
1018             return new ModuleExportsAnalyzer(config,
1019                                              dependencyFilter(config),
1020                                              jdkinternals,
1021                                              reduced,
1022                                              log,
1023                                              separator).run();
1024         }
1025     }
1026 
1027 
1028     class GenDotFile extends AnalyzeDeps {
1029         final Path dotOutputDir;
1030         GenDotFile(Path dotOutputDir) {
1031             super(CommandOption.GENERATE_DOT_FILE);
1032 
1033             this.dotOutputDir = dotOutputDir;
1034         }
1035 
1036         @Override
1037         boolean run(JdepsConfiguration config) throws IOException {
1038             if ((options.showSummary || options.verbose == MODULE) &&
1039                 !options.addmods.isEmpty() && inputArgs.isEmpty()) {
1040                 // generate dot graph from the resolved graph from module
1041                 // resolution.  No class dependency analysis is performed.
1042                 return new ModuleDotGraph(config, options.apiOnly)
1043                         .genDotFiles(dotOutputDir);
1044             }
1045 
1046             Type type = getAnalyzerType();
1047             JdepsWriter writer = new DotFileWriter(dotOutputDir,
1048                                                    type,
1049                                                    options.showProfile,
1050                                                    options.showModule,
1051                                                    options.showLabel);
1052             return run(config, writer, type);
1053         }
1054     }
1055 
1056     /**
1057      * Returns a filter used during dependency analysis
1058      */
1059     private JdepsFilter dependencyFilter(JdepsConfiguration config) {
1060         // Filter specified by -filter, -package, -regex, and --require options
1061         JdepsFilter.Builder builder = new JdepsFilter.Builder();
1062 
1063         // source filters
1064         builder.includePattern(options.includePattern);
1065 
1066         // target filters
1067         builder.filter(options.filterSamePackage, options.filterSameArchive);
1068         builder.findJDKInternals(options.findJDKInternals);
1069 
1070         // --require
1071         if (!options.requires.isEmpty()) {
1072             options.requires.stream()
1073                 .forEach(mn -> {
1074                     Module m = config.findModule(mn).get();
1075                     builder.requires(mn, m.packages());
1076                 });
1077         }
1078         // -regex
1079         if (options.regex != null)
1080             builder.regex(options.regex);
1081         // -package
1082         if (!options.packageNames.isEmpty())
1083             builder.packages(options.packageNames);
1084         // -filter
1085         if (options.filterRegex != null)
1086             builder.filter(options.filterRegex);
1087 
1088         return builder.build();
1089     }
1090 
1091     public void handleOptions(String[] args) throws BadArgs {
1092         // process options
1093         for (int i=0; i < args.length; i++) {
1094             if (args[i].charAt(0) == '-') {
1095                 String name = args[i];
1096                 Option option = getOption(name);
1097                 String param = null;
1098                 if (option.hasArg) {
1099                     if (name.startsWith("-") && name.indexOf('=') > 0) {
1100                         param = name.substring(name.indexOf('=') + 1, name.length());
1101                     } else if (i + 1 < args.length) {
1102                         param = args[++i];
1103                     }
1104                     if (param == null || param.isEmpty() || param.charAt(0) == '-') {
1105                         throw new BadArgs("err.missing.arg", name).showUsage(true);
1106                     }
1107                 }
1108                 option.process(this, name, param);
1109                 if (option.ignoreRest()) {
1110                     i = args.length;
1111                 }
1112             } else {
1113                 // process rest of the input arguments
1114                 for (; i < args.length; i++) {
1115                     String name = args[i];
1116                     if (name.charAt(0) == '-') {
1117                         throw new BadArgs("err.option.after.class", name).showUsage(true);
1118                     }
1119                     inputArgs.add(name);
1120                 }
1121             }
1122         }
1123     }
1124 
1125     private Option getOption(String name) throws BadArgs {
1126         for (Option o : recognizedOptions) {
1127             if (o.matches(name)) {
1128                 return o;
1129             }
1130         }
1131         throw new BadArgs("err.unknown.option", name).showUsage(true);
1132     }
1133 
1134     private void reportError(String key, Object... args) {
1135         log.println(getMessage("error.prefix") + " " + getMessage(key, args));
1136     }
1137 
1138     void warning(String key, Object... args) {
1139         log.println(getMessage("warn.prefix") + " " + getMessage(key, args));
1140     }
1141 
1142     private void showHelp() {
1143         log.println(getMessage("main.usage", PROGNAME));
1144         for (Option o : recognizedOptions) {
1145             String name = o.aliases[0].substring(1); // there must always be at least one name
1146             name = name.charAt(0) == '-' ? name.substring(1) : name;
1147             if (o.isHidden() || name.startsWith("filter:")) {
1148                 continue;
1149             }
1150             log.println(getMessage("main.opt." + name));
1151         }
1152     }
1153 
1154     private void showVersion(boolean full) {
1155         log.println(version(full ? "full" : "release"));
1156     }
1157 
1158     private String version(String key) {
1159         // key=version:  mm.nn.oo[-milestone]
1160         // key=full:     mm.mm.oo[-milestone]-build
1161         if (ResourceBundleHelper.versionRB == null) {
1162             return System.getProperty("java.version");
1163         }
1164         try {
1165             return ResourceBundleHelper.versionRB.getString(key);
1166         } catch (MissingResourceException e) {
1167             return getMessage("version.unknown", System.getProperty("java.version"));
1168         }
1169     }
1170 
1171     static String getMessage(String key, Object... args) {
1172         try {
1173             return MessageFormat.format(ResourceBundleHelper.bundle.getString(key), args);
1174         } catch (MissingResourceException e) {
1175             throw new InternalError("Missing message: " + key);
1176         }
1177     }
1178 
1179     private static class Options {
1180         boolean help;
1181         boolean version;
1182         boolean fullVersion;
1183         boolean showProfile;
1184         boolean showModule = true;
1185         boolean showSummary;
1186         boolean apiOnly;
1187         boolean showLabel;
1188         boolean findJDKInternals;
1189         boolean nowarning = false;
1190         Analyzer.Type verbose;
1191         // default filter references from same package
1192         boolean filterSamePackage = true;
1193         boolean filterSameArchive = false;
1194         Pattern filterRegex;
1195         String classpath;
1196         int depth = 1;
1197         Set<String> requires = new HashSet<>();
1198         Set<String> packageNames = new HashSet<>();
1199         Pattern regex;             // apply to the dependences
1200         Pattern includePattern;
1201         boolean inverse = false;
1202         boolean compileTimeView = false;
1203         String systemModulePath = System.getProperty("java.home");
1204         String upgradeModulePath;
1205         String modulePath;
1206         Set<String> rootModules = new HashSet<>();
1207         Set<String> addmods = new HashSet<>();
1208         Runtime.Version multiRelease;
1209 
1210         boolean hasSourcePath() {
1211             return !addmods.isEmpty() || includePattern != null;
1212         }
1213 
1214         boolean hasFilter() {
1215             return numFilters() > 0;
1216         }
1217 
1218         int numFilters() {
1219             int count = 0;
1220             if (requires.size() > 0) count++;
1221             if (regex != null) count++;
1222             if (packageNames.size() > 0) count++;
1223             return count;
1224         }
1225     }
1226 
1227     private static class ResourceBundleHelper {
1228         static final ResourceBundle versionRB;
1229         static final ResourceBundle bundle;
1230         static final ResourceBundle jdkinternals;
1231 
1232         static {
1233             Locale locale = Locale.getDefault();
1234             try {
1235                 bundle = ResourceBundle.getBundle("com.sun.tools.jdeps.resources.jdeps", locale);
1236             } catch (MissingResourceException e) {
1237                 throw new InternalError("Cannot find jdeps resource bundle for locale " + locale);
1238             }
1239             try {
1240                 versionRB = ResourceBundle.getBundle("com.sun.tools.jdeps.resources.version");
1241             } catch (MissingResourceException e) {
1242                 throw new InternalError("version.resource.missing");
1243             }
1244             try {
1245                 jdkinternals = ResourceBundle.getBundle("com.sun.tools.jdeps.resources.jdkinternals");
1246             } catch (MissingResourceException e) {
1247                 throw new InternalError("Cannot find jdkinternals resource bundle");
1248             }
1249         }
1250     }
1251 
1252     /**
1253      * Returns the recommended replacement API for the given classname;
1254      * or return null if replacement API is not known.
1255      */
1256     private Optional<String> replacementFor(String cn) {
1257         String name = cn;
1258         String value = null;
1259         while (value == null && name != null) {
1260             try {
1261                 value = ResourceBundleHelper.jdkinternals.getString(name);
1262             } catch (MissingResourceException e) {
1263                 // go up one subpackage level
1264                 int i = name.lastIndexOf('.');
1265                 name = i > 0 ? name.substring(0, i) : null;
1266             }
1267         }
1268         return Optional.ofNullable(value);
1269     }
1270 }