
org.jvnet.maven.plugin.antrun.DependencyGraph Maven / Gradle / Ivy
package org.jvnet.maven.plugin.antrun;
import org.apache.maven.artifact.Artifact;
import org.apache.maven.artifact.resolver.ArtifactNotFoundException;
import org.apache.maven.artifact.resolver.ArtifactResolutionException;
import org.apache.maven.artifact.resolver.AbstractArtifactResolutionException;
import org.apache.maven.artifact.versioning.VersionRange;
import org.apache.maven.model.Dependency;
import org.apache.maven.project.MavenProject;
import org.apache.maven.project.ProjectBuildingException;
import org.apache.commons.collections.ListUtils;
import java.io.File;
import java.util.AbstractList;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.Stack;
import java.util.TreeMap;
import java.util.Queue;
import java.util.LinkedList;
/**
* Graph of dependencies among Maven artifacts.
*
*
* This graph, which consists of interconnected {@link Node}s and {@link Edge}s,
* represents a complete dependency graph rooted at the given Maven module.
* The constructor recursively parses all POMs for the dependency and builds this information.
*
*
* For example, if you have 4 modules A,B,C, and D that has the dependencies among them as follows:
*
* A->B,C
* B->D
* C->D
*
*
* Then if you construct a graph from 'A', you'll get a graph of four nodes (each representing
* maven module A,B,C, and D) and four edges (each representing dependencies among them.)
*
*
* Once constructed, a graph is accessible in several ways:
*
*
* -
* Start with {@link #getRoot() the root node} and traverse through edges like
* {@link Node#getForwardEdges(DependencyGraph)}.
*
*
-
* Use {@link #createSubGraph(GraphVisitor)} and obtain a sub-graph that matches the given
* criteria.
*
*
*
* @author Kohsuke Kawaguchi
*/
public final class DependencyGraph {
private final MavenComponentBag bag = MavenComponentBag.get();
private final Node root;
/**
* All {@link Node}s keyed by "groupId:artifactId:classifier"
*/
private final Map nodes = new TreeMap();
/**
* If true, ignore the {@link Node} that have failed to load.
*/
private final boolean tolerateBrokenPOMs;
/**
* Forward edges.
*
* Edges are kept on {@link DependencyGraph} so that we can
* create multiple {@link DependencyGraph}s that share the same node set.
*/
private final Map> forwardEdges = new HashMap>();
private final Map> backwardEdges = new HashMap>();
/**
* Creates a full dependency graph with the given artifact at the top.
*/
public DependencyGraph(Artifact root, boolean tolerateBrokenPOMs) throws ProjectBuildingException, ArtifactResolutionException, ArtifactNotFoundException {
this.tolerateBrokenPOMs = tolerateBrokenPOMs;
Queue q = new LinkedList();
this.root = buildNode(root,q);
visitBFS(q);
}
/**
* Creates a full dependency graph with the given project at the top.
*/
public DependencyGraph(MavenProject root, boolean tolerateBrokenPOMs) throws ProjectBuildingException, ArtifactResolutionException, ArtifactNotFoundException {
this.tolerateBrokenPOMs = tolerateBrokenPOMs;
Queue q = new LinkedList();
this.root = buildNode(root,q);
visitBFS(q);
}
/**
* Completes the graph in a breadth-first fashion.
*/
private void visitBFS(Queue q) throws ArtifactResolutionException, ArtifactNotFoundException, ProjectBuildingException {
while(!q.isEmpty())
q.poll().expand(this,q);
}
/**
* Used to create a subgraph.
*
* This method assumes that all nodes and edges are connected,
* hence the 'private' access. Use {@link #createSubGraph(GraphVisitor)}
* to construct a subset reliably.
*/
private DependencyGraph(Node root, Collection nodes, Collection edges, boolean tolerateBrokenPOMs) {
this.tolerateBrokenPOMs = tolerateBrokenPOMs;
if(nodes.isEmpty()) root = null; // graph is empty
this.root = root;
if(root!=null) {
Set reachable = new HashSet();
reachable.add(root); // root is always reachable
if(!nodes.contains(root))
throw new IllegalArgumentException("root "+root+" is not a part of nodes:"+nodes);
for (Node n : nodes)
this.nodes.put(n.getId(),n);
for (Edge e : edges) {
if(contains(e.src) && contains(e.dst)) {
e.addEdge(forwardEdges,e.src);
e.addEdge(backwardEdges,e.dst);
reachable.add(e.dst);
}
}
// some nodes were unreachable
if(reachable.size()!=this.nodes.size())
throw new IllegalArgumentException();
}
}
/**
* Gets the root node.
*
*
* This is non-null unless this graph is {@link #isEmpty() empty}.
*/
public Node getRoot() {
return root;
}
/**
* Returns true if the graph contains nothing at all.
*/
public boolean isEmpty() {
return root==null;
}
/**
* Returns all nodes in this graph.
*/
public Collection getAllNodes() {
return nodes.values();
}
/**
* Checks if the graph contains the given node.
*/
public boolean contains(Node node) {
return nodes.containsKey(node.getId());
}
/**
* Gets the associated {@link Node}, or null if none exists.
*/
public Node toNode(Artifact a) throws ProjectBuildingException, ArtifactResolutionException, ArtifactNotFoundException {
String id = a.getGroupId()+':'+a.getArtifactId()+':'+a.getClassifier();
return nodes.get(id);
}
/**
* Gets the associated {@link Node}. If none exists, it will be created.
*
*
* Graph building has to be done breadth-first fashion so that the version
* conflit resolution happens correctly — that is, if there exists dependency like
* A -> B(1.0) -> C(2.0) and A -> C(2.2), then we want to pick up C(2.2), not C(2.0).
* The criteria to do this in Maven is the length of the dependency chain, and that
* can be done naturally by BFS. So we pass around a {@link Queue} to keep track of the remaining
* {@link Node}s to be expanded.
*/
private Node buildNode(Artifact a, Queue q) throws ProjectBuildingException, ArtifactResolutionException, ArtifactNotFoundException {
String id = a.getGroupId()+':'+a.getArtifactId()+':'+a.getClassifier();
Node n = nodes.get(id);
if(n==null) {
n = new Node(a,this,q);
nodes.put(id, n);
}
return n;
}
private Node buildNode(MavenProject p, Queue q) throws ProjectBuildingException, ArtifactResolutionException, ArtifactNotFoundException {
String id = p.getGroupId()+':'+p.getArtifactId()+":null";
Node n = nodes.get(id);
if(n==null) {
n = new Node(p, q);
nodes.put(id, n);
}
return n;
}
/**
* Accepts the visitor and invoke its visitor methods to create a sub-graph.
*
*
* This method is convenient for obtaining a sub-graph of dependencies
* by filtering out nodes/edges. For example, to obtain all the transitive
* dependencies that exclude provided/test dependencies, you can do:
*
*
* createSubgraph(new {@link ScopeFilter}("compile","runtime"))
*
*
* @return
* A sub-graph of this graph that consists of nodes and edges for which the visitor returns true.
* Can be an empty graph, but never null.
*/
public DependencyGraph createSubGraph(GraphVisitor visitor) {
return createSubGraph(root,visitor);
}
/**
* Accepts the visitor. Simply an alias for {@link #createSubGraph(GraphVisitor)}.
*/
public void accept(GraphVisitor visitor) {
createSubGraph(visitor);
}
/**
* Creates a full subgraph rooted at the given node.
*/
public DependencyGraph createSubGraph(Node root) {
return createSubGraph(root,new DefaultGraphVisitor());
}
/**
* Visits the graph started at the given node, and creates a sub-graph
* from visited nodes and edges.
*
*
* This is the slightly generalized version of {@link #createSubGraph(GraphVisitor)}
*/
public DependencyGraph createSubGraph(Node node, GraphVisitor visitor) {
Set visited = new HashSet();
Set nodes = new HashSet();
List edges = new ArrayList();
Stack q = new Stack();
q.push(node);
while(!q.isEmpty()) {
DependencyGraph.Node n = q.pop();
if(visitor.visit(n)) {
nodes.add(n);
for (Edge e : n.getForwardEdges(this)) {
if(visitor.visit(e)) {
edges.add(e);
if(visited.add(e.dst))
q.push(e.dst);
}
}
}
}
return new DependencyGraph(node,nodes,edges, tolerateBrokenPOMs);
}
/**
* Creates a sub-graph from the given set of nodes (which must be subset of
* nodes in the current graph) with all edges { (u,v) | u \in nodes, v \in nodes }
*/
public DependencyGraph createSubGraph(Node root, Collection nodes) {
List edges = new ArrayList();
for (List el : forwardEdges.values()) {
for (Edge e : el) {
if(nodes.contains(e.src) && nodes.contains(e.dst))
edges.add(e);
}
}
return new DependencyGraph(root,nodes,edges, tolerateBrokenPOMs);
}
public String toString() {
StringBuilder buf = new StringBuilder();
buf.append("DependencyGraph[root=").append(root).append(",\n");
buf.append(" nodes=[\n");
for (Node node : nodes.values())
buf.append(" ").append(node).append('\n');
buf.append(" ]\n");
buf.append(" edges=[\n");
for (Map.Entry> n : forwardEdges.entrySet()) {
for (Edge e : n.getValue()) {
buf.append(" ").append(e).append('\n');
}
}
buf.append(" ]\n]");
return buf.toString();
}
private interface Resolver {
File resolve() throws AbstractArtifactResolutionException;
}
private static final Resolver NULL = new Resolver() {
public File resolve() {
return null;
}
};
/**
* Node, which represents an artifact.
*
*
* A single {@link Node} can be used in multiple {@link DependencyGraph} objects,
* so the graph traversal method all takes {@link DependencyGraph} object
* to determine the context in which the operation works.
*/
public static final class Node {
/**
* Basic properties of a module.
* If {@link #pom} is non-null, this information is redundant, but it needs to be
* kept separately for those rare cases where pom==null.
*/
public final String groupId,artifactId,version,type,classifier;
private final MavenProject pom;
private /*final*/ File artifactFile;
private Resolver artifactResolver = NULL;
/**
* Represents the artifact that we want to fetch.
*/
private final Artifact artifact;
private Node(Artifact artifact, DependencyGraph g, Queue q) throws ProjectBuildingException, ArtifactResolutionException, ArtifactNotFoundException {
groupId = artifact.getGroupId();
artifactId = artifact.getArtifactId();
version = artifact.getVersion();
type = artifact.getType();
classifier = artifact.getClassifier();
this.artifact = artifact;
if("system".equals(artifact.getScope())) {
// system scoped artifacts don't have POM, so the attempt to load it will fail.
pom = null;
} else {
pom = g.bag.mavenProjectBuilder.buildFromRepository(
// this create another Artifact instance whose type is 'pom'
g.bag.factory.createProjectArtifact(artifact.getGroupId(),artifact.getArtifactId(), artifact.getVersion()),
g.bag.project.getRemoteArtifactRepositories(),
g.bag.localRepository);
q.add(this); // visit dependencies from this POM later
}
}
private void checkArtifact(final Artifact artifact, final MavenComponentBag bag) {
artifactResolver = new Resolver() {
public File resolve() throws AbstractArtifactResolutionException {
if(bag.project.getArtifact()==artifact) {
// our own module. Trying to resolve this in the usual way is most likely to fail,
// so use what we have, if any.
artifactFile =artifact.getFile();
return artifactFile;
}
if(pom!=null) {
if (pom.getRemoteArtifactRepositories()==null)
bag.resolveArtifact(artifact);
else {
//use repositories from pom and also MavenComponentBag
bag.resolveArtifact(artifact,
ListUtils.sum(pom.getRemoteArtifactRepositories(),
bag.remoteRepositories));
}
}
else
bag.resolveArtifact(artifact);
artifactFile = artifact.getFile();
if(artifactFile==null)
throw new IllegalStateException("Artifact is not resolved yet: "+artifact);
return artifactFile;
}
};
}
private Node(MavenProject pom, Queue q) {
this.pom = pom;
groupId = pom.getGroupId();
artifactId = pom.getArtifactId();
version = pom.getVersion();
type = pom.getPackaging(); // are these the same thing?
classifier = null;
artifact = pom.getArtifact();
q.add(this); // visit dependencies from this POM later
}
private void expand(DependencyGraph g, Queue q) throws ArtifactResolutionException, ArtifactNotFoundException, ProjectBuildingException {
checkArtifact(artifact,g.bag);
loadDependencies(g,q);
}
private void loadDependencies(DependencyGraph g, Queue q) throws ProjectBuildingException, ArtifactResolutionException, ArtifactNotFoundException {
for( Dependency d : (List)pom.getDependencies() ) {
// the last boolean parameter is redundant, but the version that doesn't take this
// has a bug. See MNG-2524
Artifact a = g.bag.factory.createDependencyArtifact(
d.getGroupId(), d.getArtifactId(), VersionRange.createFromVersion(d.getVersion()),
d.getType(), d.getClassifier(), d.getScope(), false);
// beware of Maven bug! make sure artifact got the value inherited from dependency
assert a.getScope().equals(d.getScope());
try {
new Edge(g,this,g.buildNode(a,q),d.getScope(),d.isOptional());
} catch (ProjectBuildingException e) {
handleNodeResolutionException(g,e);
} catch (ArtifactResolutionException e) {
handleNodeResolutionException(g,e);
} catch (ArtifactNotFoundException e) {
handleNodeResolutionException(g,e);
}
}
}
private void handleNodeResolutionException(DependencyGraph g, Exception e) throws ProjectBuildingException {
if (g.tolerateBrokenPOMs)
System.err.println("Failed to parse dependencies of " + getId() + ". trail=" + getTrail(g));
else
throw new ProjectBuildingException(getId(), "Failed to parse dependencies of " + getId() + ". trail=" + getTrail(g), e);
}
/**
* Gets the parsed POM for this artifact.
*
* @return null
* if POM is not available for this module.
* That can happen for example for system-scoped artifacts.
*/
public MavenProject getProject() {
return pom;
}
/**
* Gets the artifact file, like a jar.
*
* @return
* for system-scoped artifacts, this may null. If this node represents the current module
* being built, this field may or may not be null, depending on whether the artifact is
* already created in the current build or not.
* For all the other modules, this is never null.
* @throws AbstractArtifactResolutionException
* Failed to resolve artifacat.
*/
public File getArtifactFile() throws AbstractArtifactResolutionException {
if(artifactFile==null)
artifactFile = artifactResolver.resolve();
return artifactFile;
}
/**
* Gets the forward dependency edges (modules that this module depends on.)
*/
public List getForwardEdges(DependencyGraph g) {
return getEdges(g.forwardEdges);
}
/**
* Gets the backward dependency edges (modules that depend on this module.)
*/
public List getBackwardEdges(DependencyGraph g) {
return getEdges(g.backwardEdges);
}
private List getEdges(Map> allEdges) {
List edges = allEdges.get(this);
if(edges==null) return Collections.emptyList();
return edges;
}
/**
* Gets the nodes that the given node depends on.
*/
public List getForwardNodes(final DependencyGraph g) {
return new AbstractList() {
final List forward = getForwardEdges(g);
public Node get(int index) {
return forward.get(index).dst;
}
public int size() {
return forward.size();
}
};
}
/**
* Gets the nodes that depend on the given node.
*/
public List getBackwardNodes(final DependencyGraph g) {
return new AbstractList() {
final List backward = getBackwardEdges(g);
public Node get(int index) {
return backward.get(index).src;
}
public int size() {
return backward.size();
}
};
}
public String toString() {
return groupId+':'+artifactId+':'+version;
}
public String getId() {
return groupId+':'+artifactId+':'+classifier;
}
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Node node = (Node) o;
if (!artifactId.equals(node.artifactId)) return false;
if (classifier != null ? !classifier.equals(node.classifier) : node.classifier != null) return false;
if (!groupId.equals(node.groupId)) return false;
if (version != null ? !version.equals(node.version) : node.version != null) return false;
return true;
}
public int hashCode() {
int result;
result = groupId.hashCode();
result = 31 * result + artifactId.hashCode();
result = 31 * result + (version != null ? version.hashCode() : 0);
result = 31 * result + (classifier != null ? classifier.hashCode() : 0);
return result;
}
/**
* Builds the dependency trail from this node to the root node, in that order.
*
* This is useful as diagnostic information.
*/
public List getTrail(DependencyGraph graph) {
List trail = new ArrayList();
Node n = this;
while(n!=graph.getRoot()) {
List list = n.getBackwardEdges(graph);
if(list.isEmpty())
throw new AssertionError("Lost trail at "+trail+" from "+this+" with "+graph);
Edge e = list.get(0);
trail.add(e);
n = e.src;
}
Collections.reverse(trail);
return trail;
}
}
public static final class Edge {
/**
* The module that depends on another.
*/
public final Node src;
/**
* The module that is being dependent by another.
*/
public final Node dst;
/**
* Dependency scope. Stuff like "compile", "runtime", etc.
* Never null.
*/
public final String scope;
/**
* True if this dependency is optional.
*/
public final boolean optional;
public Edge(DependencyGraph g, Node src, Node dst, String scope, boolean optional) {
this.src = src;
this.dst = dst;
if(scope==null) scope="compile";
this.scope = scope;
this.optional = optional;
addEdge(g.forwardEdges,src);
addEdge(g.backwardEdges,dst);
}
private void addEdge(Map> edgeSet, Node index) {
List l = edgeSet.get(index);
if(l==null)
edgeSet.put(index,l=new ArrayList());
for (Edge e : l) {
if(e.src.equals(this.src) && e.dst.equals(this.dst))
return; // duplicate
}
l.add(this);
}
public String toString() {
StringBuilder buf = new StringBuilder();
buf.append(src).append("--(").append(scope);
if(optional)
buf.append("/optional");
buf.append(")-->").append(dst);
return buf.toString();
}
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Edge that = (Edge) o;
return dst.equals(that.dst) && src.equals(that.src);
}
public int hashCode() {
int result;
result = src.hashCode();
result = 31 * result + dst.hashCode();
return result;
}
}
}