DepsAnalyzer.java revision 3792:d516975e8110
1/*
2 * Copyright (c) 2016, 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
26package com.sun.tools.jdeps;
27
28import com.sun.tools.classfile.Dependency.Location;
29import java.io.IOException;
30import java.util.ArrayList;
31import java.util.Deque;
32import java.util.LinkedHashSet;
33import java.util.LinkedList;
34import java.util.List;
35import java.util.Optional;
36import java.util.Set;
37import java.util.concurrent.ConcurrentLinkedDeque;
38import java.util.stream.Collectors;
39import java.util.stream.Stream;
40
41import static com.sun.tools.jdeps.Analyzer.Type.CLASS;
42import static com.sun.tools.jdeps.Analyzer.Type.VERBOSE;
43import static com.sun.tools.jdeps.Module.trace;
44import static java.util.stream.Collectors.*;
45
46/**
47 * Dependency Analyzer.
48 *
49 * Type of filters:
50 * source filter: -include <pattern>
51 * target filter: -package, -regex, --require
52 *
53 * The initial archive set for analysis includes
54 * 1. archives specified in the command line arguments
55 * 2. observable modules matching the source filter
56 * 3. classpath archives matching the source filter or target filter
57 * 4. --add-modules and -m root modules
58 */
59public class DepsAnalyzer {
60    final JdepsConfiguration configuration;
61    final JdepsFilter filter;
62    final JdepsWriter writer;
63    final Analyzer.Type verbose;
64    final boolean apiOnly;
65
66    final DependencyFinder finder;
67    final Analyzer analyzer;
68    final List<Archive> rootArchives = new ArrayList<>();
69
70    // parsed archives
71    final Set<Archive> archives = new LinkedHashSet<>();
72
73    public DepsAnalyzer(JdepsConfiguration config,
74                        JdepsFilter filter,
75                        JdepsWriter writer,
76                        Analyzer.Type verbose,
77                        boolean apiOnly) {
78        this.configuration = config;
79        this.filter = filter;
80        this.writer = writer;
81        this.verbose = verbose;
82        this.apiOnly = apiOnly;
83
84        this.finder = new DependencyFinder(config, filter);
85        this.analyzer = new Analyzer(configuration, verbose, filter);
86
87        // determine initial archives to be analyzed
88        this.rootArchives.addAll(configuration.initialArchives());
89
90        // if -include pattern is specified, add the matching archives on
91        // classpath to the root archives
92        if (filter.hasIncludePattern() || filter.hasTargetFilter()) {
93            configuration.getModules().values().stream()
94                .filter(source -> filter.include(source) && filter.matches(source))
95                .forEach(this.rootArchives::add);
96        }
97
98        // class path archives
99        configuration.classPathArchives().stream()
100            .filter(filter::matches)
101            .forEach(this.rootArchives::add);
102
103        // Include the root modules for analysis
104        this.rootArchives.addAll(configuration.rootModules());
105
106        trace("analyze root archives: %s%n", this.rootArchives);
107    }
108
109    /*
110     * Perform runtime dependency analysis
111     */
112    public boolean run() throws IOException {
113        return run(false, 1);
114    }
115
116    /**
117     * Perform compile-time view or run-time view dependency analysis.
118     *
119     * @param compileTimeView
120     * @param maxDepth  depth of recursive analysis.  depth == 0 if -R is set
121     */
122    public boolean run(boolean compileTimeView, int maxDepth) throws IOException {
123        try {
124            // parse each packaged module or classpath archive
125            if (apiOnly) {
126                finder.parseExportedAPIs(rootArchives.stream());
127            } else {
128                finder.parse(rootArchives.stream());
129            }
130            archives.addAll(rootArchives);
131
132            int depth = maxDepth > 0 ? maxDepth : Integer.MAX_VALUE;
133
134            // transitive analysis
135            if (depth > 1) {
136                if (compileTimeView)
137                    transitiveArchiveDeps(depth-1);
138                else
139                    transitiveDeps(depth-1);
140            }
141
142            Set<Archive> archives = archives();
143
144            // analyze the dependencies collected
145            analyzer.run(archives, finder.locationToArchive());
146
147            if (writer != null) {
148                writer.generateOutput(archives, analyzer);
149            }
150        } finally {
151            finder.shutdown();
152        }
153        return true;
154    }
155
156    /**
157     * Returns the archives for reporting that has matching dependences.
158     *
159     * If --require is set, they should be excluded.
160     */
161    Set<Archive> archives() {
162        if (filter.requiresFilter().isEmpty()) {
163            return archives.stream()
164                .filter(filter::include)
165                .filter(Archive::hasDependences)
166                .collect(Collectors.toSet());
167        } else {
168            // use the archives that have dependences and not specified in --require
169            return archives.stream()
170                .filter(filter::include)
171                .filter(source -> !filter.requiresFilter().contains(source))
172                .filter(source ->
173                        source.getDependencies()
174                              .map(finder::locationToArchive)
175                              .anyMatch(a -> a != source))
176                .collect(Collectors.toSet());
177        }
178    }
179
180    /**
181     * Returns the dependences, either class name or package name
182     * as specified in the given verbose level.
183     */
184    Set<String> dependences() {
185        return analyzer.archives().stream()
186                       .map(analyzer::dependences)
187                       .flatMap(Set::stream)
188                       .collect(Collectors.toSet());
189    }
190
191    /**
192     * Returns the archives that contains the given locations and
193     * not parsed and analyzed.
194     */
195    private Set<Archive> unresolvedArchives(Stream<Location> locations) {
196        return locations.filter(l -> !finder.isParsed(l))
197                        .distinct()
198                        .map(configuration::findClass)
199                        .flatMap(Optional::stream)
200                        .filter(filter::include)
201                        .collect(toSet());
202    }
203
204    /*
205     * Recursively analyzes entire module/archives.
206    */
207    private void transitiveArchiveDeps(int depth) throws IOException {
208        Stream<Location> deps = archives.stream()
209                                        .flatMap(Archive::getDependencies);
210
211        // start with the unresolved archives
212        Set<Archive> unresolved = unresolvedArchives(deps);
213        do {
214            // parse all unresolved archives
215            Set<Location> targets = apiOnly
216                ? finder.parseExportedAPIs(unresolved.stream())
217                : finder.parse(unresolved.stream());
218            archives.addAll(unresolved);
219
220            // Add dependencies to the next batch for analysis
221            unresolved = unresolvedArchives(targets.stream());
222        } while (!unresolved.isEmpty() && depth-- > 0);
223    }
224
225    /*
226     * Recursively analyze the class dependences
227     */
228    private void transitiveDeps(int depth) throws IOException {
229        Stream<Location> deps = archives.stream()
230                                        .flatMap(Archive::getDependencies);
231
232        Deque<Location> unresolved = deps.collect(Collectors.toCollection(LinkedList::new));
233        ConcurrentLinkedDeque<Location> deque = new ConcurrentLinkedDeque<>();
234        do {
235            Location target;
236            while ((target = unresolved.poll()) != null) {
237                if (finder.isParsed(target))
238                    continue;
239
240                Archive archive = configuration.findClass(target).orElse(null);
241                if (archive != null && filter.include(archive)) {
242                    archives.add(archive);
243
244                    String name = target.getName();
245                    Set<Location> targets = apiOnly
246                            ? finder.parseExportedAPIs(archive, name)
247                            : finder.parse(archive, name);
248
249                    // build unresolved dependencies
250                    targets.stream()
251                           .filter(t -> !finder.isParsed(t))
252                           .forEach(deque::add);
253                }
254            }
255            unresolved = deque;
256            deque = new ConcurrentLinkedDeque<>();
257        } while (!unresolved.isEmpty() && depth-- > 0);
258    }
259
260    // ----- for testing purpose -----
261
262    public static enum Info {
263        REQUIRES,
264        REQUIRES_TRANSITIVE,
265        EXPORTED_API,
266        MODULE_PRIVATE,
267        QUALIFIED_EXPORTED_API,
268        INTERNAL_API,
269        JDK_INTERNAL_API,
270        JDK_REMOVED_INTERNAL_API
271    }
272
273    public static class Node {
274        public final String name;
275        public final String source;
276        public final Info info;
277        Node(String name, Info info) {
278            this(name, name, info);
279        }
280        Node(String name, String source, Info info) {
281            this.name = name;
282            this.source = source;
283            this.info = info;
284        }
285
286        @Override
287        public String toString() {
288            StringBuilder sb = new StringBuilder();
289            if (info != Info.REQUIRES && info != Info.REQUIRES_TRANSITIVE)
290                sb.append(source).append("/");
291
292            sb.append(name);
293            if (info == Info.QUALIFIED_EXPORTED_API)
294                sb.append(" (qualified)");
295            else if (info == Info.JDK_INTERNAL_API)
296                sb.append(" (JDK internal)");
297            else if (info == Info.INTERNAL_API)
298                sb.append(" (internal)");
299            return sb.toString();
300        }
301
302        @Override
303        public boolean equals(Object o) {
304            if (!(o instanceof Node))
305                return false;
306
307            Node other = (Node)o;
308            return this.name.equals(other.name) &&
309                    this.source.equals(other.source) &&
310                    this.info.equals(other.info);
311        }
312
313        @Override
314        public int hashCode() {
315            int result = name.hashCode();
316            result = 31 * result + source.hashCode();
317            result = 31 * result + info.hashCode();
318            return result;
319        }
320    }
321
322    /**
323     * Returns a graph of module dependences.
324     *
325     * Each Node represents a module and each edge is a dependence.
326     * No analysis on "requires transitive".
327     */
328    public Graph<Node> moduleGraph() {
329        Graph.Builder<Node> builder = new Graph.Builder<>();
330
331        archives().stream()
332            .forEach(m -> {
333                Node u = new Node(m.getName(), Info.REQUIRES);
334                builder.addNode(u);
335                analyzer.requires(m)
336                    .map(req -> new Node(req.getName(), Info.REQUIRES))
337                    .forEach(v -> builder.addEdge(u, v));
338            });
339        return builder.build();
340    }
341
342    /**
343     * Returns a graph of dependences.
344     *
345     * Each Node represents a class or package per the specified verbose level.
346     * Each edge indicates
347     */
348    public Graph<Node> dependenceGraph() {
349        Graph.Builder<Node> builder = new Graph.Builder<>();
350
351        archives().stream()
352            .map(analyzer.results::get)
353            .filter(deps -> !deps.dependencies().isEmpty())
354            .flatMap(deps -> deps.dependencies().stream())
355            .forEach(d -> addEdge(builder, d));
356        return builder.build();
357    }
358
359    private void addEdge(Graph.Builder<Node> builder, Analyzer.Dep dep) {
360        Archive source = dep.originArchive();
361        Archive target = dep.targetArchive();
362        String pn = dep.target();
363        if (verbose == CLASS || verbose == VERBOSE) {
364            int i = dep.target().lastIndexOf('.');
365            pn = i > 0 ? dep.target().substring(0, i) : "";
366        }
367        final Info info;
368        Module targetModule = target.getModule();
369        if (source == target) {
370            info = Info.MODULE_PRIVATE;
371        } else if (!targetModule.isNamed()) {
372            info = Info.EXPORTED_API;
373        } else if (targetModule.isExported(pn) && !targetModule.isJDKUnsupported()) {
374            info = Info.EXPORTED_API;
375        } else {
376            Module module = target.getModule();
377            if (module == Analyzer.REMOVED_JDK_INTERNALS) {
378                info = Info.JDK_REMOVED_INTERNAL_API;
379            } else if (!source.getModule().isJDK() && module.isJDK())
380                info = Info.JDK_INTERNAL_API;
381                // qualified exports or inaccessible
382            else if (module.isExported(pn, source.getModule().name()))
383                info = Info.QUALIFIED_EXPORTED_API;
384            else
385                info = Info.INTERNAL_API;
386        }
387
388        Node u = new Node(dep.origin(), source.getName(), info);
389        Node v = new Node(dep.target(), target.getName(), info);
390        builder.addEdge(u, v);
391    }
392
393}
394