ArtifactFileResolver.java

/*
 * 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 org.basepom.mojo.duplicatefinder.artifact;

import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Preconditions.checkState;
import static java.lang.String.format;
import static org.basepom.mojo.duplicatefinder.artifact.ArtifactHelper.getOutputDirectory;
import static org.basepom.mojo.duplicatefinder.artifact.ArtifactHelper.getTestOutputDirectory;
import static org.basepom.mojo.duplicatefinder.artifact.ArtifactHelper.isTestArtifact;

import org.basepom.mojo.duplicatefinder.ClasspathElement;
import org.basepom.mojo.duplicatefinder.ClasspathElement.ClasspathArtifact;
import org.basepom.mojo.duplicatefinder.ClasspathElement.ClasspathLocalFolder;

import java.io.File;
import java.io.IOException;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.Set;

import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.MoreObjects;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableMultimap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.ImmutableSortedSet;
import com.google.common.collect.Multimap;
import com.google.common.collect.MultimapBuilder;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import org.apache.maven.artifact.Artifact;
import org.apache.maven.artifact.DefaultArtifact;
import org.apache.maven.artifact.DependencyResolutionRequiredException;
import org.apache.maven.artifact.versioning.VersionRange;
import org.apache.maven.project.MavenProject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Resolves artifact references from the project into local and repository files and folders.
 * <p>
 * Only manages the dependencies because the main project can have multiple (two) folders for the project. This is not supported by this resolver.
 */
@SuppressFBWarnings("NP_NULL_ON_SOME_PATH_FROM_RETURN_VALUE")
public class ArtifactFileResolver {

    private static final Logger LOG = LoggerFactory.getLogger(ArtifactFileResolver.class);

    // A BiMultimap would come in really handy here...
    private final Multimap<File, Artifact> localFileArtifactCache;
    private final Map<Artifact, File> localArtifactFileCache;

    private final Map<Artifact, File> repoArtifactCache;

    // the artifact cache can not be a bimap, because for system artifacts, it is possible that multiple
    // maven coordinates point to the same file.
    private final Multimap<File, Artifact> repoFileCache = MultimapBuilder
            .hashKeys()
            .hashSetValues()
            .build();
    private final boolean preferLocal;

    public ArtifactFileResolver(final MavenProject project,
            final boolean preferLocal) throws DependencyResolutionRequiredException, IOException {
        checkNotNull(project, "project is null");
        this.preferLocal = preferLocal;

        // This needs to be a multimap, because it is possible by jiggling with classifiers that a local project
        // (with local folders) does map to multiple artifacts and therefore the file <-> artifact relation is not
        // 1:1 but 1:n. As found in https://github.com/basepom/duplicate-finder-maven-plugin/issues/10
        ImmutableMultimap.Builder<File, Artifact> localFileArtifactCacheBuilder = ImmutableMultimap.builder();

        // This can not be an immutable map builder, because the map is used for looking up while it is built up.
        this.repoArtifactCache = new HashMap<>(project.getArtifacts().size());

        for (final Artifact artifact : project.getArtifacts()) {
            final File repoPath = artifact.getFile().getCanonicalFile();
            final Artifact canonicalizedArtifact = ArtifactFileResolver.canonicalizeArtifact(artifact);

            checkState(repoPath.exists(), "Repository Path '%s' does not exist.", repoPath);
            final File oldFile = repoArtifactCache.put(canonicalizedArtifact, repoPath);
            checkState(oldFile == null || oldFile.equals(repoPath), "Already encountered a file for %s: %s", canonicalizedArtifact, oldFile);
            repoFileCache.put(repoPath, canonicalizedArtifact);
        }

        for (final MavenProject referencedProject : project.getProjectReferences().values()) {
            // referenced projects only have GAV coordinates but no scope.
            final Set<Artifact> repoArtifacts = findRepoArtifacts(referencedProject, repoArtifactCache);

            // This can happen if another sub-project in the reactor is e.g. used as a compiler plugin dependency.
            // In that case, the dependency will show up as a referenced project but not in the artifacts list from the project.
            // Fix up straight from the referenced project.
            if (repoArtifacts.isEmpty()) {
                LOG.debug(
                        format("Found project reference to %s but no repo reference, probably used in a plugin dependency.", referencedProject.getArtifact()));
            }

            for (final Artifact artifact : repoArtifacts) {

                final File outputDir = isTestArtifact(artifact) ? getTestOutputDirectory(referencedProject) : getOutputDirectory(referencedProject);

                if (outputDir.exists()) {
                    localFileArtifactCacheBuilder.put(outputDir, artifact);
                }
            }
        }

        this.localFileArtifactCache = localFileArtifactCacheBuilder.build();

        // Flip the File -> Artifact multimap. This also acts as a sanity check because no artifact
        // must be present more than one and the Map.Builder will choke if a key is around more than
        // once.
        ImmutableMap.Builder<Artifact, File> localArtifactFileCacheBuilder = ImmutableMap.builder();
        for (Map.Entry<File, Artifact> entry : localFileArtifactCache.entries()) {
            localArtifactFileCacheBuilder.put(entry.getValue(), entry.getKey());
        }

        this.localArtifactFileCache = localArtifactFileCacheBuilder.build();
    }

    public ImmutableMultimap<File, Artifact> resolveArtifactsForScopes(final Set<String> scopes) {
        checkNotNull(scopes, "scopes is null");

        final ImmutableMultimap.Builder<File, Artifact> inScopeBuilder = ImmutableMultimap.builder();
        for (final Artifact artifact : listArtifacts()) {
            if (artifact.getArtifactHandler().isAddedToClasspath()) {
                if (scopes.isEmpty() || scopes.contains(artifact.getScope())) {
                    final File file = resolveFileForArtifact(artifact);
                    checkState(file != null, "No file for artifact '%s' found!", artifact);
                    inScopeBuilder.put(file, artifact);
                }
            }
        }

        return inScopeBuilder.build();
    }

    public ImmutableSortedSet<ClasspathElement> getClasspathElementsForElements(final Collection<File> elements) {
        final ImmutableSortedSet.Builder<ClasspathElement> builder = ImmutableSortedSet.naturalOrder();

        for (final File element : elements) {
            resolveClasspathElementsForFile(element, builder);
        }
        return builder.build();
    }

    private void resolveClasspathElementsForFile(final File file, ImmutableSet.Builder<ClasspathElement> builder) {
        checkNotNull(file, "file is null");

        if (preferLocal && localFileArtifactCache.containsKey(file)) {
            for (Artifact artifact : localFileArtifactCache.get(file)) {
                builder.add(new ClasspathArtifact(artifact));
            }
            return;
        }

        if (repoFileCache.containsKey(file)) {
            for (Artifact artifact : repoFileCache.get(file)) {
                builder.add(new ClasspathArtifact(artifact));
            }
            return;
        }

        if (localFileArtifactCache.containsKey(file)) {
            for (Artifact artifact : localFileArtifactCache.get(file)) {
                builder.add(new ClasspathArtifact(artifact));
            }
            return;
        }

        builder.add(new ClasspathLocalFolder(file));
    }

    private File resolveFileForArtifact(final Artifact artifact) {
        checkNotNull(artifact, "artifact is null");

        if (preferLocal && localArtifactFileCache.containsKey(artifact)) {
            return localArtifactFileCache.get(artifact);
        }

        if (repoArtifactCache.containsKey(artifact)) {
            return repoArtifactCache.get(artifact);
        }

        return localArtifactFileCache.get(artifact);
    }

    @VisibleForTesting
    static DefaultArtifact canonicalizeArtifact(final Artifact artifact) {
        final VersionRange versionRange =
                artifact.getVersionRange() == null ? VersionRange.createFromVersion(artifact.getVersion()) : artifact.getVersionRange();
        String type = MoreObjects.firstNonNull(artifact.getType(), "jar");
        String classifier = artifact.getClassifier();

        if ("test-jar".equals(type) && (classifier == null || "tests".equals(classifier))) {
            type = "jar";
            classifier = "tests";
        }

        return new DefaultArtifact(artifact.getGroupId(),
                artifact.getArtifactId(),
                versionRange,
                artifact.getScope(),
                type,
                classifier,
                artifact.getArtifactHandler(),
                artifact.isOptional());
    }

    private Set<Artifact> listArtifacts() {
        return ImmutableSet.<Artifact>builder().addAll(localArtifactFileCache.keySet()).addAll(repoArtifactCache.keySet()).build();
    }

    private static Set<Artifact> findRepoArtifacts(final MavenProject project, final Map<Artifact, File> repoArtifactCache) {
        final ImmutableSet.Builder<Artifact> builder = ImmutableSet.builder();

        for (final Artifact artifact : repoArtifactCache.keySet()) {
            if (Objects.equals(project.getArtifact().getGroupId(), artifact.getGroupId())
                    && Objects.equals(project.getArtifact().getArtifactId(), artifact.getArtifactId())
                    && Objects.equals(project.getArtifact().getBaseVersion(), artifact.getBaseVersion())) {
                builder.add(artifact);
            }
        }
        return builder.build();
    }
}