You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@maven.apache.org by mi...@apache.org on 2022/04/03 14:53:08 UTC
[maven-resolver] branch master updated: [MRESOLVER-247] Avoid unnecessary dependency resolution by a Skip solution based on BFS
This is an automated email from the ASF dual-hosted git repository.
michaelo pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/maven-resolver.git
The following commit(s) were added to refs/heads/master by this push:
new 0dd0693 [MRESOLVER-247] Avoid unnecessary dependency resolution by a Skip solution based on BFS
0dd0693 is described below
commit 0dd06936464ebd917f4115554c283bd676f477f4
Author: Cai <we...@ebay.com>
AuthorDate: Sun Mar 6 23:55:04 2022 -0800
[MRESOLVER-247] Avoid unnecessary dependency resolution by a Skip solution based on BFS
This closes #158
---
.../aether/impl/DependencyResolutionSkipper.java | 59 +++++
.../impl/collect/DefaultDependencyCollector.java | 63 ++++-
.../DefaultDependencyResolutionSkipper.java | 290 +++++++++++++++++++++
.../impl/collect/DependencyProcessingContext.java | 6 +-
.../collect/NeverDependencyResolutionSkipper.java | 51 ++++
.../collect/DefaultDependencyCollectorTest.java | 24 +-
.../DefaultDependencyCollectorUseSkipTest.java | 86 ++++++
.../collect/DependencyResolutionSkipperTest.java | 214 +++++++++++++++
src/site/markdown/configuration.md | 1 +
9 files changed, 770 insertions(+), 24 deletions(-)
diff --git a/maven-resolver-impl/src/main/java/org/eclipse/aether/impl/DependencyResolutionSkipper.java b/maven-resolver-impl/src/main/java/org/eclipse/aether/impl/DependencyResolutionSkipper.java
new file mode 100644
index 0000000..649f6d3
--- /dev/null
+++ b/maven-resolver-impl/src/main/java/org/eclipse/aether/impl/DependencyResolutionSkipper.java
@@ -0,0 +1,59 @@
+package org.eclipse.aether.impl;
+
+/*
+ * 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.graph.DependencyNode;
+
+import java.util.List;
+
+/**
+ * A skipper that determines whether to skip resolving given node during the dependency collection.
+ *
+ * @noimplement This interface is not intended to be implemented by clients.
+ * @noextend This interface is not intended to be extended by clients.
+ * @provisional This type is provisional and can be changed, moved or removed without prior notice.
+ * @since 1.8.0
+ */
+public interface DependencyResolutionSkipper
+{
+ /**
+ * Check whether the resolution of current node can be skipped before resolving.
+ *
+ * @param node Current node
+ * @param parents All parent nodes of current node
+ *
+ * @return {@code true} if the node can be skipped for resolution, {@code false} if resolution required.
+ */
+ boolean skipResolution( DependencyNode node, List<DependencyNode> parents );
+
+ /**
+ * Cache the resolution result when a node is resolved by @See DependencyCollector after resolution.
+ *
+ * @param node Current node
+ * @param parents All parent nodes of current node
+ */
+ void cache( DependencyNode node, List<DependencyNode> parents );
+
+ /**
+ * Print the skip/resolve status report for all nodes.
+ */
+ void report();
+
+}
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 a773367..a895bb9 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
@@ -58,6 +58,7 @@ import org.eclipse.aether.graph.DependencyNode;
import org.eclipse.aether.graph.Exclusion;
import org.eclipse.aether.impl.ArtifactDescriptorReader;
import org.eclipse.aether.impl.DependencyCollector;
+import org.eclipse.aether.impl.DependencyResolutionSkipper;
import org.eclipse.aether.impl.RemoteRepositoryManager;
import org.eclipse.aether.impl.VersionRangeResolver;
import org.eclipse.aether.repository.ArtifactRepository;
@@ -85,6 +86,21 @@ 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 mode.
+ *
+ * @since 1.8.0
+ */
+ public static final String CONFIG_PROP_USE_SKIP = "aether.dependencyCollector.useSkip";
+
+ /**
+ * The default value for {@link #CONFIG_PROP_USE_SKIP}, {@code true}.
+ *
+ * @since 1.8.0
+ */
+ public static final boolean CONFIG_PROP_USE_SKIP_DEFAULT = true;
+
private static final String CONFIG_PROP_MAX_EXCEPTIONS = "aether.dependencyCollector.maxExceptions";
private static final int CONFIG_PROP_MAX_EXCEPTIONS_DEFAULT = 50;
@@ -151,6 +167,14 @@ public class DefaultDependencyCollector
requireNonNull( request, "request cannot be null" );
session = optimizeSession( session );
+ boolean useSkip = ConfigUtils.getBoolean(
+ session, CONFIG_PROP_USE_SKIP_DEFAULT, CONFIG_PROP_USE_SKIP
+ );
+ if ( useSkip )
+ {
+ LOGGER.debug( "Collector skip mode enabled" );
+ }
+
RequestTrace trace = RequestTrace.newChild( request.getTrace(), request );
CollectResult result = new CollectResult( request );
@@ -253,7 +277,10 @@ public class DefaultDependencyCollector
DefaultVersionFilterContext versionContext = new DefaultVersionFilterContext( session );
- Args args = new Args( session, trace, pool, context, versionContext, request );
+ Args args =
+ new Args( session, trace, pool, context, versionContext, request,
+ useSkip ? new DefaultDependencyResolutionSkipper()
+ : NeverDependencyResolutionSkipper.INSTANCE );
Results results = new Results( result, session );
DependencySelector rootDepSelector =
@@ -263,11 +290,12 @@ public class DefaultDependencyCollector
depTraverser != null ? depTraverser.deriveChildTraverser( context ) : null;
VersionFilter rootVerFilter = verFilter != null ? verFilter.deriveChildFilter( context ) : null;
+ List<DependencyNode> parents = Collections.singletonList( node );
for ( Dependency dependency : dependencies )
{
args.dependencyProcessingQueue.add(
new DependencyProcessingContext( rootDepSelector, rootDepManager, rootDepTraverser,
- rootVerFilter, repositories, managedDependencies, Collections.singletonList( node ),
+ rootVerFilter, repositories, managedDependencies, parents,
dependency ) );
}
@@ -277,6 +305,7 @@ public class DefaultDependencyCollector
false );
}
+ args.skipper.report();
errorPath = results.errorPath;
}
@@ -396,6 +425,8 @@ public class DefaultDependencyCollector
return;
}
+ //Resolve newer version first to maximize benefits of skipper
+ Collections.reverse( versions );
for ( Version version : versions )
{
Artifact originalArtifact = dependency.getArtifact().setVersion( version.toString() );
@@ -500,15 +531,20 @@ public class DefaultDependencyCollector
List<DependencyNode> children = args.pool.getChildren( key );
if ( children == null )
{
- args.pool.putChildren( key, child.getChildren() );
-
- List<DependencyNode> parents = new ArrayList<>( parentContext.parents );
- parents.add( child );
- for ( Dependency dependency : descriptorResult.getDependencies() )
+ boolean skipResolution = args.skipper.skipResolution( child, parentContext.parents );
+ if ( !skipResolution )
{
- args.dependencyProcessingQueue.add(
- new DependencyProcessingContext( childSelector, childManager, childTraverser, childFilter,
- childRepos, descriptorResult.getManagedDependencies(), parents, dependency ) );
+ List<DependencyNode> parents = new ArrayList<>( parentContext.parents.size() + 1 );
+ parents.addAll( parentContext.parents );
+ parents.add( child );
+ for ( Dependency dependency : descriptorResult.getDependencies() )
+ {
+ args.dependencyProcessingQueue.add(
+ new DependencyProcessingContext( childSelector, childManager, childTraverser, childFilter,
+ childRepos, descriptorResult.getManagedDependencies(), parents, dependency ) );
+ }
+ args.pool.putChildren( key, child.getChildren() );
+ args.skipper.cache( child, parents );
}
}
else
@@ -689,7 +725,7 @@ public class DefaultDependencyCollector
final DataPool pool;
- final Queue<DependencyProcessingContext> dependencyProcessingQueue = new ArrayDeque<>();
+ final Queue<DependencyProcessingContext> dependencyProcessingQueue = new ArrayDeque<>( 128 );
final DefaultDependencyCollectionContext collectionContext;
@@ -697,9 +733,11 @@ public class DefaultDependencyCollector
final CollectRequest request;
+ final DependencyResolutionSkipper skipper;
+
Args( RepositorySystemSession session, RequestTrace trace, DataPool pool,
DefaultDependencyCollectionContext collectionContext, DefaultVersionFilterContext versionContext,
- CollectRequest request )
+ CollectRequest request, DependencyResolutionSkipper skipper )
{
this.session = session;
this.request = request;
@@ -709,6 +747,7 @@ public class DefaultDependencyCollector
this.pool = pool;
this.collectionContext = collectionContext;
this.versionContext = versionContext;
+ this.skipper = skipper;
}
}
diff --git a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/collect/DefaultDependencyResolutionSkipper.java b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/collect/DefaultDependencyResolutionSkipper.java
new file mode 100644
index 0000000..408b0e1
--- /dev/null
+++ b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/collect/DefaultDependencyResolutionSkipper.java
@@ -0,0 +1,290 @@
+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.Artifact;
+import org.eclipse.aether.graph.DependencyNode;
+import org.eclipse.aether.impl.DependencyResolutionSkipper;
+import org.eclipse.aether.util.artifact.ArtifactIdUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.HashMap;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.atomic.AtomicInteger;
+
+final class DefaultDependencyResolutionSkipper implements DependencyResolutionSkipper
+{
+ private static final Logger LOGGER = LoggerFactory.getLogger( DefaultDependencyResolutionSkipper.class );
+
+ private Map<DependencyNode, DependencyResolutionResult> results = new LinkedHashMap<>( 256 );
+ private CacheManager cacheManager = new CacheManager();
+ private CoordinateManager coordinateManager = new CoordinateManager();
+
+ DefaultDependencyResolutionSkipper()
+ {
+ // enables default constructor
+ }
+
+ @Override
+ public boolean skipResolution( DependencyNode node, List<DependencyNode> parents )
+ {
+ DependencyResolutionResult result = new DependencyResolutionResult( node );
+ results.put( node, result );
+
+ int depth = parents.size() + 1;
+ coordinateManager.createCoordinate( node, depth );
+
+ if ( cacheManager.isVersionConflict( node ) )
+ {
+ /*
+ * Skip resolving version conflict losers (omitted for conflict)
+ */
+ result.skippedAsVersionConflict = true;
+ if ( LOGGER.isTraceEnabled() )
+ {
+ LOGGER.trace( "Skipped resolving node: {} as version conflict",
+ ArtifactIdUtils.toId( node.getArtifact() ) );
+ }
+ }
+ else if ( cacheManager.isDuplicate( node ) )
+ {
+ if ( coordinateManager.isLeftmost( node, parents ) )
+ {
+ /*
+ * Force resolving the node to retain conflict paths when its coordinate is more left than last resolved
+ * This is because Maven picks the widest scope present among conflicting dependencies
+ */
+ result.forceResolution = true;
+ if ( LOGGER.isTraceEnabled() )
+ {
+ LOGGER.trace( "Force resolving node: {} for scope selection",
+ ArtifactIdUtils.toId( node.getArtifact() ) );
+ }
+ }
+ else
+ {
+ /*
+ * Skip resolving as duplicate (depth deeper, omitted for duplicate)
+ * No need to compare depth as the depth of winner for given artifact is always shallower
+ */
+ result.skippedAsDuplicate = true;
+ if ( LOGGER.isTraceEnabled() )
+ {
+ LOGGER.trace( "Skipped resolving node: {} as duplicate",
+ ArtifactIdUtils.toId( node.getArtifact() ) );
+ }
+ }
+ }
+ else
+ {
+ result.resolve = true;
+ if ( LOGGER.isTraceEnabled() )
+ {
+ LOGGER.trace( "Resolving node: {}",
+ ArtifactIdUtils.toId( node.getArtifact() ) );
+ }
+ }
+
+ if ( result.toResolve() )
+ {
+ coordinateManager.updateLeftmost( node );
+ return false;
+ }
+
+ return true;
+ }
+
+ @Override
+ public void cache( DependencyNode node, List<DependencyNode> parents )
+ {
+ boolean parentForceResolution = parents.stream()
+ .anyMatch( n -> results.containsKey( n ) && results.get( n ).forceResolution );
+ if ( parentForceResolution )
+ {
+ if ( LOGGER.isTraceEnabled() )
+ {
+ LOGGER.trace(
+ "Won't cache as node: {} inherits from a force-resolved node and will be omitted for duplicate",
+ ArtifactIdUtils.toId( node.getArtifact() ) );
+ }
+ }
+ else
+ {
+ cacheManager.cacheWinner( node );
+ }
+ }
+
+ @Override
+ public void report()
+ {
+ if ( LOGGER.isTraceEnabled() )
+ {
+ LOGGER.trace( "Skipped {} nodes as duplicate",
+ results.entrySet().stream().filter( n -> n.getValue().skippedAsDuplicate ).count() );
+ LOGGER.trace( "Skipped {} nodes as having version conflict",
+ results.entrySet().stream().filter( n -> n.getValue().skippedAsVersionConflict ).count() );
+ LOGGER.trace( "Resolved {} nodes",
+ results.entrySet().stream().filter( n -> n.getValue().resolve ).count() );
+ LOGGER.trace( "Forced resolving {} nodes for scope selection",
+ results.entrySet().stream().filter( n -> n.getValue().forceResolution ).count() );
+ }
+ }
+
+ public Map<DependencyNode, DependencyResolutionResult> getResults()
+ {
+ return results;
+ }
+
+ static final class DependencyResolutionResult
+ {
+ DependencyNode current;
+ boolean skippedAsVersionConflict; //omitted for conflict
+ boolean skippedAsDuplicate; //omitted for duplicate, depth is deeper
+ boolean resolve; //node to resolve (winner node)
+ boolean forceResolution; //force resolving (duplicate node) for scope selection
+
+ DependencyResolutionResult( DependencyNode current )
+ {
+ this.current = current;
+ }
+
+ boolean toResolve()
+ {
+ return resolve || forceResolution;
+ }
+ }
+
+ static final class CacheManager
+ {
+
+ /**
+ * artifact -> node
+ */
+ private final Map<Artifact, DependencyNode> winners = new HashMap<>( 256 );
+
+
+ /**
+ * versionLessId -> Artifact, only cache winners
+ */
+ private final Map<String, Artifact> winnerGAs = new HashMap<>( 256 );
+
+ boolean isVersionConflict( DependencyNode node )
+ {
+ String ga = ArtifactIdUtils.toVersionlessId( node.getArtifact() );
+ if ( winnerGAs.containsKey( ga ) )
+ {
+ Artifact result = winnerGAs.get( ga );
+ return !node.getArtifact().getVersion().equals( result.getVersion() );
+ }
+
+ return false;
+ }
+
+ void cacheWinner( DependencyNode node )
+ {
+ winners.put( node.getArtifact(), node );
+ winnerGAs.put( ArtifactIdUtils.toVersionlessId( node.getArtifact() ), node.getArtifact() );
+ }
+
+ boolean isDuplicate( DependencyNode node )
+ {
+ return winners.containsKey( node.getArtifact() );
+ }
+
+ }
+
+
+ static final class CoordinateManager
+ {
+ private final Map<Integer, AtomicInteger> sequenceGen = new HashMap<>( 256 );
+
+ /**
+ * Dependency node -> Coordinate
+ */
+ private final Map<DependencyNode, Coordinate> coordinateMap = new HashMap<>( 256 );
+
+ /**
+ * Leftmost coordinate of given artifact
+ */
+ private final Map<Artifact, Coordinate> leftmostCoordinates = new HashMap<>( 256 );
+
+
+ Coordinate getCoordinate( DependencyNode node )
+ {
+ return coordinateMap.get( node );
+ }
+
+ Coordinate createCoordinate( DependencyNode node, int depth )
+ {
+ int seq = sequenceGen.computeIfAbsent( depth, k -> new AtomicInteger() ).incrementAndGet();
+ Coordinate coordinate = new Coordinate( depth, seq );
+ coordinateMap.put( node, coordinate );
+ return coordinate;
+ }
+
+ void updateLeftmost( DependencyNode current )
+ {
+ leftmostCoordinates.put( current.getArtifact(), getCoordinate( current ) );
+ }
+
+ boolean isLeftmost( DependencyNode node, List<DependencyNode> parents )
+ {
+ Coordinate leftmost = leftmostCoordinates.get( node.getArtifact() );
+ if ( leftmost != null && leftmost.depth <= parents.size() )
+ {
+ DependencyNode sameLevelNode = parents.get( leftmost.depth - 1 );
+ if ( getCoordinate( sameLevelNode ).sequence < leftmost.sequence )
+ {
+ return true;
+ }
+ }
+
+ return false;
+ }
+ }
+
+ static final class Coordinate
+ {
+ int depth;
+ int sequence;
+
+ Coordinate( int depth, int sequence )
+ {
+ this.depth = depth;
+ this.sequence = sequence;
+ }
+
+ @Override
+ public String toString()
+ {
+ return "{"
+ + "depth="
+ + depth
+ + ", sequence="
+ + sequence
+ + '}';
+ }
+ }
+
+
+}
diff --git a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/collect/DependencyProcessingContext.java b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/collect/DependencyProcessingContext.java
index 71815d0..9820ad0 100644
--- a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/collect/DependencyProcessingContext.java
+++ b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/collect/DependencyProcessingContext.java
@@ -42,7 +42,7 @@ final class DependencyProcessingContext
* All parents of the dependency in the top > down order.
*/
final List<DependencyNode> parents;
- final Dependency dependency;
+ Dependency dependency;
@SuppressWarnings( "checkstyle:parameternumber" )
DependencyProcessingContext( DependencySelector depSelector,
@@ -66,8 +66,8 @@ final class DependencyProcessingContext
DependencyProcessingContext withDependency( Dependency dependency )
{
- return new DependencyProcessingContext( depSelector, depManager, depTraverser, verFilter, repositories,
- managedDependencies, parents, dependency );
+ this.dependency = dependency;
+ return this;
}
DependencyNode getParent()
diff --git a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/collect/NeverDependencyResolutionSkipper.java b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/collect/NeverDependencyResolutionSkipper.java
new file mode 100644
index 0000000..acd6636
--- /dev/null
+++ b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/collect/NeverDependencyResolutionSkipper.java
@@ -0,0 +1,51 @@
+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.graph.DependencyNode;
+import org.eclipse.aether.impl.DependencyResolutionSkipper;
+
+import java.util.List;
+
+/**
+ * Skipper for Non-skip approach.
+ */
+final class NeverDependencyResolutionSkipper implements DependencyResolutionSkipper
+{
+ static final DependencyResolutionSkipper INSTANCE = new NeverDependencyResolutionSkipper();
+
+ @Override
+ public boolean skipResolution( DependencyNode node, List<DependencyNode> parents )
+ {
+ return false;
+ }
+
+ @Override
+ public void cache( DependencyNode node, List<DependencyNode> parents )
+ {
+
+ }
+
+ @Override
+ public void report()
+ {
+
+ }
+}
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 5d4c2aa..7d034bd 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,25 +78,25 @@ import org.junit.Test;
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 );
}
@@ -104,7 +104,13 @@ public class DefaultDependencyCollectorTest
@Before
public void setup()
{
+ setupCollector( false );
+ }
+
+ public void setupCollector( boolean useSkip )
+ {
session = TestUtils.newSession();
+ session.setConfigProperty( DefaultDependencyCollector.CONFIG_PROP_USE_SKIP, useSkip );
collector = new DefaultDependencyCollector();
collector.setArtifactDescriptorReader( newReader( "" ) );
@@ -154,12 +160,12 @@ public class DefaultDependencyCollectorTest
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/DefaultDependencyCollectorUseSkipTest.java b/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/collect/DefaultDependencyCollectorUseSkipTest.java
new file mode 100644
index 0000000..716ecde
--- /dev/null
+++ b/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/collect/DefaultDependencyCollectorUseSkipTest.java
@@ -0,0 +1,86 @@
+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.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 DefaultDependencyCollectorUseSkipTest extends DefaultDependencyCollectorTest
+{
+
+ private Dependency newDep( String coords, String scope, Collection<Exclusion> exclusions )
+ {
+ Dependency d = new Dependency( new DefaultArtifact( coords ), scope );
+ return d.setExclusions( exclusions );
+ }
+
+ @Override
+ public void setup()
+ {
+ super.setupCollector( true );
+ }
+
+ @Test
+ public void testSkipperWithDifferentExclusion() 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<Dependency> 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 ) );
+ assertEquals( root2, dep( result.getRoot(), 1 ) );
+ //the winner has transitive-1 excluded
+ assertEquals( 1, path( result.getRoot(), 0 ).getChildren().size() );
+ assertEquals( 0, path( result.getRoot(), 0, 0 ).getChildren().size() );
+ //skipped
+ assertEquals( 0, path( result.getRoot(), 1 ).getChildren().size() );
+ }
+
+}
diff --git a/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/collect/DependencyResolutionSkipperTest.java b/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/collect/DependencyResolutionSkipperTest.java
new file mode 100644
index 0000000..1d92acc
--- /dev/null
+++ b/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/collect/DependencyResolutionSkipperTest.java
@@ -0,0 +1,214 @@
+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.RepositoryException;
+import org.eclipse.aether.artifact.DefaultArtifact;
+import org.eclipse.aether.graph.DefaultDependencyNode;
+import org.eclipse.aether.graph.Dependency;
+import org.eclipse.aether.graph.DependencyNode;
+import org.eclipse.aether.internal.test.util.TestVersion;
+import org.eclipse.aether.internal.test.util.TestVersionConstraint;
+import org.junit.Test;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+
+public class DependencyResolutionSkipperTest
+{
+ private static DependencyNode makeDependencyNode( String groupId, String artifactId, String version )
+ {
+ return makeDependencyNode( groupId, artifactId, version, "compile" );
+ }
+
+ private static List<DependencyNode> 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;
+ }
+
+ @Test
+ public void testSkipVersionConflict() throws RepositoryException
+ {
+ // A -> B -> C 3.0 -> D => C3.0 SHOULD BE SKIPPED
+ // | -> E -> F -> G
+ // | -> C 2.0 -> H => C2.0 is the winner
+ 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" );
+
+ aNode.setChildren( mutableList( bNode, eNode, c2Node ) );
+ bNode.setChildren( mutableList( c3Node ) );
+ c3Node.setChildren( mutableList( dNode ) );
+ eNode.setChildren( mutableList( fNode ) );
+ fNode.setChildren( mutableList( gNode ) );
+ c2Node.setChildren( mutableList( hNode ) );
+
+ //follow the BFS resolve sequence
+ DefaultDependencyResolutionSkipper skipper = new DefaultDependencyResolutionSkipper();
+ assertFalse( skipper.skipResolution( aNode, new ArrayList<>() ) );
+ skipper.cache( aNode, new ArrayList<>() );
+ assertFalse( skipper.skipResolution( bNode, mutableList( aNode ) ) );
+ skipper.cache( bNode, mutableList( aNode ) );
+ assertFalse( skipper.skipResolution( eNode, mutableList( aNode ) ) );
+ skipper.cache( eNode, mutableList( aNode ) );
+ assertFalse( skipper.skipResolution( c2Node, mutableList( aNode ) ) );
+ skipper.cache( c2Node, mutableList( aNode ) );
+ assertTrue( skipper.skipResolution( c3Node, mutableList( aNode, bNode ) ) );//version conflict
+ assertFalse( skipper.skipResolution( fNode, mutableList( aNode, eNode ) ) );
+ skipper.cache( fNode, mutableList( aNode, eNode ) );
+ assertFalse( skipper.skipResolution( gNode, mutableList( aNode, eNode, fNode ) ) );
+ skipper.cache( gNode, mutableList( aNode, eNode, fNode ) );
+
+ Map<DependencyNode, DefaultDependencyResolutionSkipper.DependencyResolutionResult> results = skipper.getResults();
+ assertEquals( results.size(), 7 );
+
+ List<DefaultDependencyResolutionSkipper.DependencyResolutionResult> skipped =
+ results.entrySet().stream().filter( n -> n.getValue().skippedAsVersionConflict )
+ .map( s -> s.getValue() ).collect(
+ Collectors.toList() );
+ assertEquals( skipped.size(), 1 );
+ assertTrue( skipped.get( 0 ).current == c3Node );
+ }
+
+ @Test
+ public void testSkipDeeperDuplicateNode() throws RepositoryException
+ {
+ // A -> B
+ // |--> C -> B => B here will be skipped
+ // |--> D -> C => C here will be skipped
+ DependencyNode aNode = makeDependencyNode( "some-group", "A", "1.0" );
+ DependencyNode bNode = makeDependencyNode( "some-group", "B", "1.0" );
+ DependencyNode cNode = makeDependencyNode( "some-group", "C", "1.0" );
+ DependencyNode dNode = makeDependencyNode( "some-group", "D", "1.0" );
+ DependencyNode b1Node = new DefaultDependencyNode( bNode );
+ DependencyNode c1Node = new DefaultDependencyNode( cNode );
+
+ aNode.setChildren( mutableList( bNode, cNode, dNode ) );
+ bNode.setChildren( new ArrayList<>() );
+ cNode.setChildren( mutableList( b1Node ) );
+ dNode.setChildren( mutableList( c1Node ) );
+
+ //follow the BFS resolve sequence
+ DefaultDependencyResolutionSkipper skipper = new DefaultDependencyResolutionSkipper();
+ assertFalse( skipper.skipResolution( aNode, new ArrayList<>() ) );
+ skipper.cache( aNode, new ArrayList<>() );
+ assertFalse( skipper.skipResolution( bNode, mutableList( aNode ) ) );
+ skipper.cache( bNode, mutableList( aNode ) );
+ assertFalse( skipper.skipResolution( cNode, mutableList( aNode ) ) );
+ skipper.cache( cNode, mutableList( aNode ) );
+ assertFalse( skipper.skipResolution( dNode, mutableList( aNode ) ) );
+ skipper.cache( dNode, mutableList( aNode ) );
+
+ assertTrue( skipper.skipResolution( b1Node, mutableList( aNode, cNode ) ) );
+ skipper.cache( b1Node, mutableList( aNode, cNode ) );
+
+ assertTrue( skipper.skipResolution( c1Node, mutableList( aNode, dNode ) ) );
+ skipper.cache( c1Node, mutableList( aNode, dNode ) );
+
+ Map<DependencyNode, DefaultDependencyResolutionSkipper.DependencyResolutionResult> results = skipper.getResults();
+ assertEquals( results.size(), 6 );
+
+ List<DefaultDependencyResolutionSkipper.DependencyResolutionResult> skipped =
+ results.entrySet().stream().filter( n -> n.getValue().skippedAsDuplicate )
+ .map( s -> s.getValue() ).collect(
+ Collectors.toList() );
+ assertEquals( skipped.size(), 2 );
+ assertTrue( skipped.get( 0 ).current == b1Node );
+ assertTrue( skipped.get( 1 ).current == c1Node );
+ }
+
+
+ @Test
+ public void testForceResolution() throws RepositoryException
+ {
+ // A -> B -> C -> D => 3rd D here will be force-resolved
+ // |--> C -> D => 2nd D will be force-resolved
+ // |--> D => 1st D to resolve
+ DependencyNode aNode = makeDependencyNode( "some-group", "A", "1.0" );
+ DependencyNode bNode = makeDependencyNode( "some-group", "B", "1.0" );
+ DependencyNode cNode = makeDependencyNode( "some-group", "C", "1.0" );
+ DependencyNode dNode = makeDependencyNode( "some-group", "D", "1.0" );
+ DependencyNode c1Node = new DefaultDependencyNode( cNode );
+ DependencyNode d1Node = new DefaultDependencyNode( dNode );
+ DependencyNode d2Node = new DefaultDependencyNode( dNode );
+
+ aNode.setChildren( mutableList( bNode, cNode, dNode ) );
+ bNode.setChildren( mutableList( c1Node ) );
+ c1Node.setChildren( mutableList( d2Node ) );
+ cNode.setChildren( mutableList( d1Node ) );
+ dNode.setChildren( new ArrayList<>() );
+
+ //follow the BFS resolve sequence
+ DefaultDependencyResolutionSkipper skipper = new DefaultDependencyResolutionSkipper();
+ assertFalse( skipper.skipResolution( aNode, new ArrayList<>() ) );
+ skipper.cache( aNode, new ArrayList<>() );
+ assertFalse( skipper.skipResolution( bNode, mutableList( aNode ) ) );
+ skipper.cache( bNode, mutableList( aNode ) );
+ assertFalse( skipper.skipResolution( cNode, mutableList( aNode ) ) );
+ skipper.cache( cNode, mutableList( aNode ) );
+ assertFalse( skipper.skipResolution( dNode, mutableList( aNode ) ) );
+ skipper.cache( dNode, mutableList( aNode ) );
+
+ assertFalse( skipper.skipResolution( c1Node, mutableList( aNode, bNode ) ) );
+ skipper.cache( c1Node, mutableList( aNode, bNode ) );
+
+ assertFalse( skipper.skipResolution( d1Node, mutableList( aNode, cNode ) ) );
+ skipper.cache( d1Node, mutableList( aNode, cNode ) );
+
+ assertFalse( skipper.skipResolution( d2Node, mutableList( aNode, bNode, c1Node ) ) );
+ skipper.cache( d2Node, mutableList( aNode, bNode, c1Node ) );
+
+ Map<DependencyNode, DefaultDependencyResolutionSkipper.DependencyResolutionResult> results = skipper.getResults();
+ assertEquals( results.size(), 7 );
+
+ List<DefaultDependencyResolutionSkipper.DependencyResolutionResult> forceResolved =
+ results.entrySet().stream().filter( n -> n.getValue().forceResolution )
+ .map( s -> s.getValue() ).collect(
+ Collectors.toList() );
+ assertEquals( forceResolved.size(), 3 );
+ assertTrue( forceResolved.get( 0 ).current == c1Node );
+ assertTrue( forceResolved.get( 1 ).current == d1Node );
+ assertTrue( forceResolved.get( 2 ).current == d2Node );
+ }
+
+}
\ No newline at end of file
diff --git a/src/site/markdown/configuration.md b/src/site/markdown/configuration.md
index 926b688..6c3a20d 100644
--- a/src/site/markdown/configuration.md
+++ b/src/site/markdown/configuration.md
@@ -42,6 +42,7 @@ Option | Type | Description | Default Value | Supports Repo ID Suffix
`aether.connector.smartChecksums` | boolean | Flag indicating that instead of comparing the external checksum fetched from the remote repo with the calculated one, it should try to extract the reference checksum from the actual artifact requests's response headers (several (strategies supported)[included-checksum-strategies.html]). This only works for transport-http transport. | `true` | no
`aether.connector.userAgent` | String | The user agent that repository connectors should report to servers. | `"Aether"` | no
`aether.connector.wagon.config` | Object | The configuration to use for the Wagon provider. | - | yes (must be used)
+`aether.dependencyCollector.useSkip` | boolean | Flag controlling whether to skip resolving duplicate/conflicting nodes during the dependency collection process. | `true` | no
`aether.dependencyCollector.maxCycles` | int | Only up to the given amount cyclic dependencies are emitted. | `10` | no
`aether.dependencyCollector.maxExceptions` | int | Only exceptions up to the number given in this configuration property are emitted. Exceptions which exceed that number are swallowed. | `50` | no
`aether.dependencyManager.verbose` | boolean | Flag controlling the verbose mode for dependency management. If enabled, the original attributes of a dependency before its update due to dependency managemnent will be recorded in the node's `DependencyNode#getData()` when building a dependency graph. | `false` | no