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