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         new Option(false, "--ignore-missing-deps") {
 361             void process(JdepsTask task, String opt, String arg) {
 362                 task.options.ignoreMissingDeps = true;
 363             }
 364         },
 365 
 366         // ---- Target filtering options ----
 367         new Option(true, "-p", "-package", "--package") {
 368             void process(JdepsTask task, String opt, String arg) {
 369                 task.options.packageNames.add(arg);
 370             }
 371         },
 372         new Option(true, "-e", "-regex", "--regex") {
 373             void process(JdepsTask task, String opt, String arg) {
 374                 task.options.regex = Pattern.compile(arg);
 375             }
 376         },
 377         new Option(true, "--require") {
 378             void process(JdepsTask task, String opt, String arg) {
 379                 task.options.requires.add(arg);
 380                 task.options.addmods.add(arg);
 381             }
 382         },
 383         new Option(true, "-f", "-filter") {
 384             void process(JdepsTask task, String opt, String arg) {
 385                 task.options.filterRegex = Pattern.compile(arg);
 386             }
 387         },
 388         new Option(false, "-filter:package",
 389                           "-filter:archive", "-filter:module",
 390                           "-filter:none") {
 391             void process(JdepsTask task, String opt, String arg) {
 392                 switch (opt) {
 393                     case "-filter:package":
 394                         task.options.filterSamePackage = true;
 395                         task.options.filterSameArchive = false;
 396                         break;
 397                     case "-filter:archive":
 398                     case "-filter:module":
 399                         task.options.filterSameArchive = true;
 400                         task.options.filterSamePackage = false;
 401                         break;
 402                     case "-filter:none":
 403                         task.options.filterSameArchive = false;
 404                         task.options.filterSamePackage = false;
 405                         break;
 406                 }
 407             }
 408         },
 409         new Option(false, "--missing-deps") {
 410             void process(JdepsTask task, String opt, String arg) {
 411                 task.options.findMissingDeps = true;
 412             }
 413         },
 414 
 415         // ---- Source filtering options ----
 416         new Option(true, "-include") {
 417             void process(JdepsTask task, String opt, String arg) throws BadArgs {
 418                 task.options.includePattern = Pattern.compile(arg);
 419             }
 420         },
 421 
 422         new Option(false, "-P", "-profile") {
 423             void process(JdepsTask task, String opt, String arg) throws BadArgs {
 424                 task.options.showProfile = true;
 425             }
 426         },
 427 
 428         new Option(false, "-R", "-recursive") {
 429             void process(JdepsTask task, String opt, String arg) {
 430                 task.options.depth = 0;
 431                 // turn off filtering
 432                 task.options.filterSameArchive = false;
 433                 task.options.filterSamePackage = false;
 434             }
 435         },
 436 
 437         new Option(false, "-I", "--inverse") {
 438             void process(JdepsTask task, String opt, String arg) {
 439                 task.options.inverse = true;
 440                 // equivalent to the inverse of compile-time view analysis
 441                 task.options.compileTimeView = true;
 442                 task.options.filterSamePackage = true;
 443                 task.options.filterSameArchive = true;
 444             }
 445         },
 446 
 447         new Option(false, "--compile-time") {
 448             void process(JdepsTask task, String opt, String arg) {
 449                 task.options.compileTimeView = true;
 450                 task.options.filterSamePackage = true;
 451                 task.options.filterSameArchive = true;
 452                 task.options.depth = 0;
 453             }
 454         },
 455 
 456         new HiddenOption(false, "-fullversion") {
 457             void process(JdepsTask task, String opt, String arg) {
 458                 task.options.fullVersion = true;
 459             }
 460         },
 461         new HiddenOption(false, "-showlabel") {
 462             void process(JdepsTask task, String opt, String arg) {
 463                 task.options.showLabel = true;
 464             }
 465         },
 466         new HiddenOption(false, "--hide-show-module") {
 467             void process(JdepsTask task, String opt, String arg) {
 468                 task.options.showModule = false;
 469             }
 470         },
 471         new HiddenOption(true, "-depth") {
 472             void process(JdepsTask task, String opt, String arg) throws BadArgs {
 473                 try {
 474                     task.options.depth = Integer.parseInt(arg);
 475                 } catch (NumberFormatException e) {
 476                     throw new BadArgs("err.invalid.arg.for.option", opt);
 477                 }
 478             }
 479         },
 480     };
 481 
 482     private static final String PROGNAME = "jdeps";
 483     private final Options options = new Options();
 484     private final List<String> inputArgs = new ArrayList<>();
 485 
 486     private Command command;
 487     private PrintWriter log;
 488     void setLog(PrintWriter out) {
 489         log = out;
 490     }
 491 
 492     /**
 493      * Result codes.
 494      */
 495     static final int EXIT_OK = 0,       // Completed with no errors.
 496                      EXIT_ERROR = 1,    // Completed but reported errors.
 497                      EXIT_CMDERR = 2,   // Bad command-line arguments
 498                      EXIT_SYSERR = 3,   // System error or resource exhaustion.
 499                      EXIT_ABNORMAL = 4; // terminated abnormally
 500 
 501     int run(String... args) {
 502         if (log == null) {
 503             log = new PrintWriter(System.out);
 504         }
 505         try {
 506             handleOptions(args);
 507             if (options.help) {
 508                 showHelp();
 509             }
 510             if (options.version || options.fullVersion) {
 511                 showVersion(options.fullVersion);
 512             }
 513             if (options.help || options.version || options.fullVersion) {
 514                 return EXIT_OK;
 515             }
 516             if (options.numFilters() > 1) {
 517                 reportError("err.invalid.filters");
 518                 return EXIT_CMDERR;
 519             }
 520 
 521             // default command to analyze dependences
 522             if (command == null) {
 523                 command = analyzeDeps();
 524             }
 525             if (!command.checkOptions()) {
 526                 return EXIT_CMDERR;
 527             }
 528 
 529             boolean ok = run();
 530             return ok ? EXIT_OK : EXIT_ERROR;
 531 
 532         } catch (BadArgs|UncheckedBadArgs e) {
 533             reportError(e.getKey(), e.getArgs());
 534             if (e.showUsage()) {
 535                 log.println(getMessage("main.usage.summary", PROGNAME));
 536             }
 537             return EXIT_CMDERR;
 538         } catch (ResolutionException e) {
 539             reportError("err.exception.message", e.getMessage());
 540             return EXIT_CMDERR;
 541         } catch (IOException e) {
 542             e.printStackTrace();
 543             return EXIT_CMDERR;
 544         } catch (MultiReleaseException e) {
 545             reportError(e.getKey(), e.getParams());
 546             return EXIT_CMDERR;  // could be EXIT_ABNORMAL sometimes
 547         } finally {
 548             log.flush();
 549         }
 550     }
 551 
 552     boolean run() throws IOException {
 553         try (JdepsConfiguration config = buildConfig()) {
 554             if (!options.nowarning) {
 555                 // detect split packages
 556                 config.splitPackages().entrySet()
 557                       .stream()
 558                       .sorted(Map.Entry.comparingByKey())
 559                       .forEach(e -> warning("warn.split.package",
 560                                             e.getKey(),
 561                                             e.getValue().stream().collect(joining(" "))));
 562             }
 563 
 564             // check if any module specified in --add-modules, --require, and -m is missing
 565             options.addmods.stream()
 566                 .filter(mn -> !JdepsConfiguration.isToken(mn))
 567                 .forEach(mn -> config.findModule(mn).orElseThrow(() ->
 568                     new UncheckedBadArgs(new BadArgs("err.module.not.found", mn))));
 569 
 570             return command.run(config);
 571         }
 572     }
 573 
 574     private JdepsConfiguration buildConfig() throws IOException {
 575         JdepsConfiguration.Builder builder =
 576             new JdepsConfiguration.Builder(options.systemModulePath);
 577 
 578         builder.upgradeModulePath(options.upgradeModulePath)
 579                .appModulePath(options.modulePath)
 580                .addmods(options.addmods)
 581                .addmods(command.addModules());
 582 
 583         if (options.classpath != null)
 584             builder.addClassPath(options.classpath);
 585 
 586         if (options.multiRelease != null)
 587             builder.multiRelease(options.multiRelease);
 588 
 589         // build the root set of archives to be analyzed
 590         for (String s : inputArgs) {
 591             Path p = Paths.get(s);
 592             if (Files.exists(p)) {
 593                 builder.addRoot(p);
 594             } else {
 595                 warning("warn.invalid.arg", s);
 596             }
 597         }
 598 
 599         return builder.build();
 600     }
 601 
 602     // ---- factory methods to create a Command
 603 
 604     private AnalyzeDeps analyzeDeps() throws BadArgs {
 605         return options.inverse ? new InverseAnalyzeDeps()
 606                                : new AnalyzeDeps();
 607     }
 608 
 609     private GenDotFile genDotFile(Path dir) throws BadArgs {
 610         if (Files.exists(dir) && (!Files.isDirectory(dir) || !Files.isWritable(dir))) {
 611             throw new BadArgs("err.invalid.path", dir.toString());
 612         }
 613         return new GenDotFile(dir);
 614     }
 615 
 616     private GenModuleInfo genModuleInfo(Path dir, boolean openModule) throws BadArgs {
 617         if (Files.exists(dir) && (!Files.isDirectory(dir) || !Files.isWritable(dir))) {
 618             throw new BadArgs("err.invalid.path", dir.toString());
 619         }
 620         return new GenModuleInfo(dir, openModule);
 621     }
 622 
 623     private ListModuleDeps listModuleDeps(CommandOption option) throws BadArgs {
 624         // no need to record the dependences on the same archive or same package
 625         options.filterSameArchive = true;
 626         options.filterSamePackage = true;
 627         // do transitive dependence analysis
 628         options.depth = 0;
 629         switch (option) {
 630             case LIST_DEPS:
 631                 return new ListModuleDeps(option, true, false);
 632             case LIST_REDUCED_DEPS:
 633                 return new ListModuleDeps(option, true, true);
 634             case PRINT_MODULE_DEPS:
 635                 return new ListModuleDeps(option, false, true, ",");
 636             default:
 637                 throw new IllegalArgumentException(option.toString());
 638         }
 639     }
 640 
 641     private CheckModuleDeps checkModuleDeps(Set<String> mods) throws BadArgs {
 642         return new CheckModuleDeps(mods);
 643     }
 644 
 645     abstract class Command {
 646         final CommandOption option;
 647         protected Command(CommandOption option) {
 648             this.option = option;
 649         }
 650 
 651         /**
 652          * Returns true if the command-line options are all valid;
 653          * otherwise, returns false.
 654          */
 655         abstract boolean checkOptions();
 656 
 657         /**
 658          * Do analysis
 659          */
 660         abstract boolean run(JdepsConfiguration config) throws IOException;
 661 
 662         /**
 663          * Includes all modules on system module path and application module path
 664          *
 665          * When a named module is analyzed, it will analyze the dependences
 666          * only.  The method should be overridden when this command should
 667          * analyze all modules instead.
 668          */
 669         Set<String> addModules() {
 670             return Set.of();
 671         }
 672 
 673         @Override
 674         public String toString() {
 675             return option.toString();
 676         }
 677     }
 678 
 679 
 680     /**
 681      * Analyze dependences
 682      */
 683     class AnalyzeDeps extends Command {
 684         JdepsWriter writer;
 685         AnalyzeDeps() {
 686             this(CommandOption.ANALYZE_DEPS);
 687         }
 688 
 689         AnalyzeDeps(CommandOption option) {
 690             super(option);
 691         }
 692 
 693         @Override
 694         boolean checkOptions() {
 695             if (options.findJDKInternals || options.findMissingDeps) {
 696                 // cannot set any filter, -verbose and -summary option
 697                 if (options.showSummary || options.verbose != null) {
 698                     reportError("err.invalid.options", "-summary or -verbose",
 699                         options.findJDKInternals ? "-jdkinternals" : "--missing-deps");
 700                     return false;
 701                 }
 702                 if (options.hasFilter()) {
 703                     reportError("err.invalid.options", "--package, --regex, --require",
 704                         options.findJDKInternals ? "-jdkinternals" : "--missing-deps");
 705                     return false;
 706                 }
 707             }
 708             if (options.showSummary) {
 709                 // -summary cannot use with -verbose option
 710                 if (options.verbose != null) {
 711                     reportError("err.invalid.options", "-v, -verbose", "-s, -summary");
 712                     return false;
 713                 }
 714             }
 715 
 716             if (!inputArgs.isEmpty() && !options.rootModules.isEmpty()) {
 717                 reportError("err.invalid.arg.for.option", "-m");
 718             }
 719             if (inputArgs.isEmpty() && !options.hasSourcePath()) {
 720                 showHelp();
 721                 return false;
 722             }
 723             return true;
 724         }
 725 
 726         /*
 727          * Default is to show package-level dependencies
 728          */
 729         Type getAnalyzerType() {
 730             if (options.showSummary)
 731                 return Type.SUMMARY;
 732 
 733             if (options.findJDKInternals || options.findMissingDeps)
 734                 return Type.CLASS;
 735 
 736             // default to package-level verbose
 737            return options.verbose != null ? options.verbose : PACKAGE;
 738         }
 739 
 740         @Override
 741         boolean run(JdepsConfiguration config) throws IOException {
 742             Type type = getAnalyzerType();
 743             // default to package-level verbose
 744             JdepsWriter writer = new SimpleWriter(log,
 745                                                   type,
 746                                                   options.showProfile,
 747                                                   options.showModule);
 748 
 749             return run(config, writer, type);
 750         }
 751 
 752         boolean run(JdepsConfiguration config, JdepsWriter writer, Type type)
 753             throws IOException
 754         {
 755             // analyze the dependencies
 756             DepsAnalyzer analyzer = new DepsAnalyzer(config,
 757                                                      dependencyFilter(config),
 758                                                      writer,
 759                                                      type,
 760                                                      options.apiOnly);
 761 
 762             boolean ok = analyzer.run(options.compileTimeView, options.depth);
 763 
 764             // print skipped entries, if any
 765             if (!options.nowarning) {
 766                 analyzer.archives()
 767                     .forEach(archive -> archive.reader()
 768                         .skippedEntries().stream()
 769                         .forEach(name -> warning("warn.skipped.entry", name)));
 770             }
 771 
 772             if (options.findJDKInternals && !options.nowarning) {
 773                 Map<String, String> jdkInternals = new TreeMap<>();
 774                 Set<String> deps = analyzer.dependences();
 775                 // find the ones with replacement
 776                 deps.forEach(cn -> replacementFor(cn).ifPresent(
 777                     repl -> jdkInternals.put(cn, repl))
 778                 );
 779 
 780                 if (!deps.isEmpty()) {
 781                     log.println();
 782                     warning("warn.replace.useJDKInternals", getMessage("jdeps.wiki.url"));
 783                 }
 784 
 785                 if (!jdkInternals.isEmpty()) {
 786                     log.println();
 787                     String internalApiTitle = getMessage("internal.api.column.header");
 788                     String replacementApiTitle = getMessage("public.api.replacement.column.header");
 789                     log.format("%-40s %s%n", internalApiTitle, replacementApiTitle);
 790                     log.format("%-40s %s%n",
 791                                internalApiTitle.replaceAll(".", "-"),
 792                                replacementApiTitle.replaceAll(".", "-"));
 793                     jdkInternals.entrySet().stream()
 794                         .forEach(e -> {
 795                             String key = e.getKey();
 796                             String[] lines = e.getValue().split("\\n");
 797                             for (String s : lines) {
 798                                 log.format("%-40s %s%n", key, s);
 799                                 key = "";
 800                             }
 801                         });
 802                 }
 803             }
 804             return ok;
 805         }
 806     }
 807 
 808 
 809     class InverseAnalyzeDeps extends AnalyzeDeps {
 810         InverseAnalyzeDeps() {
 811         }
 812 
 813         @Override
 814         boolean checkOptions() {
 815             if (options.depth != 1) {
 816                 reportError("err.invalid.options", "-R", "--inverse");
 817                 return false;
 818             }
 819 
 820             if (options.numFilters() == 0) {
 821                 reportError("err.filter.not.specified");
 822                 return false;
 823             }
 824 
 825             if (!super.checkOptions()) {
 826                 return false;
 827             }
 828 
 829             return true;
 830         }
 831 
 832         @Override
 833         boolean run(JdepsConfiguration config) throws IOException {
 834             Type type = getAnalyzerType();
 835 
 836             InverseDepsAnalyzer analyzer =
 837                 new InverseDepsAnalyzer(config,
 838                                         dependencyFilter(config),
 839                                         writer,
 840                                         type,
 841                                         options.apiOnly);
 842             boolean ok = analyzer.run();
 843 
 844             log.println();
 845             if (!options.requires.isEmpty())
 846                 log.println(getMessage("inverse.transitive.dependencies.on",
 847                                        options.requires));
 848             else
 849                 log.println(getMessage("inverse.transitive.dependencies.matching",
 850                                        options.regex != null
 851                                            ? options.regex.toString()
 852                                            : "packages " + options.packageNames));
 853 
 854             analyzer.inverseDependences()
 855                     .stream()
 856                     .sorted(comparator())
 857                     .map(this::toInversePath)
 858                     .forEach(log::println);
 859             return ok;
 860         }
 861 
 862         private String toInversePath(Deque<Archive> path) {
 863             return path.stream()
 864                        .map(Archive::getName)
 865                        .collect(joining(" <- "));
 866         }
 867 
 868         /*
 869          * Returns a comparator for sorting the inversed path, grouped by
 870          * the first module name, then the shortest path and then sort by
 871          * the module names of each path
 872          */
 873         private Comparator<Deque<Archive>> comparator() {
 874             return Comparator.<Deque<Archive>, String>
 875                 comparing(deque -> deque.peekFirst().getName())
 876                     .thenComparingInt(Deque::size)
 877                     .thenComparing(this::toInversePath);
 878         }
 879 
 880         /*
 881          * Returns true if --require is specified so that all modules are
 882          * analyzed to find all modules that depend on the modules specified in the
 883          * --require option directly and indirectly
 884          */
 885         Set<String> addModules() {
 886             return options.requires.size() > 0 ? Set.of("ALL-SYSTEM") : Set.of();
 887         }
 888     }
 889 
 890 
 891     class GenModuleInfo extends Command {
 892         final Path dir;
 893         final boolean openModule;
 894         GenModuleInfo(Path dir, boolean openModule) {
 895             super(CommandOption.GENERATE_MODULE_INFO);
 896             this.dir = dir;
 897             this.openModule = openModule;
 898         }
 899 
 900         @Override
 901         boolean checkOptions() {
 902             if (options.classpath != null) {
 903                 reportError("err.invalid.options", "-classpath",
 904                             option);
 905                 return false;
 906             }
 907             if (options.hasFilter()) {
 908                 reportError("err.invalid.options", "--package, --regex, --require",
 909                             option);
 910                 return false;
 911             }
 912             return true;
 913         }
 914 
 915         @Override
 916         boolean run(JdepsConfiguration config) throws IOException {
 917             // check if any JAR file contains unnamed package
 918             for (String arg : inputArgs) {
 919                 try (ClassFileReader reader = ClassFileReader.newInstance(Paths.get(arg))) {
 920                     Optional<String> classInUnnamedPackage =
 921                         reader.entries().stream()
 922                              .filter(n -> n.endsWith(".class"))
 923                              .filter(cn -> toPackageName(cn).isEmpty())
 924                              .findFirst();
 925 
 926                     if (classInUnnamedPackage.isPresent()) {
 927                         if (classInUnnamedPackage.get().equals("module-info.class")) {
 928                             reportError("err.genmoduleinfo.not.jarfile", arg);
 929                         } else {
 930                             reportError("err.genmoduleinfo.unnamed.package", arg);
 931                         }
 932                         return false;
 933                     }
 934                 }
 935             }
 936 
 937             ModuleInfoBuilder builder
 938                  = new ModuleInfoBuilder(config, inputArgs, dir, openModule);
 939             boolean ok = builder.run();
 940 
 941             if (!ok && !options.nowarning) {
 942                 reportError("err.missing.dependences");
 943                 builder.visitMissingDeps(new SimpleDepVisitor());
 944             }
 945             return ok;
 946         }
 947 
 948         private String toPackageName(String name) {
 949             int i = name.lastIndexOf('/');
 950             return i > 0 ? name.replace('/', '.').substring(0, i) : "";
 951         }
 952     }
 953 
 954     class CheckModuleDeps extends Command {
 955         final Set<String> modules;
 956         CheckModuleDeps(Set<String> mods) {
 957             super(CommandOption.CHECK_MODULES);
 958             this.modules = mods;
 959         }
 960 
 961         @Override
 962         boolean checkOptions() {
 963             if (!inputArgs.isEmpty()) {
 964                 reportError("err.invalid.options", inputArgs, "--check");
 965                 return false;
 966             }
 967             return true;
 968         }
 969 
 970         @Override
 971         boolean run(JdepsConfiguration config) throws IOException {
 972             if (!config.initialArchives().isEmpty()) {
 973                 String list = config.initialArchives().stream()
 974                                     .map(Archive::getPathName).collect(joining(" "));
 975                 throw new UncheckedBadArgs(new BadArgs("err.invalid.options",
 976                                                        list, "--check"));
 977             }
 978             return new ModuleAnalyzer(config, log, modules).run();
 979         }
 980 
 981         /*
 982          * Returns true to analyze all modules
 983          */
 984         Set<String> addModules() {
 985             return Set.of("ALL-SYSTEM", "ALL-MODULE-PATH");
 986         }
 987     }
 988 
 989     class ListModuleDeps extends Command {
 990         final boolean jdkinternals;
 991         final boolean reduced;
 992         final String separator;
 993         ListModuleDeps(CommandOption option, boolean jdkinternals, boolean reduced) {
 994             this(option, jdkinternals, reduced, System.getProperty("line.separator"));
 995         }
 996         ListModuleDeps(CommandOption option, boolean jdkinternals, boolean reduced, String sep) {
 997             super(option);
 998             this.jdkinternals = jdkinternals;
 999             this.reduced = reduced;
1000             this.separator = sep;
1001         }
1002 
1003         @Override
1004         boolean checkOptions() {
1005             if (options.showSummary || options.verbose != null) {
1006                 reportError("err.invalid.options", "-summary or -verbose", option);
1007                 return false;
1008             }
1009             if (options.findJDKInternals) {
1010                 reportError("err.invalid.options", "-jdkinternals", option);
1011                 return false;
1012             }
1013             if (options.findMissingDeps) {
1014                 reportError("err.invalid.options", "--missing-deps", option);
1015                 return false;
1016             }
1017 
1018             if (!inputArgs.isEmpty() && !options.rootModules.isEmpty()) {
1019                 reportError("err.invalid.arg.for.option", "-m");
1020             }
1021             if (inputArgs.isEmpty() && !options.hasSourcePath()) {
1022                 showHelp();
1023                 return false;
1024             }
1025             return true;
1026         }
1027 
1028         @Override
1029         boolean run(JdepsConfiguration config) throws IOException {
1030             ModuleExportsAnalyzer analyzer = new ModuleExportsAnalyzer(config,
1031                                                                        dependencyFilter(config),
1032                                                                        jdkinternals,
1033                                                                        reduced,
1034                                                                        log,
1035                                                                        separator);
1036             boolean ok = analyzer.run(options.depth, options.ignoreMissingDeps);
1037             if (!ok) {
1038                 reportError("err.cant.list.module.deps");
1039                 log.println();
1040                 analyzer.visitMissingDeps(new SimpleDepVisitor());
1041             }
1042             return ok;
1043         }
1044     }
1045 
1046     class GenDotFile extends AnalyzeDeps {
1047         final Path dotOutputDir;
1048         GenDotFile(Path dotOutputDir) {
1049             super(CommandOption.GENERATE_DOT_FILE);
1050 
1051             this.dotOutputDir = dotOutputDir;
1052         }
1053 
1054         @Override
1055         boolean run(JdepsConfiguration config) throws IOException {
1056             if ((options.showSummary || options.verbose == MODULE) &&
1057                 !options.addmods.isEmpty() && inputArgs.isEmpty()) {
1058                 // generate dot graph from the resolved graph from module
1059                 // resolution.  No class dependency analysis is performed.
1060                 return new ModuleDotGraph(config, options.apiOnly)
1061                         .genDotFiles(dotOutputDir);
1062             }
1063 
1064             Type type = getAnalyzerType();
1065             JdepsWriter writer = new DotFileWriter(dotOutputDir,
1066                                                    type,
1067                                                    options.showProfile,
1068                                                    options.showModule,
1069                                                    options.showLabel);
1070             return run(config, writer, type);
1071         }
1072     }
1073 
1074     class SimpleDepVisitor implements Analyzer.Visitor {
1075         private Archive source;
1076         @Override
1077         public void visitDependence(String origin, Archive originArchive, String target, Archive targetArchive) {
1078             if (source != originArchive) {
1079                 source = originArchive;
1080                 log.format("%s%n", originArchive);
1081             }
1082             log.format("   %-50s -> %-50s %s%n", origin, target, targetArchive.getName());
1083         }
1084     }
1085 
1086     /**
1087      * Returns a filter used during dependency analysis
1088      */
1089     private JdepsFilter dependencyFilter(JdepsConfiguration config) {
1090         // Filter specified by -filter, -package, -regex, and --require options
1091         JdepsFilter.Builder builder = new JdepsFilter.Builder();
1092 
1093         // source filters
1094         builder.includePattern(options.includePattern);
1095 
1096         // target filters
1097         builder.filter(options.filterSamePackage, options.filterSameArchive);
1098         builder.findJDKInternals(options.findJDKInternals);
1099         builder.findMissingDeps(options.findMissingDeps);
1100 
1101         // --require
1102         if (!options.requires.isEmpty()) {
1103             options.requires.stream()
1104                 .forEach(mn -> {
1105                     Module m = config.findModule(mn).get();
1106                     builder.requires(mn, m.packages());
1107                 });
1108         }
1109         // -regex
1110         if (options.regex != null)
1111             builder.regex(options.regex);
1112         // -package
1113         if (!options.packageNames.isEmpty())
1114             builder.packages(options.packageNames);
1115         // -filter
1116         if (options.filterRegex != null)
1117             builder.filter(options.filterRegex);
1118 
1119         return builder.build();
1120     }
1121 
1122     public void handleOptions(String[] args) throws BadArgs {
1123         // process options
1124         for (int i=0; i < args.length; i++) {
1125             if (args[i].charAt(0) == '-') {
1126                 String name = args[i];
1127                 Option option = getOption(name);
1128                 String param = null;
1129                 if (option.hasArg) {
1130                     if (name.startsWith("-") && name.indexOf('=') > 0) {
1131                         param = name.substring(name.indexOf('=') + 1, name.length());
1132                     } else if (i + 1 < args.length) {
1133                         param = args[++i];
1134                     }
1135                     if (param == null || param.isEmpty() || param.charAt(0) == '-') {
1136                         throw new BadArgs("err.missing.arg", name).showUsage(true);
1137                     }
1138                 }
1139                 option.process(this, name, param);
1140                 if (option.ignoreRest()) {
1141                     i = args.length;
1142                 }
1143             } else {
1144                 // process rest of the input arguments
1145                 for (; i < args.length; i++) {
1146                     String name = args[i];
1147                     if (name.charAt(0) == '-') {
1148                         throw new BadArgs("err.option.after.class", name).showUsage(true);
1149                     }
1150                     inputArgs.add(name);
1151                 }
1152             }
1153         }
1154     }
1155 
1156     private Option getOption(String name) throws BadArgs {
1157         for (Option o : recognizedOptions) {
1158             if (o.matches(name)) {
1159                 return o;
1160             }
1161         }
1162         throw new BadArgs("err.unknown.option", name).showUsage(true);
1163     }
1164 
1165     private void reportError(String key, Object... args) {
1166         log.println(getMessage("error.prefix") + " " + getMessage(key, args));
1167     }
1168 
1169     void warning(String key, Object... args) {
1170         log.println(getMessage("warn.prefix") + " " + getMessage(key, args));
1171     }
1172 
1173     private void showHelp() {
1174         log.println(getMessage("main.usage", PROGNAME));
1175         for (Option o : recognizedOptions) {
1176             String name = o.aliases[0].substring(1); // there must always be at least one name
1177             name = name.charAt(0) == '-' ? name.substring(1) : name;
1178             if (o.isHidden() || name.startsWith("filter:")) {
1179                 continue;
1180             }
1181             log.println(getMessage("main.opt." + name));
1182         }
1183     }
1184 
1185     private void showVersion(boolean full) {
1186         log.println(version(full ? "full" : "release"));
1187     }
1188 
1189     private String version(String key) {
1190         // key=version:  mm.nn.oo[-milestone]
1191         // key=full:     mm.mm.oo[-milestone]-build
1192         try {
1193             return ResourceBundleHelper.getVersion(key);
1194         } catch (MissingResourceException e) {
1195             return getMessage("version.unknown", System.getProperty("java.version"));
1196         }
1197     }
1198 
1199     static String getMessage(String key, Object... args) {
1200         try {
1201             return MessageFormat.format(ResourceBundleHelper.getMessage(key), args);
1202         } catch (MissingResourceException e) {
1203             throw new InternalError("Missing message: " + key);
1204         }
1205     }
1206 
1207     private static class Options {
1208         boolean help;
1209         boolean version;
1210         boolean fullVersion;
1211         boolean showProfile;
1212         boolean showModule = true;
1213         boolean showSummary;
1214         boolean apiOnly;
1215         boolean showLabel;
1216         boolean findJDKInternals;
1217         boolean findMissingDeps;
1218         boolean ignoreMissingDeps;
1219         boolean nowarning = false;
1220         Analyzer.Type verbose;
1221         // default filter references from same package
1222         boolean filterSamePackage = true;
1223         boolean filterSameArchive = false;
1224         Pattern filterRegex;
1225         String classpath;
1226         int depth = 1;
1227         Set<String> requires = new HashSet<>();
1228         Set<String> packageNames = new HashSet<>();
1229         Pattern regex;             // apply to the dependences
1230         Pattern includePattern;
1231         boolean inverse = false;
1232         boolean compileTimeView = false;
1233         String systemModulePath = System.getProperty("java.home");
1234         String upgradeModulePath;
1235         String modulePath;
1236         Set<String> rootModules = new HashSet<>();
1237         Set<String> addmods = new HashSet<>();
1238         Runtime.Version multiRelease;
1239 
1240         boolean hasSourcePath() {
1241             return !addmods.isEmpty() || includePattern != null;
1242         }
1243 
1244         boolean hasFilter() {
1245             return numFilters() > 0;
1246         }
1247 
1248         int numFilters() {
1249             int count = 0;
1250             if (requires.size() > 0) count++;
1251             if (regex != null) count++;
1252             if (packageNames.size() > 0) count++;
1253             return count;
1254         }
1255     }
1256 
1257     private static class ResourceBundleHelper {
1258         static final String LS = System.lineSeparator();
1259         static final ResourceBundle versionRB;
1260         static final ResourceBundle bundle;
1261         static final ResourceBundle jdkinternals;
1262 
1263         static {
1264             Locale locale = Locale.getDefault();
1265             try {
1266                 bundle = ResourceBundle.getBundle("com.sun.tools.jdeps.resources.jdeps", locale);
1267             } catch (MissingResourceException e) {
1268                 throw new InternalError("Cannot find jdeps resource bundle for locale " + locale);
1269             }
1270             try {
1271                 versionRB = ResourceBundle.getBundle("com.sun.tools.jdeps.resources.version");
1272             } catch (MissingResourceException e) {
1273                 throw new InternalError("version.resource.missing");
1274             }
1275             try {
1276                 jdkinternals = ResourceBundle.getBundle("com.sun.tools.jdeps.resources.jdkinternals");
1277             } catch (MissingResourceException e) {
1278                 throw new InternalError("Cannot find jdkinternals resource bundle");
1279             }
1280         }
1281 
1282         static String getMessage(String key) {
1283             return bundle.getString(key).replace("\n", LS);
1284         }
1285 
1286         static String getVersion(String key) {
1287             if (ResourceBundleHelper.versionRB == null) {
1288                 return System.getProperty("java.version");
1289             }
1290             return versionRB.getString(key).replace("\n", LS);
1291         }
1292 
1293         static String getSuggestedReplacement(String key) {
1294             return ResourceBundleHelper.jdkinternals.getString(key).replace("\n", LS);
1295         }
1296     }
1297 
1298     /**
1299      * Returns the recommended replacement API for the given classname;
1300      * or return null if replacement API is not known.
1301      */
1302     private Optional<String> replacementFor(String cn) {
1303         String name = cn;
1304         String value = null;
1305         while (value == null && name != null) {
1306             try {
1307                 value = ResourceBundleHelper.getSuggestedReplacement(name);
1308             } catch (MissingResourceException e) {
1309                 // go up one subpackage level
1310                 int i = name.lastIndexOf('.');
1311                 name = i > 0 ? name.substring(0, i) : null;
1312             }
1313         }
1314         return Optional.ofNullable(value);
1315     }
1316 }