
com.google.cloud.tools.opensource.dependencies.DependencyGraphBuilder Maven / Gradle / Ivy
/*
* Copyright 2018 Google LLC.
*
* 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.cloud.tools.opensource.dependencies;
import static com.google.cloud.tools.opensource.dependencies.RepositoryUtility.CENTRAL;
import static com.google.cloud.tools.opensource.dependencies.RepositoryUtility.mavenRepositoryFromUrl;
import static com.google.common.collect.ImmutableList.toImmutableList;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import java.nio.file.Path;
import java.util.ArrayDeque;
import java.util.List;
import java.util.Queue;
import org.eclipse.aether.DefaultRepositorySystemSession;
import org.eclipse.aether.RepositorySystem;
import org.eclipse.aether.artifact.Artifact;
import org.eclipse.aether.collection.CollectRequest;
import org.eclipse.aether.graph.DefaultDependencyNode;
import org.eclipse.aether.graph.Dependency;
import org.eclipse.aether.graph.DependencyFilter;
import org.eclipse.aether.graph.DependencyNode;
import org.eclipse.aether.repository.LocalRepository;
import org.eclipse.aether.repository.RemoteRepository;
import org.eclipse.aether.resolution.ArtifactResult;
import org.eclipse.aether.resolution.DependencyRequest;
import org.eclipse.aether.resolution.DependencyResolutionException;
import org.eclipse.aether.resolution.DependencyResult;
import org.eclipse.aether.util.graph.visitor.PathRecordingDependencyVisitor;
/**
* This class builds dependency graphs for Maven artifacts.
*
* A Maven dependency graph is the tree you see in {@code mvn dependency:tree} output. This graph
* has the following attributes:
*
*
* - It contains at most one node for the same group ID and artifact ID. (dependency
* mediation)
*
- The scope of a dependency affects the scope of its children's dependencies as per Maven:
* Dependency Scope
*
- It does not contain transitive provided-scope dependencies.
*
- It does not contain transitive optional dependencies.
*
*
* A full dependency graph is a dependency tree where each node's dependencies are fully resolved
* recursively. This graph has the following attributes:
*
*
* - The same artifact, which has the same group:artifact:version, appears in different nodes in
* the graph.
*
- The scope of a dependency does not affect the scope of its children's dependencies.
*
- Provided-scope and optional dependencies are not treated differently than any other
* dependency.
*
*/
public final class DependencyGraphBuilder {
private static final RepositorySystem system = RepositoryUtility.newRepositorySystem();
/** Maven Repositories to use when resolving dependencies. */
private final ImmutableList repositories;
private Path localRepository;
static {
OsProperties.detectOsProperties().forEach(System::setProperty);
}
public DependencyGraphBuilder() {
this(ImmutableList.of(CENTRAL.getUrl()));
}
/**
* @param mavenRepositoryUrls Maven repository URLs to search for dependencies
* @throws IllegalArgumentException if a URL is malformed or does not have an allowed scheme
*/
public DependencyGraphBuilder(Iterable mavenRepositoryUrls) {
ImmutableList.Builder repositoryListBuilder = ImmutableList.builder();
for (String mavenRepositoryUrl : mavenRepositoryUrls) {
RemoteRepository repository = mavenRepositoryFromUrl(mavenRepositoryUrl);
repositoryListBuilder.add(repository);
}
this.repositories = repositoryListBuilder.build();
}
/**
* Enable temporary repositories for tests.
*/
void setLocalRepository(Path localRepository) {
this.localRepository = localRepository;
}
private DependencyNode resolveCompileTimeDependencies(
List dependencyNodes, boolean fullDependencies)
throws DependencyResolutionException {
ImmutableList.Builder dependenciesBuilder = ImmutableList.builder();
for (DependencyNode dependencyNode : dependencyNodes) {
Dependency dependency = dependencyNode.getDependency();
if (dependency == null) {
// Root DependencyNode has null dependency field.
dependenciesBuilder.add(new Dependency(dependencyNode.getArtifact(), "compile"));
} else {
// The dependency field carries exclusions
dependenciesBuilder.add(dependency.setScope("compile"));
}
}
ImmutableList dependencyList = dependenciesBuilder.build();
DefaultRepositorySystemSession session =
fullDependencies
? RepositoryUtility.newSessionForFullDependency(system)
: RepositoryUtility.newSession(system);
if (localRepository != null) {
LocalRepository local = new LocalRepository(localRepository.toAbsolutePath().toString());
session.setLocalRepositoryManager(system.newLocalRepositoryManager(session, local));
}
CollectRequest collectRequest = new CollectRequest();
if (dependencyList.size() == 1) {
// With setRoot, the result includes dependencies with `optional:true` or `provided`
collectRequest.setRoot(dependencyList.get(0));
} else {
collectRequest.setDependencies(dependencyList);
}
for (RemoteRepository repository : repositories) {
collectRequest.addRepository(repository);
}
DependencyRequest dependencyRequest = new DependencyRequest();
dependencyRequest.setCollectRequest(collectRequest);
// resolveDependencies equals to calling both collectDependencies (build dependency tree) and
// resolveArtifacts (download JAR files).
DependencyResult dependencyResult = system.resolveDependencies(session, dependencyRequest);
return dependencyResult.getRoot();
}
/**
* Finds the full compile time, transitive dependency graph including duplicates, conflicting
* versions, and dependencies with 'provided' scope.
* In the event of I/O errors, missing artifacts, and other problems, it can
* return an incomplete graph.
*
* @param artifacts Maven artifacts to retrieve their dependencies
* @return dependency graph representing the tree of Maven artifacts
*/
public DependencyGraphResult buildFullDependencyGraph(List artifacts) {
ImmutableList dependencyNodes =
artifacts.stream().map(DefaultDependencyNode::new).collect(toImmutableList());
return buildDependencyGraph(dependencyNodes, GraphTraversalOption.FULL);
}
/**
* Builds the transitive dependency graph as seen by Maven. It does not include duplicates and
* conflicting versions. That is, this resolves conflicting versions by picking the first version
* seen. This is how Maven normally operates.
*
* In the event of I/O errors, missing artifacts, and other problems, it can
* return an incomplete graph.
*/
public DependencyGraphResult buildMavenDependencyGraph(Dependency dependency) {
return buildDependencyGraph(
ImmutableList.of(new DefaultDependencyNode(dependency)), GraphTraversalOption.MAVEN);
}
private DependencyGraphResult buildDependencyGraph(
List dependencyNodes, GraphTraversalOption traversalOption) {
boolean fullDependency = traversalOption == GraphTraversalOption.FULL;
DependencyNode node;
ImmutableSet.Builder artifactProblems = ImmutableSet.builder();
try {
node = resolveCompileTimeDependencies(dependencyNodes, fullDependency);
} catch (DependencyResolutionException ex) {
DependencyResult result = ex.getResult();
node = result.getRoot();
for (ArtifactResult artifactResult : result.getArtifactResults()) {
Artifact resolvedArtifact = artifactResult.getArtifact();
if (resolvedArtifact != null) {
continue;
}
Artifact requestedArtifact = artifactResult.getRequest().getArtifact();
artifactProblems.add(createUnresolvableArtifactProblem(node, requestedArtifact));
}
}
DependencyGraph graph = levelOrder(node);
return new DependencyGraphResult(graph, artifactProblems.build());
}
/**
* Returns a problem describing that {@code artifact} is unresolvable in the {@code
* dependencyGraph}.
*/
public static UnresolvableArtifactProblem createUnresolvableArtifactProblem(
DependencyNode dependencyGraph, Artifact artifact) {
ImmutableList> paths = findArtifactPaths(dependencyGraph, artifact);
if (paths.isEmpty()) {
// On certain conditions, Maven throws ArtifactDescriptorException even when the
// (transformed) dependency dependencyGraph does not contain the problematic artifact any
// more.
// https://issues.apache.org/jira/browse/MNG-6732
return new UnresolvableArtifactProblem(artifact);
} else {
return new UnresolvableArtifactProblem(paths.get(0));
}
}
private static ImmutableList> findArtifactPaths(
DependencyNode root, Artifact artifact) {
String coordinates = Artifacts.toCoordinates(artifact);
DependencyFilter filter =
(node, parents) ->
node.getArtifact() != null // artifact is null at a root dummy node.
&& Artifacts.toCoordinates(node.getArtifact()).equals(coordinates);
PathRecordingDependencyVisitor visitor = new PathRecordingDependencyVisitor(filter);
root.accept(visitor);
return ImmutableList.copyOf(visitor.getPaths());
}
private static final class LevelOrderQueueItem {
final DependencyNode dependencyNode;
// Null for the first item
final DependencyPath parentPath;
LevelOrderQueueItem(DependencyNode dependencyNode, DependencyPath parentPath) {
this.dependencyNode = dependencyNode;
this.parentPath = parentPath;
}
}
private enum GraphTraversalOption {
/** Normal Maven dependency graph */
MAVEN,
/** The full dependency graph */
FULL;
}
/**
* Returns a dependency graph by traversing dependency tree in level-order (breadth-first search).
*
* When {@code graphTraversalOption} is FULL_DEPENDENCY or FULL_DEPENDENCY_WITH_PROVIDED, then
* it resolves the dependency of the artifact of each node in the dependency tree; otherwise it
* just follows the given dependency tree starting with firstNode.
*
* @param firstNode node to start traversal
*/
public static DependencyGraph levelOrder(DependencyNode firstNode) {
DependencyGraph graph = new DependencyGraph();
Queue queue = new ArrayDeque<>();
queue.add(new LevelOrderQueueItem(firstNode, null));
while (!queue.isEmpty()) {
LevelOrderQueueItem item = queue.poll();
DependencyNode dependencyNode = item.dependencyNode;
DependencyPath parentPath = item.parentPath;
Artifact artifact = dependencyNode.getArtifact();
if (artifact != null && parentPath != null) {
// When requesting dependencies of 2 or more artifacts, root DependencyNode's artifact is
// set to null
// When there's a parent dependency node with the same groupId and artifactId as
// the dependency, Maven will not pick up the dependency. For example, if there's a
// dependency path "g1:a1:2.0 / ... / g1:a1:1.0" (the leftmost node as root), then Maven's
// dependency mediation always picks up g1:a1:2.0 over g1:a1:1.0.
String groupIdAndArtifactId = Artifacts.makeKey(artifact);
boolean parentHasSameKey =
parentPath.getArtifacts().stream()
.map(Artifacts::makeKey)
.anyMatch(key -> key.equals(groupIdAndArtifactId));
if (parentHasSameKey) {
continue;
}
}
// parentPath is null for the first item
DependencyPath path =
parentPath == null
? new DependencyPath(artifact)
: parentPath.append(dependencyNode.getDependency());
graph.addPath(path);
for (DependencyNode child : dependencyNode.getChildren()) {
queue.add(new LevelOrderQueueItem(child, path));
}
}
return graph;
}
}