content: /*
   * Copyright (c) 2015, 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 static openjdk.tools.jdeps.Module.*;
  import static openjdk.tools.jdeps.Analyzer.NOT_FOUND;
  import static java.util.stream.Collectors.*;
  
  import openjdk.tools.classfile.AccessFlags;
  import openjdk.tools.classfile.ClassFile;
  import openjdk.tools.classfile.ConstantPoolException;
  import openjdk.tools.classfile.Dependencies;
  import openjdk.tools.classfile.Dependencies.ClassFileError;
  import openjdk.tools.classfile.Dependency;
  import openjdk.tools.classfile.Dependency.Location;
  
  import java.io.IOException;
  import java.io.UncheckedIOException;
  import java.nio.file.Paths;
  import java.util.Collections;
  import java.util.Deque;
  import java.util.HashMap;
  import java.util.HashSet;
  import java.util.Map;
  import java.util.Optional;
  import java.util.Set;
  import java.util.concurrent.Callable;
  import java.util.concurrent.ConcurrentHashMap;
  import java.util.concurrent.ConcurrentLinkedDeque;
  import java.util.concurrent.ExecutionException;
  import java.util.concurrent.ExecutorService;
  import java.util.concurrent.Executors;
  import java.util.concurrent.FutureTask;
  import java.util.stream.Stream;
  
  /**
   * Parses class files and finds dependences
   */
  class DependencyFinder {
      private static Finder API_FINDER = new Finder(true);
      private static Finder CLASS_FINDER = new Finder(false);
  
      private final JdepsConfiguration configuration;
      private final JdepsFilter filter;
  
      private final Map<Finder, Deque<Archive>> parsedArchives = new ConcurrentHashMap<>();
      private final Map<Location, Archive> parsedClasses = new ConcurrentHashMap<>();
  
      private final ExecutorService pool = Executors.newFixedThreadPool(2);
      private final Deque<FutureTask<Set<Location>>> tasks = new ConcurrentLinkedDeque<>();
  
      DependencyFinder(JdepsConfiguration configuration,
                       JdepsFilter filter) {
          this.configuration = configuration;
          this.filter = filter;
          this.parsedArchives.put(API_FINDER, new ConcurrentLinkedDeque<>());
          this.parsedArchives.put(CLASS_FINDER, new ConcurrentLinkedDeque<>());
      }
  
      Map<Location, Archive> locationToArchive() {
          return parsedClasses;
      }
  
      /**
       * Returns the modules of all dependencies found
       */
      Stream<Archive> getDependences(Archive source) {
          return source.getDependencies()
                       .map(this::locationToArchive)
                       .filter(a -> a != source);
      }
  
      /**
       * Returns the location to archive map; or NOT_FOUND.
       *
       * Location represents a parsed class.
       */
      Archive locationToArchive(Location location) {
          return parsedClasses.containsKey(location)
              ? parsedClasses.get(location)
              : configuration.findClass(location).orElse(NOT_FOUND);
      }
  
      /**
       * Returns a map from an archive to its required archives
       */
      Map<Archive, Set<Archive>> dependences() {
          Map<Archive, Set<Archive>> map = new HashMap<>();
          parsedArchives.values().stream()
              .flatMap(Deque::stream)
              .filter(a -> !a.isEmpty())
              .forEach(source -> {
                  Set<Archive> deps = getDependences(source).collect(toSet());
                  if (!deps.isEmpty()) {
                      map.put(source, deps);
                  }
          });
          return map;
      }
  
      boolean isParsed(Location location) {
          return parsedClasses.containsKey(location);
      }
  
      /**
       * Parses all class files from the given archive stream and returns
       * all target locations.
       */
      public Set<Location> parse(Stream<? extends Archive> archiveStream) {
          archiveStream.forEach(archive -> parse(archive, CLASS_FINDER));
          return waitForTasksCompleted();
      }
  
      /**
       * Parses the exported API class files from the given archive stream and
       * returns all target locations.
       */
      public Set<Location> parseExportedAPIs(Stream<? extends Archive> archiveStream) {
          archiveStream.forEach(archive -> parse(archive, API_FINDER));
          return waitForTasksCompleted();
      }
  
      /**
       * Parses the named class from the given archive and
       * returns all target locations the named class references.
       */
      public Set<Location> parse(Archive archive, String name) {
          try {
              return parse(archive, CLASS_FINDER, name);
          } catch (IOException e) {
              throw new UncheckedIOException(e);
          }
      }
  
      /**
       * Parses the exported API of the named class from the given archive and
       * returns all target locations the named class references.
       */
      public Set<Location> parseExportedAPIs(Archive archive, String name)
      {
          try {
              return parse(archive, API_FINDER, name);
          } catch (IOException e) {
              throw new UncheckedIOException(e);
          }
      }
  
      private Optional<FutureTask<Set<Location>>> parse(Archive archive, Finder finder) {
          if (parsedArchives.get(finder).contains(archive))
              return Optional.empty();
  
          parsedArchives.get(finder).add(archive);
  
          trace("parsing %s %s%n", archive.getName(), archive.getPathName());
          FutureTask<Set<Location>> task = new FutureTask<>(() -> {
              Set<Location> targets = new HashSet<>();
              for (ClassFile cf : archive.reader().getClassFiles()) {
                  if (cf.access_flags.is(AccessFlags.ACC_MODULE))
                      continue;
  
                  String classFileName;
                  try {
                      classFileName = cf.getName();
                  } catch (ConstantPoolException e) {
                      throw new ClassFileError(e);
                  }
  
                  // filter source class/archive
                  String cn = classFileName.replace('/', '.');
                  if (!finder.accept(archive, cn, cf.access_flags))
                      continue;
  
                  // tests if this class matches the -include
                  if (!filter.matches(cn))
                      continue;
  
                  for (Dependency d : finder.findDependencies(cf)) {
                      if (filter.accepts(d)) {
                          archive.addClass(d.getOrigin(), d.getTarget());
                          targets.add(d.getTarget());
                      } else {
                          // ensure that the parsed class is added the archive
                          archive.addClass(d.getOrigin());
                      }
                      parsedClasses.putIfAbsent(d.getOrigin(), archive);
                  }
              }
              return targets;
          });
          tasks.add(task);
          pool.submit(task);
          return Optional.of(task);
      }
  
      private Set<Location> parse(Archive archive, Finder finder, String name)
          throws IOException
      {
          ClassFile cf = archive.reader().getClassFile(name);
          if (cf == null) {
              throw new IllegalArgumentException(archive.getName() +
                  " does not contain " + name);
          }
  
          if (cf.access_flags.is(AccessFlags.ACC_MODULE))
              return Collections.emptySet();
  
          Set<Location> targets = new HashSet<>();
          String cn;
          try {
              cn =  cf.getName().replace('/', '.');
          } catch (ConstantPoolException e) {
              throw new Dependencies.ClassFileError(e);
          }
  
          if (!finder.accept(archive, cn, cf.access_flags))
              return targets;
  
          // tests if this class matches the -include
          if (!filter.matches(cn))
              return targets;
  
          // skip checking filter.matches
          for (Dependency d : finder.findDependencies(cf)) {
              if (filter.accepts(d)) {
                  targets.add(d.getTarget());
                  archive.addClass(d.getOrigin(), d.getTarget());
              } else {
                  // ensure that the parsed class is added the archive
                  archive.addClass(d.getOrigin());
              }
              parsedClasses.putIfAbsent(d.getOrigin(), archive);
          }
          return targets;
      }
  
      /*
       * Waits until all submitted tasks are completed.
       */
      private Set<Location> waitForTasksCompleted() {
          try {
              Set<Location> targets = new HashSet<>();
              FutureTask<Set<Location>> task;
              while ((task = tasks.poll()) != null) {
                  // wait for completion
                  targets.addAll(task.get());
              }
              return targets;
          } catch (InterruptedException|ExecutionException e) {
              throw new Error(e);
          }
      }
  
      /*
       * Shutdown the executor service.
       */
      void shutdown() {
          pool.shutdown();
      }
  
      private interface SourceFilter {
          boolean accept(Archive archive, String cn, AccessFlags accessFlags);
      }
  
      private static class Finder implements Dependency.Finder, SourceFilter {
          private final Dependency.Finder finder;
          private final boolean apiOnly;
          Finder(boolean apiOnly) {
              this.apiOnly = apiOnly;
              this.finder = apiOnly
                  ? Dependencies.getAPIFinder(AccessFlags.ACC_PROTECTED)
                  : Dependencies.getClassDependencyFinder();
  
          }
  
          @Override
          public boolean accept(Archive archive, String cn, AccessFlags accessFlags) {
              int i = cn.lastIndexOf('.');
              String pn = i > 0 ? cn.substring(0, i) : "";
  
              // if -apionly is specified, analyze only exported and public types
              // All packages are exported in unnamed module.
              return apiOnly ? archive.getModule().isExported(pn) &&
                                   accessFlags.is(AccessFlags.ACC_PUBLIC)
                             : true;
          }
  
          @Override
          public Iterable<? extends Dependency> findDependencies(ClassFile classfile) {
              return finder.findDependencies(classfile);
          }
      }
  }
  