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