You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@maven.apache.org by cs...@apache.org on 2022/04/08 11:16:22 UTC

[maven-resolver] branch master updated: [MRESOLVER-248] Make DF and BF collector implementations coexist (#161)

This is an automated email from the ASF dual-hosted git repository.

cstamas 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 4b9255bd [MRESOLVER-248] Make DF and BF collector implementations coexist (#161)
4b9255bd is described below

commit 4b9255bdf3b060e7c50fe6f09c091c2d8c9ff399
Author: Tamas Cservenak <ta...@cservenak.net>
AuthorDate: Fri Apr 8 13:16:17 2022 +0200

    [MRESOLVER-248] Make DF and BF collector implementations coexist (#161)
    
    Revive replaced DF collector, and make them both coexists. Default one remains "old" DF, while new BF may be activated on demand (based on session config).
---
 .../aether/impl/DependencyResolutionSkipper.java   |  59 --
 .../eclipse/aether/impl/guice/AetherModule.java    |  22 +
 .../impl/collect/CachingArtifactTypeRegistry.java  |   5 +-
 .../aether/internal/impl/collect/DataPool.java     |  23 +-
 .../DefaultDependencyCollectionContext.java        |   6 +-
 .../impl/collect/DefaultDependencyCollector.java   | 903 +--------------------
 .../impl/collect/DefaultDependencyCycle.java       |  10 +-
 ...efaultDependencyGraphTransformationContext.java |   9 +-
 .../DefaultDependencyResolutionSkipper.java        | 290 -------
 .../impl/collect/DefaultVersionFilterContext.java  |  14 +-
 .../impl/collect/DependencyCollectorDelegate.java  |  96 +++
 .../collect/NeverDependencyResolutionSkipper.java  |  51 --
 .../aether/internal/impl/collect/ObjectPool.java   |   1 +
 .../BfDependencyCollector.java}                    | 124 +--
 .../{ => bf}/DependencyProcessingContext.java      |   7 +-
 .../collect/bf/DependencyResolutionSkipper.java    | 359 ++++++++
 .../DfDependencyCollector.java}                    | 333 +++-----
 .../aether/internal/impl/collect/df/NodeStack.java |  79 ++
 .../impl/collect/DefaultDependencyCycleTest.java   |   6 +-
 .../BfDependencyCollectorTest.java}                |  29 +-
 .../BfDependencyCollectorUseSkipTest.java}         |  14 +-
 .../{ => bf}/DependencyResolutionSkipperTest.java  |  57 +-
 .../DfDependencyCollectorTest.java}                |  32 +-
 src/site/markdown/configuration.md                 |   3 +-
 24 files changed, 884 insertions(+), 1648 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
deleted file mode 100644
index 649f6d36..00000000
--- a/maven-resolver-impl/src/main/java/org/eclipse/aether/impl/DependencyResolutionSkipper.java
+++ /dev/null
@@ -1,59 +0,0 @@
-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/impl/guice/AetherModule.java b/maven-resolver-impl/src/main/java/org/eclipse/aether/impl/guice/AetherModule.java
index 9629dc97..93ef99ec 100644
--- a/maven-resolver-impl/src/main/java/org/eclipse/aether/impl/guice/AetherModule.java
+++ b/maven-resolver-impl/src/main/java/org/eclipse/aether/impl/guice/AetherModule.java
@@ -48,6 +48,9 @@ import org.eclipse.aether.internal.impl.checksum.Sha1ChecksumAlgorithmFactory;
 import org.eclipse.aether.internal.impl.checksum.Sha256ChecksumAlgorithmFactory;
 import org.eclipse.aether.internal.impl.checksum.Sha512ChecksumAlgorithmFactory;
 import org.eclipse.aether.internal.impl.checksum.DefaultChecksumAlgorithmFactorySelector;
+import org.eclipse.aether.internal.impl.collect.DependencyCollectorDelegate;
+import org.eclipse.aether.internal.impl.collect.bf.BfDependencyCollector;
+import org.eclipse.aether.internal.impl.collect.df.DfDependencyCollector;
 import org.eclipse.aether.internal.impl.synccontext.DefaultSyncContextFactory;
 import org.eclipse.aether.internal.impl.synccontext.named.NamedLockFactorySelector;
 import org.eclipse.aether.internal.impl.synccontext.named.SimpleNamedLockFactorySelector;
@@ -132,8 +135,14 @@ public class AetherModule
                 .to( DefaultRepositorySystem.class ).in( Singleton.class );
         bind( ArtifactResolver.class ) //
                 .to( DefaultArtifactResolver.class ).in( Singleton.class );
+
         bind( DependencyCollector.class ) //
                 .to( DefaultDependencyCollector.class ).in( Singleton.class );
+        bind( DependencyCollectorDelegate.class ).annotatedWith( Names.named( BfDependencyCollector.NAME ) )
+                .to( BfDependencyCollector.class ).in( Singleton.class );
+        bind( DependencyCollectorDelegate.class ).annotatedWith( Names.named( DfDependencyCollector.NAME ) )
+                .to( DfDependencyCollector.class ).in( Singleton.class );
+
         bind( Deployer.class ) //
                 .to( DefaultDeployer.class ).in( Singleton.class );
         bind( Installer.class ) //
@@ -212,6 +221,19 @@ public class AetherModule
 
     }
 
+    @Provides
+    @Singleton
+    Map<String, DependencyCollectorDelegate> dependencyCollectorDelegates(
+            @Named( BfDependencyCollector.NAME ) DependencyCollectorDelegate bf,
+            @Named( DfDependencyCollector.NAME ) DependencyCollectorDelegate df
+    )
+    {
+        Map<String, DependencyCollectorDelegate> dependencyCollectorDelegates = new HashMap<>();
+        dependencyCollectorDelegates.put( BfDependencyCollector.NAME, bf );
+        dependencyCollectorDelegates.put( DfDependencyCollector.NAME, df );
+        return dependencyCollectorDelegates;
+    }
+
     @Provides
     @Singleton
     Map<String, ProvidedChecksumsSource> provideChecksumSources(
diff --git a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/collect/CachingArtifactTypeRegistry.java b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/collect/CachingArtifactTypeRegistry.java
index a2602344..1a9a98f5 100644
--- a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/collect/CachingArtifactTypeRegistry.java
+++ b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/collect/CachingArtifactTypeRegistry.java
@@ -27,9 +27,10 @@ import org.eclipse.aether.artifact.ArtifactType;
 import org.eclipse.aether.artifact.ArtifactTypeRegistry;
 
 /**
- * A short-lived artifact type registry that caches results from a presumedly slower type registry.
+ * A short-lived artifact type registry that caches results from a presumably slower type registry.
+ * Internal helper class for collector implementations.
  */
-class CachingArtifactTypeRegistry
+public class CachingArtifactTypeRegistry
     implements ArtifactTypeRegistry
 {
 
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 4a145558..04ebdf41 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
@@ -48,8 +48,9 @@ import org.eclipse.aether.version.Version;
 import org.eclipse.aether.version.VersionConstraint;
 
 /**
+ * Internal helper class for collector implementations.
  */
-final class DataPool
+public final class DataPool
 {
 
     private static final String ARTIFACT_POOL = DataPool.class.getName() + "$Artifact";
@@ -58,7 +59,7 @@ final class DataPool
 
     private static final String DESCRIPTORS = DataPool.class.getName() + "$Descriptors";
 
-    static final ArtifactDescriptorResult NO_DESCRIPTOR =
+    public static final ArtifactDescriptorResult NO_DESCRIPTOR =
         new ArtifactDescriptorResult( new ArtifactDescriptorRequest() );
 
     private ObjectPool<Artifact> artifacts;
@@ -72,7 +73,7 @@ final class DataPool
     private final Map<Object, List<DependencyNode>> nodes = new HashMap<>( 256 );
 
     @SuppressWarnings( "unchecked" )
-    DataPool( RepositorySystemSession session )
+    public DataPool( RepositorySystemSession session )
     {
         RepositoryCache cache = session.getCache();
 
@@ -103,7 +104,7 @@ final class DataPool
 
         if ( descriptors == null )
         {
-            descriptors = Collections.synchronizedMap( new WeakHashMap<Object, Descriptor>( 256 ) );
+            descriptors = Collections.synchronizedMap( new WeakHashMap<>( 256 ) );
             if ( cache != null )
             {
                 cache.put( session, DESCRIPTORS, descriptors );
@@ -121,12 +122,12 @@ final class DataPool
         return dependencies.intern( dependency );
     }
 
-    Object toKey( ArtifactDescriptorRequest request )
+    public Object toKey( ArtifactDescriptorRequest request )
     {
         return request.getArtifact();
     }
 
-    ArtifactDescriptorResult getDescriptor( Object key, ArtifactDescriptorRequest request )
+    public ArtifactDescriptorResult getDescriptor( Object key, ArtifactDescriptorRequest request )
     {
         Descriptor descriptor = descriptors.get( key );
         if ( descriptor != null )
@@ -136,22 +137,22 @@ final class DataPool
         return null;
     }
 
-    void putDescriptor( Object key, ArtifactDescriptorResult result )
+    public void putDescriptor( Object key, ArtifactDescriptorResult result )
     {
         descriptors.put( key, new GoodDescriptor( result ) );
     }
 
-    void putDescriptor( Object key, ArtifactDescriptorException e )
+    public void putDescriptor( Object key, ArtifactDescriptorException e )
     {
         descriptors.put( key, BadDescriptor.INSTANCE );
     }
 
-    Object toKey( VersionRangeRequest request )
+    public Object toKey( VersionRangeRequest request )
     {
         return new ConstraintKey( request );
     }
 
-    VersionRangeResult getConstraint( Object key, VersionRangeRequest request )
+    public VersionRangeResult getConstraint( Object key, VersionRangeRequest request )
     {
         Constraint constraint = constraints.get( key );
         if ( constraint != null )
@@ -161,7 +162,7 @@ final class DataPool
         return null;
     }
 
-    void putConstraint( Object key, VersionRangeResult result )
+    public void putConstraint( Object key, VersionRangeResult result )
     {
         constraints.put( key, new Constraint( result ) );
     }
diff --git a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/collect/DefaultDependencyCollectionContext.java b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/collect/DefaultDependencyCollectionContext.java
index 3bf4fe1a..117af880 100644
--- a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/collect/DefaultDependencyCollectionContext.java
+++ b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/collect/DefaultDependencyCollectionContext.java
@@ -27,9 +27,9 @@ import org.eclipse.aether.collection.DependencyCollectionContext;
 import org.eclipse.aether.graph.Dependency;
 
 /**
- * @see DefaultDependencyCollector
+ * Internal helper class for collector implementations.
  */
-final class DefaultDependencyCollectionContext
+public final class DefaultDependencyCollectionContext
     implements DependencyCollectionContext
 {
 
@@ -41,7 +41,7 @@ final class DefaultDependencyCollectionContext
 
     private List<Dependency> managedDependencies;
 
-    DefaultDependencyCollectionContext( RepositorySystemSession session, Artifact artifact,
+    public DefaultDependencyCollectionContext( RepositorySystemSession session, Artifact artifact,
                                                Dependency dependency, List<Dependency> managedDependencies )
     {
         this.session = session;
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 a895bb9b..f7f6ea26 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
@@ -19,914 +19,79 @@ package org.eclipse.aether.internal.impl.collect;
  * under the License.
  */
 
-import java.util.ArrayDeque;
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.LinkedHashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.Queue;
-
-import static java.util.Objects.requireNonNull;
-import static org.eclipse.aether.internal.impl.collect.DefaultDependencyCycle.find;
-
 import javax.inject.Inject;
 import javax.inject.Named;
 import javax.inject.Singleton;
 
-import org.eclipse.aether.DefaultRepositorySystemSession;
-import org.eclipse.aether.RepositoryException;
+import java.util.HashMap;
+import java.util.Map;
+
 import org.eclipse.aether.RepositorySystemSession;
-import org.eclipse.aether.RequestTrace;
-import org.eclipse.aether.artifact.Artifact;
-import org.eclipse.aether.artifact.ArtifactProperties;
 import org.eclipse.aether.collection.CollectRequest;
 import org.eclipse.aether.collection.CollectResult;
 import org.eclipse.aether.collection.DependencyCollectionException;
-import org.eclipse.aether.collection.DependencyGraphTransformer;
-import org.eclipse.aether.collection.DependencyManagement;
-import org.eclipse.aether.collection.DependencyManager;
-import org.eclipse.aether.collection.DependencySelector;
-import org.eclipse.aether.collection.DependencyTraverser;
-import org.eclipse.aether.collection.VersionFilter;
-import org.eclipse.aether.graph.DefaultDependencyNode;
-import org.eclipse.aether.graph.Dependency;
-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;
-import org.eclipse.aether.repository.RemoteRepository;
-import org.eclipse.aether.resolution.ArtifactDescriptorException;
-import org.eclipse.aether.resolution.ArtifactDescriptorRequest;
-import org.eclipse.aether.resolution.ArtifactDescriptorResult;
-import org.eclipse.aether.resolution.VersionRangeRequest;
-import org.eclipse.aether.resolution.VersionRangeResolutionException;
-import org.eclipse.aether.resolution.VersionRangeResult;
+import org.eclipse.aether.internal.impl.collect.bf.BfDependencyCollector;
+import org.eclipse.aether.internal.impl.collect.df.DfDependencyCollector;
 import org.eclipse.aether.spi.locator.Service;
 import org.eclipse.aether.spi.locator.ServiceLocator;
 import org.eclipse.aether.util.ConfigUtils;
-import org.eclipse.aether.util.graph.manager.DependencyManagerUtils;
-import org.eclipse.aether.util.graph.transformer.TransformationContextKeys;
-import org.eclipse.aether.version.Version;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
+
+import static java.util.Objects.requireNonNull;
 
 /**
+ * Default implementation of {@link DependencyCollector} that merely indirect to selected delegate.
  */
 @Singleton
 @Named
 public class DefaultDependencyCollector
-    implements DependencyCollector, Service
+        implements DependencyCollector, Service
 {
+    private static final String CONFIG_PROP_COLLECTOR_IMPL = "aether.collector.impl";
 
-    /**
-     * 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";
+    private static final String DEFAULT_COLLECTOR_IMPL = DfDependencyCollector.NAME;
+
+    private final Map<String, DependencyCollectorDelegate> delegates;
 
     /**
-     * The default value for {@link #CONFIG_PROP_USE_SKIP}, {@code true}.
+     * Default ctor for SL.
      *
-     * @since 1.8.0
+     * @deprecated SL is to be removed.
      */
-    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;
-
-    private static final String CONFIG_PROP_MAX_CYCLES = "aether.dependencyCollector.maxCycles";
-
-    private static final int CONFIG_PROP_MAX_CYCLES_DEFAULT = 10;
-
-    private static final Logger LOGGER = LoggerFactory.getLogger( DefaultDependencyCollector.class );
-
-    private RemoteRepositoryManager remoteRepositoryManager;
-
-    private ArtifactDescriptorReader descriptorReader;
-
-    private VersionRangeResolver versionRangeResolver;
-
+    @Deprecated
     public DefaultDependencyCollector()
     {
-        // enables default constructor
+        this.delegates = new HashMap<>();
     }
 
     @Inject
-    DefaultDependencyCollector( RemoteRepositoryManager remoteRepositoryManager,
-                                ArtifactDescriptorReader artifactDescriptorReader,
-                                VersionRangeResolver versionRangeResolver )
+    public DefaultDependencyCollector( Map<String, DependencyCollectorDelegate> delegates )
     {
-        setRemoteRepositoryManager( remoteRepositoryManager );
-        setArtifactDescriptorReader( artifactDescriptorReader );
-        setVersionRangeResolver( versionRangeResolver );
+        this.delegates = requireNonNull( delegates );
     }
 
+    @Override
     public void initService( ServiceLocator locator )
     {
-        setRemoteRepositoryManager( locator.getService( RemoteRepositoryManager.class ) );
-        setArtifactDescriptorReader( locator.getService( ArtifactDescriptorReader.class ) );
-        setVersionRangeResolver( locator.getService( VersionRangeResolver.class ) );
+        BfDependencyCollector bf = new BfDependencyCollector();
+        bf.initService( locator );
+        DfDependencyCollector df = new DfDependencyCollector();
+        df.initService( locator );
+        this.delegates.put( BfDependencyCollector.NAME, bf );
+        this.delegates.put( DfDependencyCollector.NAME, df );
     }
 
-    public DefaultDependencyCollector setRemoteRepositoryManager( RemoteRepositoryManager remoteRepositoryManager )
-    {
-        this.remoteRepositoryManager =
-                requireNonNull( remoteRepositoryManager, "remote repository provider cannot be null" );
-        return this;
-    }
-
-    public DefaultDependencyCollector setArtifactDescriptorReader( ArtifactDescriptorReader artifactDescriptorReader )
-    {
-        descriptorReader = requireNonNull( artifactDescriptorReader, "artifact descriptor reader cannot be null" );
-        return this;
-    }
-
-    public DefaultDependencyCollector setVersionRangeResolver( VersionRangeResolver versionRangeResolver )
-    {
-        this.versionRangeResolver =
-                requireNonNull( versionRangeResolver, "version range resolver cannot be null" );
-        return this;
-    }
-
-    @SuppressWarnings( "checkstyle:methodlength" )
+    @Override
     public CollectResult collectDependencies( RepositorySystemSession session, CollectRequest request )
-        throws DependencyCollectionException
-    {
-        requireNonNull( session, "session cannot be null" );
-        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 );
-
-        DependencySelector depSelector = session.getDependencySelector();
-        DependencyManager depManager = session.getDependencyManager();
-        DependencyTraverser depTraverser = session.getDependencyTraverser();
-        VersionFilter verFilter = session.getVersionFilter();
-
-        Dependency root = request.getRoot();
-        List<RemoteRepository> repositories = request.getRepositories();
-        List<Dependency> dependencies = request.getDependencies();
-        List<Dependency> managedDependencies = request.getManagedDependencies();
-
-        Map<String, Object> stats = new LinkedHashMap<>();
-        long time1 = System.nanoTime();
-
-        DefaultDependencyNode node;
-        if ( root != null )
-        {
-            List<? extends Version> versions;
-            VersionRangeResult rangeResult;
-            try
-            {
-                VersionRangeRequest rangeRequest =
-                    new VersionRangeRequest( root.getArtifact(), request.getRepositories(),
-                                             request.getRequestContext() );
-                rangeRequest.setTrace( trace );
-                rangeResult = versionRangeResolver.resolveVersionRange( session, rangeRequest );
-                versions = filterVersions( root, rangeResult, verFilter, new DefaultVersionFilterContext( session ) );
-            }
-            catch ( VersionRangeResolutionException e )
-            {
-                result.addException( e );
-                throw new DependencyCollectionException( result, e.getMessage() );
-            }
-
-            Version version = versions.get( versions.size() - 1 );
-            root = root.setArtifact( root.getArtifact().setVersion( version.toString() ) );
-
-            ArtifactDescriptorResult descriptorResult;
-            try
-            {
-                ArtifactDescriptorRequest descriptorRequest = new ArtifactDescriptorRequest();
-                descriptorRequest.setArtifact( root.getArtifact() );
-                descriptorRequest.setRepositories( request.getRepositories() );
-                descriptorRequest.setRequestContext( request.getRequestContext() );
-                descriptorRequest.setTrace( trace );
-                if ( isLackingDescriptor( root.getArtifact() ) )
-                {
-                    descriptorResult = new ArtifactDescriptorResult( descriptorRequest );
-                }
-                else
-                {
-                    descriptorResult = descriptorReader.readArtifactDescriptor( session, descriptorRequest );
-                }
-            }
-            catch ( ArtifactDescriptorException e )
-            {
-                result.addException( e );
-                throw new DependencyCollectionException( result, e.getMessage() );
-            }
-
-            root = root.setArtifact( descriptorResult.getArtifact() );
-
-            if ( !session.isIgnoreArtifactDescriptorRepositories() )
-            {
-                repositories = remoteRepositoryManager.aggregateRepositories( session, repositories,
-                                                                              descriptorResult.getRepositories(),
-                                                                              true );
-            }
-            dependencies = mergeDeps( dependencies, descriptorResult.getDependencies() );
-            managedDependencies = mergeDeps( managedDependencies, descriptorResult.getManagedDependencies() );
-
-            node = new DefaultDependencyNode( root );
-            node.setRequestContext( request.getRequestContext() );
-            node.setRelocations( descriptorResult.getRelocations() );
-            node.setVersionConstraint( rangeResult.getVersionConstraint() );
-            node.setVersion( version );
-            node.setAliases( descriptorResult.getAliases() );
-            node.setRepositories( request.getRepositories() );
-        }
-        else
-        {
-            node = new DefaultDependencyNode( request.getRootArtifact() );
-            node.setRequestContext( request.getRequestContext() );
-            node.setRepositories( request.getRepositories() );
-        }
-
-        result.setRoot( node );
-
-        boolean traverse = root == null || depTraverser == null || depTraverser.traverseDependency( root );
-        String errorPath = null;
-        if ( traverse && !dependencies.isEmpty() )
-        {
-            DataPool pool = new DataPool( session );
-
-            DefaultDependencyCollectionContext context =
-                new DefaultDependencyCollectionContext( session, request.getRootArtifact(), root, managedDependencies );
-
-            DefaultVersionFilterContext versionContext = new DefaultVersionFilterContext( session );
-
-            Args args =
-                    new Args( session, trace, pool, context, versionContext, request,
-                            useSkip ? new DefaultDependencyResolutionSkipper()
-                                    : NeverDependencyResolutionSkipper.INSTANCE );
-            Results results = new Results( result, session );
-
-            DependencySelector rootDepSelector =
-                    depSelector != null ? depSelector.deriveChildSelector( context ) : null;
-            DependencyManager rootDepManager = depManager != null ? depManager.deriveChildManager( context ) : null;
-            DependencyTraverser rootDepTraverser =
-                    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, parents,
-                                dependency ) );
-            }
-
-            while ( !args.dependencyProcessingQueue.isEmpty() )
-            {
-                processDependency( args, results, args.dependencyProcessingQueue.remove(), Collections.emptyList(),
-                        false );
-            }
-
-            args.skipper.report();
-            errorPath = results.errorPath;
-        }
-
-        long time2 = System.nanoTime();
-
-        DependencyGraphTransformer transformer = session.getDependencyGraphTransformer();
-        if ( transformer != null )
-        {
-            try
-            {
-                DefaultDependencyGraphTransformationContext context =
-                    new DefaultDependencyGraphTransformationContext( session );
-                context.put( TransformationContextKeys.STATS, stats );
-                result.setRoot( transformer.transformGraph( node, context ) );
-            }
-            catch ( RepositoryException e )
-            {
-                result.addException( e );
-            }
-        }
-
-        long time3 = System.nanoTime();
-        stats.put( "DefaultDependencyCollector.collectTime", time2 - time1 );
-        stats.put( "DefaultDependencyCollector.transformTime", time3 - time2 );
-        LOGGER.debug( "Dependency collection stats {}", stats );
-
-        if ( errorPath != null )
-        {
-            throw new DependencyCollectionException( result, "Failed to collect dependencies at " + errorPath );
-        }
-        if ( !result.getExceptions().isEmpty() )
-        {
-            throw new DependencyCollectionException( result );
-        }
-
-        return result;
-    }
-
-    private static RepositorySystemSession optimizeSession( RepositorySystemSession session )
-    {
-        DefaultRepositorySystemSession optimized = new DefaultRepositorySystemSession( session );
-        optimized.setArtifactTypeRegistry( CachingArtifactTypeRegistry.newInstance( session ) );
-        return optimized;
-    }
-
-    private List<Dependency> mergeDeps( List<Dependency> dominant, List<Dependency> recessive )
-    {
-        List<Dependency> result;
-        if ( dominant == null || dominant.isEmpty() )
-        {
-            result = recessive;
-        }
-        else if ( recessive == null || recessive.isEmpty() )
-        {
-            result = dominant;
-        }
-        else
-        {
-            int initialCapacity = dominant.size() + recessive.size();
-            result = new ArrayList<>( initialCapacity );
-            Collection<String> ids = new HashSet<>( initialCapacity, 1.0f );
-            for ( Dependency dependency : dominant )
-            {
-                ids.add( getId( dependency.getArtifact() ) );
-                result.add( dependency );
-            }
-            for ( Dependency dependency : recessive )
-            {
-                if ( !ids.contains( getId( dependency.getArtifact() ) ) )
-                {
-                    result.add( dependency );
-                }
-            }
-        }
-        return result;
-    }
-
-    private static String getId( Artifact a )
-    {
-        return a.getGroupId() + ':' + a.getArtifactId() + ':' + a.getClassifier() + ':' + a.getExtension();
-    }
-
-    @SuppressWarnings( "checkstyle:parameternumber" )
-    private void processDependency( Args args, Results results, DependencyProcessingContext context,
-                                    List<Artifact> relocations, boolean disableVersionManagement )
-    {
-
-        if ( context.depSelector != null && !context.depSelector.selectDependency( context.dependency ) )
-        {
-            return;
-        }
-
-        PremanagedDependency preManaged =
-                PremanagedDependency.create( context.depManager, context.dependency, disableVersionManagement,
-                        args.premanagedState );
-        Dependency dependency = preManaged.managedDependency;
-
-        boolean noDescriptor = isLackingDescriptor( dependency.getArtifact() );
-
-        boolean traverse =
-                !noDescriptor && ( context.depTraverser == null || context.depTraverser.traverseDependency(
-                        dependency ) );
-
-        List<? extends Version> versions;
-        VersionRangeResult rangeResult;
-        try
-        {
-            VersionRangeRequest rangeRequest = createVersionRangeRequest( args, context.repositories, dependency );
-
-            rangeResult = cachedResolveRangeResult( rangeRequest, args.pool, args.session );
-
-            versions = filterVersions( dependency, rangeResult, context.verFilter, args.versionContext );
-        }
-        catch ( VersionRangeResolutionException e )
-        {
-            results.addException( dependency, e, context.parents );
-            return;
-        }
-
-        //Resolve newer version first to maximize benefits of skipper
-        Collections.reverse( versions );
-        for ( Version version : versions )
-        {
-            Artifact originalArtifact = dependency.getArtifact().setVersion( version.toString() );
-            Dependency d = dependency.setArtifact( originalArtifact );
-
-            ArtifactDescriptorRequest descriptorRequest =
-                    createArtifactDescriptorRequest( args, context.repositories, d );
-
-            final ArtifactDescriptorResult descriptorResult =
-                    noDescriptor
-                            ? new ArtifactDescriptorResult( descriptorRequest )
-                            : resolveCachedArtifactDescriptor( args.pool, descriptorRequest, args.session,
-                                    context.withDependency( d ), results );
-
-            if ( descriptorResult != null )
-            {
-                d = d.setArtifact( descriptorResult.getArtifact() );
-
-                int cycleEntry = find( context.parents, d.getArtifact() );
-                if ( cycleEntry >= 0 )
-                {
-                    results.addCycle( context.parents, cycleEntry, d );
-                    DependencyNode cycleNode = context.parents.get( cycleEntry );
-                    if ( cycleNode.getDependency() != null )
-                    {
-                        DefaultDependencyNode child =
-                                createDependencyNode( relocations, preManaged, rangeResult, version, d,
-                                        descriptorResult, cycleNode );
-                        context.getParent().getChildren().add( child );
-                        continue;
-                    }
-                }
-
-                if ( !descriptorResult.getRelocations().isEmpty() )
-                {
-                    boolean disableVersionManagementSubsequently =
-                        originalArtifact.getGroupId().equals( d.getArtifact().getGroupId() )
-                            && originalArtifact.getArtifactId().equals( d.getArtifact().getArtifactId() );
-
-                    processDependency( args, results, context.withDependency( d ), descriptorResult.getRelocations(),
-                            disableVersionManagementSubsequently );
-                    return;
-                }
-                else
-                {
-                    d = args.pool.intern( d.setArtifact( args.pool.intern( d.getArtifact() ) ) );
-
-                    List<RemoteRepository> repos =
-                        getRemoteRepositories( rangeResult.getRepository( version ), context.repositories );
-
-                    DefaultDependencyNode child =
-                        createDependencyNode( relocations, preManaged, rangeResult, version, d,
-                                              descriptorResult.getAliases(), repos, args.request.getRequestContext() );
-
-                    context.getParent().getChildren().add( child );
-
-                    boolean recurse = traverse && !descriptorResult.getDependencies().isEmpty();
-                    if ( recurse )
-                    {
-                        doRecurse( args, context.withDependency( d ), descriptorResult, child );
-                    }
-                }
-            }
-            else
-            {
-                List<RemoteRepository> repos =
-                    getRemoteRepositories( rangeResult.getRepository( version ), context.repositories );
-                DefaultDependencyNode child =
-                    createDependencyNode( relocations, preManaged, rangeResult, version, d, null, repos,
-                                          args.request.getRequestContext() );
-                context.getParent().getChildren().add( child );
-            }
-        }
-    }
-
-    @SuppressWarnings( "checkstyle:parameternumber" )
-    private void doRecurse( Args args, DependencyProcessingContext parentContext,
-                            ArtifactDescriptorResult descriptorResult, DefaultDependencyNode child )
+            throws DependencyCollectionException
     {
-        DefaultDependencyCollectionContext context = args.collectionContext;
-        context.set( parentContext.dependency, descriptorResult.getManagedDependencies() );
-
-        DependencySelector childSelector =
-                parentContext.depSelector != null ? parentContext.depSelector.deriveChildSelector( context ) : null;
-        DependencyManager childManager =
-                parentContext.depManager != null ? parentContext.depManager.deriveChildManager( context ) : null;
-        DependencyTraverser childTraverser =
-                parentContext.depTraverser != null ? parentContext.depTraverser.deriveChildTraverser( context ) : null;
-        VersionFilter childFilter =
-                parentContext.verFilter != null ? parentContext.verFilter.deriveChildFilter( context ) : null;
-
-        final List<RemoteRepository> childRepos =
-                args.ignoreRepos
-                        ? parentContext.repositories
-                        : remoteRepositoryManager.aggregateRepositories( args.session, parentContext.repositories,
-                        descriptorResult.getRepositories(), true );
-
-        Object key =
-                args.pool.toKey( parentContext.dependency.getArtifact(), childRepos, childSelector, childManager,
-                        childTraverser, childFilter );
-
-        List<DependencyNode> children = args.pool.getChildren( key );
-        if ( children == null )
-        {
-            boolean skipResolution = args.skipper.skipResolution( child, parentContext.parents );
-            if ( !skipResolution )
-            {
-                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
-        {
-            child.setChildren( children );
-        }
-    }
-
-    private ArtifactDescriptorResult resolveCachedArtifactDescriptor( DataPool pool,
-                                                                      ArtifactDescriptorRequest descriptorRequest,
-                                                                      RepositorySystemSession session,
-                                                                      DependencyProcessingContext context,
-                                                                      Results results )
-    {
-        Object key = pool.toKey( descriptorRequest );
-        ArtifactDescriptorResult descriptorResult = pool.getDescriptor( key, descriptorRequest );
-        if ( descriptorResult == null )
-        {
-            try
-            {
-                descriptorResult = descriptorReader.readArtifactDescriptor( session, descriptorRequest );
-                pool.putDescriptor( key, descriptorResult );
-            }
-            catch ( ArtifactDescriptorException e )
-            {
-                results.addException( context.dependency, e, context.parents );
-                pool.putDescriptor( key, e );
-                return null;
-            }
-
-        }
-        else if ( descriptorResult == DataPool.NO_DESCRIPTOR )
+        String delegateName = ConfigUtils.getString( session, DEFAULT_COLLECTOR_IMPL, CONFIG_PROP_COLLECTOR_IMPL );
+        DependencyCollectorDelegate delegate = delegates.get( delegateName );
+        if ( delegate == null )
         {
-            return null;
+            throw new IllegalArgumentException( "Unknown collector impl: '" + delegateName
+                    + "', known implementations are " + delegates.keySet() );
         }
-
-        return descriptorResult;
-    }
-
-    @SuppressWarnings( "checkstyle:parameternumber" )
-    private static DefaultDependencyNode createDependencyNode( List<Artifact> relocations,
-                                                               PremanagedDependency preManaged,
-                                                               VersionRangeResult rangeResult, Version version,
-                                                               Dependency d, Collection<Artifact> aliases,
-                                                               List<RemoteRepository> repos, String requestContext )
-    {
-        DefaultDependencyNode child = new DefaultDependencyNode( d );
-        preManaged.applyTo( child );
-        child.setRelocations( relocations );
-        child.setVersionConstraint( rangeResult.getVersionConstraint() );
-        child.setVersion( version );
-        child.setAliases( aliases );
-        child.setRepositories( repos );
-        child.setRequestContext( requestContext );
-        return child;
-    }
-
-    private static DefaultDependencyNode createDependencyNode( List<Artifact> relocations,
-                                                               PremanagedDependency preManaged,
-                                                               VersionRangeResult rangeResult, Version version,
-                                                               Dependency d, ArtifactDescriptorResult descriptorResult,
-                                                               DependencyNode cycleNode )
-    {
-        DefaultDependencyNode child =
-            createDependencyNode( relocations, preManaged, rangeResult, version, d, descriptorResult.getAliases(),
-                                  cycleNode.getRepositories(), cycleNode.getRequestContext() );
-        child.setChildren( cycleNode.getChildren() );
-        return child;
+        return delegate.collectDependencies( session, request );
     }
-
-    private static ArtifactDescriptorRequest createArtifactDescriptorRequest( Args args,
-                                                                              List<RemoteRepository> repositories,
-                                                                              Dependency d )
-    {
-        ArtifactDescriptorRequest descriptorRequest = new ArtifactDescriptorRequest();
-        descriptorRequest.setArtifact( d.getArtifact() );
-        descriptorRequest.setRepositories( repositories );
-        descriptorRequest.setRequestContext( args.request.getRequestContext() );
-        descriptorRequest.setTrace( args.trace );
-        return descriptorRequest;
-    }
-
-    private static VersionRangeRequest createVersionRangeRequest( Args args, List<RemoteRepository> repositories,
-                                                                  Dependency dependency )
-    {
-        VersionRangeRequest rangeRequest = new VersionRangeRequest();
-        rangeRequest.setArtifact( dependency.getArtifact() );
-        rangeRequest.setRepositories( repositories );
-        rangeRequest.setRequestContext( args.request.getRequestContext() );
-        rangeRequest.setTrace( args.trace );
-        return rangeRequest;
-    }
-
-    private VersionRangeResult cachedResolveRangeResult( VersionRangeRequest rangeRequest, DataPool pool,
-                                                         RepositorySystemSession session )
-        throws VersionRangeResolutionException
-    {
-        Object key = pool.toKey( rangeRequest );
-        VersionRangeResult rangeResult = pool.getConstraint( key, rangeRequest );
-        if ( rangeResult == null )
-        {
-            rangeResult = versionRangeResolver.resolveVersionRange( session, rangeRequest );
-            pool.putConstraint( key, rangeResult );
-        }
-        return rangeResult;
-    }
-
-    private static boolean isLackingDescriptor( Artifact artifact )
-    {
-        return artifact.getProperty( ArtifactProperties.LOCAL_PATH, null ) != null;
-    }
-
-    private static List<RemoteRepository> getRemoteRepositories( ArtifactRepository repository,
-                                                                 List<RemoteRepository> repositories )
-    {
-        if ( repository instanceof RemoteRepository )
-        {
-            return Collections.singletonList( (RemoteRepository) repository );
-        }
-        if ( repository != null )
-        {
-            return Collections.emptyList();
-        }
-        return repositories;
-    }
-
-    private static List<? extends Version> filterVersions( Dependency dependency, VersionRangeResult rangeResult,
-                                                           VersionFilter verFilter,
-                                                           DefaultVersionFilterContext verContext )
-        throws VersionRangeResolutionException
-    {
-        if ( rangeResult.getVersions().isEmpty() )
-        {
-            throw new VersionRangeResolutionException( rangeResult,
-                                                       "No versions available for " + dependency.getArtifact()
-                                                           + " within specified range" );
-        }
-
-        List<? extends Version> versions;
-        if ( verFilter != null && rangeResult.getVersionConstraint().getRange() != null )
-        {
-            verContext.set( dependency, rangeResult );
-            try
-            {
-                verFilter.filterVersions( verContext );
-            }
-            catch ( RepositoryException e )
-            {
-                throw new VersionRangeResolutionException( rangeResult,
-                                                           "Failed to filter versions for " + dependency.getArtifact()
-                                                               + ": " + e.getMessage(), e );
-            }
-            versions = verContext.get();
-            if ( versions.isEmpty() )
-            {
-                throw new VersionRangeResolutionException( rangeResult,
-                                                           "No acceptable versions for " + dependency.getArtifact()
-                                                               + ": " + rangeResult.getVersions() );
-            }
-        }
-        else
-        {
-            versions = rangeResult.getVersions();
-        }
-        return versions;
-    }
-
-    static class Args
-    {
-
-        final RepositorySystemSession session;
-
-        final boolean ignoreRepos;
-
-        final boolean premanagedState;
-
-        final RequestTrace trace;
-
-        final DataPool pool;
-
-        final Queue<DependencyProcessingContext> dependencyProcessingQueue = new ArrayDeque<>( 128 );
-
-        final DefaultDependencyCollectionContext collectionContext;
-
-        final DefaultVersionFilterContext versionContext;
-
-        final CollectRequest request;
-
-        final DependencyResolutionSkipper skipper;
-
-        Args( RepositorySystemSession session, RequestTrace trace, DataPool pool,
-                     DefaultDependencyCollectionContext collectionContext, DefaultVersionFilterContext versionContext,
-                     CollectRequest request, DependencyResolutionSkipper skipper )
-        {
-            this.session = session;
-            this.request = request;
-            this.ignoreRepos = session.isIgnoreArtifactDescriptorRepositories();
-            this.premanagedState = ConfigUtils.getBoolean( session, false, DependencyManagerUtils.CONFIG_PROP_VERBOSE );
-            this.trace = trace;
-            this.pool = pool;
-            this.collectionContext = collectionContext;
-            this.versionContext = versionContext;
-            this.skipper = skipper;
-        }
-
-    }
-
-    static class Results
-    {
-
-        private final CollectResult result;
-
-        final int maxExceptions;
-
-        final int maxCycles;
-
-        String errorPath;
-
-        Results( CollectResult result, RepositorySystemSession session )
-        {
-            this.result = result;
-
-            maxExceptions =
-                    ConfigUtils.getInteger( session, CONFIG_PROP_MAX_EXCEPTIONS_DEFAULT, CONFIG_PROP_MAX_EXCEPTIONS );
-
-            maxCycles = ConfigUtils.getInteger( session, CONFIG_PROP_MAX_CYCLES_DEFAULT, CONFIG_PROP_MAX_CYCLES );
-        }
-
-        public void addException( Dependency dependency, Exception e, List<DependencyNode> nodes )
-        {
-            if ( maxExceptions < 0 || result.getExceptions().size() < maxExceptions )
-            {
-                result.addException( e );
-                if ( errorPath == null )
-                {
-                    StringBuilder buffer = new StringBuilder( 256 );
-                    for ( DependencyNode node : nodes )
-                    {
-                        if ( buffer.length() > 0 )
-                        {
-                            buffer.append( " -> " );
-                        }
-                        Dependency dep = node.getDependency();
-                        if ( dep != null )
-                        {
-                            buffer.append( dep.getArtifact() );
-                        }
-                    }
-                    if ( buffer.length() > 0 )
-                    {
-                        buffer.append( " -> " );
-                    }
-                    buffer.append( dependency.getArtifact() );
-                    errorPath = buffer.toString();
-                }
-            }
-        }
-
-        public void addCycle( List<DependencyNode> nodes, int cycleEntry, Dependency dependency )
-        {
-            if ( maxCycles < 0 || result.getCycles().size() < maxCycles )
-            {
-                result.addCycle( new DefaultDependencyCycle( nodes, cycleEntry, dependency ) );
-            }
-        }
-
-    }
-
-    static class PremanagedDependency
-    {
-
-        final String premanagedVersion;
-
-        final String premanagedScope;
-
-        final Boolean premanagedOptional;
-
-        /**
-         * @since 1.1.0
-         */
-        final Collection<Exclusion> premanagedExclusions;
-
-        /**
-         * @since 1.1.0
-         */
-        final Map<String, String> premanagedProperties;
-
-        final int managedBits;
-
-        final Dependency managedDependency;
-
-        final boolean premanagedState;
-
-        @SuppressWarnings( "checkstyle:parameternumber" )
-        PremanagedDependency( String premanagedVersion, String premanagedScope, Boolean premanagedOptional,
-                              Collection<Exclusion> premanagedExclusions, Map<String, String> premanagedProperties,
-                              int managedBits, Dependency managedDependency, boolean premanagedState )
-        {
-            this.premanagedVersion = premanagedVersion;
-            this.premanagedScope = premanagedScope;
-            this.premanagedOptional = premanagedOptional;
-            this.premanagedExclusions =
-                premanagedExclusions != null
-                    ? Collections.unmodifiableCollection( new ArrayList<>( premanagedExclusions ) )
-                    : null;
-
-            this.premanagedProperties =
-                premanagedProperties != null
-                    ? Collections.unmodifiableMap( new HashMap<>( premanagedProperties ) )
-                    : null;
-
-            this.managedBits = managedBits;
-            this.managedDependency = managedDependency;
-            this.premanagedState = premanagedState;
-        }
-
-        static PremanagedDependency create( DependencyManager depManager, Dependency dependency,
-                                            boolean disableVersionManagement, boolean premanagedState )
-        {
-            DependencyManagement depMngt = depManager != null ? depManager.manageDependency( dependency ) : null;
-
-            int managedBits = 0;
-            String premanagedVersion = null;
-            String premanagedScope = null;
-            Boolean premanagedOptional = null;
-            Collection<Exclusion> premanagedExclusions = null;
-            Map<String, String> premanagedProperties = null;
-
-            if ( depMngt != null )
-            {
-                if ( depMngt.getVersion() != null && !disableVersionManagement )
-                {
-                    Artifact artifact = dependency.getArtifact();
-                    premanagedVersion = artifact.getVersion();
-                    dependency = dependency.setArtifact( artifact.setVersion( depMngt.getVersion() ) );
-                    managedBits |= DependencyNode.MANAGED_VERSION;
-                }
-                if ( depMngt.getProperties() != null )
-                {
-                    Artifact artifact = dependency.getArtifact();
-                    premanagedProperties = artifact.getProperties();
-                    dependency = dependency.setArtifact( artifact.setProperties( depMngt.getProperties() ) );
-                    managedBits |= DependencyNode.MANAGED_PROPERTIES;
-                }
-                if ( depMngt.getScope() != null )
-                {
-                    premanagedScope = dependency.getScope();
-                    dependency = dependency.setScope( depMngt.getScope() );
-                    managedBits |= DependencyNode.MANAGED_SCOPE;
-                }
-                if ( depMngt.getOptional() != null )
-                {
-                    premanagedOptional = dependency.isOptional();
-                    dependency = dependency.setOptional( depMngt.getOptional() );
-                    managedBits |= DependencyNode.MANAGED_OPTIONAL;
-                }
-                if ( depMngt.getExclusions() != null )
-                {
-                    premanagedExclusions = dependency.getExclusions();
-                    dependency = dependency.setExclusions( depMngt.getExclusions() );
-                    managedBits |= DependencyNode.MANAGED_EXCLUSIONS;
-                }
-            }
-            return new PremanagedDependency( premanagedVersion, premanagedScope, premanagedOptional,
-                                             premanagedExclusions, premanagedProperties, managedBits, dependency,
-                                             premanagedState );
-
-        }
-
-        public void applyTo( DefaultDependencyNode child )
-        {
-            child.setManagedBits( managedBits );
-            if ( premanagedState )
-            {
-                child.setData( DependencyManagerUtils.NODE_DATA_PREMANAGED_VERSION, premanagedVersion );
-                child.setData( DependencyManagerUtils.NODE_DATA_PREMANAGED_SCOPE, premanagedScope );
-                child.setData( DependencyManagerUtils.NODE_DATA_PREMANAGED_OPTIONAL, premanagedOptional );
-                child.setData( DependencyManagerUtils.NODE_DATA_PREMANAGED_EXCLUSIONS, premanagedExclusions );
-                child.setData( DependencyManagerUtils.NODE_DATA_PREMANAGED_PROPERTIES, premanagedProperties );
-            }
-        }
-
-    }
-
 }
diff --git a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/collect/DefaultDependencyCycle.java b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/collect/DefaultDependencyCycle.java
index dd1565c9..d417b70e 100644
--- a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/collect/DefaultDependencyCycle.java
+++ b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/collect/DefaultDependencyCycle.java
@@ -30,17 +30,17 @@ import org.eclipse.aether.graph.DependencyNode;
 import org.eclipse.aether.util.artifact.ArtifactIdUtils;
 
 /**
- * @see DefaultDependencyCollector
+ * Default implementation of {@link DependencyCycle}.
+ * Internal helper class for collector implementations.
  */
-final class DefaultDependencyCycle
+public final class DefaultDependencyCycle
     implements DependencyCycle
 {
-
     private final List<Dependency> dependencies;
 
     private final int cycleEntry;
 
-    DefaultDependencyCycle( List<DependencyNode> nodes, int cycleEntry, Dependency dependency )
+    public DefaultDependencyCycle( List<DependencyNode> nodes, int cycleEntry, Dependency dependency )
     {
         // skip root node unless it actually has a dependency or is considered the cycle entry (due to its label)
         int offset = ( cycleEntry > 0 && nodes.get( 0 ).getDependency() == null ) ? 1 : 0;
@@ -60,11 +60,13 @@ final class DefaultDependencyCycle
         this.cycleEntry = cycleEntry;
     }
 
+    @Override
     public List<Dependency> getPrecedingDependencies()
     {
         return dependencies.subList( 0, cycleEntry );
     }
 
+    @Override
     public List<Dependency> getCyclicDependencies()
     {
         return dependencies.subList( cycleEntry, dependencies.size() );
diff --git a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/collect/DefaultDependencyGraphTransformationContext.java b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/collect/DefaultDependencyGraphTransformationContext.java
index 41c01269..721eab5e 100644
--- a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/collect/DefaultDependencyGraphTransformationContext.java
+++ b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/collect/DefaultDependencyGraphTransformationContext.java
@@ -27,8 +27,10 @@ import org.eclipse.aether.RepositorySystemSession;
 import org.eclipse.aether.collection.DependencyGraphTransformationContext;
 
 /**
+ * Default implementation of {@link DependencyGraphTransformationContext}.
+ * Internal helper class for collector implementations.
  */
-class DefaultDependencyGraphTransformationContext
+public class DefaultDependencyGraphTransformationContext
     implements DependencyGraphTransformationContext
 {
 
@@ -36,22 +38,25 @@ class DefaultDependencyGraphTransformationContext
 
     private final Map<Object, Object> map;
 
-    DefaultDependencyGraphTransformationContext( RepositorySystemSession session )
+    public DefaultDependencyGraphTransformationContext( RepositorySystemSession session )
     {
         this.session = session;
         this.map = new HashMap<>();
     }
 
+    @Override
     public RepositorySystemSession getSession()
     {
         return session;
     }
 
+    @Override
     public Object get( Object key )
     {
         return map.get( requireNonNull( key, "key cannot be null" ) );
     }
 
+    @Override
     public Object put( Object key, Object value )
     {
         requireNonNull( key, "key cannot be null" );
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
deleted file mode 100644
index 408b0e16..00000000
--- a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/collect/DefaultDependencyResolutionSkipper.java
+++ /dev/null
@@ -1,290 +0,0 @@
-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/DefaultVersionFilterContext.java b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/collect/DefaultVersionFilterContext.java
index bfea0626..97c16c63 100644
--- a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/collect/DefaultVersionFilterContext.java
+++ b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/collect/DefaultVersionFilterContext.java
@@ -34,9 +34,10 @@ import org.eclipse.aether.version.Version;
 import org.eclipse.aether.version.VersionConstraint;
 
 /**
- * @see DefaultDependencyCollector
+ * Default implementation of {@link VersionFilter.VersionFilterContext}.
+ * Internal helper class for collector implementations.
  */
-final class DefaultVersionFilterContext
+public final class DefaultVersionFilterContext
     implements VersionFilter.VersionFilterContext
 {
     private final RepositorySystemSession session;
@@ -47,7 +48,7 @@ final class DefaultVersionFilterContext
 
     private List<Version> versions;
 
-    DefaultVersionFilterContext( RepositorySystemSession session )
+    public DefaultVersionFilterContext( RepositorySystemSession session )
     {
         this.session = session;
     }
@@ -64,36 +65,43 @@ final class DefaultVersionFilterContext
         return new ArrayList<>( versions );
     }
 
+    @Override
     public RepositorySystemSession getSession()
     {
         return session;
     }
 
+    @Override
     public Dependency getDependency()
     {
         return dependency;
     }
 
+    @Override
     public VersionConstraint getVersionConstraint()
     {
         return result.getVersionConstraint();
     }
 
+    @Override
     public int getCount()
     {
         return versions.size();
     }
 
+    @Override
     public ArtifactRepository getRepository( Version version )
     {
         return result.getRepository( version );
     }
 
+    @Override
     public List<RemoteRepository> getRepositories()
     {
         return Collections.unmodifiableList( result.getRequest().getRepositories() );
     }
 
+    @Override
     public Iterator<Version> iterator()
     {
         return versions.iterator();
diff --git a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/collect/DependencyCollectorDelegate.java b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/collect/DependencyCollectorDelegate.java
new file mode 100644
index 00000000..c7c2c985
--- /dev/null
+++ b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/collect/DependencyCollectorDelegate.java
@@ -0,0 +1,96 @@
+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.impl.ArtifactDescriptorReader;
+import org.eclipse.aether.impl.DependencyCollector;
+import org.eclipse.aether.impl.RemoteRepositoryManager;
+import org.eclipse.aether.impl.VersionRangeResolver;
+import org.eclipse.aether.spi.locator.ServiceLocator;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import static java.util.Objects.requireNonNull;
+
+/**
+ * Helper class for delegate implementations, they MUST subclass this class.
+ *
+ * @since 1.8.0
+ */
+public abstract class DependencyCollectorDelegate implements DependencyCollector
+{
+    protected static final String CONFIG_PROP_MAX_EXCEPTIONS = "aether.dependencyCollector.maxExceptions";
+
+    protected static final int CONFIG_PROP_MAX_EXCEPTIONS_DEFAULT = 50;
+
+    protected static final String CONFIG_PROP_MAX_CYCLES = "aether.dependencyCollector.maxCycles";
+
+    protected static final int CONFIG_PROP_MAX_CYCLES_DEFAULT = 10;
+
+    protected final Logger logger = LoggerFactory.getLogger( getClass() );
+
+    protected RemoteRepositoryManager remoteRepositoryManager;
+
+    protected ArtifactDescriptorReader descriptorReader;
+
+    protected VersionRangeResolver versionRangeResolver;
+
+    public DependencyCollectorDelegate()
+    {
+        // enables default constructor
+    }
+
+    protected DependencyCollectorDelegate( RemoteRepositoryManager remoteRepositoryManager,
+                           ArtifactDescriptorReader artifactDescriptorReader,
+                           VersionRangeResolver versionRangeResolver )
+    {
+        setRemoteRepositoryManager( remoteRepositoryManager );
+        setArtifactDescriptorReader( artifactDescriptorReader );
+        setVersionRangeResolver( versionRangeResolver );
+    }
+
+    public void initService( ServiceLocator locator )
+    {
+        setRemoteRepositoryManager( locator.getService( RemoteRepositoryManager.class ) );
+        setArtifactDescriptorReader( locator.getService( ArtifactDescriptorReader.class ) );
+        setVersionRangeResolver( locator.getService( VersionRangeResolver.class ) );
+    }
+
+    public DependencyCollector setRemoteRepositoryManager( RemoteRepositoryManager remoteRepositoryManager )
+    {
+        this.remoteRepositoryManager =
+                requireNonNull( remoteRepositoryManager, "remote repository manager cannot be null" );
+        return this;
+    }
+
+    public DependencyCollector setArtifactDescriptorReader( ArtifactDescriptorReader artifactDescriptorReader )
+    {
+        descriptorReader = requireNonNull( artifactDescriptorReader, "artifact descriptor reader cannot be null" );
+        return this;
+    }
+
+    public DependencyCollector setVersionRangeResolver( VersionRangeResolver versionRangeResolver )
+    {
+        this.versionRangeResolver =
+                requireNonNull( versionRangeResolver, "version range resolver cannot be null" );
+        return this;
+    }
+
+}
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
deleted file mode 100644
index acd6636f..00000000
--- a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/collect/NeverDependencyResolutionSkipper.java
+++ /dev/null
@@ -1,51 +0,0 @@
-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/main/java/org/eclipse/aether/internal/impl/collect/ObjectPool.java b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/collect/ObjectPool.java
index c4117eb0..0d5f3799 100644
--- a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/collect/ObjectPool.java
+++ b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/collect/ObjectPool.java
@@ -27,6 +27,7 @@ import java.util.WeakHashMap;
 /**
  * Pool of immutable object instances, used to avoid excessive memory consumption of (dirty) dependency graph which
  * tends to have many duplicate artifacts/dependencies.
+ * Internal helper class for collector implementations.
  */
 class ObjectPool<T>
 {
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/bf/BfDependencyCollector.java
similarity index 90%
copy from maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/collect/DefaultDependencyCollector.java
copy to maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/collect/bf/BfDependencyCollector.java
index a895bb9b..4beca799 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/bf/BfDependencyCollector.java
@@ -1,4 +1,4 @@
-package org.eclipse.aether.internal.impl.collect;
+package org.eclipse.aether.internal.impl.collect.bf;
 
 /*
  * Licensed to the Apache Software Foundation (ASF) under one
@@ -19,6 +19,10 @@ package org.eclipse.aether.internal.impl.collect;
  * under the License.
  */
 
+import javax.inject.Inject;
+import javax.inject.Named;
+import javax.inject.Singleton;
+
 import java.util.ArrayDeque;
 import java.util.ArrayList;
 import java.util.Collection;
@@ -30,13 +34,6 @@ import java.util.List;
 import java.util.Map;
 import java.util.Queue;
 
-import static java.util.Objects.requireNonNull;
-import static org.eclipse.aether.internal.impl.collect.DefaultDependencyCycle.find;
-
-import javax.inject.Inject;
-import javax.inject.Named;
-import javax.inject.Singleton;
-
 import org.eclipse.aether.DefaultRepositorySystemSession;
 import org.eclipse.aether.RepositoryException;
 import org.eclipse.aether.RepositorySystemSession;
@@ -57,10 +54,15 @@ import org.eclipse.aether.graph.Dependency;
 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.internal.impl.collect.CachingArtifactTypeRegistry;
+import org.eclipse.aether.internal.impl.collect.DataPool;
+import org.eclipse.aether.internal.impl.collect.DefaultDependencyCollectionContext;
+import org.eclipse.aether.internal.impl.collect.DefaultDependencyCycle;
+import org.eclipse.aether.internal.impl.collect.DefaultDependencyGraphTransformationContext;
+import org.eclipse.aether.internal.impl.collect.DefaultVersionFilterContext;
+import org.eclipse.aether.internal.impl.collect.DependencyCollectorDelegate;
 import org.eclipse.aether.repository.ArtifactRepository;
 import org.eclipse.aether.repository.RemoteRepository;
 import org.eclipse.aether.resolution.ArtifactDescriptorException;
@@ -70,93 +72,52 @@ import org.eclipse.aether.resolution.VersionRangeRequest;
 import org.eclipse.aether.resolution.VersionRangeResolutionException;
 import org.eclipse.aether.resolution.VersionRangeResult;
 import org.eclipse.aether.spi.locator.Service;
-import org.eclipse.aether.spi.locator.ServiceLocator;
 import org.eclipse.aether.util.ConfigUtils;
 import org.eclipse.aether.util.graph.manager.DependencyManagerUtils;
 import org.eclipse.aether.util.graph.transformer.TransformationContextKeys;
 import org.eclipse.aether.version.Version;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
+
+import static java.util.Objects.requireNonNull;
+import static org.eclipse.aether.internal.impl.collect.DefaultDependencyCycle.find;
 
 /**
+ * Breadth-first {@link org.eclipse.aether.impl.DependencyCollector}
+ *
+ * @since 1.8.0
  */
 @Singleton
-@Named
-public class DefaultDependencyCollector
-    implements DependencyCollector, Service
+@Named( BfDependencyCollector.NAME )
+public class BfDependencyCollector
+    extends DependencyCollectorDelegate implements Service
 {
+    public static final String NAME = "bf";
 
     /**
-     * The key in the repository session's {@link org.eclipse.aether.RepositorySystemSession#getConfigProperties()
+     * The key in the repository session's {@link 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";
+    public static final String CONFIG_PROP_SKIPPER = "aether.dependencyCollector.bf.skipper";
 
     /**
-     * The default value for {@link #CONFIG_PROP_USE_SKIP}, {@code true}.
+     * The default value for {@link #CONFIG_PROP_SKIPPER}, {@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;
-
-    private static final String CONFIG_PROP_MAX_CYCLES = "aether.dependencyCollector.maxCycles";
-
-    private static final int CONFIG_PROP_MAX_CYCLES_DEFAULT = 10;
+    public static final boolean CONFIG_PROP_SKIPPER_DEFAULT = true;
 
-    private static final Logger LOGGER = LoggerFactory.getLogger( DefaultDependencyCollector.class );
-
-    private RemoteRepositoryManager remoteRepositoryManager;
-
-    private ArtifactDescriptorReader descriptorReader;
-
-    private VersionRangeResolver versionRangeResolver;
-
-    public DefaultDependencyCollector()
+    public BfDependencyCollector()
     {
         // enables default constructor
     }
 
     @Inject
-    DefaultDependencyCollector( RemoteRepositoryManager remoteRepositoryManager,
-                                ArtifactDescriptorReader artifactDescriptorReader,
-                                VersionRangeResolver versionRangeResolver )
-    {
-        setRemoteRepositoryManager( remoteRepositoryManager );
-        setArtifactDescriptorReader( artifactDescriptorReader );
-        setVersionRangeResolver( versionRangeResolver );
-    }
-
-    public void initService( ServiceLocator locator )
-    {
-        setRemoteRepositoryManager( locator.getService( RemoteRepositoryManager.class ) );
-        setArtifactDescriptorReader( locator.getService( ArtifactDescriptorReader.class ) );
-        setVersionRangeResolver( locator.getService( VersionRangeResolver.class ) );
-    }
-
-    public DefaultDependencyCollector setRemoteRepositoryManager( RemoteRepositoryManager remoteRepositoryManager )
+    BfDependencyCollector( RemoteRepositoryManager remoteRepositoryManager,
+                           ArtifactDescriptorReader artifactDescriptorReader,
+                           VersionRangeResolver versionRangeResolver )
     {
-        this.remoteRepositoryManager =
-                requireNonNull( remoteRepositoryManager, "remote repository provider cannot be null" );
-        return this;
-    }
-
-    public DefaultDependencyCollector setArtifactDescriptorReader( ArtifactDescriptorReader artifactDescriptorReader )
-    {
-        descriptorReader = requireNonNull( artifactDescriptorReader, "artifact descriptor reader cannot be null" );
-        return this;
-    }
-
-    public DefaultDependencyCollector setVersionRangeResolver( VersionRangeResolver versionRangeResolver )
-    {
-        this.versionRangeResolver =
-                requireNonNull( versionRangeResolver, "version range resolver cannot be null" );
-        return this;
+        super( remoteRepositoryManager, artifactDescriptorReader, versionRangeResolver );
     }
 
     @SuppressWarnings( "checkstyle:methodlength" )
@@ -168,11 +129,11 @@ public class DefaultDependencyCollector
         session = optimizeSession( session );
 
         boolean useSkip = ConfigUtils.getBoolean(
-                session, CONFIG_PROP_USE_SKIP_DEFAULT, CONFIG_PROP_USE_SKIP
+                session, CONFIG_PROP_SKIPPER_DEFAULT, CONFIG_PROP_SKIPPER
         );
         if ( useSkip )
         {
-            LOGGER.debug( "Collector skip mode enabled" );
+            logger.debug( "Collector skip mode enabled" );
         }
 
         RequestTrace trace = RequestTrace.newChild( request.getTrace(), request );
@@ -279,8 +240,8 @@ public class DefaultDependencyCollector
 
             Args args =
                     new Args( session, trace, pool, context, versionContext, request,
-                            useSkip ? new DefaultDependencyResolutionSkipper()
-                                    : NeverDependencyResolutionSkipper.INSTANCE );
+                            useSkip ? DependencyResolutionSkipper.defaultSkipper()
+                                    : DependencyResolutionSkipper.neverSkipper() );
             Results results = new Results( result, session );
 
             DependencySelector rootDepSelector =
@@ -328,9 +289,12 @@ public class DefaultDependencyCollector
         }
 
         long time3 = System.nanoTime();
-        stats.put( "DefaultDependencyCollector.collectTime", time2 - time1 );
-        stats.put( "DefaultDependencyCollector.transformTime", time3 - time2 );
-        LOGGER.debug( "Dependency collection stats {}", stats );
+        if ( logger.isDebugEnabled() )
+        {
+            stats.put( "BfDependencyCollector.collectTime", time2 - time1 );
+            stats.put( "BfDependencyCollector.transformTime", time3 - time2 );
+            logger.debug( "Dependency collection stats {}", stats );
+        }
 
         if ( errorPath != null )
         {
@@ -694,15 +658,13 @@ public class DefaultDependencyCollector
             catch ( RepositoryException e )
             {
                 throw new VersionRangeResolutionException( rangeResult,
-                                                           "Failed to filter versions for " + dependency.getArtifact()
-                                                               + ": " + e.getMessage(), e );
+                        "Failed to filter versions for " + dependency.getArtifact(), e );
             }
             versions = verContext.get();
             if ( versions.isEmpty() )
             {
                 throw new VersionRangeResolutionException( rangeResult,
-                                                           "No acceptable versions for " + dependency.getArtifact()
-                                                               + ": " + rangeResult.getVersions() );
+                        "No acceptable versions for " + dependency.getArtifact() + ": " + rangeResult.getVersions() );
             }
         }
         else
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/bf/DependencyProcessingContext.java
similarity index 95%
rename from maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/collect/DependencyProcessingContext.java
rename to maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/collect/bf/DependencyProcessingContext.java
index 9820ad07..ee2e7720 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/bf/DependencyProcessingContext.java
@@ -1,4 +1,4 @@
-package org.eclipse.aether.internal.impl.collect;
+package org.eclipse.aether.internal.impl.collect.bf;
 
 /*
  * Licensed to the Apache Software Foundation (ASF) under one
@@ -29,6 +29,11 @@ import org.eclipse.aether.graph.Dependency;
 import org.eclipse.aether.graph.DependencyNode;
 import org.eclipse.aether.repository.RemoteRepository;
 
+/**
+ * Internal helper for {@link BfDependencyCollector}.
+ *
+ * @since 1.8.0
+ */
 final class DependencyProcessingContext
 {
     final DependencySelector depSelector;
diff --git a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/collect/bf/DependencyResolutionSkipper.java b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/collect/bf/DependencyResolutionSkipper.java
new file mode 100644
index 00000000..b82a8799
--- /dev/null
+++ b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/collect/bf/DependencyResolutionSkipper.java
@@ -0,0 +1,359 @@
+package org.eclipse.aether.internal.impl.collect.bf;
+
+/*
+ * 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.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;
+
+/**
+ * A skipper that determines whether to skip resolving given node during the dependency collection.
+ * Internal helper for {@link BfDependencyCollector}.
+ *
+ * @since 1.8.0
+ */
+abstract class 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.
+     */
+    abstract boolean skipResolution( DependencyNode node, List<DependencyNode> parents );
+
+    /**
+     * Cache the resolution result when a node is resolved by {@link BfDependencyCollector) after resolution.
+     *
+     * @param node    Current node
+     * @param parents All parent nodes of current node
+     */
+    abstract void cache( DependencyNode node, List<DependencyNode> parents );
+
+    /**
+     * Print the skip/resolve status report for all nodes.
+     */
+    abstract void report();
+
+    /**
+     * Returns new instance of "default" skipper.
+     *
+     * Note: type is specialized for testing purposes.
+     */
+    public static DefaultDependencyResolutionSkipper defaultSkipper()
+    {
+        return new DefaultDependencyResolutionSkipper();
+    }
+
+    /**
+     * Returns instance of "never" skipper.
+     */
+    public static DependencyResolutionSkipper neverSkipper()
+    {
+        return NeverDependencyResolutionSkipper.INSTANCE;
+    }
+
+    /**
+     * NEVER implementation.
+     */
+    private static final class NeverDependencyResolutionSkipper extends DependencyResolutionSkipper
+    {
+        private 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()
+        {
+        }
+    }
+
+    /**
+     * Visible for testing.
+     */
+    static final class DefaultDependencyResolutionSkipper extends DependencyResolutionSkipper
+    {
+        private static final Logger LOGGER = LoggerFactory.getLogger( DependencyResolutionSkipper.class );
+
+        private final Map<DependencyNode, DependencyResolutionResult> results = new LinkedHashMap<>( 256 );
+        private final CacheManager cacheManager = new CacheManager();
+        private final CoordinateManager coordinateManager = new CoordinateManager();
+
+        @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;
+        }
+
+        private 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() );
+            }
+
+        }
+
+
+        private 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 );
+                    return getCoordinate( sameLevelNode ).sequence < leftmost.sequence;
+                }
+
+                return false;
+            }
+        }
+
+        private 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
+                        + '}';
+            }
+        }
+    }
+
+    /**
+     * Visible for testing.
+     */
+    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;
+        }
+    }
+}
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/df/DfDependencyCollector.java
similarity index 73%
copy from maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/collect/DefaultDependencyCollector.java
copy to maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/collect/df/DfDependencyCollector.java
index a895bb9b..a310af3b 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/df/DfDependencyCollector.java
@@ -1,4 +1,4 @@
-package org.eclipse.aether.internal.impl.collect;
+package org.eclipse.aether.internal.impl.collect.df;
 
 /*
  * Licensed to the Apache Software Foundation (ASF) under one
@@ -19,7 +19,6 @@ package org.eclipse.aether.internal.impl.collect;
  * under the License.
  */
 
-import java.util.ArrayDeque;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
@@ -28,10 +27,7 @@ import java.util.HashSet;
 import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
-import java.util.Queue;
-
 import static java.util.Objects.requireNonNull;
-import static org.eclipse.aether.internal.impl.collect.DefaultDependencyCycle.find;
 
 import javax.inject.Inject;
 import javax.inject.Named;
@@ -57,10 +53,15 @@ import org.eclipse.aether.graph.Dependency;
 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.internal.impl.collect.CachingArtifactTypeRegistry;
+import org.eclipse.aether.internal.impl.collect.DataPool;
+import org.eclipse.aether.internal.impl.collect.DefaultDependencyCollectionContext;
+import org.eclipse.aether.internal.impl.collect.DefaultDependencyCycle;
+import org.eclipse.aether.internal.impl.collect.DefaultDependencyGraphTransformationContext;
+import org.eclipse.aether.internal.impl.collect.DefaultVersionFilterContext;
+import org.eclipse.aether.internal.impl.collect.DependencyCollectorDelegate;
 import org.eclipse.aether.repository.ArtifactRepository;
 import org.eclipse.aether.repository.RemoteRepository;
 import org.eclipse.aether.resolution.ArtifactDescriptorException;
@@ -70,93 +71,35 @@ import org.eclipse.aether.resolution.VersionRangeRequest;
 import org.eclipse.aether.resolution.VersionRangeResolutionException;
 import org.eclipse.aether.resolution.VersionRangeResult;
 import org.eclipse.aether.spi.locator.Service;
-import org.eclipse.aether.spi.locator.ServiceLocator;
 import org.eclipse.aether.util.ConfigUtils;
 import org.eclipse.aether.util.graph.manager.DependencyManagerUtils;
 import org.eclipse.aether.util.graph.transformer.TransformationContextKeys;
 import org.eclipse.aether.version.Version;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 /**
+ * Depth-first {@link org.eclipse.aether.impl.DependencyCollector} (the "original" default). Originally
+ * this class was located a package higher (as "default" implementation).
+ *
+ * @since 1.8.0
  */
 @Singleton
-@Named
-public class DefaultDependencyCollector
-    implements DependencyCollector, Service
+@Named( DfDependencyCollector.NAME )
+public class DfDependencyCollector
+        extends DependencyCollectorDelegate implements Service
 {
+    public static final String NAME = "df";
 
-    /**
-     * 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;
-
-    private static final String CONFIG_PROP_MAX_CYCLES = "aether.dependencyCollector.maxCycles";
-
-    private static final int CONFIG_PROP_MAX_CYCLES_DEFAULT = 10;
-
-    private static final Logger LOGGER = LoggerFactory.getLogger( DefaultDependencyCollector.class );
-
-    private RemoteRepositoryManager remoteRepositoryManager;
-
-    private ArtifactDescriptorReader descriptorReader;
-
-    private VersionRangeResolver versionRangeResolver;
-
-    public DefaultDependencyCollector()
+    public DfDependencyCollector()
     {
         // enables default constructor
     }
 
     @Inject
-    DefaultDependencyCollector( RemoteRepositoryManager remoteRepositoryManager,
-                                ArtifactDescriptorReader artifactDescriptorReader,
-                                VersionRangeResolver versionRangeResolver )
+    DfDependencyCollector( RemoteRepositoryManager remoteRepositoryManager,
+                           ArtifactDescriptorReader artifactDescriptorReader,
+                           VersionRangeResolver versionRangeResolver )
     {
-        setRemoteRepositoryManager( remoteRepositoryManager );
-        setArtifactDescriptorReader( artifactDescriptorReader );
-        setVersionRangeResolver( versionRangeResolver );
-    }
-
-    public void initService( ServiceLocator locator )
-    {
-        setRemoteRepositoryManager( locator.getService( RemoteRepositoryManager.class ) );
-        setArtifactDescriptorReader( locator.getService( ArtifactDescriptorReader.class ) );
-        setVersionRangeResolver( locator.getService( VersionRangeResolver.class ) );
-    }
-
-    public DefaultDependencyCollector setRemoteRepositoryManager( RemoteRepositoryManager remoteRepositoryManager )
-    {
-        this.remoteRepositoryManager =
-                requireNonNull( remoteRepositoryManager, "remote repository provider cannot be null" );
-        return this;
-    }
-
-    public DefaultDependencyCollector setArtifactDescriptorReader( ArtifactDescriptorReader artifactDescriptorReader )
-    {
-        descriptorReader = requireNonNull( artifactDescriptorReader, "artifact descriptor reader cannot be null" );
-        return this;
-    }
-
-    public DefaultDependencyCollector setVersionRangeResolver( VersionRangeResolver versionRangeResolver )
-    {
-        this.versionRangeResolver =
-                requireNonNull( versionRangeResolver, "version range resolver cannot be null" );
-        return this;
+        super( remoteRepositoryManager, artifactDescriptorReader, versionRangeResolver );
     }
 
     @SuppressWarnings( "checkstyle:methodlength" )
@@ -167,14 +110,6 @@ 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 );
@@ -272,40 +207,23 @@ public class DefaultDependencyCollector
         {
             DataPool pool = new DataPool( session );
 
+            NodeStack nodes = new NodeStack();
+            nodes.push( node );
+
             DefaultDependencyCollectionContext context =
                 new DefaultDependencyCollectionContext( session, request.getRootArtifact(), root, managedDependencies );
 
             DefaultVersionFilterContext versionContext = new DefaultVersionFilterContext( session );
 
-            Args args =
-                    new Args( session, trace, pool, context, versionContext, request,
-                            useSkip ? new DefaultDependencyResolutionSkipper()
-                                    : NeverDependencyResolutionSkipper.INSTANCE );
+            Args args = new Args( session, trace, pool, nodes, context, versionContext, request );
             Results results = new Results( result, session );
 
-            DependencySelector rootDepSelector =
-                    depSelector != null ? depSelector.deriveChildSelector( context ) : null;
-            DependencyManager rootDepManager = depManager != null ? depManager.deriveChildManager( context ) : null;
-            DependencyTraverser rootDepTraverser =
-                    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, parents,
-                                dependency ) );
-            }
+            process( args, results, dependencies, repositories,
+                     depSelector != null ? depSelector.deriveChildSelector( context ) : null,
+                     depManager != null ? depManager.deriveChildManager( context ) : null,
+                     depTraverser != null ? depTraverser.deriveChildTraverser( context ) : null,
+                     verFilter != null ? verFilter.deriveChildFilter( context ) : null );
 
-            while ( !args.dependencyProcessingQueue.isEmpty() )
-            {
-                processDependency( args, results, args.dependencyProcessingQueue.remove(), Collections.emptyList(),
-                        false );
-            }
-
-            args.skipper.report();
             errorPath = results.errorPath;
         }
 
@@ -328,9 +246,12 @@ public class DefaultDependencyCollector
         }
 
         long time3 = System.nanoTime();
-        stats.put( "DefaultDependencyCollector.collectTime", time2 - time1 );
-        stats.put( "DefaultDependencyCollector.transformTime", time3 - time2 );
-        LOGGER.debug( "Dependency collection stats {}", stats );
+        if ( logger.isDebugEnabled() )
+        {
+            stats.put( "BfDependencyCollector.collectTime", time2 - time1 );
+            stats.put( "BfDependencyCollector.transformTime", time3 - time2 );
+            logger.debug( "Dependency collection stats {}", stats );
+        }
 
         if ( errorPath != null )
         {
@@ -389,73 +310,90 @@ public class DefaultDependencyCollector
     }
 
     @SuppressWarnings( "checkstyle:parameternumber" )
-    private void processDependency( Args args, Results results, DependencyProcessingContext context,
+    private void process( final Args args, Results results, List<Dependency> dependencies,
+                          List<RemoteRepository> repositories, DependencySelector depSelector,
+                          DependencyManager depManager, DependencyTraverser depTraverser, VersionFilter verFilter )
+    {
+        for ( Dependency dependency : dependencies )
+        {
+            processDependency( args, results, repositories, depSelector, depManager, depTraverser, verFilter,
+                               dependency );
+        }
+    }
+
+    @SuppressWarnings( "checkstyle:parameternumber" )
+    private void processDependency( Args args, Results results, List<RemoteRepository> repositories,
+                                    DependencySelector depSelector, DependencyManager depManager,
+                                    DependencyTraverser depTraverser, VersionFilter verFilter, Dependency dependency )
+    {
+
+        List<Artifact> relocations = Collections.emptyList();
+        processDependency( args, results, repositories, depSelector, depManager, depTraverser, verFilter, dependency,
+                           relocations, false );
+    }
+
+    @SuppressWarnings( "checkstyle:parameternumber" )
+    private void processDependency( Args args, Results results, List<RemoteRepository> repositories,
+                                    DependencySelector depSelector, DependencyManager depManager,
+                                    DependencyTraverser depTraverser, VersionFilter verFilter, Dependency dependency,
                                     List<Artifact> relocations, boolean disableVersionManagement )
     {
 
-        if ( context.depSelector != null && !context.depSelector.selectDependency( context.dependency ) )
+        if ( depSelector != null && !depSelector.selectDependency( dependency ) )
         {
             return;
         }
 
         PremanagedDependency preManaged =
-                PremanagedDependency.create( context.depManager, context.dependency, disableVersionManagement,
-                        args.premanagedState );
-        Dependency dependency = preManaged.managedDependency;
+            PremanagedDependency.create( depManager, dependency, disableVersionManagement, args.premanagedState );
+        dependency = preManaged.managedDependency;
 
         boolean noDescriptor = isLackingDescriptor( dependency.getArtifact() );
 
-        boolean traverse =
-                !noDescriptor && ( context.depTraverser == null || context.depTraverser.traverseDependency(
-                        dependency ) );
+        boolean traverse = !noDescriptor && ( depTraverser == null || depTraverser.traverseDependency( dependency ) );
 
         List<? extends Version> versions;
         VersionRangeResult rangeResult;
         try
         {
-            VersionRangeRequest rangeRequest = createVersionRangeRequest( args, context.repositories, dependency );
+            VersionRangeRequest rangeRequest = createVersionRangeRequest( args, repositories, dependency );
 
             rangeResult = cachedResolveRangeResult( rangeRequest, args.pool, args.session );
 
-            versions = filterVersions( dependency, rangeResult, context.verFilter, args.versionContext );
+            versions = filterVersions( dependency, rangeResult, verFilter, args.versionContext );
         }
         catch ( VersionRangeResolutionException e )
         {
-            results.addException( dependency, e, context.parents );
+            results.addException( dependency, e, args.nodes );
             return;
         }
 
-        //Resolve newer version first to maximize benefits of skipper
-        Collections.reverse( versions );
         for ( Version version : versions )
         {
             Artifact originalArtifact = dependency.getArtifact().setVersion( version.toString() );
             Dependency d = dependency.setArtifact( originalArtifact );
 
-            ArtifactDescriptorRequest descriptorRequest =
-                    createArtifactDescriptorRequest( args, context.repositories, d );
+            ArtifactDescriptorRequest descriptorRequest = createArtifactDescriptorRequest( args, repositories, d );
 
             final ArtifactDescriptorResult descriptorResult =
-                    noDescriptor
-                            ? new ArtifactDescriptorResult( descriptorRequest )
-                            : resolveCachedArtifactDescriptor( args.pool, descriptorRequest, args.session,
-                                    context.withDependency( d ), results );
-
+                getArtifactDescriptorResult( args, results, noDescriptor, d, descriptorRequest );
             if ( descriptorResult != null )
             {
                 d = d.setArtifact( descriptorResult.getArtifact() );
 
-                int cycleEntry = find( context.parents, d.getArtifact() );
+                DependencyNode node = args.nodes.top();
+
+                int cycleEntry = DefaultDependencyCycle.find( args.nodes.nodes, d.getArtifact() );
                 if ( cycleEntry >= 0 )
                 {
-                    results.addCycle( context.parents, cycleEntry, d );
-                    DependencyNode cycleNode = context.parents.get( cycleEntry );
+                    results.addCycle( args.nodes, cycleEntry, d );
+                    DependencyNode cycleNode = args.nodes.get( cycleEntry );
                     if ( cycleNode.getDependency() != null )
                     {
                         DefaultDependencyNode child =
-                                createDependencyNode( relocations, preManaged, rangeResult, version, d,
-                                        descriptorResult, cycleNode );
-                        context.getParent().getChildren().add( child );
+                            createDependencyNode( relocations, preManaged, rangeResult, version, d, descriptorResult,
+                                                  cycleNode );
+                        node.getChildren().add( child );
                         continue;
                     }
                 }
@@ -466,8 +404,8 @@ public class DefaultDependencyCollector
                         originalArtifact.getGroupId().equals( d.getArtifact().getGroupId() )
                             && originalArtifact.getArtifactId().equals( d.getArtifact().getArtifactId() );
 
-                    processDependency( args, results, context.withDependency( d ), descriptorResult.getRelocations(),
-                            disableVersionManagementSubsequently );
+                    processDependency( args, results, repositories, depSelector, depManager, depTraverser, verFilter, d,
+                                       descriptorResult.getRelocations(), disableVersionManagementSubsequently );
                     return;
                 }
                 else
@@ -475,77 +413,69 @@ public class DefaultDependencyCollector
                     d = args.pool.intern( d.setArtifact( args.pool.intern( d.getArtifact() ) ) );
 
                     List<RemoteRepository> repos =
-                        getRemoteRepositories( rangeResult.getRepository( version ), context.repositories );
+                        getRemoteRepositories( rangeResult.getRepository( version ), repositories );
 
                     DefaultDependencyNode child =
                         createDependencyNode( relocations, preManaged, rangeResult, version, d,
                                               descriptorResult.getAliases(), repos, args.request.getRequestContext() );
 
-                    context.getParent().getChildren().add( child );
+                    node.getChildren().add( child );
 
                     boolean recurse = traverse && !descriptorResult.getDependencies().isEmpty();
                     if ( recurse )
                     {
-                        doRecurse( args, context.withDependency( d ), descriptorResult, child );
+                        doRecurse( args, results, repositories, depSelector, depManager, depTraverser, verFilter, d,
+                                   descriptorResult, child );
                     }
                 }
             }
             else
             {
+                DependencyNode node = args.nodes.top();
                 List<RemoteRepository> repos =
-                    getRemoteRepositories( rangeResult.getRepository( version ), context.repositories );
+                    getRemoteRepositories( rangeResult.getRepository( version ), repositories );
                 DefaultDependencyNode child =
                     createDependencyNode( relocations, preManaged, rangeResult, version, d, null, repos,
                                           args.request.getRequestContext() );
-                context.getParent().getChildren().add( child );
+                node.getChildren().add( child );
             }
         }
     }
 
     @SuppressWarnings( "checkstyle:parameternumber" )
-    private void doRecurse( Args args, DependencyProcessingContext parentContext,
+    private void doRecurse( Args args, Results results, List<RemoteRepository> repositories,
+                            DependencySelector depSelector, DependencyManager depManager,
+                            DependencyTraverser depTraverser, VersionFilter verFilter, Dependency d,
                             ArtifactDescriptorResult descriptorResult, DefaultDependencyNode child )
     {
         DefaultDependencyCollectionContext context = args.collectionContext;
-        context.set( parentContext.dependency, descriptorResult.getManagedDependencies() );
+        context.set( d, descriptorResult.getManagedDependencies() );
 
-        DependencySelector childSelector =
-                parentContext.depSelector != null ? parentContext.depSelector.deriveChildSelector( context ) : null;
-        DependencyManager childManager =
-                parentContext.depManager != null ? parentContext.depManager.deriveChildManager( context ) : null;
-        DependencyTraverser childTraverser =
-                parentContext.depTraverser != null ? parentContext.depTraverser.deriveChildTraverser( context ) : null;
-        VersionFilter childFilter =
-                parentContext.verFilter != null ? parentContext.verFilter.deriveChildFilter( context ) : null;
+        DependencySelector childSelector = depSelector != null ? depSelector.deriveChildSelector( context ) : null;
+        DependencyManager childManager = depManager != null ? depManager.deriveChildManager( context ) : null;
+        DependencyTraverser childTraverser = depTraverser != null ? depTraverser.deriveChildTraverser( context ) : null;
+        VersionFilter childFilter = verFilter != null ? verFilter.deriveChildFilter( context ) : null;
 
         final List<RemoteRepository> childRepos =
-                args.ignoreRepos
-                        ? parentContext.repositories
-                        : remoteRepositoryManager.aggregateRepositories( args.session, parentContext.repositories,
-                        descriptorResult.getRepositories(), true );
+            args.ignoreRepos
+                ? repositories
+                : remoteRepositoryManager.aggregateRepositories( args.session, repositories,
+                                                                 descriptorResult.getRepositories(), true );
 
         Object key =
-                args.pool.toKey( parentContext.dependency.getArtifact(), childRepos, childSelector, childManager,
-                        childTraverser, childFilter );
+            args.pool.toKey( d.getArtifact(), childRepos, childSelector, childManager, childTraverser, childFilter );
 
         List<DependencyNode> children = args.pool.getChildren( key );
         if ( children == null )
         {
-            boolean skipResolution = args.skipper.skipResolution( child, parentContext.parents );
-            if ( !skipResolution )
-            {
-                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 );
-            }
+            args.pool.putChildren( key, child.getChildren() );
+
+            args.nodes.push( child );
+
+            process( args, results, descriptorResult.getDependencies(), childRepos, childSelector, childManager,
+                     childTraverser, childFilter );
+
+            args.nodes.pop();
         }
         else
         {
@@ -553,11 +483,20 @@ public class DefaultDependencyCollector
         }
     }
 
+    private ArtifactDescriptorResult getArtifactDescriptorResult( Args args, Results results, boolean noDescriptor,
+                                                                  Dependency d,
+                                                                  ArtifactDescriptorRequest descriptorRequest )
+    {
+        return noDescriptor
+                   ? new ArtifactDescriptorResult( descriptorRequest )
+                   : resolveCachedArtifactDescriptor( args.pool, descriptorRequest, args.session, d, results, args );
+
+    }
+
     private ArtifactDescriptorResult resolveCachedArtifactDescriptor( DataPool pool,
                                                                       ArtifactDescriptorRequest descriptorRequest,
-                                                                      RepositorySystemSession session,
-                                                                      DependencyProcessingContext context,
-                                                                      Results results )
+                                                                      RepositorySystemSession session, Dependency d,
+                                                                      Results results, Args args )
     {
         Object key = pool.toKey( descriptorRequest );
         ArtifactDescriptorResult descriptorResult = pool.getDescriptor( key, descriptorRequest );
@@ -570,7 +509,7 @@ public class DefaultDependencyCollector
             }
             catch ( ArtifactDescriptorException e )
             {
-                results.addException( context.dependency, e, context.parents );
+                results.addException( d, e, args.nodes );
                 pool.putDescriptor( key, e );
                 return null;
             }
@@ -694,15 +633,13 @@ public class DefaultDependencyCollector
             catch ( RepositoryException e )
             {
                 throw new VersionRangeResolutionException( rangeResult,
-                                                           "Failed to filter versions for " + dependency.getArtifact()
-                                                               + ": " + e.getMessage(), e );
+                        "Failed to filter versions for " + dependency.getArtifact(), e );
             }
             versions = verContext.get();
             if ( versions.isEmpty() )
             {
                 throw new VersionRangeResolutionException( rangeResult,
-                                                           "No acceptable versions for " + dependency.getArtifact()
-                                                               + ": " + rangeResult.getVersions() );
+                        "No acceptable versions for " + dependency.getArtifact() + ": " + rangeResult.getVersions() );
             }
         }
         else
@@ -725,7 +662,7 @@ public class DefaultDependencyCollector
 
         final DataPool pool;
 
-        final Queue<DependencyProcessingContext> dependencyProcessingQueue = new ArrayDeque<>( 128 );
+        final NodeStack nodes;
 
         final DefaultDependencyCollectionContext collectionContext;
 
@@ -733,11 +670,9 @@ public class DefaultDependencyCollector
 
         final CollectRequest request;
 
-        final DependencyResolutionSkipper skipper;
-
-        Args( RepositorySystemSession session, RequestTrace trace, DataPool pool,
+        Args( RepositorySystemSession session, RequestTrace trace, DataPool pool, NodeStack nodes,
                      DefaultDependencyCollectionContext collectionContext, DefaultVersionFilterContext versionContext,
-                     CollectRequest request, DependencyResolutionSkipper skipper )
+                     CollectRequest request )
         {
             this.session = session;
             this.request = request;
@@ -745,9 +680,9 @@ public class DefaultDependencyCollector
             this.premanagedState = ConfigUtils.getBoolean( session, false, DependencyManagerUtils.CONFIG_PROP_VERBOSE );
             this.trace = trace;
             this.pool = pool;
+            this.nodes = nodes;
             this.collectionContext = collectionContext;
             this.versionContext = versionContext;
-            this.skipper = skipper;
         }
 
     }
@@ -773,7 +708,7 @@ public class DefaultDependencyCollector
             maxCycles = ConfigUtils.getInteger( session, CONFIG_PROP_MAX_CYCLES_DEFAULT, CONFIG_PROP_MAX_CYCLES );
         }
 
-        public void addException( Dependency dependency, Exception e, List<DependencyNode> nodes )
+        public void addException( Dependency dependency, Exception e, NodeStack nodes )
         {
             if ( maxExceptions < 0 || result.getExceptions().size() < maxExceptions )
             {
@@ -781,13 +716,13 @@ public class DefaultDependencyCollector
                 if ( errorPath == null )
                 {
                     StringBuilder buffer = new StringBuilder( 256 );
-                    for ( DependencyNode node : nodes )
+                    for ( int i = 0; i < nodes.size(); i++ )
                     {
                         if ( buffer.length() > 0 )
                         {
                             buffer.append( " -> " );
                         }
-                        Dependency dep = node.getDependency();
+                        Dependency dep = nodes.get( i ).getDependency();
                         if ( dep != null )
                         {
                             buffer.append( dep.getArtifact() );
@@ -803,11 +738,11 @@ public class DefaultDependencyCollector
             }
         }
 
-        public void addCycle( List<DependencyNode> nodes, int cycleEntry, Dependency dependency )
+        public void addCycle( NodeStack nodes, int cycleEntry, Dependency dependency )
         {
             if ( maxCycles < 0 || result.getCycles().size() < maxCycles )
             {
-                result.addCycle( new DefaultDependencyCycle( nodes, cycleEntry, dependency ) );
+                result.addCycle( new DefaultDependencyCycle( nodes.nodes, cycleEntry, dependency ) );
             }
         }
 
@@ -929,4 +864,4 @@ public class DefaultDependencyCollector
 
     }
 
-}
+}
\ No newline at end of file
diff --git a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/collect/df/NodeStack.java b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/collect/df/NodeStack.java
new file mode 100644
index 00000000..dafe7a2f
--- /dev/null
+++ b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/collect/df/NodeStack.java
@@ -0,0 +1,79 @@
+package org.eclipse.aether.internal.impl.collect.df;
+
+/*
+ * 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 java.util.ArrayList;
+
+import org.eclipse.aether.graph.DependencyNode;
+
+/**
+ * Internal helper for {@link DfDependencyCollector}. Originally (pre-1.8.0) this same class was located a
+ * package higher.
+ *
+ * @since 1.8.0
+ */
+final class NodeStack
+{
+
+    @SuppressWarnings( {"checkstyle:magicnumber" } )
+    // CHECKSTYLE_OFF: MagicNumber
+    ArrayList<DependencyNode> nodes = new ArrayList<>( 96 );
+    // CHECKSTYLE_ON: MagicNumber
+
+    public DependencyNode top()
+    {
+        if ( nodes.isEmpty() )
+        {
+            throw new IllegalStateException( "stack empty" );
+        }
+        return nodes.get( nodes.size() - 1 );
+    }
+
+    public void push( DependencyNode node )
+    {
+        nodes.add( node );
+    }
+
+    public void pop()
+    {
+        if ( nodes.isEmpty() )
+        {
+            throw new IllegalStateException( "stack empty" );
+        }
+        nodes.remove( nodes.size() - 1 );
+    }
+
+    public int size()
+    {
+        return nodes.size();
+    }
+
+    public DependencyNode get( int index )
+    {
+        return nodes.get( index );
+    }
+
+    @Override
+    public String toString()
+    {
+        return nodes.toString();
+    }
+
+}
\ No newline at end of file
diff --git a/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/collect/DefaultDependencyCycleTest.java b/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/collect/DefaultDependencyCycleTest.java
index 0a382f8c..66dade8c 100644
--- a/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/collect/DefaultDependencyCycleTest.java
+++ b/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/collect/DefaultDependencyCycleTest.java
@@ -19,6 +19,9 @@ package org.eclipse.aether.internal.impl.collect;
  * under the License.
  */
 
+import java.util.ArrayList;
+import java.util.List;
+
 import org.eclipse.aether.artifact.DefaultArtifact;
 import org.eclipse.aether.graph.DefaultDependencyNode;
 import org.eclipse.aether.graph.Dependency;
@@ -26,9 +29,6 @@ import org.eclipse.aether.graph.DependencyCycle;
 import org.eclipse.aether.graph.DependencyNode;
 import org.junit.Test;
 
-import java.util.ArrayList;
-import java.util.List;
-
 import static org.junit.Assert.assertEquals;
 
 public class DefaultDependencyCycleTest
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/bf/BfDependencyCollectorTest.java
similarity index 98%
copy from maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/collect/DefaultDependencyCollectorTest.java
copy to maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/collect/bf/BfDependencyCollectorTest.java
index 7d034bdd..ea162f94 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/bf/BfDependencyCollectorTest.java
@@ -1,4 +1,4 @@
-package org.eclipse.aether.internal.impl.collect;
+package org.eclipse.aether.internal.impl.collect.bf;
 
 /*
  * Licensed to the Apache Software Foundation (ASF) under one
@@ -18,14 +18,6 @@ package org.eclipse.aether.internal.impl.collect;
  * specific language governing permissions and limitations
  * under the License.
  */
-import static java.util.Objects.requireNonNull;
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertNotEquals;
-import static org.junit.Assert.assertNotNull;
-import static org.junit.Assert.assertNull;
-import static org.junit.Assert.assertSame;
-import static org.junit.Assert.assertTrue;
-import static org.junit.Assert.fail;
 
 import java.io.IOException;
 import java.util.ArrayList;
@@ -73,12 +65,21 @@ import org.eclipse.aether.util.graph.version.HighestVersionFilter;
 import org.junit.Before;
 import org.junit.Test;
 
+import static java.util.Objects.requireNonNull;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertSame;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
 /**
  */
-public class DefaultDependencyCollectorTest
+public class BfDependencyCollectorTest
 {
 
-    protected DefaultDependencyCollector collector;
+    protected BfDependencyCollector collector;
 
     protected DefaultRepositorySystemSession session;
 
@@ -107,12 +108,12 @@ public class DefaultDependencyCollectorTest
         setupCollector( false );
     }
 
-    public void setupCollector( boolean useSkip )
+    public void setupCollector( boolean useSkipper )
     {
         session = TestUtils.newSession();
-        session.setConfigProperty( DefaultDependencyCollector.CONFIG_PROP_USE_SKIP, useSkip );
+        session.setConfigProperty( BfDependencyCollector.CONFIG_PROP_SKIPPER, useSkipper );
 
-        collector = new DefaultDependencyCollector();
+        collector = new BfDependencyCollector();
         collector.setArtifactDescriptorReader( newReader( "" ) );
         collector.setVersionRangeResolver( new StubVersionRangeResolver() );
         collector.setRemoteRepositoryManager( new StubRemoteRepositoryManager() );
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/bf/BfDependencyCollectorUseSkipTest.java
similarity index 96%
rename from maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/collect/DefaultDependencyCollectorUseSkipTest.java
rename to maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/collect/bf/BfDependencyCollectorUseSkipTest.java
index 716ecdef..5daa678b 100644
--- 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/bf/BfDependencyCollectorUseSkipTest.java
@@ -1,4 +1,4 @@
-package org.eclipse.aether.internal.impl.collect;
+package org.eclipse.aether.internal.impl.collect.bf;
 /*
  * Licensed to the Apache Software Foundation (ASF) under one
  * or more contributor license agreements.  See the NOTICE file
@@ -18,6 +18,11 @@ package org.eclipse.aether.internal.impl.collect;
  * under the License.
  */
 
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+
 import org.eclipse.aether.artifact.DefaultArtifact;
 import org.eclipse.aether.collection.CollectRequest;
 import org.eclipse.aether.collection.CollectResult;
@@ -29,14 +34,9 @@ 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
+public class BfDependencyCollectorUseSkipTest extends BfDependencyCollectorTest
 {
 
     private Dependency newDep( String coords, String scope, Collection<Exclusion> exclusions )
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/bf/DependencyResolutionSkipperTest.java
similarity index 82%
rename from maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/collect/DependencyResolutionSkipperTest.java
rename to maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/collect/bf/DependencyResolutionSkipperTest.java
index 1d92acc0..eeb0a02d 100644
--- 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/bf/DependencyResolutionSkipperTest.java
@@ -1,4 +1,4 @@
-package org.eclipse.aether.internal.impl.collect;
+package org.eclipse.aether.internal.impl.collect.bf;
 /*
  * Licensed to the Apache Software Foundation (ASF) under one
  * or more contributor license agreements.  See the NOTICE file
@@ -18,7 +18,12 @@ package org.eclipse.aether.internal.impl.collect;
  * under the License.
  */
 
-import org.eclipse.aether.RepositoryException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
 import org.eclipse.aether.artifact.DefaultArtifact;
 import org.eclipse.aether.graph.DefaultDependencyNode;
 import org.eclipse.aether.graph.Dependency;
@@ -27,12 +32,6 @@ 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;
@@ -61,7 +60,7 @@ public class DependencyResolutionSkipperTest
     }
 
     @Test
-    public void testSkipVersionConflict() throws RepositoryException
+    public void testSkipVersionConflict()
     {
         // A -> B -> C 3.0 -> D   => C3.0 SHOULD BE SKIPPED
         // | -> E -> F -> G
@@ -84,7 +83,7 @@ public class DependencyResolutionSkipperTest
         c2Node.setChildren( mutableList( hNode ) );
 
         //follow the BFS resolve sequence
-        DefaultDependencyResolutionSkipper skipper = new DefaultDependencyResolutionSkipper();
+        DependencyResolutionSkipper.DefaultDependencyResolutionSkipper skipper = DependencyResolutionSkipper.defaultSkipper();
         assertFalse( skipper.skipResolution( aNode, new ArrayList<>() ) );
         skipper.cache( aNode, new ArrayList<>() );
         assertFalse( skipper.skipResolution( bNode, mutableList( aNode ) ) );
@@ -99,19 +98,19 @@ public class DependencyResolutionSkipperTest
         assertFalse( skipper.skipResolution( gNode, mutableList( aNode, eNode, fNode ) ) );
         skipper.cache( gNode, mutableList( aNode, eNode, fNode ) );
 
-        Map<DependencyNode, DefaultDependencyResolutionSkipper.DependencyResolutionResult> results = skipper.getResults();
+        Map<DependencyNode, DependencyResolutionSkipper.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() );
+        List<DependencyResolutionSkipper.DependencyResolutionResult> skipped =
+                results.values().stream()
+                        .filter( dependencyResolutionResult -> dependencyResolutionResult.skippedAsVersionConflict )
+                        .collect( Collectors.toList() );
         assertEquals( skipped.size(), 1 );
         assertTrue( skipped.get( 0 ).current == c3Node );
     }
 
     @Test
-    public void testSkipDeeperDuplicateNode() throws RepositoryException
+    public void testSkipDeeperDuplicateNode()
     {
         // A -> B
         // |--> C -> B  => B here will be skipped
@@ -129,7 +128,7 @@ public class DependencyResolutionSkipperTest
         dNode.setChildren( mutableList( c1Node ) );
 
         //follow the BFS resolve sequence
-        DefaultDependencyResolutionSkipper skipper = new DefaultDependencyResolutionSkipper();
+        DependencyResolutionSkipper.DefaultDependencyResolutionSkipper skipper = DependencyResolutionSkipper.defaultSkipper();
         assertFalse( skipper.skipResolution( aNode, new ArrayList<>() ) );
         skipper.cache( aNode, new ArrayList<>() );
         assertFalse( skipper.skipResolution( bNode, mutableList( aNode ) ) );
@@ -145,13 +144,13 @@ public class DependencyResolutionSkipperTest
         assertTrue( skipper.skipResolution( c1Node, mutableList( aNode, dNode ) ) );
         skipper.cache( c1Node, mutableList( aNode, dNode ) );
 
-        Map<DependencyNode, DefaultDependencyResolutionSkipper.DependencyResolutionResult> results = skipper.getResults();
+        Map<DependencyNode, DependencyResolutionSkipper.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() );
+        List<DependencyResolutionSkipper.DefaultDependencyResolutionSkipper.DependencyResolutionResult> skipped =
+                results.values().stream()
+                        .filter( dependencyResolutionResult -> dependencyResolutionResult.skippedAsDuplicate )
+                        .collect( Collectors.toList() );
         assertEquals( skipped.size(), 2 );
         assertTrue( skipped.get( 0 ).current == b1Node );
         assertTrue( skipped.get( 1 ).current == c1Node );
@@ -159,7 +158,7 @@ public class DependencyResolutionSkipperTest
 
 
     @Test
-    public void testForceResolution() throws RepositoryException
+    public void testForceResolution()
     {
         // A -> B -> C -> D => 3rd D here will be force-resolved
         // |--> C -> D => 2nd D will be force-resolved
@@ -179,7 +178,7 @@ public class DependencyResolutionSkipperTest
         dNode.setChildren( new ArrayList<>() );
 
         //follow the BFS resolve sequence
-        DefaultDependencyResolutionSkipper skipper = new DefaultDependencyResolutionSkipper();
+        DependencyResolutionSkipper.DefaultDependencyResolutionSkipper skipper = DependencyResolutionSkipper.defaultSkipper();
         assertFalse( skipper.skipResolution( aNode, new ArrayList<>() ) );
         skipper.cache( aNode, new ArrayList<>() );
         assertFalse( skipper.skipResolution( bNode, mutableList( aNode ) ) );
@@ -198,13 +197,13 @@ public class DependencyResolutionSkipperTest
         assertFalse( skipper.skipResolution( d2Node, mutableList( aNode, bNode, c1Node ) ) );
         skipper.cache( d2Node, mutableList( aNode, bNode, c1Node ) );
 
-        Map<DependencyNode, DefaultDependencyResolutionSkipper.DependencyResolutionResult> results = skipper.getResults();
+        Map<DependencyNode, DependencyResolutionSkipper.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() );
+        List<DependencyResolutionSkipper.DefaultDependencyResolutionSkipper.DependencyResolutionResult> forceResolved =
+                results.values().stream()
+                        .filter( dependencyResolutionResult -> dependencyResolutionResult.forceResolution )
+                        .collect( Collectors.toList() );
         assertEquals( forceResolved.size(), 3 );
         assertTrue( forceResolved.get( 0 ).current == c1Node );
         assertTrue( forceResolved.get( 1 ).current == d1Node );
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/df/DfDependencyCollectorTest.java
similarity index 96%
rename from maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/collect/DefaultDependencyCollectorTest.java
rename to maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/collect/df/DfDependencyCollectorTest.java
index 7d034bdd..b641bbd6 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/df/DfDependencyCollectorTest.java
@@ -1,4 +1,4 @@
-package org.eclipse.aether.internal.impl.collect;
+package org.eclipse.aether.internal.impl.collect.df;
 
 /*
  * Licensed to the Apache Software Foundation (ASF) under one
@@ -75,44 +75,38 @@ import org.junit.Test;
 
 /**
  */
-public class DefaultDependencyCollectorTest
+public class DfDependencyCollectorTest
 {
 
-    protected DefaultDependencyCollector collector;
+    private DfDependencyCollector collector;
 
-    protected DefaultRepositorySystemSession session;
+    private DefaultRepositorySystemSession session;
 
-    protected DependencyGraphParser parser;
+    private DependencyGraphParser parser;
 
-    protected RemoteRepository repository;
+    private RemoteRepository repository;
 
-    protected IniArtifactDescriptorReader newReader( String prefix )
+    private IniArtifactDescriptorReader newReader( String prefix )
     {
         return new IniArtifactDescriptorReader( "artifact-descriptions/" + prefix );
     }
 
-    protected Dependency newDep( String coords )
+    private Dependency newDep( String coords )
     {
         return newDep( coords, "" );
     }
 
-    protected Dependency newDep( String coords, String scope )
+    private Dependency newDep( String coords, String scope )
     {
         return new Dependency( new DefaultArtifact( coords ), scope );
     }
 
     @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 = new DfDependencyCollector();
         collector.setArtifactDescriptorReader( newReader( "" ) );
         collector.setVersionRangeResolver( new StubVersionRangeResolver() );
         collector.setRemoteRepositoryManager( new StubRemoteRepositoryManager() );
@@ -160,12 +154,12 @@ public class DefaultDependencyCollectorTest
         parents.removeLast();
     }
 
-    protected Dependency dep( DependencyNode root, int... coords )
+    private Dependency dep( DependencyNode root, int... coords )
     {
         return path( root, coords ).getDependency();
     }
 
-    protected DependencyNode path( DependencyNode root, int... coords )
+    private DependencyNode path( DependencyNode root, int... coords )
     {
         try
         {
@@ -652,4 +646,4 @@ public class DefaultDependencyCollectorTest
 
     }
 
-}
+}
\ No newline at end of file
diff --git a/src/site/markdown/configuration.md b/src/site/markdown/configuration.md
index 6c3a20d0..1524530a 100644
--- a/src/site/markdown/configuration.md
+++ b/src/site/markdown/configuration.md
@@ -42,9 +42,10 @@ 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.dependencyCollector.impl` | String | The name of the dependency collector implementation to use: depth-first (original) named `df`, and breadth-first (new in 1.8.0) named `bf`. Both collectors produce equivalent results, but they may differ performance wise, depending on project being applied to. Our experience shows that existing `df` is well suited for smaller to medium size projects, while `bf` may perform better on huge projects with many dependencies. Experiment (and come ba [...]
+`aether.dependencyCollector.bf.skipper` | boolean | Flag controlling whether to skip resolving duplicate/conflicting nodes during the breadth-first (`bf`) dependency collection process. | `true` | 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
 `aether.enhancedLocalRepository.trackingFilename` | String | Filename of the file in which to track the remote repositories. | `"_remote.repositories"` | no
 `aether.interactive` | boolean | A flag indicating whether interaction with the user is allowed. | `false` | no