diff --git a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/collect/ConflictWinnerFinder.java b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/collect/ConflictWinnerFinder.java new file mode 100644 index 000000000..bcdd022f6 --- /dev/null +++ b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/collect/ConflictWinnerFinder.java @@ -0,0 +1,243 @@ +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.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.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.LinkedHashSet; +import java.util.Objects; + +import static org.eclipse.aether.internal.impl.collect.DependencyResolveReconciler.ga; +import static org.eclipse.aether.internal.impl.collect.DependencyResolveReconciler.gav; + +/** + * Find the conflict winner by transforming a cloned dependency graph + */ +public class ConflictWinnerFinder +{ + + private static final Logger LOGGER = LoggerFactory.getLogger( ConflictWinnerFinder.class ); + + /** + * Find artifacts that conflicts with the winner. Only artifacts resolved before winner are considered as + * conflicts. + * + * @return + */ + Collection getVersionConflicts( RepositorySystemSession session, CollectResult result ) + throws DependencyCollectionException + { + LinkedHashSet conflicts = new LinkedHashSet<>(); + HashMap> conflictMap = resolveConflictWinners( session, result ); + if ( conflictMap == null ) + { + return conflicts; + } + + for ( String key : conflictMap.keySet() ) + { + Collection col = conflictMap.get( key ); + if ( col != null && col.size() > 1 ) // more than ONE + { + Conflict[] array = col.toArray( new Conflict[0] ); + + //find winner + Artifact winner = null; + int index = 0; + for ( int i = 0; i < array.length; i++ ) + { + Conflict cur = array[i]; + if ( cur.conflictedWith == null ) + { + winner = cur.artifact; + index = i; + break; + } + } + + //find conflicts: resolved before winner && version not equals + for ( int i = 0; i < array.length; i++ ) + { + if ( i < index ) + { + LOGGER.debug( "Found dependency: {} that conflicts with: {} ", array[i], winner ); + conflicts.add( array[i] ); + } + } + } + } + + return conflicts; + } + + /** + * Use the ConflictResolver to find out all conflict winners based on a cloned dependency graph + * + * @param session + * @param result + * @return + * @throws DependencyCollectionException + */ + private HashMap> resolveConflictWinners( 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 null; + } + + //clone graph + CloningDependencyVisitor vis = new CloningDependencyVisitor(); + DependencyNode root = result.getRoot(); + root.accept( vis ); + root = vis.getRootNode(); + + //this part copied from DefaultDependencyCollector + 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 ); + } + + DependencyConflictWinnersVisitor conflicts = new DependencyConflictWinnersVisitor(); + root.accept( conflicts ); + + LOGGER.debug( "Finished to resolve conflict winners in : {} ", ( System.nanoTime() - start ) ); + return conflicts.conflictMap; + } + + + //find out all conflict winners + static class DependencyConflictWinnersVisitor + implements DependencyVisitor + { + /** + * The version conflicts of dependencies, conflicts were put to the map following the resolve sequence. ga -> + * conflict + */ + final HashMap> conflictMap = new HashMap<>(); + + public boolean visitEnter( DependencyNode node ) + { + Artifact a = node.getArtifact(); + Conflict conflict = new Conflict( a ); + conflictMap.computeIfAbsent( ga( a ), k -> new ArrayList<>() ).add( conflict ); + DependencyNode winner = (DependencyNode) node.getData().get( ConflictResolver.NODE_DATA_WINNER ); + if ( winner != null ) + { + if ( !ArtifactIdUtils.equalsId( a, winner.getArtifact() ) ) + { + conflict.conflictedWith = winner.getArtifact(); + } + } + return true; + } + + + public boolean visitLeave( DependencyNode node ) + { + return true; + } + } + + static class Conflict + { + Artifact artifact; + Artifact conflictedWith; + String gav; + String ga; + String version; + private final int hashCode; + + Conflict( Artifact artifact ) + { + this.artifact = artifact; + this.version = artifact.getVersion(); + this.ga = ga( artifact ); + this.gav = gav( 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/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..8645d66e1 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,28 @@ 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 ) + { + 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 +538,64 @@ 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 + List cachedChildren = args.pool.getChildren( key ); + DependencyResolveReconciler.CacheResult result = args.reconciler.findCache( child, cachedChildren, depth ); + if ( result != null ) + { + if ( result.candidateWithSameKey ) + { + child.setChildren( result.dependencyNodes ); + } + else if ( result.candidateWithLowerDepth ) + { + //No need to set the children as the result can be ignored (won't be picked up) + args.reconciler.addSkip( child, key, descriptorResult.getDependencies(), parents, + result.parentPathsOfCandidateLowerDepth ); + LOGGER.debug( "Skipped resolving artifact {} of depth {}", child.getArtifact(), depth ); + } + } + else + { + args.nodes.push( child ); + LOGGER.debug( "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 +788,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 +806,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..a930109e5 --- /dev/null +++ b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/collect/DependencyResolveReconciler.java @@ -0,0 +1,327 @@ +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.RepositorySystemSession; +import org.eclipse.aether.artifact.Artifact; +import org.eclipse.aether.collection.CollectResult; +import org.eclipse.aether.collection.DependencyCollectionException; +import org.eclipse.aether.graph.Dependency; +import org.eclipse.aether.graph.DependencyNode; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Objects; +import java.util.Set; + +/** + * Skip resolve if depth is deeper, then reconcile the result by resolving the version conflicts. + */ +public class DependencyResolveReconciler +{ + + private static final Logger LOGGER = LoggerFactory.getLogger( DependencyResolveReconciler.class ); + + /** + * Cache maven dependency resolve result by depth gav -> 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<>(); + } + + public void cacheChildrenWithDepth( DependencyNode node, List parents ) + { + int depth = parents.size() + 1; + Artifact artifact = node.getArtifact(); + List children = node.getChildren(); + nodesWithDepth.put( gav( artifact ), new DependencyResolveResult( children, parents, depth ) ); + } + + + public CacheResult findCache( DependencyNode current, List cacheByGraphKey, int depth ) + { + DependencyResolveResult result = nodesWithDepth.get( gav( current.getArtifact() ) ); + if ( result != null ) + { + if ( result.depth <= depth ) + { + CacheResult cache = new CacheResult(); + cache.dependencyNodes = result.children; + cache.candidateWithLowerDepth = true; + cache.parentPathsOfCandidateLowerDepth = result.parentPaths; + return cache; + } + } + + return null; + } + + /** + * Record the nodes has been skipped + * + * @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 + { + ConflictWinnerFinder winnerFinder = new ConflictWinnerFinder(); + Collection conflicts = winnerFinder.getVersionConflicts( session, result ); + if ( conflicts == null || conflicts.isEmpty() ) + { + return Collections.emptyList(); + } + + //find all nodes that contain conflict nodes. + HashSet conflictedNodes = new HashSet<>( findNodeHasConflictsInParentPaths( conflicts ) ); + + //Evict node from cache if the cached node is based on parent paths contains conflicting nodes + for ( String gav : conflictedNodes ) + { + nodesWithDepth.remove( gav ); + } + + //get the least set of nodes need to reconcile + Set filteredSkips = filterSkippedNodes( conflictedNodes ); + if ( filteredSkips.size() == 0 ) + { + return Collections.emptyList(); + } + + LOGGER.debug( "Skipped resolving {} nodes, and reconciled {} nodes to solve {} dependency conflicts.", + skippedNodes.size(), filteredSkips.size(), conflicts.size() ); + return filteredSkips; + } + + + private LinkedHashSet filterSkippedNodes( Set conflictedNodes ) + { + LinkedHashSet skips = new LinkedHashSet<>(); + for ( DependencyResolveSkip skip : skippedNodes ) + { + //SKIP: skipped node's GAV differs with the one evicted from cache + if ( !conflictedNodes.contains( gav( skip.node.getArtifact() ) ) ) + { + continue; + } + + //SKIP: the node is skipped as expected when the parent path includes all segments of skipped path + boolean selfContained = true; + for ( DependencyNode parentNode : skip.parentPathsOfCandidateLowerDepth ) + { + if ( !skip.parentPathsOfCurrentNode.contains( parentNode ) ) + { + selfContained = false; + break; + } + } + + if ( selfContained ) + { + continue; + } + + skips.add( skip ); + } + + //group nodes by artifact + HashMap> reconcileNodes = new HashMap<>(); + for ( DependencyResolveSkip skip : skips ) + { + reconcileNodes.computeIfAbsent( skip.node.getArtifact(), k -> new ArrayList<>() ).add( skip ); + } + + //only reconcile the node with lowest depth + LinkedHashSet filteredSkips = new LinkedHashSet<>(); + for ( Artifact key : reconcileNodes.keySet() ) + { + Collection col = reconcileNodes.get( key ); + List list = new ArrayList<>( col ); + list.sort( Comparator.comparingInt( o -> o.depth ) ); + LOGGER.debug( "Reconcile: {}", list.get( 0 ) ); + filteredSkips.add( list.get( 0 ) ); + } + return filteredSkips; + } + + + private List findNodeHasConflictsInParentPaths( Collection conflicts ) + { + Set keys = nodesWithDepth.keySet(); + Set conflictIds = new HashSet<>(); + for ( ConflictWinnerFinder.Conflict c : conflicts ) + { + conflictIds.add( c.gav ); + } + + List results = new ArrayList<>(); + for ( String key : keys ) + { + List parents = nodesWithDepth.get( key ).parentPaths; + for ( DependencyNode p : parents ) + { + if ( p.getArtifact() != null ) + { + if ( conflictIds.contains( gav( p.getArtifact() ) ) ) + { + results.add( key ); + break; + } + } + } + } + + return results; + } + + static String ga( Artifact artifact ) + { + return artifact.getGroupId() + ":" + artifact.getArtifactId(); + } + + static String gav( Artifact artifact ) + { + return ga( artifact ) + ":" + artifact.getVersion(); + } + + + 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 + { + private final List children; + private final List parentPaths; + private 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; + boolean candidateWithSameKey; + boolean candidateWithLowerDepth; + } + + +} 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..c3de72d06 --- /dev/null +++ b/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/collect/DependencyResolveReconcilerTest.java @@ -0,0 +1,157 @@ +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 ) ); + gNode.setChildren( mutableList( dNode ) ); + 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 ) ); + reconciler.addSkip( dNode, new DataPool.GraphKey( dNode.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 ) ); + } +}