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