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 }