text: /*
   * Copyright 2016 The Bazel Authors. All rights reserved.
   *
   * Licensed under the Apache License, Version 2.0 (the "License");
   * you may not use this file except in compliance with the License.
   * You may obtain a copy of the License at
   *
   *    http://www.apache.org/licenses/LICENSE-2.0
   *
   * Unless required by applicable law or agreed to in writing, software
   * distributed under the License is distributed on an "AS IS" BASIS,
   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   * See the License for the specific language governing permissions and
   * limitations under the License.
   */
  package com.google.idea.blaze.java.sync.importer;
  
  import static java.util.stream.Collectors.toList;
  
  import com.google.common.collect.ArrayListMultimap;
  import com.google.common.collect.ImmutableList;
  import com.google.common.collect.ImmutableMap;
  import com.google.common.collect.ImmutableSet;
  import com.google.common.collect.Lists;
  import com.google.common.collect.Maps;
  import com.google.common.collect.Multimap;
  import com.google.common.collect.Sets;
  import com.google.devtools.intellij.ideinfo.IntellijIdeInfo.Dependency.DependencyType;
  import com.google.idea.blaze.base.ideinfo.ArtifactLocation;
  import com.google.idea.blaze.base.ideinfo.Dependency;
  import com.google.idea.blaze.base.ideinfo.JavaIdeInfo;
  import com.google.idea.blaze.base.ideinfo.LibraryArtifact;
  import com.google.idea.blaze.base.ideinfo.TargetIdeInfo;
  import com.google.idea.blaze.base.ideinfo.TargetKey;
  import com.google.idea.blaze.base.ideinfo.TargetMap;
  import com.google.idea.blaze.base.model.LibraryKey;
  import com.google.idea.blaze.base.model.SyncState;
  import com.google.idea.blaze.base.model.primitives.WorkspaceRoot;
  import com.google.idea.blaze.base.projectview.ProjectViewSet;
  import com.google.idea.blaze.base.scope.BlazeContext;
  import com.google.idea.blaze.base.scope.output.PrintOutput;
  import com.google.idea.blaze.base.settings.Blaze;
  import com.google.idea.blaze.base.settings.BuildSystem;
  import com.google.idea.blaze.base.sync.projectview.ImportRoots;
  import com.google.idea.blaze.base.sync.projectview.WorkspaceLanguageSettings;
  import com.google.idea.blaze.base.sync.workspace.ArtifactLocationDecoder;
  import com.google.idea.blaze.java.JavaBlazeRules;
  import com.google.idea.blaze.java.sync.BlazeJavaSyncAugmenter;
  import com.google.idea.blaze.java.sync.DuplicateSourceDetector;
  import com.google.idea.blaze.java.sync.importer.emptylibrary.EmptyLibrary;
  import com.google.idea.blaze.java.sync.jdeps.JdepsMap;
  import com.google.idea.blaze.java.sync.model.BlazeContentEntry;
  import com.google.idea.blaze.java.sync.model.BlazeJarLibrary;
  import com.google.idea.blaze.java.sync.model.BlazeJavaImportResult;
  import com.google.idea.blaze.java.sync.source.SourceArtifact;
  import com.google.idea.blaze.java.sync.source.SourceDirectoryCalculator;
  import com.google.idea.blaze.java.sync.workingset.JavaWorkingSet;
  import com.intellij.openapi.project.Project;
  import java.util.Arrays;
  import java.util.Collection;
  import java.util.Comparator;
  import java.util.LinkedHashMap;
  import java.util.List;
  import java.util.Map;
  import java.util.Set;
  import javax.annotation.Nullable;
  
  /** Builds a BlazeWorkspace. */
  public final class BlazeJavaWorkspaceImporter {
    private final Project project;
    private final WorkspaceRoot workspaceRoot;
    private final BuildSystem buildSystem;
    private final ImportRoots importRoots;
    private final TargetMap targetMap;
    private final JdepsMap jdepsMap;
    @Nullable private final JavaWorkingSet workingSet;
    private final ArtifactLocationDecoder artifactLocationDecoder;
    private final DuplicateSourceDetector duplicateSourceDetector = new DuplicateSourceDetector();
    private final JavaSourceFilter sourceFilter;
    private final WorkspaceLanguageSettings workspaceLanguageSettings;
    private final List<BlazeJavaSyncAugmenter> augmenters;
    private final ProjectViewSet projectViewSet;
    @Nullable private final SyncState oldSyncState;
  
    public BlazeJavaWorkspaceImporter(
        Project project,
        WorkspaceRoot workspaceRoot,
        ProjectViewSet projectViewSet,
        WorkspaceLanguageSettings workspaceLanguageSettings,
        TargetMap targetMap,
        JavaSourceFilter sourceFilter,
        JdepsMap jdepsMap,
        @Nullable JavaWorkingSet workingSet,
        ArtifactLocationDecoder artifactLocationDecoder,
        @Nullable SyncState oldSyncState) {
      this.project = project;
      this.workspaceRoot = workspaceRoot;
      this.buildSystem = Blaze.getBuildSystem(project);
      this.importRoots = ImportRoots.builder(workspaceRoot, buildSystem).add(projectViewSet).build();
      this.targetMap = targetMap;
      this.sourceFilter = sourceFilter;
      this.jdepsMap = jdepsMap;
      this.workingSet = workingSet;
      this.artifactLocationDecoder = artifactLocationDecoder;
      this.workspaceLanguageSettings = workspaceLanguageSettings;
      this.augmenters = Arrays.asList(BlazeJavaSyncAugmenter.EP_NAME.getExtensions());
      this.projectViewSet = projectViewSet;
      this.oldSyncState = oldSyncState;
    }
  
    public BlazeJavaImportResult importWorkspace(BlazeContext context) {
      WorkspaceBuilder workspaceBuilder = new WorkspaceBuilder();
      for (TargetIdeInfo target : sourceFilter.sourceTargets) {
        addTargetAsSource(
            workspaceBuilder, target, sourceFilter.targetToJavaSources.get(target.getKey()));
      }
  
      SourceDirectoryCalculator sourceDirectoryCalculator = new SourceDirectoryCalculator();
      ImmutableList<BlazeContentEntry> contentEntries =
          sourceDirectoryCalculator.calculateContentEntries(
              project,
              context,
              workspaceRoot,
              artifactLocationDecoder,
              importRoots,
              workspaceBuilder.sourceArtifacts,
              workspaceBuilder.javaPackageManifests);
  
      int totalContentEntryCount = 0;
      for (BlazeContentEntry contentEntry : contentEntries) {
        totalContentEntryCount += contentEntry.sources.size();
      }
      context.output(PrintOutput.log("Java content entry count: " + totalContentEntryCount));
  
      BlazeJavaImportResult.Builder importResultBuilder = BlazeJavaImportResult.builder();
      ImmutableMap<LibraryKey, BlazeJarLibrary> libraries =
          buildLibraries(context, workspaceBuilder, sourceFilter.libraryTargets, importResultBuilder);
  
      duplicateSourceDetector.reportDuplicates(context);
  
      String sourceVersion = findSourceVersion(targetMap);
  
      return importResultBuilder
          .setContentEntries(contentEntries)
          .setLibraries(libraries)
          .setBuildOutputJars(ImmutableList.sortedCopyOf(workspaceBuilder.buildOutputJars))
          .setJavaSourceFiles(ImmutableSet.copyOf(workspaceBuilder.addedSourceFiles))
          .setSourceVersion(sourceVersion)
          .build();
    }
  
    private ImmutableMap<LibraryKey, BlazeJarLibrary> buildLibraries(
        BlazeContext context,
        WorkspaceBuilder workspaceBuilder,
        List<TargetIdeInfo> libraryTargets,
        BlazeJavaImportResult.Builder importResultBuilder) {
      // Build library maps
      Multimap<TargetKey, BlazeJarLibrary> targetKeyToLibrary = ArrayListMultimap.create();
      Map<String, BlazeJarLibrary> jdepsPathToLibrary = Maps.newHashMap();
  
      // Add any output jars from source rules
      for (TargetKey key : workspaceBuilder.outputJarsFromSourceTargets.keySet()) {
        Collection<BlazeJarLibrary> jars = workspaceBuilder.outputJarsFromSourceTargets.get(key);
        targetKeyToLibrary.putAll(key, jars);
        for (BlazeJarLibrary library : jars) {
          addLibraryToJdeps(jdepsPathToLibrary, library);
        }
      }
  
      for (TargetIdeInfo target : libraryTargets) {
        JavaIdeInfo javaIdeInfo = target.getJavaIdeInfo();
        if (javaIdeInfo == null) {
          continue;
        }
        List<LibraryArtifact> allJars = Lists.newArrayList();
        allJars.addAll(javaIdeInfo.getJars());
        Collection<BlazeJarLibrary> libraries =
            allJars.stream().map(jar -> new BlazeJarLibrary(jar, target.getKey())).collect(toList());
  
        targetKeyToLibrary.putAll(target.getKey(), libraries);
        for (BlazeJarLibrary library : libraries) {
          addLibraryToJdeps(jdepsPathToLibrary, library);
        }
      }
  
      // Preserve classpath order. Add leaf level dependencies first and work the way up. This
      // prevents conflicts when a JAR repackages it's dependencies. In such a case we prefer to
      // resolve symbols from the original JAR rather than the repackaged version.
      // Using accessOrdered LinkedHashMap because jars that are present in `workspaceBuilder.jdeps`
      // and in `workspaceBuilder.directDeps`, we want to treat it as a directDep
      Map<LibraryKey, BlazeJarLibrary> result =
          new LinkedHashMap<>(
              /* initialCapacity= */ 32, /* loadFactor= */ 0.75f, /* accessOrder= */ true);
  
      // Collect jars from jdep references
      for (String jdepsPath : workspaceBuilder.jdeps) {
        ArtifactLocation artifact = ExecutionPathHelper.parse(workspaceRoot, buildSystem, jdepsPath);
        if (sourceFilter.jdepsPathsForExcludedJars.contains(artifact.getRelativePath())) {
          continue;
        }
        BlazeJarLibrary library = jdepsPathToLibrary.get(artifact.getRelativePath());
        if (library == null) {
          // It's in the target's jdeps, but our aspect never attached to the target building it.
          // Perhaps it's an implicit dependency, or not referenced in an attribute we propagate
          // along. Make a best-effort attempt to add it to the project anyway.
          ArtifactLocation srcJar = guessSrcJarLocation(artifact);
          ImmutableList<ArtifactLocation> srcJars =
              srcJar != null ? ImmutableList.of(srcJar) : ImmutableList.of();
          library =
              new BlazeJarLibrary(
                  new LibraryArtifact(artifact, null, srcJars), /* targetKey= */ null);
        }
        result.put(library.key, library);
      }
  
      // Collect jars referenced by direct deps from your working set
      for (TargetKey deps : workspaceBuilder.directDeps) {
        for (BlazeJarLibrary library : targetKeyToLibrary.get(deps)) {
          result.put(library.key, library);
        }
      }
  
      // Collect generated jars from source rules
      for (BlazeJarLibrary library : workspaceBuilder.generatedJarsFromSourceTargets) {
        result.put(library.key, library);
      }
  
      // Filter out any libraries corresponding to empty jars
      return EmptyLibrary.removeEmptyLibraries(
          project, context, artifactLocationDecoder, result, oldSyncState, importResultBuilder);
    }
  
    private void addLibraryToJdeps(
        Map<String, BlazeJarLibrary> jdepsPathToLibrary, BlazeJarLibrary library) {
      LibraryArtifact libraryArtifact = library.libraryArtifact;
      ArtifactLocation interfaceJar = libraryArtifact.getInterfaceJar();
      if (interfaceJar != null) {
        jdepsPathToLibrary.put(interfaceJar.getRelativePath(), library);
      }
      ArtifactLocation classJar = libraryArtifact.getClassJar();
      if (classJar != null) {
        jdepsPathToLibrary.put(classJar.getRelativePath(), library);
      }
    }
  
    private void addTargetAsSource(
        WorkspaceBuilder workspaceBuilder,
        TargetIdeInfo target,
        Collection<ArtifactLocation> javaSources) {
      JavaIdeInfo javaIdeInfo = target.getJavaIdeInfo();
      if (javaIdeInfo == null) {
        return;
      }
  
      TargetKey targetKey = target.getKey();
      Collection<String> jars = jdepsMap.getDependenciesForTarget(targetKey);
      if (jars != null) {
        workspaceBuilder.jdeps.addAll(jars);
      }
  
      // Add all deps if this target is in the current working set
      if (workingSet == null || workingSet.isTargetInWorkingSet(target)) {
        // Add self, so we pick up our own gen jars if in working set
        workspaceBuilder.directDeps.add(targetKey);
        for (Dependency dep : target.getDependencies()) {
          if (dep.getDependencyType() != DependencyType.COMPILE_TIME) {
            continue;
          }
          // forward deps from java proto_library aspect targets
          TargetIdeInfo depTarget = targetMap.get(dep.getTargetKey());
          if (depTarget != null
              && JavaBlazeRules.getJavaProtoLibraryKinds().contains(depTarget.getKind())) {
            workspaceBuilder.directDeps.addAll(
                depTarget.getDependencies().stream().map(Dependency::getTargetKey).collect(toList()));
          } else {
            workspaceBuilder.directDeps.add(dep.getTargetKey());
          }
        }
      }
  
      for (ArtifactLocation artifactLocation : javaSources) {
        if (artifactLocation.isSource()) {
          duplicateSourceDetector.add(targetKey, artifactLocation);
          workspaceBuilder.sourceArtifacts.add(new SourceArtifact(targetKey, artifactLocation));
          workspaceBuilder.addedSourceFiles.add(artifactLocation);
        }
      }
  
      ArtifactLocation manifest = javaIdeInfo.getPackageManifest();
      if (manifest != null) {
        workspaceBuilder.javaPackageManifests.put(targetKey, manifest);
      }
      for (LibraryArtifact libraryArtifact : javaIdeInfo.getJars()) {
        ArtifactLocation classJar = libraryArtifact.getClassJar();
        if (classJar != null) {
          workspaceBuilder.buildOutputJars.add(classJar);
        }
      }
      workspaceBuilder.generatedJarsFromSourceTargets.addAll(
          javaIdeInfo.getGeneratedJars().stream()
              .map(jar -> new BlazeJarLibrary(jar, targetKey))
              .collect(toList()));
      if (javaIdeInfo.getFilteredGenJar() != null) {
        workspaceBuilder.generatedJarsFromSourceTargets.add(
            new BlazeJarLibrary(javaIdeInfo.getFilteredGenJar(), targetKey));
      }
      if (JavaSourceFilter.isJavaProtoTarget(target)) {
        // add generated jars from all proto library targets in the project
        javaIdeInfo.getJars().stream()
            .map(jar -> new BlazeJarLibrary(jar, targetKey))
            .forEach(workspaceBuilder.generatedJarsFromSourceTargets::add);
      }
  
      for (BlazeJavaSyncAugmenter augmenter : augmenters) {
        augmenter.addJarsForSourceTarget(
            workspaceLanguageSettings,
            projectViewSet,
            target,
            workspaceBuilder.outputJarsFromSourceTargets.get(targetKey),
            workspaceBuilder.generatedJarsFromSourceTargets);
      }
    }
  
    @Nullable
    private String findSourceVersion(TargetMap targetMap) {
      return targetMap.targets().stream()
          .filter(t -> t.getJavaToolchainIdeInfo() != null)
          .map(t -> t.getJavaToolchainIdeInfo().getSourceVersion())
          .max(Comparator.naturalOrder())
          .orElse(null);
    }
  
    private static class WorkspaceBuilder {
      Set<String> jdeps = Sets.newHashSet();
      Set<TargetKey> directDeps = Sets.newHashSet();
      Set<ArtifactLocation> addedSourceFiles = Sets.newHashSet();
      Multimap<TargetKey, BlazeJarLibrary> outputJarsFromSourceTargets = ArrayListMultimap.create();
      List<BlazeJarLibrary> generatedJarsFromSourceTargets = Lists.newArrayList();
      List<ArtifactLocation> buildOutputJars = Lists.newArrayList();
      List<SourceArtifact> sourceArtifacts = Lists.newArrayList();
      Map<TargetKey, ArtifactLocation> javaPackageManifests = Maps.newHashMap();
    }
  
    /**
     * Uses a filename heuristic to guess the location of a source jar corresponding to the given
     * output jar.
     */
    @Nullable
    private static ArtifactLocation guessSrcJarLocation(ArtifactLocation outputJar) {
      String srcJarRelPath = guessSrcJarRelativePath(outputJar.getRelativePath());
      if (srcJarRelPath == null) {
        return null;
      }
      // we don't check whether the source jar actually exists, to avoid unnecessary file system
      // operations
      return ArtifactLocation.Builder.copy(outputJar).setRelativePath(srcJarRelPath).build();
    }
  
    @Nullable
    private static String guessSrcJarRelativePath(String relPath) {
      if (relPath.endsWith("-hjar.jar")) {
        return relPath.substring(0, relPath.length() - "-hjar.jar".length()) + "-src.jar";
      }
      if (relPath.endsWith("-ijar.jar")) {
        return relPath.substring(0, relPath.length() - "-ijar.jar".length()) + "-src.jar";
      }
      return null;
    }
  }
  