diff --git a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/collect/DataPool.java b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/collect/DataPool.java index 4a145558d..039f95b5b 100644 --- a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/collect/DataPool.java +++ b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/collect/DataPool.java @@ -362,17 +362,17 @@ public int hashCode() static final class GraphKey { - private final Artifact artifact; + final Artifact artifact; - private final List repositories; + final List repositories; - private final DependencySelector selector; + final DependencySelector selector; - private final DependencyManager manager; + final DependencyManager manager; - private final DependencyTraverser traverser; + final DependencyTraverser traverser; - private final VersionFilter filter; + final VersionFilter filter; private final int hashCode; diff --git a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/collect/DefaultDependencyCollector.java b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/collect/DefaultDependencyCollector.java index a46499dfd..55d82162c 100644 --- a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/collect/DefaultDependencyCollector.java +++ b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/collect/DefaultDependencyCollector.java @@ -80,6 +80,20 @@ public class DefaultDependencyCollector implements DependencyCollector, Service { + /** + * The key in the repository session's {@link org.eclipse.aether.RepositorySystemSession#getConfigProperties() + * configuration properties} used to store a {@link Boolean} flag controlling the resolver's skip & reconcile mode. + * + * @since 1.7.3 + */ + public static final String CONFIG_PROP_USE_SKIP_RECONCILE = "aether.dependencyCollector.useSkipReconcile"; + + /** + * The default value for {@link #CONFIG_PROP_USE_SKIP_RECONCILE}, {@code true}. + * + * @since 1.7.3 + */ + public static final boolean CONFIG_PROP_USE_SKIP_RECONCILE_DEFAULT = true; private static final String CONFIG_PROP_MAX_EXCEPTIONS = "aether.dependencyCollector.maxExceptions"; @@ -147,6 +161,14 @@ public CollectResult collectDependencies( RepositorySystemSession session, Colle requireNonNull( request, "request cannot be null" ); session = optimizeSession( session ); + boolean useSkipReconcile = ConfigUtils.getBoolean( + session, CONFIG_PROP_USE_SKIP_RECONCILE_DEFAULT, CONFIG_PROP_USE_SKIP_RECONCILE + ); + if ( useSkipReconcile ) + { + LOGGER.debug( "Collector skip & reconcile enabled" ); + } + RequestTrace trace = RequestTrace.newChild( request.getTrace(), request ); CollectResult result = new CollectResult( request ); @@ -252,7 +274,10 @@ public CollectResult collectDependencies( RepositorySystemSession session, Colle DefaultVersionFilterContext versionContext = new DefaultVersionFilterContext( session ); - Args args = new Args( session, trace, pool, nodes, context, versionContext, request ); + DependencyResolveReconciler dependencyResolveReconciler = + useSkipReconcile ? new DependencyResolveReconciler() : null; + Args args = new Args( session, trace, pool, nodes, dependencyResolveReconciler, + context, versionContext, request ); Results results = new Results( result, session ); process( args, results, dependencies, repositories, @@ -261,6 +286,29 @@ public CollectResult collectDependencies( RepositorySystemSession session, Colle depTraverser != null ? depTraverser.deriveChildTraverser( context ) : null, verFilter != null ? verFilter.deriveChildFilter( context ) : null ); + if ( args.reconciler != null ) + { + //reconcile the skipped nodes + Collection reconcileNodes = + args.reconciler.getNodesToReconcile( session, result ); + for ( DependencyResolveReconciler.DependencyResolveSkip skip : reconcileNodes ) + { + LOGGER.debug( "Reconcile: {} ", skip ); + Args newArgs = + new Args( session, trace, pool, new NodeStack(), args.reconciler, context, versionContext, + request ); + DataPool.GraphKey key = skip.graphKey; + List parents = skip.parentPathsOfCurrentNode; + for ( DependencyNode parent : parents ) + { + newArgs.nodes.push( parent ); + } + newArgs.nodes.push( skip.node ); + process( newArgs, results, skip.dependencies, key.repositories, key.selector, key.manager, + key.traverser, key.filter ); + } + } + errorPath = results.errorPath; } @@ -491,29 +539,57 @@ private void doRecurse( Args args, Results results, List repos VersionFilter childFilter = verFilter != null ? verFilter.deriveChildFilter( context ) : null; final List childRepos = - args.ignoreRepos - ? repositories - : remoteRepositoryManager.aggregateRepositories( args.session, repositories, - descriptorResult.getRepositories(), true ); + args.ignoreRepos + ? repositories + : remoteRepositoryManager.aggregateRepositories( args.session, repositories, + descriptorResult.getRepositories(), true ); - Object key = - args.pool.toKey( d.getArtifact(), childRepos, childSelector, childManager, childTraverser, childFilter ); + Object key = args.pool.toKey( + d.getArtifact(), childRepos, childSelector, childManager, childTraverser, childFilter + ); - List children = args.pool.getChildren( key ); - if ( children == null ) + if ( args.reconciler == null ) { - args.pool.putChildren( key, child.getChildren() ); + List children = args.pool.getChildren( key ); + if ( children == null ) + { + args.pool.putChildren( key, child.getChildren() ); - args.nodes.push( child ); + args.nodes.push( child ); - process( args, results, descriptorResult.getDependencies(), childRepos, childSelector, childManager, - childTraverser, childFilter ); + process( args, results, descriptorResult.getDependencies(), childRepos, childSelector, childManager, + childTraverser, childFilter ); - args.nodes.pop(); + args.nodes.pop(); + } + else + { + child.setChildren( children ); + } } else { - child.setChildren( children ); + List parents = args.nodes.getParentNodes(); + int depth = parents.size() + 1; //the depth if pushed the child to stack + DependencyResolveReconciler.CacheResult result = + args.reconciler.findCandidateWithLowerDepth( child, depth ); + if ( result != null ) + { + //Do not set the children as the result can be most likely ignored(won't be picked up) + args.reconciler.addSkip( child, key, descriptorResult.getDependencies(), parents, + result.parentPathsOfCandidateLowerDepth ); + LOGGER.trace( "Skipped resolving artifact {} of depth {}", child.getArtifact(), depth ); + } + else + { + args.nodes.push( child ); + LOGGER.trace( "Resolving artifact {} of depth {}", child.getArtifact(), depth ); + process( args, results, descriptorResult.getDependencies(), childRepos, childSelector, childManager, + childTraverser, childFilter ); + args.pool.putChildren( key, child.getChildren() ); + args.reconciler.cacheChildrenWithDepth( child, parents ); + args.nodes.pop(); + } } } @@ -706,9 +782,16 @@ static class Args final CollectRequest request; + /** + * Nullable: is {@code null} if {@link #CONFIG_PROP_USE_SKIP_RECONCILE} evaluates to {@code false}. + */ + final DependencyResolveReconciler reconciler; + + @SuppressWarnings( "checkstyle:parameternumber" ) Args( RepositorySystemSession session, RequestTrace trace, DataPool pool, NodeStack nodes, - DefaultDependencyCollectionContext collectionContext, DefaultVersionFilterContext versionContext, - CollectRequest request ) + DependencyResolveReconciler reconciler, + DefaultDependencyCollectionContext collectionContext, DefaultVersionFilterContext versionContext, + CollectRequest request ) { this.session = session; this.request = request; @@ -717,6 +800,7 @@ static class Args this.trace = trace; this.pool = pool; this.nodes = nodes; + this.reconciler = reconciler; this.collectionContext = collectionContext; this.versionContext = versionContext; } diff --git a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/collect/DependencyResolveReconciler.java b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/collect/DependencyResolveReconciler.java new file mode 100644 index 000000000..39b55e652 --- /dev/null +++ b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/collect/DependencyResolveReconciler.java @@ -0,0 +1,435 @@ +package org.eclipse.aether.internal.impl.collect; + +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +import org.eclipse.aether.DefaultRepositorySystemSession; +import org.eclipse.aether.RepositoryException; +import org.eclipse.aether.RepositorySystemSession; +import org.eclipse.aether.artifact.Artifact; +import org.eclipse.aether.collection.CollectResult; +import org.eclipse.aether.collection.DependencyCollectionException; +import org.eclipse.aether.collection.DependencyGraphTransformer; +import org.eclipse.aether.graph.Dependency; +import org.eclipse.aether.graph.DependencyNode; +import org.eclipse.aether.graph.DependencyVisitor; +import org.eclipse.aether.util.artifact.ArtifactIdUtils; +import org.eclipse.aether.util.graph.transformer.ConflictResolver; +import org.eclipse.aether.util.graph.visitor.CloningDependencyVisitor; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.stream.Collectors; + +/** + * Skip resolve a node if depth is deeper, then reconcile the result by resolving the dependency conflicts. + */ +public class DependencyResolveReconciler +{ + + private static final Logger LOGGER = LoggerFactory.getLogger( DependencyResolveReconciler.class ); + + /** + * Cache maven dependency resolve result by depth artifact -> result + */ + private final HashMap nodesWithDepth; + + /** + * Track the nodes that have been skipped to resolve for later reconciling + */ + private final LinkedHashSet skippedNodes; + + public DependencyResolveReconciler() + { + this.nodesWithDepth = new HashMap<>( 256 ); + this.skippedNodes = new LinkedHashSet<>(); + } + + /** + * Cache the node with children, parents path & depth information + * + * @param node + * @param parents + */ + public void cacheChildrenWithDepth( DependencyNode node, List parents ) + { + int depth = parents.size() + 1; + Artifact artifact = node.getArtifact(); + List children = node.getChildren(); + nodesWithDepth.put( artifact, new DependencyResolveResult( children, parents, depth ) ); + } + + + /** + * Return a candidate node if the depth of current node is deeper than the cached one + * + * @param current + * @param depth + * @return + */ + public CacheResult findCandidateWithLowerDepth( DependencyNode current, int depth ) + { + DependencyResolveResult result = nodesWithDepth.get( current.getArtifact() ); + if ( result != null && result.depth <= depth ) + { + CacheResult cache = new CacheResult(); + cache.dependencyNodes = result.children; + cache.parentPathsOfCandidateLowerDepth = result.parentPaths; + return cache; + } + + return null; + } + + /** + * Record a node that has been skipped by another node + * + * @param current current dependency node + * @param key the graph key + * @param dependencies children of current dependency node + * @param parents parents of current dependency node + * @param skippedBy parents of the node that the skipped node is reusing + */ + public void addSkip( DependencyNode current, Object key, List dependencies, + List parents, List skippedBy ) + { + if ( dependencies != null && dependencies.size() > 0 ) + { + DependencyResolveSkip skip = + new DependencyResolveSkip( current, (DataPool.GraphKey) key, dependencies, parents, skippedBy, + parents.size() + 1 ); + skippedNodes.add( skip ); + } + } + + /** + * Find all skipped Nodes that need to be reconciled. + * + * @return + */ + public Collection getNodesToReconcile( RepositorySystemSession session, + CollectResult result ) + throws DependencyCollectionException + { + long start = System.nanoTime(); + + DefaultRepositorySystemSession cloned = new DefaultRepositorySystemSession( session ); + // set as verbose so that the winner will be recorded in the DependencyNode.getData + cloned.setConfigProperty( ConflictResolver.CONFIG_PROP_VERBOSE, true ); + DependencyGraphTransformer transformer = session.getDependencyGraphTransformer(); + if ( transformer == null ) + { + return Collections.emptyList(); + } + + //cloned root node so it won't affect original root node + CloningDependencyVisitor vis = new CloningDependencyVisitor(); + DependencyNode root = result.getRoot(); + root.accept( vis ); + root = vis.getRootNode(); + + try + { + DefaultDependencyGraphTransformationContext context = + new DefaultDependencyGraphTransformationContext( cloned ); + root = transformer.transformGraph( root, context ); + } + catch ( RepositoryException e ) + { + result.addException( e ); + } + + if ( !result.getExceptions().isEmpty() ) + { + throw new DependencyCollectionException( result ); + } + + //transform the cloned root node to find out conflicts and nodes we want to reconcile + ReconcilingClonedGraphVisitor reconciler = new ReconcilingClonedGraphVisitor( + skippedNodes, vis.getClonedNodes() ); + root.accept( reconciler ); + + Set toReconcile = reconciler.toReconcile; + LOGGER.debug( "Skipped resolving {} nodes and decided to reconcile {} nodes to solve dependency conflicts", + skippedNodes.size(), toReconcile.size() ); + LOGGER.debug( "Finished to compute the nodes required to be reconciled in: {} ", + ( System.nanoTime() - start ) ); + + if ( !toReconcile.isEmpty() ) + { + removeConflictedNodes( toReconcile, reconciler.conflicts ); + } + + return toReconcile; + } + + /** + * Remove all cached nodes that has any conflict losers in parent paths from nodesWithDepth to make sure it won't + * reuse the incorrect result when reconciling the given nodes. + * + * @param toReconcile + * @param conflicts + */ + private void removeConflictedNodes( + Set toReconcile, + Set conflicts ) + { + //remove cache that belongs to the skipped nodes to be reconciled + Set toRemove = toReconcile.stream().map( r -> r.node.getArtifact() ).collect( Collectors.toSet() ); + + //remove cache if the parent path inherits from a invalid node + Set invalidArtifacts = conflicts.stream().map( c -> c.artifact ).collect( Collectors.toSet() ); + invalidArtifacts.addAll( toRemove ); + Set keys = nodesWithDepth.keySet(); + for ( Artifact key : keys ) + { + List parents = nodesWithDepth.get( key ).parentPaths; + for ( DependencyNode p : parents ) + { + if ( invalidArtifacts.contains( p.getArtifact() ) ) + { + toRemove.add( key ); + break; + } + } + } + + toRemove.forEach( r -> + { + nodesWithDepth.remove( r ); + LOGGER.trace( "Removed conflicted node from cache: {} ", r ); + } ); + } + + + /** + * Find out all nodes need to be reconciled: the nodes were skipped by other nodes but are actually selected. Note: + * this visitor accepts the transformed graph based on the cloned root node. + */ + static class ReconcilingClonedGraphVisitor + implements DependencyVisitor + { + + /** + * Cloned Node -> Original Node + */ + final Map reverseClonedNodes; + final Map skippedNodes; + final Set toReconcile; + + /** + * Conflicts of dependencies + */ + final Set conflicts; + + ReconcilingClonedGraphVisitor( + LinkedHashSet skippedNodes, + Map clonedNodeMap ) + { + this.toReconcile = new LinkedHashSet<>(); + this.conflicts = new LinkedHashSet<>(); + + this.skippedNodes = skippedNodes.stream().collect( + Collectors.toMap( x -> x.node, x -> x ) ); + + Map reverseMap = new HashMap<>(); + clonedNodeMap.forEach( ( key, value ) -> reverseMap.put( value, key ) ); + this.reverseClonedNodes = reverseMap; + } + + + public boolean visitEnter( DependencyNode node ) + { + // the cloned node after transformation + DependencyNode clonedNode = node; + //the original node (before transformation) corresponding to the cloned node + DependencyNode origin = this.reverseClonedNodes.get( clonedNode ); + DependencyNode winner = (DependencyNode) clonedNode.getData().get( ConflictResolver.NODE_DATA_WINNER ); + // winner null means the node is actually selected, not a conflict loser + if ( winner == null ) + { + /* + * Cases when children is empty + * 1) skipped node:skipped by other nodes but we need to reconcile + * as it is selected in the cloned dependency graph + * 2) discarded node: original node has children but current node (after transformation) has no children + * Note: children has been removed by ConflictResolver.removeLosers after transformation + */ + if ( clonedNode.getChildren().size() == 0 && origin.getChildren().size() == 0 ) + { + if ( skippedNodes.containsKey( origin ) ) + { + toReconcile.add( skippedNodes.get( origin ) ); + } + } + } + else + { + if ( !ArtifactIdUtils.equalsId( clonedNode.getArtifact(), winner.getArtifact() ) ) + { + Conflict conflict = new Conflict( clonedNode.getArtifact() ); + conflict.conflictedWith = winner.getArtifact(); + conflicts.add( conflict ); + } + } + return true; + } + + public boolean visitLeave( DependencyNode node ) + { + return true; + } + } + + static class DependencyResolveSkip + { + int depth; + DependencyNode node; + DataPool.GraphKey graphKey; + List dependencies; + List parentPathsOfCurrentNode; + List parentPathsOfCandidateLowerDepth; + private final int hashCode; + + DependencyResolveSkip( DependencyNode node, DataPool.GraphKey graphKey, List dependencies, + List parents, List skippedBy, int depth ) + { + this.node = node; + this.graphKey = graphKey; + this.dependencies = dependencies; + this.parentPathsOfCurrentNode = parents; + this.parentPathsOfCandidateLowerDepth = skippedBy; + this.depth = depth; + hashCode = Objects.hash( this.node ); + } + + @Override + public boolean equals( Object o ) + { + if ( this == o ) + { + return true; + } + if ( o == null || getClass() != o.getClass() ) + { + return false; + } + DependencyResolveSkip that = (DependencyResolveSkip) o; + return Objects.equals( node, that.node ); + } + + @Override + public int hashCode() + { + return hashCode; + } + + @Override + public String toString() + { + return "{" + + "node=" + + node.getArtifact() + + ", parentPathsOfCandidate=" + + parentPathsOfCandidateLowerDepth + + ", parentPathsOfCurrentNode=" + + parentPathsOfCurrentNode + + ", depth=" + + depth + + '}'; + } + } + + + static final class DependencyResolveResult + { + final List children; + final List parentPaths; + int depth; + + DependencyResolveResult( List children, List parents, int depth ) + { + this.children = children; + this.parentPaths = parents; + this.depth = depth; + } + } + + + static class CacheResult + { + List dependencyNodes; + List parentPathsOfCandidateLowerDepth; + } + + + static class Conflict + { + Artifact artifact; + Artifact conflictedWith; + private final int hashCode; + + Conflict( Artifact artifact ) + { + this.artifact = artifact; + hashCode = Objects.hash( this.artifact ); + } + + @Override + public boolean equals( Object obj ) + { + if ( obj == this ) + { + return true; + } + else if ( !( obj instanceof Conflict ) ) + { + return false; + } + Conflict that = (Conflict) obj; + return Objects.equals( artifact, that.artifact ); + } + + @Override + public int hashCode() + { + return hashCode; + } + + @Override + public String toString() + { + return "{" + + "artifact=" + + artifact + + ( conflictedWith == null ? "" : ", conflicted with: " + conflictedWith ) + + '}'; + } + } + + +} diff --git a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/collect/NodeStack.java b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/collect/NodeStack.java index 668dbc4b0..6b3f6544d 100644 --- a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/collect/NodeStack.java +++ b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/collect/NodeStack.java @@ -19,7 +19,9 @@ * under the License. */ +import java.util.ArrayList; import java.util.Arrays; +import java.util.List; import org.eclipse.aether.artifact.Artifact; import org.eclipse.aether.graph.DependencyNode; @@ -66,6 +68,16 @@ public void pop() size--; } + public List getParentNodes() + { + List parents = new ArrayList<>(); + for ( int i = size - 1; i >= 0; i-- ) + { + parents.add( nodes[i] ); + } + return parents; + } + public int find( Artifact artifact ) { for ( int i = size - 1; i >= 0; i-- ) diff --git a/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/collect/DefaultDependencyCollectorReconcilerTest.java b/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/collect/DefaultDependencyCollectorReconcilerTest.java new file mode 100644 index 000000000..c9be5a5b0 --- /dev/null +++ b/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/collect/DefaultDependencyCollectorReconcilerTest.java @@ -0,0 +1,95 @@ +package org.eclipse.aether.internal.impl.collect; +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +import org.eclipse.aether.artifact.DefaultArtifact; +import org.eclipse.aether.collection.CollectRequest; +import org.eclipse.aether.collection.CollectResult; +import org.eclipse.aether.collection.DependencyCollectionException; +import org.eclipse.aether.graph.Dependency; +import org.eclipse.aether.graph.Exclusion; +import org.eclipse.aether.internal.test.util.DependencyGraphParser; +import org.eclipse.aether.util.graph.manager.TransitiveDependencyManager; +import org.eclipse.aether.util.graph.selector.ExclusionDependencySelector; +import org.junit.Ignore; +import org.junit.Test; + +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.List; + +import static org.junit.Assert.assertEquals; + +public class DefaultDependencyCollectorReconcilerTest extends DefaultDependencyCollectorTest +{ + + @Override + public void setup() + { + super.setupCollector( true ); + } + + @Override + @Ignore + public void testCyclicDependencies() throws Exception + { + //do nothing + } + + private Dependency newDep( String coords, String scope, Collection exclusions ) + { + Dependency d = new Dependency( new DefaultArtifact( coords ), scope ); + return d.setExclusions( exclusions ); + } + + + @Test + /** + * Skip won't break exclusions. + */ + public void testSkipAndReconcileWithDifferentExclusion() throws DependencyCollectionException + { + collector.setArtifactDescriptorReader( newReader( "managed/" ) ); + parser = new DependencyGraphParser( "artifact-descriptions/managed/" ); + session.setDependencyManager( new TransitiveDependencyManager() ); + + ExclusionDependencySelector exclSel1 = new ExclusionDependencySelector(); + session.setDependencySelector( exclSel1 ); + + Dependency root1 = newDep( "gid:root:ext:ver", "compile", + Collections.singleton( new Exclusion( "gid", "transitive-1", "", "ext" ) ) ); + Dependency root2 = newDep( "gid:root:ext:ver", "compile", + Collections.singleton( new Exclusion( "gid", "transitive-2", "", "ext" ) ) ); + List dependencies = Arrays.asList( root1, root2 ); + CollectRequest request = new CollectRequest( dependencies, null, Arrays.asList( repository ) ); + request.addManagedDependency( newDep( "gid:direct:ext:managed-by-dominant-request" ) ); + request.addManagedDependency( newDep( "gid:transitive-1:ext:managed-by-root" ) ); + CollectResult result = collector.collectDependencies( session, request ); + assertEquals( 0, result.getExceptions().size() ); + assertEquals( 2, result.getRoot().getChildren().size() ); + assertEquals( root1, dep( result.getRoot(), 0 ) ); + //transitive-1 not excluded + assertEquals( 1, path( result.getRoot(), 0 ).getChildren().size() ); + //has transitive-2 excluded + assertEquals( 0, path( result.getRoot(), 1 ).getChildren().size() ); + assertEquals( root2, dep( result.getRoot(), 1 ) ); + } + +} diff --git a/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/collect/DefaultDependencyCollectorTest.java b/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/collect/DefaultDependencyCollectorTest.java index 5d4c2aa01..9145ee83a 100644 --- a/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/collect/DefaultDependencyCollectorTest.java +++ b/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/collect/DefaultDependencyCollectorTest.java @@ -78,33 +78,39 @@ public class DefaultDependencyCollectorTest { - private DefaultDependencyCollector collector; + protected DefaultDependencyCollector collector; - private DefaultRepositorySystemSession session; + protected DefaultRepositorySystemSession session; - private DependencyGraphParser parser; + protected DependencyGraphParser parser; - private RemoteRepository repository; + protected RemoteRepository repository; - private IniArtifactDescriptorReader newReader( String prefix ) + protected IniArtifactDescriptorReader newReader( String prefix ) { return new IniArtifactDescriptorReader( "artifact-descriptions/" + prefix ); } - private Dependency newDep( String coords ) + protected Dependency newDep( String coords ) { return newDep( coords, "" ); } - private Dependency newDep( String coords, String scope ) + protected Dependency newDep( String coords, String scope ) { return new Dependency( new DefaultArtifact( coords ), scope ); } @Before public void setup() + { + setupCollector(false); + } + + public void setupCollector(boolean skipAndReconcile) { session = TestUtils.newSession(); + session.setConfigProperty(DefaultDependencyCollector.CONFIG_PROP_USE_SKIP_RECONCILE, skipAndReconcile); collector = new DefaultDependencyCollector(); collector.setArtifactDescriptorReader( newReader( "" ) ); @@ -154,12 +160,12 @@ private static void assertEqualSubtree( DependencyNode expected, DependencyNode parents.removeLast(); } - private Dependency dep( DependencyNode root, int... coords ) + protected Dependency dep( DependencyNode root, int... coords ) { return path( root, coords ).getDependency(); } - private DependencyNode path( DependencyNode root, int... coords ) + protected DependencyNode path( DependencyNode root, int... coords ) { try { diff --git a/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/collect/DependencyResolveReconcilerTest.java b/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/collect/DependencyResolveReconcilerTest.java new file mode 100644 index 000000000..9a3ed997b --- /dev/null +++ b/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/collect/DependencyResolveReconcilerTest.java @@ -0,0 +1,159 @@ +package org.eclipse.aether.internal.impl.collect; +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +import org.eclipse.aether.DefaultRepositorySystemSession; +import org.eclipse.aether.RepositoryException; +import org.eclipse.aether.artifact.DefaultArtifact; +import org.eclipse.aether.collection.CollectRequest; +import org.eclipse.aether.collection.CollectResult; +import org.eclipse.aether.collection.DependencyGraphTransformer; +import org.eclipse.aether.graph.DefaultDependencyNode; +import org.eclipse.aether.graph.Dependency; +import org.eclipse.aether.graph.DependencyNode; +import org.eclipse.aether.internal.impl.StubRemoteRepositoryManager; +import org.eclipse.aether.internal.impl.StubVersionRangeResolver; +import org.eclipse.aether.internal.test.util.TestUtils; +import org.eclipse.aether.internal.test.util.TestVersion; +import org.eclipse.aether.internal.test.util.TestVersionConstraint; +import org.eclipse.aether.util.graph.transformer.*; +import org.junit.Before; +import org.junit.Test; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; + +import static org.junit.Assert.assertEquals; + +/** + * Test with skip & reconcile mode + */ +public class DependencyResolveReconcilerTest +{ + protected DefaultDependencyCollector collector; + + protected DefaultRepositorySystemSession session; + + protected Dependency newDep( String coords ) + { + return newDep( coords, "" ); + } + + protected Dependency newDep( String coords, String scope ) + { + return new Dependency( new DefaultArtifact( coords ), scope ); + } + + protected DependencyGraphTransformer newTransformer() + { + return new ConflictResolver( new NearestVersionSelector(), new JavaScopeSelector(), + new SimpleOptionalitySelector(), new JavaScopeDeriver() ); + } + + private static DependencyNode makeDependencyNode( String groupId, String artifactId, String version ) + { + return makeDependencyNode( groupId, artifactId, version, "compile" ); + } + + private static List mutableList( DependencyNode... nodes ) + { + return new ArrayList<>( Arrays.asList( nodes ) ); + } + + private static DependencyNode makeDependencyNode( String groupId, String artifactId, String version, String scope ) + { + DefaultDependencyNode node = new DefaultDependencyNode( + new Dependency( new DefaultArtifact( groupId + ':' + artifactId + ':' + version ), scope ) + ); + node.setVersion( new TestVersion( version ) ); + node.setVersionConstraint( new TestVersionConstraint( node.getVersion() ) ); + return node; + } + + @Before + public void setup() + { + session = TestUtils.newSession(); + session.setDependencyGraphTransformer( newTransformer() ); + + collector = new DefaultDependencyCollector(); + collector.setVersionRangeResolver( new StubVersionRangeResolver() ); + collector.setRemoteRepositoryManager( new StubRemoteRepositoryManager() ); + } + + + @Test + public void testReconcile() throws RepositoryException + { + // A -> B -> C 3.0 -> D -> Z => 1) D of C3.0 is resolved + // |--> E -> F --> G -> D -> Z => 2) The D here is skipped as the depth is deeper and reuse the result in step 1 + // |--> C 2.0 -> H => maven picks C 2.0, so the D of C3.0 in step 1 is no longer valid, need reconcile the skipped D node in step 2 + DependencyNode aNode = makeDependencyNode( "some-group", "A", "1.0" ); + DependencyNode bNode = makeDependencyNode( "some-group", "B", "1.0" ); + DependencyNode c3Node = makeDependencyNode( "some-group", "C", "3.0" ); + DependencyNode dNode = makeDependencyNode( "some-group", "D", "1.0" ); + DependencyNode eNode = makeDependencyNode( "some-group", "E", "1.0" ); + DependencyNode fNode = makeDependencyNode( "some-group", "F", "1.0" ); + DependencyNode c2Node = makeDependencyNode( "some-group", "C", "2.0" ); + DependencyNode gNode = makeDependencyNode( "some-group", "G", "1.0" ); + DependencyNode hNode = makeDependencyNode( "some-group", "H", "1.0" ); + DependencyNode zNode = makeDependencyNode( "some-group", "Z", "1.0" ); + aNode.setChildren( mutableList( bNode, eNode, c2Node ) ); + bNode.setChildren( mutableList( c3Node ) ); + c3Node.setChildren( mutableList( dNode ) ); + dNode.setChildren( mutableList( zNode ) ); + eNode.setChildren( mutableList( fNode ) ); + fNode.setChildren( mutableList( gNode ) ); + c2Node.setChildren( mutableList( hNode ) ); + + CollectRequest request = new CollectRequest(); + request.addDependency( new Dependency( aNode.getArtifact(), "compile" ) ); + CollectResult result = new CollectResult( request ); + result.setRoot( aNode ); + + //follow the resolve sequence + DependencyResolveReconciler reconciler = new DependencyResolveReconciler(); + reconciler.cacheChildrenWithDepth( aNode, new ArrayList<>() ); + reconciler.cacheChildrenWithDepth( bNode, mutableList( aNode ) ); + reconciler.cacheChildrenWithDepth( c3Node, mutableList( aNode, bNode ) ); + reconciler.cacheChildrenWithDepth( dNode, mutableList( aNode, bNode, c3Node ) ); + reconciler.cacheChildrenWithDepth( eNode, mutableList( aNode ) ); + reconciler.cacheChildrenWithDepth( fNode, mutableList( aNode, eNode ) ); + reconciler.cacheChildrenWithDepth( gNode, mutableList( aNode, eNode, fNode ) ); + DependencyNode clonedDNode = new DefaultDependencyNode( dNode ); + gNode.setChildren( mutableList( clonedDNode ) ); + reconciler.addSkip( clonedDNode, + new DataPool.GraphKey( clonedDNode.getArtifact(), null, null, null, null, null ), + Arrays.asList( newDep( "some-group:Z:ext:1.0" ) ), mutableList( aNode, eNode, fNode, gNode ), + mutableList( aNode, bNode, c3Node ) ); + reconciler.cacheChildrenWithDepth( c2Node, mutableList( aNode ) ); + + Collection skips = + reconciler.getNodesToReconcile( session, result ); + assertEquals( skips.size(), 1 ); + + DependencyResolveReconciler.DependencyResolveSkip skip = + skips.toArray( new DependencyResolveReconciler.DependencyResolveSkip[0] )[0]; + assertEquals( skip.depth, 5 ); + assertEquals( skip.node.getArtifact().getArtifactId(), "D" ); + assertEquals( skip.parentPathsOfCandidateLowerDepth, mutableList( aNode, bNode, c3Node ) ); + } +} diff --git a/maven-resolver-util/src/main/java/org/eclipse/aether/util/graph/visitor/CloningDependencyVisitor.java b/maven-resolver-util/src/main/java/org/eclipse/aether/util/graph/visitor/CloningDependencyVisitor.java index 7c57db1cf..a81a3ff9e 100644 --- a/maven-resolver-util/src/main/java/org/eclipse/aether/util/graph/visitor/CloningDependencyVisitor.java +++ b/maven-resolver-util/src/main/java/org/eclipse/aether/util/graph/visitor/CloningDependencyVisitor.java @@ -61,6 +61,16 @@ public final DependencyNode getRootNode() return root; } + /** + * Get the cloned node map + * + * @return The cloned node map + */ + public final Map getClonedNodes() + { + return clones; + } + /** * Creates a clone of the specified node. *