8156575: Add jdeps -addmods, -system, -inverse options
authormchung
Thu, 19 May 2016 11:01:26 -0700
changeset 38525 a2169f8fa712
parent 38524 badd925c1d2f
child 38526 6f5838874afc
child 38527 cf6b80042c2f
8156575: Add jdeps -addmods, -system, -inverse options Reviewed-by: dfuchs
langtools/src/jdk.jdeps/share/classes/com/sun/tools/jdeps/InverseDepsAnalyzer.java
langtools/src/jdk.jdeps/share/classes/com/sun/tools/jdeps/JdepsConfiguration.java
langtools/src/jdk.jdeps/share/classes/com/sun/tools/jdeps/JdepsTask.java
langtools/src/jdk.jdeps/share/classes/com/sun/tools/jdeps/resources/jdeps.properties
langtools/test/tools/jdeps/lib/JdepsUtil.java
langtools/test/tools/jdeps/modules/InverseDeps.java
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/langtools/src/jdk.jdeps/share/classes/com/sun/tools/jdeps/InverseDepsAnalyzer.java	Thu May 19 11:01:26 2016 -0700
@@ -0,0 +1,250 @@
+/*
+ * Copyright (c) 2016, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation.  Oracle designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+
+package com.sun.tools.jdeps;
+
+import static com.sun.tools.jdeps.JdepsFilter.DEFAULT_FILTER;
+import static com.sun.tools.jdeps.Module.trace;
+import static com.sun.tools.jdeps.Graph.*;
+
+import java.lang.module.ModuleDescriptor.Requires;
+import java.io.IOException;
+import java.util.Collections;
+import java.util.Deque;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.LinkedList;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+/**
+ * Inverse transitive dependency analysis (compile-time view)
+ */
+public class InverseDepsAnalyzer extends DepsAnalyzer {
+    // the end points for the resulting paths to be reported
+    private final Map<Archive, Set<Archive>> endPoints = new HashMap<>();
+    // target archives for inverse transitive dependence analysis
+    private final Set<Archive> targets = new HashSet<>();
+
+    public InverseDepsAnalyzer(JdepsConfiguration config,
+                               JdepsFilter filter,
+                               JdepsWriter writer,
+                               Analyzer.Type verbose,
+                               boolean apiOnly) {
+        super(config, filter, writer, verbose, apiOnly);
+    }
+
+    public boolean run() throws IOException {
+        try {
+            if (apiOnly) {
+                finder.parseExportedAPIs(rootArchives.stream());
+            } else {
+                finder.parse(rootArchives.stream());
+            }
+            archives.addAll(rootArchives);
+
+            Set<Archive> archives = archives();
+
+            // If -package or -regex is specified, the archives that reference
+            // the matching types are used as the targets for inverse
+            // transitive analysis.  If -requires is specified, the
+            // specified modules are the targets.
+
+            if (filter.requiresFilter().isEmpty()) {
+                targets.addAll(archives);
+            } else {
+                filter.requiresFilter().stream()
+                      .map(configuration::findModule)
+                      .flatMap(Optional::stream)
+                      .forEach(targets::add);
+            }
+
+            // If -package or -regex is specified, the end points are
+            // the matching archives.  If -requires is specified,
+            // the end points are the modules specified in -requires.
+            if (filter.requiresFilter().isEmpty()) {
+                Map<Archive, Set<Archive>> dependences = finder.dependences();
+                targets.forEach(source -> endPoints.put(source, dependences.get(source)));
+            } else {
+                targets.forEach(t -> endPoints.put(t, Collections.emptySet()));
+            }
+
+            analyzer.run(archives, finder.locationToArchive());
+
+            // print the first-level of dependencies
+            if (writer != null) {
+                writer.generateOutput(archives, analyzer);
+            }
+
+        } finally {
+            finder.shutdown();
+        }
+        return true;
+    }
+
+    /**
+     * Returns the target archives determined from the dependency analysis.
+     *
+     * Inverse transitive dependency will find all nodes that depend
+     * upon the returned set of archives directly and indirectly.
+     */
+    public Set<Archive> targets() {
+        return Collections.unmodifiableSet(targets);
+    }
+
+    /**
+     * Finds all inverse transitive dependencies using the given requires set
+     * as the targets, if non-empty.  If the given requires set is empty,
+     * use the archives depending the packages specified in -regex or -p options.
+     */
+    public Set<Deque<Archive>> inverseDependences() throws IOException {
+        // create a new dependency finder to do the analysis
+        DependencyFinder dependencyFinder = new DependencyFinder(configuration, DEFAULT_FILTER);
+        try {
+            // parse all archives in unnamed module to get compile-time dependences
+            Stream<Archive> archives =
+                Stream.concat(configuration.initialArchives().stream(),
+                              configuration.classPathArchives().stream());
+            if (apiOnly) {
+                dependencyFinder.parseExportedAPIs(archives);
+            } else {
+                dependencyFinder.parse(archives);
+            }
+
+            Graph.Builder<Archive> builder = new Graph.Builder<>();
+            // include all target nodes
+            targets().forEach(builder::addNode);
+
+            // transpose the module graph - may filter JDK module
+            configuration.getModules().values().stream()
+                .filter(filter::include)
+                .forEach(m -> {
+                    builder.addNode(m);
+                    m.descriptor().requires().stream()
+                        .map(Requires::name)
+                        .map(configuration::findModule)  // must be present
+                        .forEach(v -> builder.addEdge(v.get(), m));
+                });
+
+            // add the dependences from the analysis
+            Map<Archive, Set<Archive>> dependences = dependencyFinder.dependences();
+            dependences.entrySet().stream()
+                .forEach(e -> {
+                    Archive u = e.getKey();
+                    builder.addNode(u);
+                    e.getValue().forEach(v -> builder.addEdge(v, u));
+                });
+
+            // transposed dependence graph.
+            Graph<Archive> graph = builder.build();
+            trace("targets: %s%n", targets());
+
+            // Traverse from the targets and find all paths
+            // rebuild a graph with all nodes that depends on targets
+            // targets directly and indirectly
+            return targets().stream()
+                .map(t -> findPaths(graph, t))
+                .flatMap(Set::stream)
+                .collect(Collectors.toSet());
+        } finally {
+            dependencyFinder.shutdown();
+        }
+    }
+
+    /**
+     * Returns all paths reachable from the given targets.
+     */
+    private Set<Deque<Archive>> findPaths(Graph<Archive> graph, Archive target) {
+
+        // path is in reversed order
+        Deque<Archive> path = new LinkedList<>();
+        path.push(target);
+
+        Set<Edge<Archive>> visited = new HashSet<>();
+
+        Deque<Edge<Archive>> deque = new LinkedList<>();
+        deque.addAll(graph.edgesFrom(target));
+        if (deque.isEmpty()) {
+            return makePaths(path).collect(Collectors.toSet());
+        }
+
+        Set<Deque<Archive>> allPaths = new HashSet<>();
+        while (!deque.isEmpty()) {
+            Edge<Archive> edge = deque.pop();
+
+            if (visited.contains(edge))
+                continue;
+
+            Archive node = edge.v;
+            path.addLast(node);
+            visited.add(edge);
+
+            Set<Edge<Archive>> unvisitedDeps = graph.edgesFrom(node)
+                    .stream()
+                    .filter(e -> !visited.contains(e))
+                    .collect(Collectors.toSet());
+
+            trace("visiting %s %s (%s)%n", edge, path, unvisitedDeps);
+            if (unvisitedDeps.isEmpty()) {
+                makePaths(path).forEach(allPaths::add);
+                path.removeLast();
+            }
+
+            // push unvisited adjacent edges
+            unvisitedDeps.stream().forEach(deque::push);
+
+
+            // when the adjacent edges of a node are visited, pop it from the path
+            while (!path.isEmpty()) {
+                if (visited.containsAll(graph.edgesFrom(path.peekLast())))
+                    path.removeLast();
+                else
+                    break;
+            }
+        }
+
+       return allPaths;
+    }
+
+    /**
+     * Prepend end point to the path
+     */
+    private Stream<Deque<Archive>> makePaths(Deque<Archive> path) {
+        Set<Archive> nodes = endPoints.get(path.peekFirst());
+        if (nodes == null || nodes.isEmpty()) {
+            return Stream.of(new LinkedList<>(path));
+        } else {
+            return nodes.stream().map(n -> {
+                Deque<Archive> newPath = new LinkedList<>();
+                newPath.addFirst(n);
+                newPath.addAll(path);
+                return newPath;
+            });
+        }
+    }
+}
--- a/langtools/src/jdk.jdeps/share/classes/com/sun/tools/jdeps/JdepsConfiguration.java	Thu May 19 10:55:33 2016 -0700
+++ b/langtools/src/jdk.jdeps/share/classes/com/sun/tools/jdeps/JdepsConfiguration.java	Thu May 19 11:01:26 2016 -0700
@@ -65,7 +65,10 @@
 import java.util.stream.Stream;
 
 public class JdepsConfiguration {
-    private static final String MODULE_INFO = "module-info.class";
+    // the token for "all modules on the module path"
+    public static final String ALL_MODULE_PATH = "ALL-MODULE-PATH";
+    public static final String ALL_DEFAULT = "ALL-DEFAULT";
+    public static final String MODULE_INFO = "module-info.class";
 
     private final SystemModuleFinder system;
     private final ModuleFinder finder;
@@ -83,7 +86,8 @@
                                ModuleFinder finder,
                                Set<String> roots,
                                List<Path> classpaths,
-                               List<Archive> initialArchives)
+                               List<Archive> initialArchives,
+                               boolean allDefaultModules)
         throws IOException
     {
         trace("root: %s%n", roots);
@@ -91,15 +95,13 @@
         this.system = systemModulePath;
         this.finder = finder;
 
-        // build root set
-        Set<String> mods = new HashSet<>();
+        // build root set for resolution
+        Set<String> mods = new HashSet<>(roots);
 
-        if (initialArchives.isEmpty() && classpaths.isEmpty() && !roots.isEmpty()) {
-            // named module as root. No initial unnamed module
-            mods.addAll(roots);
-        } else {
-            // unnamed module
-            mods.addAll(roots);
+        // add default modules to the root set
+        // unnamed module
+        if (!initialArchives.isEmpty() || !classpaths.isEmpty() ||
+                roots.isEmpty() || allDefaultModules) {
             mods.addAll(systemModulePath.defaultSystemRoots());
         }
 
@@ -190,6 +192,10 @@
         return system.find(m.name()).isPresent();
     }
 
+    boolean isValidToken(String name) {
+        return ALL_MODULE_PATH.equals(name) || ALL_DEFAULT.equals(name);
+    }
+
     /**
      * Returns the modules that the given module can read
      */
@@ -299,6 +305,7 @@
     }
 
     static class SystemModuleFinder implements ModuleFinder {
+        private static final String JAVA_HOME = System.getProperty("java.home");
         private static final String JAVA_SE = "java.se";
 
         private final FileSystem fileSystem;
@@ -306,41 +313,50 @@
         private final Map<String, ModuleReference> systemModules;
 
         SystemModuleFinder() {
-            this(System.getProperty("java.home"));
+            if (Files.isRegularFile(Paths.get(JAVA_HOME, "lib", "modules"))) {
+                // jrt file system
+                this.fileSystem = FileSystems.getFileSystem(URI.create("jrt:/"));
+                this.root = fileSystem.getPath("/modules");
+                this.systemModules = walk(root);
+            } else {
+                // exploded image
+                this.fileSystem = FileSystems.getDefault();
+                root = Paths.get(JAVA_HOME, "modules");
+                this.systemModules = ModuleFinder.ofSystem().findAll().stream()
+                    .collect(toMap(mref -> mref.descriptor().name(), Function.identity()));
+            }
         }
-        SystemModuleFinder(String javaHome) {
-            final FileSystem fs;
-            final Path root;
-            final Map<String, ModuleReference> systemModules;
-            if (javaHome != null) {
-                if (Files.isRegularFile(Paths.get(javaHome, "lib", "modules"))) {
-                    try {
-                        // jrt file system
-                        fs = FileSystems.getFileSystem(URI.create("jrt:/"));
-                        root = fs.getPath("/modules");
-                        systemModules = Files.walk(root, 1)
-                            .filter(path -> !path.equals(root))
-                            .map(this::toModuleReference)
-                            .collect(toMap(mref -> mref.descriptor().name(),
-                                           Function.identity()));
-                    } catch (IOException e) {
-                        throw new UncheckedIOException(e);
-                    }
-                } else {
-                    // exploded image
-                    fs = FileSystems.getDefault();
-                    root = Paths.get(javaHome, "modules");
-                    systemModules = ModuleFinder.ofSystem().findAll().stream()
-                        .collect(toMap(mref -> mref.descriptor().name(), Function.identity()));
-                }
+
+        SystemModuleFinder(String javaHome) throws IOException {
+            if (javaHome == null) {
+                // -system none
+                this.fileSystem = null;
+                this.root = null;
+                this.systemModules = Collections.emptyMap();
             } else {
-                fs = null;
-                root = null;
-                systemModules = Collections.emptyMap();
+                if (Files.isRegularFile(Paths.get(javaHome, "lib", "modules")))
+                    throw new IllegalArgumentException("Invalid java.home: " + javaHome);
+
+                // alternate java.home
+                Map<String, String> env = new HashMap<>();
+                env.put("java.home", javaHome);
+                // a remote run-time image
+                this.fileSystem = FileSystems.newFileSystem(URI.create("jrt:/"), env);
+                this.root = fileSystem.getPath("/modules");
+                this.systemModules = walk(root);
             }
-            this.fileSystem = fs;
-            this.root = root;
-            this.systemModules = systemModules;
+        }
+
+        private Map<String, ModuleReference> walk(Path root) {
+            try {
+                return Files.walk(root, 1)
+                    .filter(path -> !path.equals(root))
+                    .map(this::toModuleReference)
+                    .collect(toMap(mref -> mref.descriptor().name(),
+                                    Function.identity()));
+            } catch (IOException e) {
+                throw new UncheckedIOException(e);
+            }
         }
 
         private ModuleReference toModuleReference(Path path) {
@@ -437,8 +453,6 @@
     }
 
     public static class Builder {
-        // the token for "all modules on the module path"
-        private static final String ALL_MODULE_PATH = "ALL-MODULE-PATH";
 
         final SystemModuleFinder systemModulePath;
         final Set<String> rootModules = new HashSet<>();
@@ -449,13 +463,16 @@
         ModuleFinder upgradeModulePath;
         ModuleFinder appModulePath;
         boolean addAllApplicationModules;
+        boolean addAllDefaultModules;
 
         public Builder() {
-            this(System.getProperty("java.home"));
+            this.systemModulePath = new SystemModuleFinder();
         }
 
-        public Builder(String systemModulePath) {
-            this.systemModulePath = new SystemModuleFinder(systemModulePath);;
+        public Builder(String javaHome) throws IOException {
+            this.systemModulePath = SystemModuleFinder.JAVA_HOME.equals(javaHome)
+                ? new SystemModuleFinder()
+                : new SystemModuleFinder(javaHome);
         }
 
         public Builder upgradeModulePath(String upgradeModulePath) {
@@ -474,6 +491,9 @@
                     case ALL_MODULE_PATH:
                         this.addAllApplicationModules = true;
                         break;
+                    case ALL_DEFAULT:
+                        this.addAllDefaultModules = true;
+                        break;
                     default:
                         this.rootModules.add(mn);
                 }
@@ -531,11 +551,13 @@
                     .map(mref -> mref.descriptor().name())
                     .forEach(rootModules::add);
             }
+
             return new JdepsConfiguration(systemModulePath,
                                           finder,
                                           rootModules,
                                           classPaths,
-                                          initialArchives);
+                                          initialArchives,
+                                          addAllDefaultModules);
         }
 
         private static ModuleFinder createModulePathFinder(String mpaths) {
--- a/langtools/src/jdk.jdeps/share/classes/com/sun/tools/jdeps/JdepsTask.java	Thu May 19 10:55:33 2016 -0700
+++ b/langtools/src/jdk.jdeps/share/classes/com/sun/tools/jdeps/JdepsTask.java	Thu May 19 11:01:26 2016 -0700
@@ -28,6 +28,7 @@
 import static com.sun.tools.jdeps.Analyzer.NOT_FOUND;
 import static com.sun.tools.jdeps.Analyzer.Type.*;
 import static com.sun.tools.jdeps.JdepsWriter.*;
+import static com.sun.tools.jdeps.JdepsConfiguration.ALL_MODULE_PATH;
 
 import java.io.IOException;
 import java.io.PrintWriter;
@@ -37,6 +38,8 @@
 import java.nio.file.Paths;
 import java.text.MessageFormat;
 import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.Deque;
 import java.util.HashSet;
 import java.util.List;
 import java.util.Locale;
@@ -246,6 +249,25 @@
                 task.options.upgradeModulePath = arg;
             }
         },
+        new Option(true, "-system") {
+            void process(JdepsTask task, String opt, String arg) throws BadArgs {
+                if (arg.equals("none")) {
+                    task.options.systemModulePath = null;
+                } else {
+                    Path path = Paths.get(arg);
+                    if (Files.isRegularFile(path.resolve("lib").resolve("modules")))
+                        task.options.systemModulePath = arg;
+                    else
+                        throw new BadArgs("err.invalid.path", arg);
+                }
+            }
+        },
+        new Option(true, "-addmods") {
+            void process(JdepsTask task, String opt, String arg) throws BadArgs {
+                Set<String> mods = Set.of(arg.split(","));
+                task.options.addmods.addAll(mods);
+            }
+        },
         new Option(true, "-m") {
             void process(JdepsTask task, String opt, String arg) throws BadArgs {
                 task.options.rootModule = arg;
@@ -264,7 +286,7 @@
                 task.options.regex = Pattern.compile(arg);
             }
         },
-        new Option(true, "-module") {
+        new Option(true, "-requires") {
             void process(JdepsTask task, String opt, String arg) {
                 task.options.requires.add(arg);
             }
@@ -315,6 +337,7 @@
                 task.options.showProfile = true;
             }
         },
+
         new Option(false, "-R", "-recursive") {
             void process(JdepsTask task, String opt, String arg) {
                 task.options.depth = 0;
@@ -323,6 +346,17 @@
                 task.options.filterSamePackage = false;
             }
         },
+
+        new Option(false, "-I", "-inverse") {
+            void process(JdepsTask task, String opt, String arg) {
+                task.options.inverse = true;
+                // equivalent to the inverse of compile-time view analysis
+                task.options.compileTimeView = true;
+                task.options.filterSamePackage = true;
+                task.options.filterSameArchive = true;
+            }
+        },
+
         new Option(false, "-ct", "-compile-time") {
             void process(JdepsTask task, String opt, String arg) {
                 task.options.compileTimeView = true;
@@ -423,6 +457,16 @@
                 return EXIT_CMDERR;
             }
 
+            if (options.inverse && options.depth != 1) {
+                reportError("err.invalid.inverse.option", "-R");
+                return EXIT_CMDERR;
+            }
+
+            if (options.inverse && options.numFilters() == 0) {
+                reportError("err.invalid.filters");
+                return EXIT_CMDERR;
+            }
+
             if ((options.findJDKInternals) && (options.hasFilter() || options.showSummary)) {
                 showHelp();
                 return EXIT_CMDERR;
@@ -463,8 +507,9 @@
             .forEach(e -> System.out.format("split package: %s %s%n", e.getKey(),
                                             e.getValue().toString()));
 
-        // check if any module specified in -module is missing
+        // check if any module specified in -requires is missing
         Stream.concat(options.addmods.stream(), options.requires.stream())
+              .filter(mn -> !config.isValidToken(mn))
               .forEach(mn -> config.findModule(mn).orElseThrow(() ->
                   new UncheckedBadArgs(new BadArgs("err.module.not.found", mn))));
 
@@ -484,7 +529,11 @@
             return new ModuleAnalyzer(config, log).genDotFiles(options.dotOutputDir);
         }
 
-        return analyzeDeps(config);
+        if (options.inverse) {
+            return analyzeInverseDeps(config);
+        } else {
+            return analyzeDeps(config);
+        }
     }
 
     private JdepsConfiguration buildConfig() throws IOException {
@@ -540,7 +589,7 @@
         boolean ok = analyzer.run(options.compileTimeView, options.depth);
 
         // print skipped entries, if any
-        analyzer.analyzer.archives()
+        analyzer.archives()
             .forEach(archive -> archive.reader()
                 .skippedEntries().stream()
                 .forEach(name -> warning("warn.skipped.entry",
@@ -569,6 +618,40 @@
         return ok;
     }
 
+    private boolean analyzeInverseDeps(JdepsConfiguration config) throws IOException {
+        JdepsWriter writer = new SimpleWriter(log,
+                                              options.verbose,
+                                              options.showProfile,
+                                              options.showModule);
+
+        InverseDepsAnalyzer analyzer = new InverseDepsAnalyzer(config,
+                                                               dependencyFilter(config),
+                                                               writer,
+                                                               options.verbose,
+                                                               options.apiOnly);
+        boolean ok = analyzer.run();
+
+        log.println();
+        if (!options.requires.isEmpty())
+            log.format("Inverse transitive dependences on %s%n", options.requires);
+        else
+            log.format("Inverse transitive dependences matching %s%n",
+                options.regex != null
+                    ? options.regex.toString()
+                    : "packages " + options.packageNames);
+
+        analyzer.inverseDependences().stream()
+                .sorted(Comparator.comparing(this::sortPath))
+                .forEach(path -> log.println(path.stream()
+                                                .map(Archive::getName)
+                                                .collect(Collectors.joining(" <- "))));
+        return ok;
+    }
+
+    private String sortPath(Deque<Archive> path) {
+        return path.peekFirst().getName();
+    }
+
     private boolean genModuleInfo(JdepsConfiguration config) throws IOException {
         ModuleInfoBuilder builder
             = new ModuleInfoBuilder(config, inputArgs, options.genModuleInfo);
@@ -603,7 +686,7 @@
      * Returns a filter used during dependency analysis
      */
     private JdepsFilter dependencyFilter(JdepsConfiguration config) {
-        // Filter specified by -filter, -package, -regex, and -module options
+        // Filter specified by -filter, -package, -regex, and -requires options
         JdepsFilter.Builder builder = new JdepsFilter.Builder();
 
         // source filters
@@ -613,7 +696,7 @@
         builder.filter(options.filterSamePackage, options.filterSameArchive);
         builder.findJDKInternals(options.findJDKInternals);
 
-        // -module
+        // -requires
         if (!options.requires.isEmpty()) {
             options.requires.stream()
                 .forEach(mn -> {
@@ -753,6 +836,7 @@
         Pattern regex;             // apply to the dependences
         Pattern includePattern;
         Pattern includeSystemModulePattern;
+        boolean inverse = false;
         boolean compileTimeView = false;
         Set<String> checkModuleDeps;
         String systemModulePath = System.getProperty("java.home");
--- a/langtools/src/jdk.jdeps/share/classes/com/sun/tools/jdeps/resources/jdeps.properties	Thu May 19 10:55:33 2016 -0700
+++ b/langtools/src/jdk.jdeps/share/classes/com/sun/tools/jdeps/resources/jdeps.properties	Thu May 19 11:01:26 2016 -0700
@@ -48,10 +48,10 @@
 main.opt.e=\
 \  -e <regex> -regex <regex>         Finds dependences matching the given pattern.
 
-main.opt.module=\
-\  -module <module-name>             Finds dependences matching the given module\n\
+main.opt.requires=\
+\  -requires <module-name>           Finds dependences matching the given module\n\
 \                                    name (may be given multiple times).\n\
-\                                    -package, -regex, -module are mutual exclusive.
+\                                    -package, -regex, -requires are mutual exclusive.
 
 main.opt.include=\n\
  \Options to filter classes to be analyzed:\n\
@@ -73,6 +73,13 @@
 main.opt.upgrademodulepath=\
 \  -upgrademodulepath <module path>...  Specify upgrade module path
 
+main.opt.system=\
+\  -system <java-home>               Specify an alternate system module path
+
+main.opt.addmods=\
+\  -addmods <module-name>[,<module-name>...]\n\
+\                                    Adds modules to the root set for analysis
+
 main.opt.m=\
 \  -m <module-name>                  Specify the root module for analysis
 
@@ -82,6 +89,15 @@
 \                                    -e, -foption is specified, only the matching\n\
 \                                    dependences are analyzed.
 
+main.opt.I=\
+\  -I           -inverse             Analyzes the dependences per other given options\n\
+\                                    and then find all artifacts that directly\n\
+\                                    and indirectly depend on the matching nodes.\n\
+\                                    This is equivalent to the inverse of\n\
+\                                    compile-time view analysis and print\n\
+\                                    dependency summary.  This option must use\n\
+\                                    with -requires, -package or -regex option.
+
 main.opt.ct=\
 \  -ct          -compile-time        Compile-time view of transitive dependencies\n\
 \                                    i.e. compile-time view of -R option.\n\
@@ -140,9 +156,11 @@
 err.exception.message={0}
 err.invalid.path=invalid path: {0}
 err.invalid.module.option=Cannot set {0} with {1} option.
-err.invalid.filters=Only one of -package (-p), -regex (-e), -module option can be set
+err.invalid.filters=Only one of -package (-p), -regex (-e), -requires option can be set
 err.module.not.found=module not found: {0}
 err.root.module.not.set=root module set empty
+err.invalid.inverse.option={0} cannot be used with -inverse option
+err.inverse.filter.not.set={0} cannot be used with -inverse option
 warn.invalid.arg=Path not exist: {0}
 warn.split.package=package {0} defined in {1} {2}
 warn.replace.useJDKInternals=\
--- a/langtools/test/tools/jdeps/lib/JdepsUtil.java	Thu May 19 10:55:33 2016 -0700
+++ b/langtools/test/tools/jdeps/lib/JdepsUtil.java	Thu May 19 11:01:26 2016 -0700
@@ -23,6 +23,7 @@
 
 import com.sun.tools.jdeps.Analyzer;
 import com.sun.tools.jdeps.DepsAnalyzer;
+import com.sun.tools.jdeps.InverseDepsAnalyzer;
 import com.sun.tools.jdeps.JdepsConfiguration;
 import com.sun.tools.jdeps.JdepsFilter;
 import com.sun.tools.jdeps.JdepsWriter;
@@ -199,6 +200,11 @@
             return new ModuleAnalyzer(configuration(), pw, mods);
         }
 
+        public InverseDepsAnalyzer getInverseDepsAnalyzer() throws IOException {
+            return new InverseDepsAnalyzer(configuration(), filter.build(), writer(),
+                                           verbose, false);
+        }
+
         public void dumpOutput(PrintStream out) {
             out.println(sw.toString());
         }
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/langtools/test/tools/jdeps/modules/InverseDeps.java	Thu May 19 11:01:26 2016 -0700
@@ -0,0 +1,295 @@
+/*
+ * Copyright (c) 2016, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+
+/*
+ * @test
+ * @summary Tests split packages
+ * @library ../lib
+ * @build CompilerUtils JdepsUtil
+ * @modules jdk.jdeps/com.sun.tools.jdeps
+ * @run testng InverseDeps
+ */
+
+import java.io.File;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.Arrays;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+import com.sun.tools.jdeps.Archive;
+import com.sun.tools.jdeps.InverseDepsAnalyzer;
+import org.testng.annotations.BeforeTest;
+import org.testng.annotations.DataProvider;
+import org.testng.annotations.Test;
+
+import static org.testng.Assert.assertTrue;
+import static org.testng.Assert.assertFalse;
+import static org.testng.Assert.assertEquals;
+
+
+public class InverseDeps {
+    private static final String TEST_SRC = System.getProperty("test.src");
+    private static final String TEST_CLASSES = System.getProperty("test.classes");
+
+    private static final Path SRC_DIR = Paths.get(TEST_SRC, "src");
+    private static final Path MODS_DIR = Paths.get("mods");
+    private static final Path LIBS_DIR = Paths.get("libs");
+
+    private static final Set<String> modules = new LinkedHashSet(
+        List.of("unsafe", "m4", "m5", "m6", "m7")
+    );
+
+    /**
+     * Compiles classes used by the test
+     */
+    @BeforeTest
+    public void compileAll() throws Exception {
+        CompilerUtils.cleanDir(MODS_DIR);
+
+        for (String mn : modules) {
+            // compile a module
+            assertTrue(CompilerUtils.compileModule(SRC_DIR, MODS_DIR, mn));
+
+            // create JAR files with no module-info.class
+            Path root = MODS_DIR.resolve(mn);
+            JdepsUtil.createJar(LIBS_DIR.resolve(mn + ".jar"), root,
+                Files.walk(root, Integer.MAX_VALUE)
+                    .filter(f -> {
+                        String fn = f.getFileName().toString();
+                        return fn.endsWith(".class") && !fn.equals("module-info.class");
+                    }));
+        }
+    }
+
+    @DataProvider(name = "testrequires")
+    public Object[][] expected1() {
+        return new Object[][] {
+            // -requires and result
+            { "java.sql", new String[][] {
+                    new String[] { "java.sql", "m5" },
+                }
+            },
+            { "java.compiler", new String[][] {
+                    new String[] { "java.compiler", "m5" },
+                    new String[] { "java.compiler", "m4", "m5" },
+                }
+            },
+            { "java.logging", new String[][]{
+                    new String[] {"java.logging", "m5"},
+                    new String[] {"java.logging", "m4", "m5"},
+                    new String[] {"java.logging", "java.sql", "m5"},
+                }
+            },
+            { "jdk.unsupported", new String[][] {
+                    new String[] {"jdk.unsupported", "unsafe", "m6", "m7"},
+                    new String[] {"jdk.unsupported", "unsafe", "m7"}
+                }
+            },
+        };
+    }
+
+    @Test(dataProvider = "testrequires")
+    public void testrequires(String name, String[][] expected) throws Exception {
+        JdepsUtil.Command jdeps = JdepsUtil.newCommand(
+            String.format("jdeps -inverse -modulepath %s -requires %s -addmods %s%n",
+                MODS_DIR, name, modules.stream().collect(Collectors.joining(","))
+        ));
+        jdeps.appModulePath(MODS_DIR.toString())
+             .addmods(modules)
+             .requires(Set.of(name));
+
+        runJdeps(jdeps, expected);
+
+        // automatic module
+        jdeps = JdepsUtil.newCommand(
+            String.format("jdeps -inverse -modulepath %s -requires %s -addmods ALL-MODULE-PATH%n",
+                          LIBS_DIR, name)
+        );
+        jdeps.appModulePath(MODS_DIR.toString())
+            .addmods(Set.of("ALL-MODULE-PATH"))
+            .requires(Set.of(name));
+
+        runJdeps(jdeps, expected);
+    }
+
+    @DataProvider(name = "testpackage")
+    public Object[][] expected2() {
+        return new Object[][] {
+            // -package and result
+            { "p4", new String[][] {
+                        new String[] { "m4", "m5"},
+                    }
+            },
+            { "javax.tools", new String[][] {
+                        new String[] {"java.compiler", "m5"},
+                        new String[] {"java.compiler", "m4", "m5"},
+                    }
+            },
+            { "sun.misc", new String[][] {
+                        new String[] {"jdk.unsupported", "unsafe", "m6", "m7"},
+                        new String[] {"jdk.unsupported", "unsafe", "m7"}
+                    }
+            }
+        };
+    }
+
+    @Test(dataProvider = "testpackage")
+    public void testpackage(String name, String[][] expected) throws Exception {
+        JdepsUtil.Command jdeps = JdepsUtil.newCommand(
+            String.format("jdeps -inverse -modulepath %s -package %s -addmods %s%n",
+                MODS_DIR, name, modules.stream().collect(Collectors.joining(","))
+        ));
+        jdeps.appModulePath(MODS_DIR.toString())
+            .addmods(modules)
+            .matchPackages(Set.of(name));
+
+        runJdeps(jdeps, expected);
+    }
+
+    @DataProvider(name = "testregex")
+    public Object[][] expected3() {
+        return new Object[][] {
+            // -regex and result
+            { "org.safe.Lib", new String[][] {
+                    new String[] { "unsafe", "m7"},
+                    new String[] { "unsafe", "m6", "m7"},
+                }
+            },
+            { "java.util.logging.*|org.safe.Lib", new String[][] {
+                    new String[] { "unsafe", "m7"},
+                    new String[] { "unsafe", "m6", "m7"},
+                    new String[] { "java.logging", "m5"},
+                }
+            }
+        };
+    }
+
+    @Test(dataProvider = "testregex")
+    public void testregex(String name, String[][] expected) throws Exception {
+        JdepsUtil.Command jdeps = JdepsUtil.newCommand(
+            String.format("jdeps -inverse -modulepath %s -regex %s -addmods %s%n",
+                MODS_DIR, name, modules.stream().collect(Collectors.joining(",")))
+        );
+
+        jdeps.appModulePath(MODS_DIR.toString())
+             .addmods(modules)
+             .regex(name);
+
+        runJdeps(jdeps, expected);
+    }
+
+    @DataProvider(name = "classpath")
+    public Object[][] expected4() {
+        return new Object[][] {
+            // -regex and result
+            { "sun.misc.Unsafe", new String[][] {
+                    new String[] {"jdk.unsupported", "unsafe.jar", "m6.jar", "m7.jar"},
+                    new String[] {"jdk.unsupported", "unsafe.jar", "m7.jar"}
+                }
+            },
+            { "org.safe.Lib", new String[][] {
+                    new String[] { "unsafe.jar", "m7.jar"},
+                    new String[] { "unsafe.jar", "m6.jar", "m7.jar"},
+                }
+            },
+            { "java.util.logging.*|org.safe.Lib", new String[][] {
+                    new String[] { "unsafe.jar", "m7.jar"},
+                    new String[] { "unsafe.jar", "m6.jar", "m7.jar"},
+                    new String[] { "java.logging", "m5.jar"},
+                }
+            }
+        };
+    }
+
+    @Test(dataProvider = "classpath")
+    public void testClassPath(String name, String[][] expected) throws Exception {
+        // -classpath
+        String cpath = modules.stream()
+            .filter(mn -> !mn.equals("m7"))
+            .map(mn -> LIBS_DIR.resolve(mn + ".jar").toString())
+            .collect(Collectors.joining(File.pathSeparator));
+
+        Path jarfile = LIBS_DIR.resolve("m7.jar");
+        JdepsUtil.Command jdeps = JdepsUtil.newCommand(
+            String.format("jdeps -inverse -classpath %s -regex %s %s%n",
+                cpath, name, jarfile)
+        );
+        jdeps.verbose("-verbose:class")
+             .addClassPath(cpath)
+             .regex(name).addRoot(jarfile);
+        runJdeps(jdeps, expected);
+
+        // all JAR files on the command-line arguments
+        Set<Path> paths = modules.stream()
+            .map(mn -> LIBS_DIR.resolve(mn + ".jar"))
+            .collect(Collectors.toSet());
+        jdeps = JdepsUtil.newCommand(
+            String.format("jdeps -inverse -regex %s %s%n", name, paths)
+        );
+        jdeps.verbose("-verbose:class").regex(name);
+        paths.forEach(jdeps::addRoot);
+        runJdeps(jdeps, expected);
+
+    }
+
+    private void runJdeps(JdepsUtil.Command jdeps, String[][] expected)  throws Exception {
+        InverseDepsAnalyzer analyzer = jdeps.getInverseDepsAnalyzer();
+
+        assertTrue(analyzer.run());
+
+        // get the inverse transitive dependences
+        List<String[]> paths = analyzer.inverseDependences().stream()
+            .map(deque -> deque.stream()
+                               .map(Archive::getName)
+                               .collect(Collectors.toList()).toArray(new String[0]))
+            .collect(Collectors.toList());
+
+        jdeps.dumpOutput(System.err);
+        paths.forEach(path -> System.err.println(Arrays.stream(path)
+                .collect(Collectors.joining(" <- "))));
+
+        // verify the dependences
+        assertEquals(paths.size(), expected.length);
+
+        for (int i=0; i < paths.size(); i++) {
+            String[] path = paths.get(i);
+            boolean noneMatched = Arrays.stream(expected)
+                    .filter(array -> array.length == path.length)
+                    .noneMatch(array -> Arrays.equals(array, path));
+            if (noneMatched)
+                System.err.format("Expected: %s found: %s%n",
+                                  Arrays.stream(expected)
+                                      .map(Arrays::toString)
+                                      .collect(Collectors.joining(", ")),
+                    Arrays.toString(path));
+
+            assertFalse(noneMatched);
+        }
+
+    }
+
+}