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