001/*
002 * Licensed under the Apache License, Version 2.0 (the "License");
003 * you may not use this file except in compliance with the License.
004 * You may obtain a copy of the License at
005 *
006 * http://www.apache.org/licenses/LICENSE-2.0
007 *
008 * Unless required by applicable law or agreed to in writing, software
009 * distributed under the License is distributed on an "AS IS" BASIS,
010 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
011 * See the License for the specific language governing permissions and
012 * limitations under the License.
013 */
014
015package org.basepom.mojo.dvc.dependency;
016
017import static com.google.common.base.Preconditions.checkNotNull;
018import static com.google.common.collect.ImmutableSet.toImmutableSet;
019
020import org.basepom.mojo.dvc.Context;
021import org.basepom.mojo.dvc.PluginLog;
022import org.basepom.mojo.dvc.QualifiedName;
023
024import java.util.Objects;
025
026import com.google.common.base.Throwables;
027import com.google.common.collect.ImmutableList;
028import com.google.common.collect.ImmutableMap;
029import com.google.common.collect.ImmutableSet;
030import org.apache.maven.RepositoryUtils;
031import org.apache.maven.project.DefaultDependencyResolutionRequest;
032import org.apache.maven.project.DependencyResolutionException;
033import org.apache.maven.project.DependencyResolutionRequest;
034import org.apache.maven.project.DependencyResolutionResult;
035import org.apache.maven.project.MavenProject;
036import org.apache.maven.project.ProjectBuildingException;
037import org.apache.maven.project.ProjectBuildingResult;
038import org.eclipse.aether.artifact.Artifact;
039import org.eclipse.aether.artifact.DefaultArtifact;
040import org.eclipse.aether.graph.Dependency;
041import org.eclipse.aether.graph.DependencyFilter;
042import org.eclipse.aether.graph.DependencyNode;
043import org.eclipse.aether.repository.RemoteRepository;
044import org.eclipse.aether.transfer.ArtifactTransferException;
045import org.eclipse.aether.transfer.NoRepositoryLayoutException;
046import org.eclipse.aether.util.artifact.JavaScopes;
047
048/**
049 * Builds a map of dependencies required by a specific project or another dependency.
050 */
051public final class DependencyMapBuilder {
052
053    private static final PluginLog LOG = new PluginLog(DependencyMapBuilder.class);
054
055    private final Context context;
056
057    public DependencyMapBuilder(final Context context) {
058        this.context = checkNotNull(context, "context is null");
059    }
060
061    /**
062     * Create a map of dependencies for a given dependency node (representing an element on the dependency tree).
063     *
064     * @param dependencyNode     The dependency node to use.
065     * @param projectScopeFilter A scope limiting filter to mask out dependencies out of scope.
066     * @return A map of dependencies for this given dependency node.
067     * @throws DependencyResolutionException Dependency resolution failed.
068     * @throws ProjectBuildingException      Maven project could not be built.
069     */
070    public DependencyMap mapDependency(final DependencyNode dependencyNode,
071            final DependencyFilter projectScopeFilter)
072            throws DependencyResolutionException, ProjectBuildingException {
073        checkNotNull(dependencyNode, "dependencyNode is null");
074        checkNotNull(projectScopeFilter, "projectScopeFilter is null");
075
076        // build the project
077        final ProjectBuildingResult result = context.getProjectBuilder()
078                .build(convertFromAetherDependency(dependencyNode), false, context.createProjectBuildingRequest());
079
080        // now resolve the project representing the dependency.
081        final MavenProject project = result.getProject();
082        return mapProject(project, projectScopeFilter);
083    }
084
085    /**
086     * Create a map of names to dependencies for a given project.
087     *
088     * @param project     The current maven project.
089     * @param scopeFilter A scope limiting filter to mask out dependencies out of scope.
090     * @return A map of dependencies for this given dependency node.
091     * @throws DependencyResolutionException Dependency resolution failed.
092     */
093    public DependencyMap mapProject(final MavenProject project,
094            final DependencyFilter scopeFilter)
095            throws DependencyResolutionException {
096        checkNotNull(project, "project is null");
097        checkNotNull(scopeFilter, "scopeFilter is null");
098
099        final DependencyResolutionRequest request = new DefaultDependencyResolutionRequest();
100        request.setRepositorySession(context.getRepositorySystemSession());
101        request.setMavenProject(project);
102        request.setResolutionFilter(scopeFilter);
103
104        DependencyResolutionResult result;
105
106        try {
107            result = context.getProjectDependenciesResolver().resolve(request);
108        } catch (DependencyResolutionException e) {
109            result = e.getResult();
110            // try to resolve using the reactor projects
111            final ImmutableSet<ProjectKey> reactorProjects = context.getReactorProjects().stream()
112                    .map(ProjectKey::fromProject).collect(toImmutableSet());
113
114            // resolve all dependencies that are matched by the reactor.
115            final ImmutableSet<Dependency> reactorDependencies = result.getUnresolvedDependencies().stream()
116                    .filter(d -> reactorProjects.contains(ProjectKey.fromDependency(d)))
117                    .collect(toImmutableSet());
118
119            result.getUnresolvedDependencies().removeAll(reactorDependencies);
120            result.getResolvedDependencies().addAll(reactorDependencies);
121
122            if (!context.isUnresolvedSystemArtifactsFailBuild()) {
123                final ImmutableSet<Dependency> systemDependencies = result.getUnresolvedDependencies().stream()
124                        .filter(d -> JavaScopes.SYSTEM.equals(d.getScope()))
125                        .collect(toImmutableSet());
126
127                result.getUnresolvedDependencies().removeAll(systemDependencies);
128                result.getResolvedDependencies().addAll(systemDependencies);
129            }
130
131            if (!context.isOptionalDependenciesMustExist()) {
132                final ImmutableSet<Dependency> optionalDependencies = result.getUnresolvedDependencies().stream()
133                        .filter(Dependency::isOptional)
134                        .collect(toImmutableSet());
135
136                result.getUnresolvedDependencies().removeAll(optionalDependencies);
137                result.getResolvedDependencies().addAll(optionalDependencies);
138            }
139
140            if (!result.getUnresolvedDependencies().isEmpty()) {
141                final Throwable t = Throwables.getRootCause(e);
142                RemoteRepository repository = null;
143
144                if (t instanceof NoRepositoryLayoutException) {
145                    repository = ((NoRepositoryLayoutException) t).getRepository();
146                } else if (t instanceof ArtifactTransferException) {
147                    repository = ((ArtifactTransferException) t).getRepository();
148                }
149
150                if (repository != null && "legacy".equals(repository.getContentType())) {
151                    LOG.warn("Could not access a legacy repository for artifacts:  %s; Reason: %s", result.getUnresolvedDependencies(), t.getMessage());
152                } else {
153                    throw e;
154                }
155            }
156        }
157
158        final DependencyNode graph = result.getDependencyGraph();
159        final ImmutableMap.Builder<QualifiedName, DependencyNode> dependencyCollector = ImmutableMap.builder();
160        final ImmutableMap<QualifiedName, DependencyNode> directDependencies = loadDependencyTree(graph, scopeFilter, dependencyCollector);
161        dependencyCollector.putAll(directDependencies);
162        return new DependencyMap(dependencyCollector.build(), directDependencies);
163    }
164
165    private ImmutableMap<QualifiedName, DependencyNode> loadDependencyTree(final DependencyNode node,
166            final DependencyFilter filter,
167            final ImmutableMap.Builder<QualifiedName, DependencyNode> allDependencyCollector) {
168        final ImmutableMap.Builder<QualifiedName, DependencyNode> builder = ImmutableMap.builder();
169        for (final DependencyNode dependencyNode : node.getChildren()) {
170            if (dependencyNode.getManagedBits() != 0) {
171                if ((dependencyNode.getManagedBits() & DependencyNode.MANAGED_VERSION) != 0) {
172                    LOG.debug("%s -> Managed Version!", dependencyNode.getArtifact());
173                }
174                if ((dependencyNode.getManagedBits() & DependencyNode.MANAGED_SCOPE) != 0) {
175                    LOG.debug("%s -> Managed Scope!", dependencyNode.getArtifact());
176                }
177                if ((dependencyNode.getManagedBits() & DependencyNode.MANAGED_OPTIONAL) != 0) {
178                    LOG.debug("%s -> Managed Optional!", dependencyNode.getArtifact());
179                }
180                if ((dependencyNode.getManagedBits() & DependencyNode.MANAGED_PROPERTIES) != 0) {
181                    LOG.debug("%s -> Managed Properties!", dependencyNode.getArtifact());
182                }
183                if ((dependencyNode.getManagedBits() & DependencyNode.MANAGED_EXCLUSIONS) != 0) {
184                    LOG.debug("%s -> Managed Exclusions!", dependencyNode.getArtifact());
185                }
186            }
187
188            if (filter.accept(dependencyNode, ImmutableList.of(node))) {
189                final QualifiedName name = QualifiedName.fromDependencyNode(dependencyNode);
190                builder.put(name, dependencyNode);
191
192                allDependencyCollector.putAll(loadDependencyTree(dependencyNode, filter, allDependencyCollector));
193            }
194        }
195        return builder.build();
196    }
197
198    static org.apache.maven.artifact.Artifact convertFromAetherDependency(final DependencyNode dependencyNode) {
199        Artifact aetherArtifact = convertToPomArtifact(dependencyNode.getArtifact());
200
201        final org.apache.maven.artifact.Artifact mavenArtifact = RepositoryUtils.toArtifact(aetherArtifact);
202        mavenArtifact.setScope(dependencyNode.getDependency().getScope());
203        mavenArtifact.setOptional(dependencyNode.getDependency().isOptional());
204
205        return mavenArtifact;
206    }
207
208    static Artifact convertToPomArtifact(final Artifact artifact) {
209        // pom artifact has no classifier. If this is already a pom artifact, don't touch it.
210        if (artifact.getClassifier().isEmpty() && "pom".equals(artifact.getExtension())) {
211            return artifact;
212        }
213
214        // create a POM artifact.
215        return new DefaultArtifact(artifact.getGroupId(), artifact.getArtifactId(), "pom", artifact.getVersion());
216    }
217
218    private static final class ProjectKey {
219
220        private final String groupId;
221        private final String artifactId;
222        private final String version;
223
224        public static ProjectKey fromProject(final MavenProject project) {
225            checkNotNull(project, "project; is null");
226            return new ProjectKey(project.getGroupId(), project.getArtifactId(), project.getVersion());
227        }
228
229        public static ProjectKey fromDependency(final Dependency dependency) {
230            checkNotNull(dependency, "artifact; is null");
231            return new ProjectKey(dependency.getArtifact().getGroupId(),
232                    dependency.getArtifact().getArtifactId(),
233                    dependency.getArtifact().getVersion());
234        }
235
236        private ProjectKey(final String groupId, final String artifactId, final String version) {
237            this.groupId = checkNotNull(groupId, "groupId is null");
238            this.artifactId = checkNotNull(artifactId, "artifactId is null");
239            this.version = checkNotNull(version, "version is null");
240        }
241
242        @Override
243        public boolean equals(Object o) {
244            if (this == o) {
245                return true;
246            }
247            if (o == null || getClass() != o.getClass()) {
248                return false;
249            }
250            ProjectKey that = (ProjectKey) o;
251            return groupId.equals(that.groupId)
252                    && artifactId.equals(that.artifactId)
253                    && version.equals(that.version);
254        }
255
256        @Override
257        public int hashCode() {
258            return Objects.hash(groupId, artifactId, version);
259        }
260    }
261}