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