1/*
2 * Copyright (c) 2013, 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
26package com.sun.tools.jdeps;
27
28import com.sun.tools.classfile.Dependency.Location;
29
30import java.io.BufferedReader;
31import java.io.IOException;
32import java.io.InputStream;
33import java.io.InputStreamReader;
34import java.io.UncheckedIOException;
35import java.util.Collections;
36import java.util.Comparator;
37import java.util.HashMap;
38import java.util.HashSet;
39import java.util.Map;
40import java.util.Objects;
41import java.util.Set;
42import java.util.stream.Collectors;
43import java.util.stream.Stream;
44
45/**
46 * Dependency Analyzer.
47 */
48public class Analyzer {
49    /**
50     * Type of the dependency analysis.  Appropriate level of data
51     * will be stored.
52     */
53    public enum Type {
54        SUMMARY,
55        MODULE,  // equivalent to summary in addition, print module descriptor
56        PACKAGE,
57        CLASS,
58        VERBOSE
59    }
60
61    /**
62     * Filter to be applied when analyzing the dependencies from the given archives.
63     * Only the accepted dependencies are recorded.
64     */
65    interface Filter {
66        boolean accepts(Location origin, Archive originArchive,
67                        Location target, Archive targetArchive);
68    }
69
70    protected final JdepsConfiguration configuration;
71    protected final Type type;
72    protected final Filter filter;
73    protected final Map<Archive, Dependences> results = new HashMap<>();
74    protected final Map<Location, Archive> locationToArchive = new HashMap<>();
75    static final Archive NOT_FOUND
76        = new Archive(JdepsTask.getMessage("artifact.not.found"));
77
78    /**
79     * Constructs an Analyzer instance.
80     *
81     * @param type Type of the dependency analysis
82     * @param filter
83     */
84    Analyzer(JdepsConfiguration config, Type type, Filter filter) {
85        this.configuration = config;
86        this.type = type;
87        this.filter = filter;
88    }
89
90    /**
91     * Performs the dependency analysis on the given archives.
92     */
93    boolean run(Iterable<? extends Archive> archives,
94                Map<Location, Archive> locationMap)
95    {
96        this.locationToArchive.putAll(locationMap);
97
98        // traverse and analyze all dependencies
99        for (Archive archive : archives) {
100            Dependences deps = new Dependences(archive, type);
101            archive.visitDependences(deps);
102            results.put(archive, deps);
103        }
104        return true;
105    }
106
107    /**
108     * Returns the analyzed archives
109     */
110    Set<Archive> archives() {
111        return results.keySet();
112    }
113
114    /**
115     * Returns true if the given archive has dependences.
116     */
117    boolean hasDependences(Archive archive) {
118        if (results.containsKey(archive)) {
119            return results.get(archive).dependencies().size() > 0;
120        }
121        return false;
122    }
123
124    /**
125     * Returns the dependences, either class name or package name
126     * as specified in the given verbose level, from the given source.
127     */
128    Set<String> dependences(Archive source) {
129        if (!results.containsKey(source)) {
130            return Collections.emptySet();
131        }
132
133        return results.get(source).dependencies()
134                      .stream()
135                      .map(Dep::target)
136                      .collect(Collectors.toSet());
137    }
138
139    /**
140     * Returns the direct dependences of the given source
141     */
142    Stream<Archive> requires(Archive source) {
143        if (!results.containsKey(source)) {
144            return Stream.empty();
145        }
146        return results.get(source).requires()
147                      .stream();
148    }
149
150    interface Visitor {
151        /**
152         * Visits a recorded dependency from origin to target which can be
153         * a fully-qualified classname, a package name, a module or
154         * archive name depending on the Analyzer's type.
155         */
156        public void visitDependence(String origin, Archive originArchive,
157                                    String target, Archive targetArchive);
158    }
159
160    /**
161     * Visit the dependencies of the given source.
162     * If the requested level is SUMMARY, it will visit the required archives list.
163     */
164    void visitDependences(Archive source, Visitor v, Type level) {
165        if (level == Type.SUMMARY) {
166            final Dependences result = results.get(source);
167            final Set<Archive> reqs = result.requires();
168            Stream<Archive> stream = reqs.stream();
169            if (reqs.isEmpty()) {
170                if (hasDependences(source)) {
171                    // If reqs.isEmpty() and we have dependences, then it means
172                    // that the dependences are from 'source' onto itself.
173                    stream = Stream.of(source);
174                }
175            }
176            stream.sorted(Comparator.comparing(Archive::getName))
177                  .forEach(archive -> {
178                      Profile profile = result.getTargetProfile(archive);
179                      v.visitDependence(source.getName(), source,
180                                        profile != null ? profile.profileName()
181                                                        : archive.getName(), archive);
182                  });
183        } else {
184            Dependences result = results.get(source);
185            if (level != type) {
186                // requesting different level of analysis
187                result = new Dependences(source, level);
188                source.visitDependences(result);
189            }
190            result.dependencies().stream()
191                  .sorted(Comparator.comparing(Dep::origin)
192                                    .thenComparing(Dep::target))
193                  .forEach(d -> v.visitDependence(d.origin(), d.originArchive(),
194                                                  d.target(), d.targetArchive()));
195        }
196    }
197
198    void visitDependences(Archive source, Visitor v) {
199        visitDependences(source, v, type);
200    }
201
202    /**
203     * Dependences contains the dependencies for an Archive that can have one or
204     * more classes.
205     */
206    class Dependences implements Archive.Visitor {
207        protected final Archive archive;
208        protected final Set<Archive> requires;
209        protected final Set<Dep> deps;
210        protected final Type level;
211        private Profile profile;
212        Dependences(Archive archive, Type level) {
213            this.archive = archive;
214            this.deps = new HashSet<>();
215            this.requires = new HashSet<>();
216            this.level = level;
217        }
218
219        Set<Dep> dependencies() {
220            return deps;
221        }
222
223        Set<Archive> requires() {
224            return requires;
225        }
226
227        Profile getTargetProfile(Archive target) {
228            if (target.getModule().isJDK()) {
229                return Profile.getProfile((Module) target);
230            } else {
231                return null;
232            }
233        }
234
235        /*
236         * Returns the archive that contains the given location.
237         */
238        Archive findArchive(Location t) {
239            // local in this archive
240            if (archive.getClasses().contains(t))
241                return archive;
242
243            Archive target;
244            if (locationToArchive.containsKey(t)) {
245                target = locationToArchive.get(t);
246            } else {
247                // special case JDK removed API
248                target = configuration.findClass(t)
249                    .orElseGet(() -> REMOVED_JDK_INTERNALS.contains(t)
250                                        ? REMOVED_JDK_INTERNALS
251                                        : NOT_FOUND);
252            }
253            return locationToArchive.computeIfAbsent(t, _k -> target);
254        }
255
256        // return classname or package name depending on the level
257        private String getLocationName(Location o) {
258            if (level == Type.CLASS || level == Type.VERBOSE) {
259                return VersionHelper.get(o.getClassName());
260            } else {
261                String pkg = o.getPackageName();
262                return pkg.isEmpty() ? "<unnamed>" : pkg;
263            }
264        }
265
266        @Override
267        public void visit(Location o, Location t) {
268            Archive targetArchive = findArchive(t);
269            if (filter.accepts(o, archive, t, targetArchive)) {
270                addDep(o, t);
271                if (archive != targetArchive && !requires.contains(targetArchive)) {
272                    requires.add(targetArchive);
273                }
274            }
275            if (targetArchive.getModule().isNamed()) {
276                Profile p = Profile.getProfile(t.getPackageName());
277                if (profile == null || (p != null && p.compareTo(profile) > 0)) {
278                    profile = p;
279                }
280            }
281        }
282
283        private Dep curDep;
284        protected Dep addDep(Location o, Location t) {
285            String origin = getLocationName(o);
286            String target = getLocationName(t);
287            Archive targetArchive = findArchive(t);
288            if (curDep != null &&
289                    curDep.origin().equals(origin) &&
290                    curDep.originArchive() == archive &&
291                    curDep.target().equals(target) &&
292                    curDep.targetArchive() == targetArchive) {
293                return curDep;
294            }
295
296            Dep e = new Dep(origin, archive, target, targetArchive);
297            if (deps.contains(e)) {
298                for (Dep e1 : deps) {
299                    if (e.equals(e1)) {
300                        curDep = e1;
301                    }
302                }
303            } else {
304                deps.add(e);
305                curDep = e;
306            }
307            return curDep;
308        }
309    }
310
311    /*
312     * Class-level or package-level dependency
313     */
314    class Dep {
315        final String origin;
316        final Archive originArchive;
317        final String target;
318        final Archive targetArchive;
319
320        Dep(String origin, Archive originArchive, String target, Archive targetArchive) {
321            this.origin = origin;
322            this.originArchive = originArchive;
323            this.target = target;
324            this.targetArchive = targetArchive;
325        }
326
327        String origin() {
328            return origin;
329        }
330
331        Archive originArchive() {
332            return originArchive;
333        }
334
335        String target() {
336            return target;
337        }
338
339        Archive targetArchive() {
340            return targetArchive;
341        }
342
343        @Override
344        @SuppressWarnings("unchecked")
345        public boolean equals(Object o) {
346            if (o instanceof Dep) {
347                Dep d = (Dep) o;
348                return this.origin.equals(d.origin) &&
349                        this.originArchive == d.originArchive &&
350                        this.target.equals(d.target) &&
351                        this.targetArchive == d.targetArchive;
352            }
353            return false;
354        }
355
356        @Override
357        public int hashCode() {
358            return Objects.hash(this.origin,
359                                this.originArchive,
360                                this.target,
361                                this.targetArchive);
362        }
363
364        public String toString() {
365            return String.format("%s (%s) -> %s (%s)%n",
366                    origin, originArchive.getName(),
367                    target, targetArchive.getName());
368        }
369    }
370
371    static final Jdk8Internals REMOVED_JDK_INTERNALS = new Jdk8Internals();
372
373    static class Jdk8Internals extends Module {
374        private final String JDK8_INTERNALS = "/com/sun/tools/jdeps/resources/jdk8_internals.txt";
375        private final Set<String> jdk8Internals;
376        private Jdk8Internals() {
377            super("JDK removed internal API");
378            try (InputStream in = JdepsTask.class.getResourceAsStream(JDK8_INTERNALS);
379                 BufferedReader reader = new BufferedReader(new InputStreamReader(in))) {
380                this.jdk8Internals = reader.lines()
381                                          .filter(ln -> !ln.startsWith("#"))
382                                          .collect(Collectors.toSet());
383            } catch (IOException e) {
384                throw new UncheckedIOException(e);
385            }
386        }
387
388        public boolean contains(Location location) {
389            String cn = location.getClassName();
390            int i = cn.lastIndexOf('.');
391            String pn = i > 0 ? cn.substring(0, i) : "";
392
393            return jdk8Internals.contains(pn);
394        }
395
396        @Override
397        public String name() {
398            return getName();
399        }
400
401        @Override
402        public boolean isJDK() {
403            return true;
404        }
405
406        @Override
407        public boolean isExported(String pn) {
408            return false;
409        }
410    }
411}
412