1 /* 2 * Copyright (c) 2016, 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 26 package com.sun.tools.jdeps; 27 28 import com.sun.tools.classfile.Dependency.Location; 29 import java.io.IOException; 30 import java.util.ArrayList; 31 import java.util.Deque; 32 import java.util.HashSet; 33 import java.util.LinkedHashSet; 34 import java.util.LinkedList; 35 import java.util.List; 36 import java.util.Map; 37 import java.util.Optional; 38 import java.util.Set; 39 import java.util.concurrent.ConcurrentLinkedDeque; 40 import java.util.stream.Collectors; 41 import java.util.stream.Stream; 42 43 import static com.sun.tools.jdeps.Analyzer.Type.CLASS; 44 import static com.sun.tools.jdeps.Analyzer.Type.VERBOSE; 45 import static com.sun.tools.jdeps.Module.trace; 46 import static java.util.stream.Collectors.*; 47 48 /** 49 * Dependency Analyzer. 50 * 51 * Type of filters: 52 * source filter: -include <pattern> 53 * target filter: -package, -regex, --require 54 * 55 * The initial archive set for analysis includes 56 * 1. archives specified in the command line arguments 57 * 2. observable modules matching the source filter 58 * 3. classpath archives matching the source filter or target filter 59 * 4. --add-modules and -m root modules 60 */ 61 public class DepsAnalyzer { 62 final JdepsConfiguration configuration; 63 final JdepsFilter filter; 64 final JdepsWriter writer; 65 final Analyzer.Type verbose; 66 final boolean apiOnly; 67 68 final DependencyFinder finder; 69 final Analyzer analyzer; 70 final List<Archive> rootArchives = new ArrayList<>(); 71 72 // parsed archives 73 final Set<Archive> archives = new LinkedHashSet<>(); 74 75 public DepsAnalyzer(JdepsConfiguration config, 76 JdepsFilter filter, 77 JdepsWriter writer, 78 Analyzer.Type verbose, 79 boolean apiOnly) { 80 this.configuration = config; 81 this.filter = filter; 82 this.writer = writer; 83 this.verbose = verbose; 84 this.apiOnly = apiOnly; 85 86 this.finder = new DependencyFinder(config, filter); 87 this.analyzer = new Analyzer(configuration, verbose, filter); 88 89 // determine initial archives to be analyzed 90 this.rootArchives.addAll(configuration.initialArchives()); 91 92 // if -include pattern is specified, add the matching archives on 93 // classpath to the root archives 94 if (filter.hasIncludePattern() || filter.hasTargetFilter()) { 95 configuration.getModules().values().stream() 96 .filter(source -> filter.include(source) && filter.matches(source)) 97 .forEach(this.rootArchives::add); 98 } 99 100 // class path archives 101 configuration.classPathArchives().stream() 102 .filter(filter::matches) 103 .forEach(this.rootArchives::add); 104 105 // Include the root modules for analysis 106 this.rootArchives.addAll(configuration.rootModules()); 107 108 trace("analyze root archives: %s%n", this.rootArchives); 109 } 110 111 /* 112 * Perform runtime dependency analysis 113 */ 114 public boolean run() throws IOException { 115 return run(false, 1); 116 } 117 118 /** 119 * Perform compile-time view or run-time view dependency analysis. 120 * 121 * @param compileTimeView 122 * @param maxDepth depth of recursive analysis. depth == 0 if -R is set 123 */ 124 public boolean run(boolean compileTimeView, int maxDepth) throws IOException { 125 try { 126 // parse each packaged module or classpath archive 127 if (apiOnly) { 128 finder.parseExportedAPIs(rootArchives.stream()); 129 } else { 130 finder.parse(rootArchives.stream()); 131 } 132 archives.addAll(rootArchives); 133 134 int depth = maxDepth > 0 ? maxDepth : Integer.MAX_VALUE; 135 136 // transitive analysis 137 if (depth > 1) { 138 if (compileTimeView) 139 transitiveArchiveDeps(depth-1); 140 else 141 transitiveDeps(depth-1); 142 } 143 144 Set<Archive> archives = archives(); 145 146 // analyze the dependencies collected 147 analyzer.run(archives, finder.locationToArchive()); 148 149 if (writer != null) { 150 writer.generateOutput(archives, analyzer); 151 } 152 } finally { 153 finder.shutdown(); 154 } 155 return true; 156 } 157 158 /** 159 * Returns the archives for reporting that has matching dependences. 160 * 161 * If --require is set, they should be excluded. 162 */ 163 Set<Archive> archives() { 164 if (filter.requiresFilter().isEmpty()) { 165 return archives.stream() 166 .filter(filter::include) 167 .filter(Archive::hasDependences) 168 .collect(Collectors.toSet()); 169 } else { 170 // use the archives that have dependences and not specified in --require 171 return archives.stream() 172 .filter(filter::include) 173 .filter(source -> !filter.requiresFilter().contains(source)) 174 .filter(source -> 175 source.getDependencies() 176 .map(finder::locationToArchive) 177 .anyMatch(a -> a != source)) 178 .collect(Collectors.toSet()); 179 } 180 } 181 182 /** 183 * Returns the dependences, either class name or package name 184 * as specified in the given verbose level. 185 */ 186 Set<String> dependences() { 187 return analyzer.archives().stream() 188 .map(analyzer::dependences) 189 .flatMap(Set::stream) 190 .collect(Collectors.toSet()); 191 } 192 193 /** 194 * Returns the archives that contains the given locations and 195 * not parsed and analyzed. 196 */ 197 private Set<Archive> unresolvedArchives(Stream<Location> locations) { 198 return locations.filter(l -> !finder.isParsed(l)) 199 .distinct() 200 .map(configuration::findClass) 201 .flatMap(Optional::stream) 202 .filter(filter::include) 203 .collect(toSet()); 204 } 205 206 /* 207 * Recursively analyzes entire module/archives. 208 */ 209 private void transitiveArchiveDeps(int depth) throws IOException { 210 Stream<Location> deps = archives.stream() 211 .flatMap(Archive::getDependencies); 212 213 // start with the unresolved archives 214 Set<Archive> unresolved = unresolvedArchives(deps); 215 do { 216 // parse all unresolved archives 217 Set<Location> targets = apiOnly 218 ? finder.parseExportedAPIs(unresolved.stream()) 219 : finder.parse(unresolved.stream()); 220 archives.addAll(unresolved); 221 222 // Add dependencies to the next batch for analysis 223 unresolved = unresolvedArchives(targets.stream()); 224 } while (!unresolved.isEmpty() && depth-- > 0); 225 } 226 227 /* 228 * Recursively analyze the class dependences 229 */ 230 private void transitiveDeps(int depth) throws IOException { 231 Stream<Location> deps = archives.stream() 232 .flatMap(Archive::getDependencies); 233 234 Deque<Location> unresolved = deps.collect(Collectors.toCollection(LinkedList::new)); 235 ConcurrentLinkedDeque<Location> deque = new ConcurrentLinkedDeque<>(); 236 do { 237 Location target; 238 while ((target = unresolved.poll()) != null) { 239 if (finder.isParsed(target)) 240 continue; 241 242 Archive archive = configuration.findClass(target).orElse(null); 243 if (archive != null && filter.include(archive)) { 244 archives.add(archive); 245 246 String name = target.getName(); 247 Set<Location> targets = apiOnly 248 ? finder.parseExportedAPIs(archive, name) 249 : finder.parse(archive, name); 250 251 // build unresolved dependencies 252 targets.stream() 253 .filter(t -> !finder.isParsed(t)) 254 .forEach(deque::add); 255 } 256 } 257 unresolved = deque; 258 deque = new ConcurrentLinkedDeque<>(); 259 } while (!unresolved.isEmpty() && depth-- > 0); 260 } 261 262 // ----- for testing purpose ----- 263 264 public static enum Info { 265 REQUIRES, 266 REQUIRES_PUBLIC, 267 EXPORTED_API, 268 MODULE_PRIVATE, 269 QUALIFIED_EXPORTED_API, 270 INTERNAL_API, 271 JDK_INTERNAL_API, 272 JDK_REMOVED_INTERNAL_API 273 } 274 275 public static class Node { 276 public final String name; 277 public final String source; 278 public final Info info; 279 Node(String name, Info info) { 280 this(name, name, info); 281 } 282 Node(String name, String source, Info info) { 283 this.name = name; 284 this.source = source; 285 this.info = info; 286 } 287 288 @Override 289 public String toString() { 290 StringBuilder sb = new StringBuilder(); 291 if (info != Info.REQUIRES && info != Info.REQUIRES_PUBLIC) 292 sb.append(source).append("/"); 293 294 sb.append(name); 295 if (info == Info.QUALIFIED_EXPORTED_API) 296 sb.append(" (qualified)"); 297 else if (info == Info.JDK_INTERNAL_API) 298 sb.append(" (JDK internal)"); 299 else if (info == Info.INTERNAL_API) 300 sb.append(" (internal)"); 301 return sb.toString(); 302 } 303 304 @Override 305 public boolean equals(Object o) { 306 if (!(o instanceof Node)) 307 return false; 308 309 Node other = (Node)o; 310 return this.name.equals(other.name) && 311 this.source.equals(other.source) && 312 this.info.equals(other.info); 313 } 314 315 @Override 316 public int hashCode() { 317 int result = name.hashCode(); 318 result = 31 * result + source.hashCode(); 319 result = 31 * result + info.hashCode(); 320 return result; 321 } 322 } 323 324 /** 325 * Returns a graph of module dependences. 326 * 327 * Each Node represents a module and each edge is a dependence. 328 * No analysis on "requires public". 329 */ 330 public Graph<Node> moduleGraph() { 331 Graph.Builder<Node> builder = new Graph.Builder<>(); 332 333 archives().stream() 334 .forEach(m -> { 335 Node u = new Node(m.getName(), Info.REQUIRES); 336 builder.addNode(u); 337 analyzer.requires(m) 338 .map(req -> new Node(req.getName(), Info.REQUIRES)) 339 .forEach(v -> builder.addEdge(u, v)); 340 }); 341 return builder.build(); 342 } 343 344 /** 345 * Returns a graph of dependences. 346 * 347 * Each Node represents a class or package per the specified verbose level. 348 * Each edge indicates 349 */ 350 public Graph<Node> dependenceGraph() { 351 Graph.Builder<Node> builder = new Graph.Builder<>(); 352 353 archives().stream() 354 .map(analyzer.results::get) 355 .filter(deps -> !deps.dependencies().isEmpty()) 356 .flatMap(deps -> deps.dependencies().stream()) 357 .forEach(d -> addEdge(builder, d)); 358 return builder.build(); 359 } 360 361 private void addEdge(Graph.Builder<Node> builder, Analyzer.Dep dep) { 362 Archive source = dep.originArchive(); 363 Archive target = dep.targetArchive(); 364 String pn = dep.target(); 365 if (verbose == CLASS || verbose == VERBOSE) { 366 int i = dep.target().lastIndexOf('.'); 367 pn = i > 0 ? dep.target().substring(0, i) : ""; 368 } 369 final Info info; 370 Module targetModule = target.getModule(); 371 if (source == target) { 372 info = Info.MODULE_PRIVATE; 373 } else if (!targetModule.isNamed()) { 374 info = Info.EXPORTED_API; 375 } else if (targetModule.isExported(pn) && !targetModule.isJDKUnsupported()) { 376 info = Info.EXPORTED_API; 377 } else { 378 Module module = target.getModule(); 379 if (module == Analyzer.REMOVED_JDK_INTERNALS) { 380 info = Info.JDK_REMOVED_INTERNAL_API; 381 } else if (!source.getModule().isJDK() && module.isJDK()) 382 info = Info.JDK_INTERNAL_API; 383 // qualified exports or inaccessible 384 else if (module.isExported(pn, source.getModule().name())) 385 info = Info.QUALIFIED_EXPORTED_API; 386 else 387 info = Info.INTERNAL_API; 388 } 389 390 Node u = new Node(dep.origin(), source.getName(), info); 391 Node v = new Node(dep.target(), target.getName(), info); 392 builder.addEdge(u, v); 393 } 394 395 }