content: /*
   * Copyright (c) 2016, Oracle and/or its affiliates. All rights reserved.
   * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
   *
   * This code is free software; you can redistribute it and/or modify it
   * under the terms of the GNU General Public License version 2 only, as
   * published by the Free Software Foundation.  Oracle designates this
   * particular file as subject to the "Classpath" exception as provided
   * by Oracle in the LICENSE file that accompanied this code.
   *
   * This code is distributed in the hope that it will be useful, but WITHOUT
   * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
   * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
   * version 2 for more details (a copy is included in the LICENSE file that
   * accompanied this code).
   *
   * You should have received a copy of the GNU General Public License version
   * 2 along with this work; if not, write to the Free Software Foundation,
   * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
   *
   * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
   * or visit www.oracle.com if you need additional information or have any
   * questions.
   */
  
  package openjdk.tools.jdeps;
  
  import openjdk.tools.classfile.Dependency.Location;
  import java.io.IOException;
  import java.util.ArrayList;
  import java.util.Collection;
  import java.util.Deque;
  import java.util.LinkedHashSet;
  import java.util.LinkedList;
  import java.util.List;
  import java.util.Optional;
  import java.util.Set;
  import java.util.concurrent.ConcurrentLinkedDeque;
  import java.util.stream.Collectors;
  import java.util.stream.Stream;
  
  import static openjdk.tools.jdeps.Analyzer.Type.CLASS;
  import static openjdk.tools.jdeps.Analyzer.Type.VERBOSE;
  import static openjdk.tools.jdeps.Module.trace;
  import static java.util.stream.Collectors.*;
  
  /**
   * Dependency Analyzer.
   *
   * Type of filters:
   * source filter: -include <pattern>
   * target filter: -package, -regex, --require
   *
   * The initial archive set for analysis includes
   * 1. archives specified in the command line arguments
   * 2. observable modules matching the source filter
   * 3. classpath archives matching the source filter or target filter
   * 4. --add-modules and -m root modules
   */
  public class DepsAnalyzer {
      final JdepsConfiguration configuration;
      final JdepsFilter filter;
      final JdepsWriter writer;
      final Analyzer.Type verbose;
      final boolean apiOnly;
  
      final DependencyFinder finder;
      final Analyzer analyzer;
      final List<Archive> rootArchives = new ArrayList<>();
  
      // parsed archives
      final Set<Archive> archives = new LinkedHashSet<>();
  
      public DepsAnalyzer(JdepsConfiguration config,
                          JdepsFilter filter,
                          JdepsWriter writer,
                          Analyzer.Type verbose,
                          boolean apiOnly) {
          this.configuration = config;
          this.filter = filter;
          this.writer = writer;
          this.verbose = verbose;
          this.apiOnly = apiOnly;
  
          this.finder = new DependencyFinder(config, filter);
          this.analyzer = new Analyzer(configuration, verbose, filter);
  
          // determine initial archives to be analyzed
          this.rootArchives.addAll(configuration.initialArchives());
  
          // if -include pattern is specified, add the matching archives on
          // classpath to the root archives
          if (filter.hasIncludePattern() || filter.hasTargetFilter()) {
              configuration.getModules().values().stream()
                  .filter(source -> include(source) && filter.matches(source))
                  .forEach(this.rootArchives::add);
          }
  
          // class path archives
          configuration.classPathArchives().stream()
              .filter(filter::matches)
              .forEach(this.rootArchives::add);
  
          // Include the root modules for analysis
          this.rootArchives.addAll(configuration.rootModules());
  
          trace("analyze root archives: %s%n", this.rootArchives);
      }
  
      /*
       * Perform runtime dependency analysis
       */
      public boolean run() throws IOException {
          return run(false, 1);
      }
  
      /**
       * Perform compile-time view or run-time view dependency analysis.
       *
       * @param compileTimeView
       * @param maxDepth  depth of recursive analysis.  depth == 0 if -R is set
       */
      public boolean run(boolean compileTimeView, int maxDepth) throws IOException {
          try {
              // parse each packaged module or classpath archive
              if (apiOnly) {
                  finder.parseExportedAPIs(rootArchives.stream());
              } else {
                  finder.parse(rootArchives.stream());
              }
              archives.addAll(rootArchives);
  
              int depth = maxDepth > 0 ? maxDepth : Integer.MAX_VALUE;
  
              // transitive analysis
              if (depth > 1) {
                  if (compileTimeView)
                      transitiveArchiveDeps(depth-1);
                  else
                      transitiveDeps(depth-1);
              }
  
              Set<Archive> archives = archives();
  
              // analyze the dependencies collected
              analyzer.run(archives, finder.locationToArchive());
  
              if (writer != null) {
                  writer.generateOutput(archives, analyzer);
              }
          } finally {
              finder.shutdown();
          }
          return true;
      }
  
      /**
       * Returns the archives for reporting that has matching dependences.
       *
       * If --require is set, they should be excluded.
       */
      Set<Archive> archives() {
          if (filter.requiresFilter().isEmpty()) {
              return archives.stream()
                  .filter(this::include)
                  .filter(Archive::hasDependences)
                  .collect(Collectors.toSet());
          } else {
              // use the archives that have dependences and not specified in --require
              return archives.stream()
                  .filter(this::include)
                  .filter(source -> !filter.requiresFilter().contains(source.getName()))
                  .filter(source ->
                          source.getDependencies()
                                .map(finder::locationToArchive)
                                .anyMatch(a -> a != source))
                  .collect(Collectors.toSet());
          }
      }
  
      /**
       * Returns the dependences, either class name or package name
       * as specified in the given verbose level.
       */
      Set<String> dependences() {
          return analyzer.archives().stream()
                         .map(analyzer::dependences)
                         .flatMap(Set::stream)
                         .collect(Collectors.toSet());
      }
  
      /**
       * Returns the archives that contains the given locations and
       * not parsed and analyzed.
       */
      private Set<Archive> unresolvedArchives(Stream<Location> locations) {
          return locations.filter(l -> !finder.isParsed(l))
                          .distinct()
                          .map(configuration::findClass)
                          .flatMap(Optional::stream)
                          .collect(toSet());
      }
  
      /*
       * Recursively analyzes entire module/archives.
      */
      private void transitiveArchiveDeps(int depth) throws IOException {
          Stream<Location> deps = archives.stream()
                                          .flatMap(Archive::getDependencies);
  
          // start with the unresolved archives
          Set<Archive> unresolved = unresolvedArchives(deps);
          do {
              // parse all unresolved archives
              Set<Location> targets = apiOnly
                  ? finder.parseExportedAPIs(unresolved.stream())
                  : finder.parse(unresolved.stream());
              archives.addAll(unresolved);
  
              // Add dependencies to the next batch for analysis
              unresolved = unresolvedArchives(targets.stream());
          } while (!unresolved.isEmpty() && depth-- > 0);
      }
  
      /*
       * Recursively analyze the class dependences
       */
      private void transitiveDeps(int depth) throws IOException {
          Stream<Location> deps = archives.stream()
                                          .flatMap(Archive::getDependencies);
  
          Deque<Location> unresolved = deps.collect(Collectors.toCollection(LinkedList::new));
          ConcurrentLinkedDeque<Location> deque = new ConcurrentLinkedDeque<>();
          do {
              Location target;
              while ((target = unresolved.poll()) != null) {
                  if (finder.isParsed(target))
                      continue;
  
                  Archive archive = configuration.findClass(target).orElse(null);
                  if (archive != null) {
                      archives.add(archive);
  
                      String name = target.getName();
                      Set<Location> targets = apiOnly
                              ? finder.parseExportedAPIs(archive, name)
                              : finder.parse(archive, name);
  
                      // build unresolved dependencies
                      targets.stream()
                             .filter(t -> !finder.isParsed(t))
                             .forEach(deque::add);
                  }
              }
              unresolved = deque;
              deque = new ConcurrentLinkedDeque<>();
          } while (!unresolved.isEmpty() && depth-- > 0);
      }
  
      /*
       * Tests if the given archive is requested for analysis.
       * It includes the root modules specified in --module, --add-modules
       * or modules specified on the command line
       *
       * This filters system module by default unless they are explicitly
       * requested.
       */
      public boolean include(Archive source) {
          Module module = source.getModule();
          // skip system module by default
          return  !module.isSystem()
                      || configuration.rootModules().contains(source);
      }
  
      // ----- for testing purpose -----
  
      public static enum Info {
          REQUIRES,
          REQUIRES_TRANSITIVE,
          EXPORTED_API,
          MODULE_PRIVATE,
          QUALIFIED_EXPORTED_API,
          INTERNAL_API,
          JDK_INTERNAL_API,
          JDK_REMOVED_INTERNAL_API
      }
  
      public static class Node {
          public final String name;
          public final String source;
          public final Info info;
          Node(String name, Info info) {
              this(name, name, info);
          }
          Node(String name, String source, Info info) {
              this.name = name;
              this.source = source;
              this.info = info;
          }
  
          @Override
          public String toString() {
              StringBuilder sb = new StringBuilder();
              if (info != Info.REQUIRES && info != Info.REQUIRES_TRANSITIVE)
                  sb.append(source).append("/");
  
              sb.append(name);
              if (info == Info.QUALIFIED_EXPORTED_API)
                  sb.append(" (qualified)");
              else if (info == Info.JDK_INTERNAL_API)
                  sb.append(" (JDK internal)");
              else if (info == Info.INTERNAL_API)
                  sb.append(" (internal)");
              return sb.toString();
          }
  
          @Override
          public boolean equals(Object o) {
              if (!(o instanceof Node))
                  return false;
  
              Node other = (Node)o;
              return this.name.equals(other.name) &&
                      this.source.equals(other.source) &&
                      this.info.equals(other.info);
          }
  
          @Override
          public int hashCode() {
              int result = name.hashCode();
              result = 31 * result + source.hashCode();
              result = 31 * result + info.hashCode();
              return result;
          }
      }
  
      /**
       * Returns a graph of module dependences.
       *
       * Each Node represents a module and each edge is a dependence.
       * No analysis on "requires transitive".
       */
      public Graph<Node> moduleGraph() {
          Graph.Builder<Node> builder = new Graph.Builder<>();
  
          archives().stream()
              .forEach(m -> {
                  Node u = new Node(m.getName(), Info.REQUIRES);
                  builder.addNode(u);
                  analyzer.requires(m)
                      .map(req -> new Node(req.getName(), Info.REQUIRES))
                      .forEach(v -> builder.addEdge(u, v));
              });
          return builder.build();
      }
  
      /**
       * Returns a graph of dependences.
       *
       * Each Node represents a class or package per the specified verbose level.
       * Each edge indicates
       */
      public Graph<Node> dependenceGraph() {
          Graph.Builder<Node> builder = new Graph.Builder<>();
  
          archives().stream()
              .map(analyzer.results::get)
              .filter(deps -> !deps.dependencies().isEmpty())
              .flatMap(deps -> deps.dependencies().stream())
              .forEach(d -> addEdge(builder, d));
          return builder.build();
      }
  
      private void addEdge(Graph.Builder<Node> builder, Analyzer.Dep dep) {
          Archive source = dep.originArchive();
          Archive target = dep.targetArchive();
          String pn = dep.target();
          if (verbose == CLASS || verbose == VERBOSE) {
              int i = dep.target().lastIndexOf('.');
              pn = i > 0 ? dep.target().substring(0, i) : "";
          }
          final Info info;
          Module targetModule = target.getModule();
          if (source == target) {
              info = Info.MODULE_PRIVATE;
          } else if (!targetModule.isNamed()) {
              info = Info.EXPORTED_API;
          } else if (targetModule.isExported(pn) && !targetModule.isJDKUnsupported()) {
              info = Info.EXPORTED_API;
          } else {
              Module module = target.getModule();
              if (module == Analyzer.REMOVED_JDK_INTERNALS) {
                  info = Info.JDK_REMOVED_INTERNAL_API;
              } else if (!source.getModule().isJDK() && module.isJDK())
                  info = Info.JDK_INTERNAL_API;
                  // qualified exports or inaccessible
              else if (module.isExported(pn, source.getModule().name()))
                  info = Info.QUALIFIED_EXPORTED_API;
              else
                  info = Info.INTERNAL_API;
          }
  
          Node u = new Node(dep.origin(), source.getName(), info);
          Node v = new Node(dep.target(), target.getName(), info);
          builder.addEdge(u, v);
      }
  
  }
  