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 }