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 package com.sun.tools.jdeps; |
|
26 |
|
27 import java.io.PrintStream; |
|
28 import java.util.Comparator; |
|
29 import java.util.HashMap; |
|
30 import java.util.HashSet; |
|
31 import java.util.List; |
|
32 import java.util.Map; |
|
33 import java.util.Objects; |
|
34 import java.util.Set; |
|
35 import java.util.stream.Collectors; |
|
36 import java.util.stream.Stream; |
|
37 |
|
38 import com.sun.tools.classfile.Dependency.Location; |
|
39 |
|
40 /** |
|
41 * Dependency Analyzer. |
|
42 */ |
|
43 public class Analyzer { |
|
44 /** |
|
45 * Type of the dependency analysis. Appropriate level of data |
|
46 * will be stored. |
|
47 */ |
|
48 public enum Type { |
|
49 SUMMARY, |
|
50 PACKAGE, |
|
51 CLASS, |
|
52 VERBOSE |
|
53 } |
|
54 |
|
55 /** |
|
56 * Filter to be applied when analyzing the dependencies from the given archives. |
|
57 * Only the accepted dependencies are recorded. |
|
58 */ |
|
59 interface Filter { |
|
60 boolean accepts(Location origin, Archive originArchive, Location target, Archive targetArchive); |
|
61 } |
|
62 |
|
63 protected final Type type; |
|
64 protected final Filter filter; |
|
65 protected final Map<Archive, ArchiveDeps> results = new HashMap<>(); |
|
66 protected final Map<Location, Archive> map = new HashMap<>(); |
|
67 private static final Archive NOT_FOUND |
|
68 = new Archive(JdepsTask.getMessage("artifact.not.found")); |
|
69 |
|
70 /** |
|
71 * Constructs an Analyzer instance. |
|
72 * |
|
73 * @param type Type of the dependency analysis |
|
74 * @param filter |
|
75 */ |
|
76 public Analyzer(Type type, Filter filter) { |
|
77 this.type = type; |
|
78 this.filter = filter; |
|
79 } |
|
80 |
|
81 /** |
|
82 * Performs the dependency analysis on the given archives. |
|
83 */ |
|
84 public boolean run(List<Archive> archives) { |
|
85 // build a map from Location to Archive |
|
86 buildLocationArchiveMap(archives); |
|
87 |
|
88 // traverse and analyze all dependencies |
|
89 for (Archive archive : archives) { |
|
90 ArchiveDeps deps = new ArchiveDeps(archive, type); |
|
91 archive.visitDependences(deps); |
|
92 results.put(archive, deps); |
|
93 } |
|
94 return true; |
|
95 } |
|
96 |
|
97 protected void buildLocationArchiveMap(List<Archive> archives) { |
|
98 // build a map from Location to Archive |
|
99 for (Archive archive: archives) { |
|
100 for (Location l: archive.getClasses()) { |
|
101 if (!map.containsKey(l)) { |
|
102 map.put(l, archive); |
|
103 } else { |
|
104 // duplicated class warning? |
|
105 } |
|
106 } |
|
107 } |
|
108 } |
|
109 |
|
110 public boolean hasDependences(Archive archive) { |
|
111 if (results.containsKey(archive)) { |
|
112 return results.get(archive).dependencies().size() > 0; |
|
113 } |
|
114 return false; |
|
115 } |
|
116 |
|
117 public Set<String> dependences(Archive source) { |
|
118 ArchiveDeps result = results.get(source); |
|
119 return result.dependencies().stream() |
|
120 .map(Dep::target) |
|
121 .collect(Collectors.toSet()); |
|
122 } |
|
123 |
|
124 public interface Visitor { |
|
125 /** |
|
126 * Visits a recorded dependency from origin to target which can be |
|
127 * a fully-qualified classname, a package name, a module or |
|
128 * archive name depending on the Analyzer's type. |
|
129 */ |
|
130 public void visitDependence(String origin, Archive originArchive, |
|
131 String target, Archive targetArchive); |
|
132 } |
|
133 |
|
134 /** |
|
135 * Visit the dependencies of the given source. |
|
136 * If the requested level is SUMMARY, it will visit the required archives list. |
|
137 */ |
|
138 public void visitDependences(Archive source, Visitor v, Type level) { |
|
139 if (level == Type.SUMMARY) { |
|
140 final ArchiveDeps result = results.get(source); |
|
141 final Set<Archive> reqs = result.requires(); |
|
142 Stream<Archive> stream = reqs.stream(); |
|
143 if (reqs.isEmpty()) { |
|
144 if (hasDependences(source)) { |
|
145 // If reqs.isEmpty() and we have dependences, then it means |
|
146 // that the dependences are from 'source' onto itself. |
|
147 stream = Stream.of(source); |
|
148 } |
|
149 } |
|
150 stream.sorted(Comparator.comparing(Archive::getName)) |
|
151 .forEach(archive -> { |
|
152 Profile profile = result.getTargetProfile(archive); |
|
153 v.visitDependence(source.getName(), source, |
|
154 profile != null ? profile.profileName() : archive.getName(), archive); |
|
155 }); |
|
156 } else { |
|
157 ArchiveDeps result = results.get(source); |
|
158 if (level != type) { |
|
159 // requesting different level of analysis |
|
160 result = new ArchiveDeps(source, level); |
|
161 source.visitDependences(result); |
|
162 } |
|
163 result.dependencies().stream() |
|
164 .sorted(Comparator.comparing(Dep::origin) |
|
165 .thenComparing(Dep::target)) |
|
166 .forEach(d -> v.visitDependence(d.origin(), d.originArchive(), d.target(), d.targetArchive())); |
|
167 } |
|
168 } |
|
169 |
|
170 public void visitDependences(Archive source, Visitor v) { |
|
171 visitDependences(source, v, type); |
|
172 } |
|
173 |
|
174 /** |
|
175 * ArchiveDeps contains the dependencies for an Archive that can have one or |
|
176 * more classes. |
|
177 */ |
|
178 class ArchiveDeps implements Archive.Visitor { |
|
179 protected final Archive archive; |
|
180 protected final Set<Archive> requires; |
|
181 protected final Set<Dep> deps; |
|
182 protected final Type level; |
|
183 private Profile profile; |
|
184 ArchiveDeps(Archive archive, Type level) { |
|
185 this.archive = archive; |
|
186 this.deps = new HashSet<>(); |
|
187 this.requires = new HashSet<>(); |
|
188 this.level = level; |
|
189 } |
|
190 |
|
191 Set<Dep> dependencies() { |
|
192 return deps; |
|
193 } |
|
194 |
|
195 Set<Archive> requires() { |
|
196 return requires; |
|
197 } |
|
198 |
|
199 Profile getTargetProfile(Archive target) { |
|
200 if (target instanceof Module) { |
|
201 return Profile.getProfile((Module) target); |
|
202 } else { |
|
203 return null; |
|
204 } |
|
205 } |
|
206 |
|
207 Archive findArchive(Location t) { |
|
208 Archive target = archive.getClasses().contains(t) ? archive : map.get(t); |
|
209 if (target == null) { |
|
210 map.put(t, target = NOT_FOUND); |
|
211 } |
|
212 return target; |
|
213 } |
|
214 |
|
215 // return classname or package name depedning on the level |
|
216 private String getLocationName(Location o) { |
|
217 if (level == Type.CLASS || level == Type.VERBOSE) { |
|
218 return o.getClassName(); |
|
219 } else { |
|
220 String pkg = o.getPackageName(); |
|
221 return pkg.isEmpty() ? "<unnamed>" : pkg; |
|
222 } |
|
223 } |
|
224 |
|
225 @Override |
|
226 public void visit(Location o, Location t) { |
|
227 Archive targetArchive = findArchive(t); |
|
228 if (filter.accepts(o, archive, t, targetArchive)) { |
|
229 addDep(o, t); |
|
230 if (archive != targetArchive && !requires.contains(targetArchive)) { |
|
231 requires.add(targetArchive); |
|
232 } |
|
233 } |
|
234 if (targetArchive instanceof Module) { |
|
235 Profile p = Profile.getProfile(t.getPackageName()); |
|
236 if (profile == null || (p != null && p.compareTo(profile) > 0)) { |
|
237 profile = p; |
|
238 } |
|
239 } |
|
240 } |
|
241 |
|
242 private Dep curDep; |
|
243 protected Dep addDep(Location o, Location t) { |
|
244 String origin = getLocationName(o); |
|
245 String target = getLocationName(t); |
|
246 Archive targetArchive = findArchive(t); |
|
247 if (curDep != null && |
|
248 curDep.origin().equals(origin) && |
|
249 curDep.originArchive() == archive && |
|
250 curDep.target().equals(target) && |
|
251 curDep.targetArchive() == targetArchive) { |
|
252 return curDep; |
|
253 } |
|
254 |
|
255 Dep e = new Dep(origin, archive, target, targetArchive); |
|
256 if (deps.contains(e)) { |
|
257 for (Dep e1 : deps) { |
|
258 if (e.equals(e1)) { |
|
259 curDep = e1; |
|
260 } |
|
261 } |
|
262 } else { |
|
263 deps.add(e); |
|
264 curDep = e; |
|
265 } |
|
266 return curDep; |
|
267 } |
|
268 } |
|
269 |
|
270 /* |
|
271 * Class-level or package-level dependency |
|
272 */ |
|
273 class Dep { |
|
274 final String origin; |
|
275 final Archive originArchive; |
|
276 final String target; |
|
277 final Archive targetArchive; |
|
278 |
|
279 Dep(String origin, Archive originArchive, String target, Archive targetArchive) { |
|
280 this.origin = origin; |
|
281 this.originArchive = originArchive; |
|
282 this.target = target; |
|
283 this.targetArchive = targetArchive; |
|
284 } |
|
285 |
|
286 String origin() { |
|
287 return origin; |
|
288 } |
|
289 |
|
290 Archive originArchive() { |
|
291 return originArchive; |
|
292 } |
|
293 |
|
294 String target() { |
|
295 return target; |
|
296 } |
|
297 |
|
298 Archive targetArchive() { |
|
299 return targetArchive; |
|
300 } |
|
301 |
|
302 @Override |
|
303 @SuppressWarnings("unchecked") |
|
304 public boolean equals(Object o) { |
|
305 if (o instanceof Dep) { |
|
306 Dep d = (Dep) o; |
|
307 return this.origin.equals(d.origin) && |
|
308 this.originArchive == d.originArchive && |
|
309 this.target.equals(d.target) && |
|
310 this.targetArchive == d.targetArchive; |
|
311 } |
|
312 return false; |
|
313 } |
|
314 |
|
315 @Override |
|
316 public int hashCode() { |
|
317 int hash = 7; |
|
318 hash = 67*hash + Objects.hashCode(this.origin) |
|
319 + Objects.hashCode(this.originArchive) |
|
320 + Objects.hashCode(this.target) |
|
321 + Objects.hashCode(this.targetArchive); |
|
322 return hash; |
|
323 } |
|
324 |
|
325 public String toString() { |
|
326 return String.format("%s (%s) -> %s (%s)%n", |
|
327 origin, originArchive.getName(), |
|
328 target, targetArchive.getName()); |
|
329 } |
|
330 } |
|
331 |
|
332 static Analyzer getExportedAPIsAnalyzer() { |
|
333 return new ModuleAccessAnalyzer(ModuleAccessAnalyzer.reexportsFilter, true); |
|
334 } |
|
335 |
|
336 static Analyzer getModuleAccessAnalyzer() { |
|
337 return new ModuleAccessAnalyzer(ModuleAccessAnalyzer.accessCheckFilter, false); |
|
338 } |
|
339 |
|
340 private static class ModuleAccessAnalyzer extends Analyzer { |
|
341 private final boolean apionly; |
|
342 ModuleAccessAnalyzer(Filter filter, boolean apionly) { |
|
343 super(Type.VERBOSE, filter); |
|
344 this.apionly = apionly; |
|
345 } |
|
346 /** |
|
347 * Verify module access |
|
348 */ |
|
349 public boolean run(List<Archive> archives) { |
|
350 // build a map from Location to Archive |
|
351 buildLocationArchiveMap(archives); |
|
352 |
|
353 // traverse and analyze all dependencies |
|
354 int count = 0; |
|
355 for (Archive archive : archives) { |
|
356 ArchiveDeps checker = new ArchiveDeps(archive, type); |
|
357 archive.visitDependences(checker); |
|
358 count += checker.dependencies().size(); |
|
359 // output if any error |
|
360 Module m = (Module)archive; |
|
361 printDependences(System.err, m, checker.dependencies()); |
|
362 results.put(archive, checker); |
|
363 } |
|
364 return count == 0; |
|
365 } |
|
366 |
|
367 private void printDependences(PrintStream out, Module m, Set<Dep> deps) { |
|
368 if (deps.isEmpty()) |
|
369 return; |
|
370 |
|
371 String msg = apionly ? "API reference:" : "inaccessible reference:"; |
|
372 deps.stream().sorted(Comparator.comparing(Dep::origin) |
|
373 .thenComparing(Dep::target)) |
|
374 .forEach(d -> out.format("%s %s (%s) -> %s (%s)%n", msg, |
|
375 d.origin(), d.originArchive().getName(), |
|
376 d.target(), d.targetArchive().getName())); |
|
377 if (apionly) { |
|
378 out.format("Dependences missing re-exports=\"true\" attribute:%n"); |
|
379 deps.stream() |
|
380 .map(Dep::targetArchive) |
|
381 .map(Archive::getName) |
|
382 .distinct() |
|
383 .sorted() |
|
384 .forEach(d -> out.format(" %s -> %s%n", m.name(), d)); |
|
385 } |
|
386 } |
|
387 |
|
388 private static Module findModule(Archive archive) { |
|
389 if (Module.class.isInstance(archive)) { |
|
390 return (Module) archive; |
|
391 } else { |
|
392 return null; |
|
393 } |
|
394 } |
|
395 |
|
396 // returns true if target is accessible by origin |
|
397 private static boolean canAccess(Location o, Archive originArchive, Location t, Archive targetArchive) { |
|
398 Module origin = findModule(originArchive); |
|
399 Module target = findModule(targetArchive); |
|
400 |
|
401 if (targetArchive == Analyzer.NOT_FOUND) { |
|
402 return false; |
|
403 } |
|
404 |
|
405 // unnamed module |
|
406 // ## should check public type? |
|
407 if (target == null) |
|
408 return true; |
|
409 |
|
410 // module-private |
|
411 if (origin == target) |
|
412 return true; |
|
413 |
|
414 return target.isAccessibleTo(t.getClassName(), origin); |
|
415 } |
|
416 |
|
417 static final Filter accessCheckFilter = new Filter() { |
|
418 @Override |
|
419 public boolean accepts(Location o, Archive originArchive, Location t, Archive targetArchive) { |
|
420 return !canAccess(o, originArchive, t, targetArchive); |
|
421 } |
|
422 }; |
|
423 |
|
424 static final Filter reexportsFilter = new Filter() { |
|
425 @Override |
|
426 public boolean accepts(Location o, Archive originArchive, Location t, Archive targetArchive) { |
|
427 Module origin = findModule(originArchive); |
|
428 Module target = findModule(targetArchive); |
|
429 if (!origin.isExportedPackage(o.getPackageName())) { |
|
430 // filter non-exported classes |
|
431 return false; |
|
432 } |
|
433 |
|
434 boolean accessible = canAccess(o, originArchive, t, targetArchive); |
|
435 if (!accessible) |
|
436 return true; |
|
437 |
|
438 String mn = target.name(); |
|
439 // skip checking re-exports for java.base |
|
440 if (origin == target || "java.base".equals(mn)) |
|
441 return false; |
|
442 |
|
443 assert origin.requires().containsKey(mn); // otherwise, should not be accessible |
|
444 if (origin.requires().get(mn)) { |
|
445 return false; |
|
446 } |
|
447 return true; |
|
448 } |
|
449 }; |
|
450 } |
|
451 } |
|