DependencyBuilder.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.inline.mojo;

import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.collect.ImmutableSet.toImmutableSet;

import java.util.List;
import java.util.Objects;

import com.google.common.base.Throwables;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import org.apache.maven.RepositoryUtils;
import org.apache.maven.execution.MavenSession;
import org.apache.maven.project.DefaultDependencyResolutionRequest;
import org.apache.maven.project.DefaultProjectBuildingRequest;
import org.apache.maven.project.DependencyResolutionException;
import org.apache.maven.project.DependencyResolutionRequest;
import org.apache.maven.project.DependencyResolutionResult;
import org.apache.maven.project.MavenProject;
import org.apache.maven.project.ProjectBuilder;
import org.apache.maven.project.ProjectBuildingException;
import org.apache.maven.project.ProjectBuildingRequest;
import org.apache.maven.project.ProjectBuildingResult;
import org.apache.maven.project.ProjectDependenciesResolver;
import org.eclipse.aether.artifact.Artifact;
import org.eclipse.aether.artifact.DefaultArtifact;
import org.eclipse.aether.graph.Dependency;
import org.eclipse.aether.graph.DependencyFilter;
import org.eclipse.aether.repository.RemoteRepository;
import org.eclipse.aether.transfer.ArtifactTransferException;
import org.eclipse.aether.transfer.NoRepositoryLayoutException;
import org.eclipse.aether.util.artifact.JavaScopes;

/**
 * Builds a map of dependencies required by a specific project or another dependency.
 */
final class DependencyBuilder {

    private static final PluginLog LOG = new PluginLog(DependencyBuilder.class);

    private final MavenProject rootProject;
    private final MavenSession mavenSession;
    private final ProjectBuilder projectBuilder;
    private final ProjectDependenciesResolver projectDependenciesResolver;
    private final List<MavenProject> reactorProjects;

    DependencyBuilder(MavenProject rootProject, MavenSession mavenSession, ProjectBuilder projectBuilder,
            ProjectDependenciesResolver projectDependenciesResolver, List<MavenProject> reactorProjects) {
        this.rootProject = rootProject;
        this.mavenSession = mavenSession;
        this.projectBuilder = projectBuilder;
        this.projectDependenciesResolver = projectDependenciesResolver;
        this.reactorProjects = ImmutableList.copyOf(reactorProjects);
    }

    /**
     * Create a map of dependencies for a given dependency node (representing an element on the dependency tree).
     *
     * @param dependency         The dependency node to use.
     * @param projectScopeFilter A scope limiting filter to mask out dependencies out of scope.
     * @return A map of dependencies for this given dependency node.
     * @throws DependencyResolutionException Dependency resolution failed.
     * @throws ProjectBuildingException      Maven project could not be built.
     */
    ImmutableList<Dependency> mapDependency(final Dependency dependency,
            final DependencyFilter projectScopeFilter)
            throws DependencyResolutionException, ProjectBuildingException {
        checkNotNull(dependency, "dependency is null");

        // build the project
        final ProjectBuildingResult result = projectBuilder.build(convertFromAetherDependency(dependency), false, createProjectBuildingRequest());

        // now resolve the project representing the dependency.
        final MavenProject project = result.getProject();
        return mapProject(project, projectScopeFilter);
    }

    /**
     * Create a map of names to dependencies for a given project.
     *
     * @param project     The current maven project.
     * @param scopeFilter A scope limiting filter to mask out dependencies out of scope.
     * @return A map of dependencies for this given dependency node.
     * @throws DependencyResolutionException Dependency resolution failed.
     */
    ImmutableList<Dependency> mapProject(final MavenProject project,
            final DependencyFilter scopeFilter)
            throws DependencyResolutionException {
        checkNotNull(project, "project is null");

        final DependencyResolutionRequest request = new DefaultDependencyResolutionRequest();
        request.setRepositorySession(createProjectBuildingRequest().getRepositorySession());
        request.setMavenProject(project);
        request.setResolutionFilter(scopeFilter);

        DependencyResolutionResult result;

        try {
            result = projectDependenciesResolver.resolve(request);
        } catch (DependencyResolutionException e) {
            result = e.getResult();
            // try to resolve using the reactor projects
            final ImmutableSet<ProjectKey> reactorProjects = this.reactorProjects.stream()
                    .map(ProjectKey::fromProject).collect(toImmutableSet());

            // resolve all dependencies that are matched by the reactor.
            final ImmutableSet<Dependency> reactorDependencies = result.getUnresolvedDependencies().stream()
                    .filter(d -> reactorProjects.contains(ProjectKey.fromDependency(d)))
                    .collect(toImmutableSet());

            result.getUnresolvedDependencies().removeAll(reactorDependencies);
            result.getResolvedDependencies().addAll(reactorDependencies);

            // remove all unresolved system dependencies
            final ImmutableSet<Dependency> systemDependencies = result.getUnresolvedDependencies().stream()
                    .filter(d -> JavaScopes.SYSTEM.equals(d.getScope()))
                    .collect(toImmutableSet());

            result.getUnresolvedDependencies().removeAll(systemDependencies);
            result.getResolvedDependencies().addAll(systemDependencies);

            // remove all unresolved optional dependencies
            final ImmutableSet<Dependency> optionalDependencies = result.getUnresolvedDependencies().stream()
                    .filter(Dependency::isOptional)
                    .collect(toImmutableSet());

            result.getUnresolvedDependencies().removeAll(optionalDependencies);
            result.getResolvedDependencies().addAll(optionalDependencies);

            if (!result.getUnresolvedDependencies().isEmpty()) {
                final Throwable t = Throwables.getRootCause(e);
                RemoteRepository repository = null;

                if (t instanceof NoRepositoryLayoutException) {
                    repository = ((NoRepositoryLayoutException) t).getRepository();
                } else if (t instanceof ArtifactTransferException) {
                    repository = ((ArtifactTransferException) t).getRepository();
                }

                if (repository != null && "legacy".equals(repository.getContentType())) {
                    LOG.warn("Could not access a legacy repository for artifacts:  %s; Reason: %s", result.getUnresolvedDependencies(), t.getMessage());
                } else {
                    throw e;
                }
            }
        }

        return ImmutableList.copyOf(result.getResolvedDependencies());
    }

    static org.apache.maven.artifact.Artifact convertFromAetherDependency(final Dependency dependency) {
        final var mavenArtifact = RepositoryUtils.toArtifact(convertToPomArtifact(dependency.getArtifact()));
        mavenArtifact.setScope(dependency.getScope());
        mavenArtifact.setOptional(dependency.isOptional());

        return mavenArtifact;
    }

    private ProjectBuildingRequest createProjectBuildingRequest() {
        DefaultProjectBuildingRequest buildingRequest = new DefaultProjectBuildingRequest(mavenSession.getProjectBuildingRequest());
        buildingRequest.setRemoteRepositories(rootProject.getRemoteArtifactRepositories());
        return buildingRequest;
    }


    static Artifact convertToPomArtifact(final Artifact artifact) {
        // pom artifact has no classifier. If this is already a pom artifact, don't touch it.
        if (artifact.getClassifier().isEmpty() && "pom".equals(artifact.getExtension())) {
            return artifact;
        }

        // create a POM artifact.
        return new DefaultArtifact(artifact.getGroupId(), artifact.getArtifactId(), "pom", artifact.getVersion());
    }

    private static final class ProjectKey {

        private final String groupId;
        private final String artifactId;
        private final String version;

        public static ProjectKey fromProject(final MavenProject project) {
            checkNotNull(project, "project; is null");
            return new ProjectKey(project.getGroupId(), project.getArtifactId(), project.getVersion());
        }

        public static ProjectKey fromDependency(final Dependency dependency) {
            checkNotNull(dependency, "artifact; is null");
            return new ProjectKey(dependency.getArtifact().getGroupId(),
                    dependency.getArtifact().getArtifactId(),
                    dependency.getArtifact().getVersion());
        }

        private ProjectKey(final String groupId, final String artifactId, final String version) {
            this.groupId = checkNotNull(groupId, "groupId is null");
            this.artifactId = checkNotNull(artifactId, "artifactId is null");
            this.version = checkNotNull(version, "version is null");
        }

        @Override
        public boolean equals(Object o) {
            if (this == o) {
                return true;
            }
            if (o == null || getClass() != o.getClass()) {
                return false;
            }
            ProjectKey that = (ProjectKey) o;
            return groupId.equals(that.groupId)
                    && artifactId.equals(that.artifactId)
                    && version.equals(that.version);
        }

        @Override
        public int hashCode() {
            return Objects.hash(groupId, artifactId, version);
        }
    }
}