DependencyFinder.java revision 3792:d516975e8110
1/*
2 * Copyright (c) 2015, 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 */
25package com.sun.tools.jdeps;
26
27import static com.sun.tools.jdeps.Module.*;
28import static com.sun.tools.jdeps.Analyzer.NOT_FOUND;
29import static java.util.stream.Collectors.*;
30
31import com.sun.tools.classfile.AccessFlags;
32import com.sun.tools.classfile.ClassFile;
33import com.sun.tools.classfile.ConstantPoolException;
34import com.sun.tools.classfile.Dependencies;
35import com.sun.tools.classfile.Dependency;
36import com.sun.tools.classfile.Dependency.Location;
37
38import java.io.IOException;
39import java.io.UncheckedIOException;
40import java.util.Collections;
41import java.util.Deque;
42import java.util.HashMap;
43import java.util.HashSet;
44import java.util.Map;
45import java.util.Optional;
46import java.util.Set;
47import java.util.concurrent.Callable;
48import java.util.concurrent.ConcurrentHashMap;
49import java.util.concurrent.ConcurrentLinkedDeque;
50import java.util.concurrent.ExecutionException;
51import java.util.concurrent.ExecutorService;
52import java.util.concurrent.Executors;
53import java.util.concurrent.FutureTask;
54import java.util.stream.Stream;
55
56/**
57 * Parses class files and finds dependences
58 */
59class DependencyFinder {
60    private static Finder API_FINDER = new Finder(true);
61    private static Finder CLASS_FINDER = new Finder(false);
62
63    private final JdepsConfiguration configuration;
64    private final JdepsFilter filter;
65
66    private final Map<Finder, Deque<Archive>> parsedArchives = new ConcurrentHashMap<>();
67    private final Map<Location, Archive> parsedClasses = new ConcurrentHashMap<>();
68
69    private final ExecutorService pool = Executors.newFixedThreadPool(2);
70    private final Deque<FutureTask<Set<Location>>> tasks = new ConcurrentLinkedDeque<>();
71
72    DependencyFinder(JdepsConfiguration configuration,
73                     JdepsFilter filter) {
74        this.configuration = configuration;
75        this.filter = filter;
76        this.parsedArchives.put(API_FINDER, new ConcurrentLinkedDeque<>());
77        this.parsedArchives.put(CLASS_FINDER, new ConcurrentLinkedDeque<>());
78    }
79
80    Map<Location, Archive> locationToArchive() {
81        return parsedClasses;
82    }
83
84    /**
85     * Returns the modules of all dependencies found
86     */
87    Stream<Archive> getDependences(Archive source) {
88        return source.getDependencies()
89                     .map(this::locationToArchive)
90                     .filter(a -> a != source);
91    }
92
93    /**
94     * Returns the location to archive map; or NOT_FOUND.
95     *
96     * Location represents a parsed class.
97     */
98    Archive locationToArchive(Location location) {
99        return parsedClasses.containsKey(location)
100            ? parsedClasses.get(location)
101            : configuration.findClass(location).orElse(NOT_FOUND);
102    }
103
104    /**
105     * Returns a map from an archive to its required archives
106     */
107    Map<Archive, Set<Archive>> dependences() {
108        Map<Archive, Set<Archive>> map = new HashMap<>();
109        parsedArchives.values().stream()
110            .flatMap(Deque::stream)
111            .filter(a -> !a.isEmpty())
112            .forEach(source -> {
113                Set<Archive> deps = getDependences(source).collect(toSet());
114                if (!deps.isEmpty()) {
115                    map.put(source, deps);
116                }
117        });
118        return map;
119    }
120
121    boolean isParsed(Location location) {
122        return parsedClasses.containsKey(location);
123    }
124
125    /**
126     * Parses all class files from the given archive stream and returns
127     * all target locations.
128     */
129    public Set<Location> parse(Stream<? extends Archive> archiveStream) {
130        archiveStream.forEach(archive -> parse(archive, CLASS_FINDER));
131        return waitForTasksCompleted();
132    }
133
134    /**
135     * Parses the exported API class files from the given archive stream and
136     * returns all target locations.
137     */
138    public Set<Location> parseExportedAPIs(Stream<? extends Archive> archiveStream) {
139        archiveStream.forEach(archive -> parse(archive, API_FINDER));
140        return waitForTasksCompleted();
141    }
142
143    /**
144     * Parses the named class from the given archive and
145     * returns all target locations the named class references.
146     */
147    public Set<Location> parse(Archive archive, String name) {
148        try {
149            return parse(archive, CLASS_FINDER, name);
150        } catch (IOException e) {
151            throw new UncheckedIOException(e);
152        }
153    }
154
155    /**
156     * Parses the exported API of the named class from the given archive and
157     * returns all target locations the named class references.
158     */
159    public Set<Location> parseExportedAPIs(Archive archive, String name)
160    {
161        try {
162            return parse(archive, API_FINDER, name);
163        } catch (IOException e) {
164            throw new UncheckedIOException(e);
165        }
166    }
167
168    private Optional<FutureTask<Set<Location>>> parse(Archive archive, Finder finder) {
169        if (parsedArchives.get(finder).contains(archive))
170            return Optional.empty();
171
172        parsedArchives.get(finder).add(archive);
173
174        trace("parsing %s %s%n", archive.getName(), archive.path());
175        FutureTask<Set<Location>> task = new FutureTask<>(new Callable<>() {
176            public Set<Location> call() throws Exception {
177                Set<Location> targets = new HashSet<>();
178                for (ClassFile cf : archive.reader().getClassFiles()) {
179                    if (cf.access_flags.is(AccessFlags.ACC_MODULE))
180                        continue;
181
182                    String classFileName;
183                    try {
184                        classFileName = cf.getName();
185                    } catch (ConstantPoolException e) {
186                        throw new Dependencies.ClassFileError(e);
187                    }
188
189                    // filter source class/archive
190                    String cn = classFileName.replace('/', '.');
191                    if (!finder.accept(archive, cn, cf.access_flags))
192                        continue;
193
194                    // tests if this class matches the -include
195                    if (!filter.matches(cn))
196                        continue;
197
198                    for (Dependency d : finder.findDependencies(cf)) {
199                        if (filter.accepts(d)) {
200                            archive.addClass(d.getOrigin(), d.getTarget());
201                            targets.add(d.getTarget());
202                        } else {
203                            // ensure that the parsed class is added the archive
204                            archive.addClass(d.getOrigin());
205                        }
206                        parsedClasses.putIfAbsent(d.getOrigin(), archive);
207                    }
208                }
209
210                return targets;
211            }
212        });
213        tasks.add(task);
214        pool.submit(task);
215        return Optional.of(task);
216    }
217
218    private Set<Location> parse(Archive archive, Finder finder, String name)
219        throws IOException
220    {
221        ClassFile cf = archive.reader().getClassFile(name);
222        if (cf == null) {
223            throw new IllegalArgumentException(archive.getName() +
224                " does not contain " + name);
225        }
226
227        if (cf.access_flags.is(AccessFlags.ACC_MODULE))
228            return Collections.emptySet();
229
230        Set<Location> targets = new HashSet<>();
231        String cn;
232        try {
233            cn =  cf.getName().replace('/', '.');
234        } catch (ConstantPoolException e) {
235            throw new Dependencies.ClassFileError(e);
236        }
237
238        if (!finder.accept(archive, cn, cf.access_flags))
239            return targets;
240
241        // tests if this class matches the -include
242        if (!filter.matches(cn))
243            return targets;
244
245        // skip checking filter.matches
246        for (Dependency d : finder.findDependencies(cf)) {
247            if (filter.accepts(d)) {
248                targets.add(d.getTarget());
249                archive.addClass(d.getOrigin(), d.getTarget());
250            } else {
251                // ensure that the parsed class is added the archive
252                archive.addClass(d.getOrigin());
253            }
254            parsedClasses.putIfAbsent(d.getOrigin(), archive);
255        }
256        return targets;
257    }
258
259    /*
260     * Waits until all submitted tasks are completed.
261     */
262    private Set<Location> waitForTasksCompleted() {
263        try {
264            Set<Location> targets = new HashSet<>();
265            FutureTask<Set<Location>> task;
266            while ((task = tasks.poll()) != null) {
267                // wait for completion
268                if (!task.isDone())
269                    targets.addAll(task.get());
270            }
271            return targets;
272        } catch (InterruptedException|ExecutionException e) {
273            throw new Error(e);
274        }
275    }
276
277    /*
278     * Shutdown the executor service.
279     */
280    void shutdown() {
281        pool.shutdown();
282    }
283
284    private interface SourceFilter {
285        boolean accept(Archive archive, String cn, AccessFlags accessFlags);
286    }
287
288    private static class Finder implements Dependency.Finder, SourceFilter {
289        private final Dependency.Finder finder;
290        private final boolean apiOnly;
291        Finder(boolean apiOnly) {
292            this.apiOnly = apiOnly;
293            this.finder = apiOnly
294                ? Dependencies.getAPIFinder(AccessFlags.ACC_PROTECTED)
295                : Dependencies.getClassDependencyFinder();
296
297        }
298
299        @Override
300        public boolean accept(Archive archive, String cn, AccessFlags accessFlags) {
301            int i = cn.lastIndexOf('.');
302            String pn = i > 0 ? cn.substring(0, i) : "";
303
304            // if -apionly is specified, analyze only exported and public types
305            // All packages are exported in unnamed module.
306            return apiOnly ? archive.getModule().isExported(pn) &&
307                                 accessFlags.is(AccessFlags.ACC_PUBLIC)
308                           : true;
309        }
310
311        @Override
312        public Iterable<? extends Dependency> findDependencies(ClassFile classfile) {
313            return finder.findDependencies(classfile);
314        }
315    }
316}
317