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

rfscholte pushed a commit to branch MSHARED-788
in repository https://gitbox.apache.org/repos/asf/maven-dependency-tree.git

commit 758de2bd07ae4d66c92d57cda2c08dd796a53a04
Author: Gabriel Belingueres <belingue...@gmail.com>
AuthorDate: Tue Jan 15 22:40:06 2019 -0300

    Corrected some problems discovered with getting the right level of 
filtering of the dependencies.
    
    Specially for Maven 3.0, I couldn't get the desired behavior using the
    provided DependencyGraphTransformer, DependencySelector, etc. so I
    copied those implementation from Eclipse aether to work on Sonatype
    aether classes. As a result it is expected to get the same output than
    Maven 3.1+ (though it will probably need more testing (just tested with
    enforcer plugin's DependencyConvergence and RequireUpperBoundDeps ITs).
---
 .../Maven31DependencyCollectorBuilder.java         |    6 +-
 .../internal/Maven3DependencyCollectorBuilder.java |   34 +-
 .../graph/internal/maven30/ConflictIdSorter.java   |  370 ++++++
 .../graph/internal/maven30/ConflictResolver.java   | 1318 ++++++++++++++++++++
 .../maven30/ExclusionDependencySelector.java       |  228 ++++
 .../graph/internal/maven30/JavaScopeDeriver.java   |   72 ++
 .../graph/internal/maven30/JavaScopeSelector.java  |  102 ++
 .../Maven3DirectScopeDependencySelector.java       |  132 ++
 .../internal/maven30/NearestVersionSelector.java   |  183 +++
 .../maven30/SimpleOptionalitySelector.java         |   64 +
 .../Maven31DirectScopeDependencySelector.java      |  132 ++
 11 files changed, 2619 insertions(+), 22 deletions(-)

diff --git 
a/src/main/java/org/apache/maven/shared/dependency/graph/internal/Maven31DependencyCollectorBuilder.java
 
b/src/main/java/org/apache/maven/shared/dependency/graph/internal/Maven31DependencyCollectorBuilder.java
index 7152ab7..62e659c 100644
--- 
a/src/main/java/org/apache/maven/shared/dependency/graph/internal/Maven31DependencyCollectorBuilder.java
+++ 
b/src/main/java/org/apache/maven/shared/dependency/graph/internal/Maven31DependencyCollectorBuilder.java
@@ -33,6 +33,7 @@ import org.apache.maven.project.ProjectBuildingRequest;
 import org.apache.maven.shared.dependency.graph.DependencyCollectorBuilder;
 import 
org.apache.maven.shared.dependency.graph.DependencyGraphBuilderException;
 import org.apache.maven.shared.dependency.graph.DependencyNode;
+import 
org.apache.maven.shared.dependency.graph.internal.maven31.Maven31DirectScopeDependencySelector;
 import org.codehaus.plexus.component.annotations.Component;
 import org.codehaus.plexus.component.annotations.Requirement;
 import org.codehaus.plexus.logging.AbstractLogEnabled;
@@ -45,11 +46,11 @@ import 
org.eclipse.aether.collection.DependencyCollectionException;
 import org.eclipse.aether.collection.DependencyGraphTransformer;
 import org.eclipse.aether.collection.DependencySelector;
 import org.eclipse.aether.graph.DependencyVisitor;
+import org.eclipse.aether.util.artifact.JavaScopes;
 import org.eclipse.aether.util.graph.manager.DependencyManagerUtils;
 import org.eclipse.aether.util.graph.selector.AndDependencySelector;
 import org.eclipse.aether.util.graph.selector.ExclusionDependencySelector;
 import org.eclipse.aether.util.graph.selector.OptionalDependencySelector;
-import org.eclipse.aether.util.graph.selector.ScopeDependencySelector;
 import org.eclipse.aether.util.graph.transformer.ConflictResolver;
 import org.eclipse.aether.util.graph.transformer.JavaScopeDeriver;
 import org.eclipse.aether.util.graph.transformer.JavaScopeSelector;
@@ -96,7 +97,8 @@ public class Maven31DependencyCollectorBuilder
             session.setDependencyGraphTransformer( transformer );
 
             DependencySelector depFilter =
-                new AndDependencySelector( new ScopeDependencySelector(), new 
OptionalDependencySelector(),
+                new AndDependencySelector( new 
Maven31DirectScopeDependencySelector( JavaScopes.TEST ),
+                                           new OptionalDependencySelector(),
                                            new ExclusionDependencySelector() );
             session.setDependencySelector( depFilter );
 
diff --git 
a/src/main/java/org/apache/maven/shared/dependency/graph/internal/Maven3DependencyCollectorBuilder.java
 
b/src/main/java/org/apache/maven/shared/dependency/graph/internal/Maven3DependencyCollectorBuilder.java
index d88b884..e27f944 100644
--- 
a/src/main/java/org/apache/maven/shared/dependency/graph/internal/Maven3DependencyCollectorBuilder.java
+++ 
b/src/main/java/org/apache/maven/shared/dependency/graph/internal/Maven3DependencyCollectorBuilder.java
@@ -33,6 +33,12 @@ import org.apache.maven.project.ProjectBuildingRequest;
 import org.apache.maven.shared.dependency.graph.DependencyCollectorBuilder;
 import 
org.apache.maven.shared.dependency.graph.DependencyGraphBuilderException;
 import org.apache.maven.shared.dependency.graph.DependencyNode;
+import 
org.apache.maven.shared.dependency.graph.internal.maven30.ConflictResolver;
+import 
org.apache.maven.shared.dependency.graph.internal.maven30.JavaScopeDeriver;
+import 
org.apache.maven.shared.dependency.graph.internal.maven30.JavaScopeSelector;
+import 
org.apache.maven.shared.dependency.graph.internal.maven30.Maven3DirectScopeDependencySelector;
+import 
org.apache.maven.shared.dependency.graph.internal.maven30.NearestVersionSelector;
+import 
org.apache.maven.shared.dependency.graph.internal.maven30.SimpleOptionalitySelector;
 import org.codehaus.plexus.component.annotations.Component;
 import org.codehaus.plexus.component.annotations.Requirement;
 import org.codehaus.plexus.logging.AbstractLogEnabled;
@@ -46,16 +52,11 @@ import 
org.sonatype.aether.collection.DependencyGraphTransformer;
 import org.sonatype.aether.collection.DependencySelector;
 import org.sonatype.aether.graph.DependencyVisitor;
 import org.sonatype.aether.util.DefaultRepositorySystemSession;
-import org.sonatype.aether.util.graph.CloningDependencyVisitor;
+import org.sonatype.aether.util.artifact.JavaScopes;
 import org.sonatype.aether.util.graph.TreeDependencyVisitor;
 import org.sonatype.aether.util.graph.selector.AndDependencySelector;
 import org.sonatype.aether.util.graph.selector.ExclusionDependencySelector;
 import org.sonatype.aether.util.graph.selector.OptionalDependencySelector;
-import org.sonatype.aether.util.graph.selector.ScopeDependencySelector;
-import 
org.sonatype.aether.util.graph.transformer.ChainedDependencyGraphTransformer;
-import org.sonatype.aether.util.graph.transformer.ConflictMarker;
-import org.sonatype.aether.util.graph.transformer.JavaDependencyContextRefiner;
-import org.sonatype.aether.util.graph.transformer.JavaEffectiveScopeCalculator;
 import org.sonatype.aether.version.VersionConstraint;
 
 /**
@@ -77,7 +78,6 @@ public class Maven3DependencyCollectorBuilder
                                                 ProjectBuildingRequest 
buildingRequest, ArtifactFilter filter )
         throws DependencyGraphBuilderException
     {
-        ClassLoader prevClassLoader = 
Thread.currentThread().getContextClassLoader();
         try
         {
             MavenProject project = buildingRequest.getProject();
@@ -93,15 +93,19 @@ public class Maven3DependencyCollectorBuilder
             DefaultRepositorySystemSession session = new 
DefaultRepositorySystemSession( repositorySystemSession );
 
             DependencyGraphTransformer transformer =
-                new ChainedDependencyGraphTransformer( new ConflictMarker(), 
new JavaEffectiveScopeCalculator(),
-                                                       new 
JavaDependencyContextRefiner() );
+                new ConflictResolver( new NearestVersionSelector(), new 
JavaScopeSelector(),
+                                      new SimpleOptionalitySelector(), new 
JavaScopeDeriver() );
             session.setDependencyGraphTransformer( transformer );
 
             DependencySelector depFilter =
-                new AndDependencySelector( new ScopeDependencySelector(), new 
OptionalDependencySelector(),
+                new AndDependencySelector( new 
Maven3DirectScopeDependencySelector( JavaScopes.TEST ),
+                                           new OptionalDependencySelector(), 
                                            new ExclusionDependencySelector() );
             session.setDependencySelector( depFilter );
 
+            session.setConfigProperty( ConflictResolver.CONFIG_PROP_VERBOSE, 
true );
+            session.setConfigProperty( "aether.dependencyManager.verbose", 
true );
+
             org.sonatype.aether.artifact.Artifact aetherArtifact =
                 (org.sonatype.aether.artifact.Artifact) Invoker.invoke( 
RepositoryUtils.class, "toArtifact",
                                                                        
Artifact.class, projectArtifact );
@@ -124,12 +128,6 @@ public class Maven3DependencyCollectorBuilder
 
             org.sonatype.aether.graph.DependencyNode rootNode = 
collectResult.getRoot();
 
-            CloningDependencyVisitor cloner = new CloningDependencyVisitor();
-            TreeDependencyVisitor treeVisitor = new TreeDependencyVisitor( 
cloner );
-            rootNode.accept( treeVisitor );
-
-            rootNode = cloner.getRootNode();
-
             if ( getLogger().isDebugEnabled() )
             {
                 logTree( rootNode );
@@ -141,10 +139,6 @@ public class Maven3DependencyCollectorBuilder
         {
             throw new DependencyGraphBuilderException( "Could not collect 
dependencies: " + e.getResult(), e );
         }
-        finally
-        {
-            Thread.currentThread().setContextClassLoader( prevClassLoader );
-        }
     }
 
     private void logTree( org.sonatype.aether.graph.DependencyNode rootNode )
diff --git 
a/src/main/java/org/apache/maven/shared/dependency/graph/internal/maven30/ConflictIdSorter.java
 
b/src/main/java/org/apache/maven/shared/dependency/graph/internal/maven30/ConflictIdSorter.java
new file mode 100644
index 0000000..44cbbe7
--- /dev/null
+++ 
b/src/main/java/org/apache/maven/shared/dependency/graph/internal/maven30/ConflictIdSorter.java
@@ -0,0 +1,370 @@
+package org.apache.maven.shared.dependency.graph.internal.maven30;
+
+/*
+ * 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 java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.IdentityHashMap;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.sonatype.aether.RepositoryException;
+import org.sonatype.aether.collection.DependencyGraphTransformationContext;
+import org.sonatype.aether.collection.DependencyGraphTransformer;
+import org.sonatype.aether.graph.DependencyNode;
+import org.sonatype.aether.util.graph.transformer.ConflictMarker;
+import org.sonatype.aether.util.graph.transformer.TransformationContextKeys;
+
+/**
+ * This class is a copy of their homonymous in the Eclipse Aether library, 
adapted to work with Sonatype Aether.
+ * 
+ * @author Gabriel Belingueres
+ * @since 3.0.2
+ */
+public final class ConflictIdSorter
+    implements DependencyGraphTransformer
+{
+
+    public DependencyNode transformGraph( DependencyNode node, 
DependencyGraphTransformationContext context )
+        throws RepositoryException
+    {
+        Map<?, ?> conflictIds = (Map<?, ?>) context.get( 
TransformationContextKeys.CONFLICT_IDS );
+        if ( conflictIds == null )
+        {
+            ConflictMarker marker = new ConflictMarker();
+            marker.transformGraph( node, context );
+
+            conflictIds = (Map<?, ?>) context.get( 
TransformationContextKeys.CONFLICT_IDS );
+        }
+
+//        @SuppressWarnings( "unchecked" )
+//        Map<String, Object> stats = (Map<String, Object>) context.get( 
TransformationContextKeys.STATS );
+//        long time1 = System.currentTimeMillis();
+
+        Map<Object, ConflictId> ids = new LinkedHashMap<Object, ConflictId>( 
256 );
+
+        // CHECKSTYLE_OFF: AvoidNestedBlocks
+        {
+            ConflictId id = null;
+            Object key = conflictIds.get( node );
+            if ( key != null )
+            {
+                id = new ConflictId( key, 0 );
+                ids.put( key, id );
+            }
+
+            Map<DependencyNode, Object> visited = new 
IdentityHashMap<DependencyNode, Object>( conflictIds.size() );
+
+            buildConflitIdDAG( ids, node, id, 0, visited, conflictIds );
+        }
+        // CHECKSTYLE_NO: AvoidNestedBlocks
+
+//        long time2 = System.currentTimeMillis();
+
+        topsortConflictIds( ids.values(), context );
+//        int cycles = topsortConflictIds( ids.values(), context );
+
+//        if ( stats != null )
+//        {
+//            long time3 = System.currentTimeMillis();
+//            stats.put( "ConflictIdSorter.graphTime", time2 - time1 );
+//            stats.put( "ConflictIdSorter.topsortTime", time3 - time2 );
+//            stats.put( "ConflictIdSorter.conflictIdCount", ids.size() );
+//            stats.put( "ConflictIdSorter.conflictIdCycleCount", cycles );
+//        }
+
+        return node;
+    }
+
+    private void buildConflitIdDAG( Map<Object, ConflictId> ids, 
DependencyNode node, ConflictId id, int depth,
+                                    Map<DependencyNode, Object> visited, 
Map<?, ?> conflictIds )
+    {
+        if ( visited.put( node, Boolean.TRUE ) != null )
+        {
+            return;
+        }
+
+        depth++;
+
+        for ( DependencyNode child : node.getChildren() )
+        {
+            Object key = conflictIds.get( child );
+            ConflictId childId = ids.get( key );
+            if ( childId == null )
+            {
+                childId = new ConflictId( key, depth );
+                ids.put( key, childId );
+            }
+            else
+            {
+                childId.pullup( depth );
+            }
+
+            if ( id != null )
+            {
+                id.add( childId );
+            }
+
+            buildConflitIdDAG( ids, child, childId, depth, visited, 
conflictIds );
+        }
+    }
+
+    private int topsortConflictIds( Collection<ConflictId> conflictIds, 
DependencyGraphTransformationContext context )
+    {
+        List<Object> sorted = new ArrayList<Object>( conflictIds.size() );
+
+        RootQueue roots = new RootQueue( conflictIds.size() / 2 );
+        for ( ConflictId id : conflictIds )
+        {
+            if ( id.inDegree <= 0 )
+            {
+                roots.add( id );
+            }
+        }
+
+        processRoots( sorted, roots );
+
+        boolean cycle = sorted.size() < conflictIds.size();
+
+        while ( sorted.size() < conflictIds.size() )
+        {
+            // cycle -> deal gracefully with nodes still having positive 
in-degree
+
+            ConflictId nearest = null;
+            for ( ConflictId id : conflictIds )
+            {
+                if ( id.inDegree <= 0 )
+                {
+                    continue;
+                }
+                if ( nearest == null || id.minDepth < nearest.minDepth
+                    || ( id.minDepth == nearest.minDepth && id.inDegree < 
nearest.inDegree ) )
+                {
+                    nearest = id;
+                }
+            }
+
+            nearest.inDegree = 0;
+            roots.add( nearest );
+
+            processRoots( sorted, roots );
+        }
+
+        Collection<Collection<Object>> cycles = Collections.emptySet();
+        if ( cycle )
+        {
+            cycles = findCycles( conflictIds );
+        }
+
+        context.put( TransformationContextKeys.SORTED_CONFLICT_IDS, sorted );
+        context.put( TransformationContextKeys.CYCLIC_CONFLICT_IDS, cycles );
+
+        return cycles.size();
+    }
+
+    private void processRoots( List<Object> sorted, RootQueue roots )
+    {
+        while ( !roots.isEmpty() )
+        {
+            ConflictId root = roots.remove();
+
+            sorted.add( root.key );
+
+            for ( ConflictId child : root.children )
+            {
+                child.inDegree--;
+                if ( child.inDegree == 0 )
+                {
+                    roots.add( child );
+                }
+            }
+        }
+    }
+
+    private Collection<Collection<Object>> findCycles( Collection<ConflictId> 
conflictIds )
+    {
+        Collection<Collection<Object>> cycles = new 
HashSet<Collection<Object>>();
+
+        Map<Object, Integer> stack = new HashMap<Object, Integer>( 128 );
+        Map<ConflictId, Object> visited = new IdentityHashMap<ConflictId, 
Object>( conflictIds.size() );
+        for ( ConflictId id : conflictIds )
+        {
+            findCycles( id, visited, stack, cycles );
+        }
+
+        return cycles;
+    }
+
+    private void findCycles( ConflictId id, Map<ConflictId, Object> visited, 
Map<Object, Integer> stack,
+                             Collection<Collection<Object>> cycles )
+    {
+        Integer depth = stack.put( id.key, stack.size() );
+        if ( depth != null )
+        {
+            stack.put( id.key, depth );
+            Collection<Object> cycle = new HashSet<Object>();
+            for ( Map.Entry<Object, Integer> entry : stack.entrySet() )
+            {
+                if ( entry.getValue() >= depth )
+                {
+                    cycle.add( entry.getKey() );
+                }
+            }
+            cycles.add( cycle );
+        }
+        else
+        {
+            if ( visited.put( id, Boolean.TRUE ) == null )
+            {
+                for ( ConflictId childId : id.children )
+                {
+                    findCycles( childId, visited, stack, cycles );
+                }
+            }
+            stack.remove( id.key );
+        }
+    }
+
+    static final class ConflictId
+    {
+
+        final Object key;
+
+        Collection<ConflictId> children = Collections.emptySet();
+
+        int inDegree;
+
+        int minDepth;
+
+        ConflictId( Object key, int depth )
+        {
+            this.key = key;
+            this.minDepth = depth;
+        }
+
+        public void add( ConflictId child )
+        {
+            if ( children.isEmpty() )
+            {
+                children = new HashSet<ConflictId>();
+            }
+            if ( children.add( child ) )
+            {
+                child.inDegree++;
+            }
+        }
+
+        public void pullup( int depth )
+        {
+            if ( depth < minDepth )
+            {
+                minDepth = depth;
+                depth++;
+                for ( ConflictId child : children )
+                {
+                    child.pullup( depth );
+                }
+            }
+        }
+
+        @Override
+        public boolean equals( Object obj )
+        {
+            if ( this == obj )
+            {
+                return true;
+            }
+            else if ( !( obj instanceof ConflictId ) )
+            {
+                return false;
+            }
+            ConflictId that = (ConflictId) obj;
+            return this.key.equals( that.key );
+        }
+
+        @Override
+        public int hashCode()
+        {
+            return key.hashCode();
+        }
+
+        @Override
+        public String toString()
+        {
+            return key + " @ " + minDepth + " <" + inDegree;
+        }
+
+    }
+
+    static final class RootQueue
+    {
+
+        private int nextOut;
+
+        private int nextIn;
+
+        private ConflictId[] ids;
+
+        RootQueue( int capacity )
+        {
+            ids = new ConflictId[capacity + 16];
+        }
+
+        boolean isEmpty()
+        {
+            return nextOut >= nextIn;
+        }
+
+        void add( ConflictId id )
+        {
+            if ( nextOut >= nextIn && nextOut > 0 )
+            {
+                nextIn -= nextOut;
+                nextOut = 0;
+            }
+            if ( nextIn >= ids.length )
+            {
+                ConflictId[] tmp = new ConflictId[ids.length + ids.length / 2 
+ 16];
+                System.arraycopy( ids, nextOut, tmp, 0, nextIn - nextOut );
+                ids = tmp;
+                nextIn -= nextOut;
+                nextOut = 0;
+            }
+            int i;
+            for ( i = nextIn - 1; i >= nextOut && id.minDepth < 
ids[i].minDepth; i-- )
+            {
+                ids[i + 1] = ids[i];
+            }
+            ids[i + 1] = id;
+            nextIn++;
+        }
+
+        ConflictId remove()
+        {
+            return ids[nextOut++];
+        }
+
+    }
+
+}
diff --git 
a/src/main/java/org/apache/maven/shared/dependency/graph/internal/maven30/ConflictResolver.java
 
b/src/main/java/org/apache/maven/shared/dependency/graph/internal/maven30/ConflictResolver.java
new file mode 100644
index 0000000..1e60a32
--- /dev/null
+++ 
b/src/main/java/org/apache/maven/shared/dependency/graph/internal/maven30/ConflictResolver.java
@@ -0,0 +1,1318 @@
+package org.apache.maven.shared.dependency.graph.internal.maven30;
+
+/*
+ * 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 java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.IdentityHashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.ListIterator;
+import java.util.Map;
+
+import org.sonatype.aether.RepositoryException;
+import org.sonatype.aether.artifact.Artifact;
+import org.sonatype.aether.collection.DependencyGraphTransformationContext;
+import org.sonatype.aether.collection.DependencyGraphTransformer;
+import org.sonatype.aether.graph.Dependency;
+import org.sonatype.aether.graph.DependencyNode;
+import org.sonatype.aether.util.ConfigUtils;
+import org.sonatype.aether.util.graph.DefaultDependencyNode;
+import org.sonatype.aether.util.graph.transformer.TransformationContextKeys;
+
+/**
+ * This class is a copy of their homonymous in the Eclipse Aether library, 
adapted to work with Sonatype Aether.
+ * 
+ * @author Gabriel Belingueres
+ * @since 3.0.2
+ */
+public final class ConflictResolver
+    implements DependencyGraphTransformer
+{
+
+    /**
+     * The key in the repository session's {@link 
RepositorySystemSession#getConfigProperties() configuration
+     * properties} used to store a {@link Boolean} flag controlling the 
transformer's verbose mode.
+     */
+    public static final String CONFIG_PROP_VERBOSE = 
"aether.conflictResolver.verbose";
+
+    /**
+     * The key in the dependency node's {@link DependencyNode#getData() custom 
data} under which a reference to the
+     * {@link DependencyNode} which has won the conflict is stored.
+     */
+    public static final String NODE_DATA_WINNER = "conflict.winner";
+
+    /**
+     * The key in the dependency node's {@link DependencyNode#getData() custom 
data} under which the scope of the
+     * dependency before scope derivation and conflict resolution is stored.
+     */
+    public static final String NODE_DATA_ORIGINAL_SCOPE = 
"conflict.originalScope";
+
+    /**
+     * The key in the dependency node's {@link DependencyNode#getData() custom 
data} under which the optional flag of
+     * the dependency before derivation and conflict resolution is stored.
+     */
+    public static final String NODE_DATA_ORIGINAL_OPTIONALITY = 
"conflict.originalOptionality";
+
+    private final VersionSelector versionSelector;
+
+    private final ScopeSelector scopeSelector;
+
+    private final ScopeDeriver scopeDeriver;
+
+    private final OptionalitySelector optionalitySelector;
+
+    /**
+     * Creates a new conflict resolver instance with the specified hooks.
+     * 
+     * @param versionSelector The version selector to use, must not be {@code 
null}.
+     * @param scopeSelector The scope selector to use, must not be {@code 
null}.
+     * @param optionalitySelector The optionality selector ot use, must not be 
{@code null}.
+     * @param scopeDeriver The scope deriver to use, must not be {@code null}.
+     */
+    public ConflictResolver( VersionSelector versionSelector, ScopeSelector 
scopeSelector,
+                             OptionalitySelector optionalitySelector, 
ScopeDeriver scopeDeriver )
+    {
+        if ( versionSelector == null )
+        {
+            throw new IllegalArgumentException( "version selector not 
specified" );
+        }
+        this.versionSelector = versionSelector;
+        if ( scopeSelector == null )
+        {
+            throw new IllegalArgumentException( "scope selector not specified" 
);
+        }
+        this.scopeSelector = scopeSelector;
+        if ( scopeDeriver == null )
+        {
+            throw new IllegalArgumentException( "scope deriver not specified" 
);
+        }
+        this.scopeDeriver = scopeDeriver;
+        if ( optionalitySelector == null )
+        {
+            throw new IllegalArgumentException( "optionality selector not 
specified" );
+        }
+        this.optionalitySelector = optionalitySelector;
+    }
+
+    public DependencyNode transformGraph( DependencyNode node, 
DependencyGraphTransformationContext context )
+        throws RepositoryException
+    {
+        List<?> sortedConflictIds = (List<?>) context.get( 
TransformationContextKeys.SORTED_CONFLICT_IDS );
+        if ( sortedConflictIds == null )
+        {
+            ConflictIdSorter sorter = new ConflictIdSorter();
+            sorter.transformGraph( node, context );
+
+            sortedConflictIds = (List<?>) context.get( 
TransformationContextKeys.SORTED_CONFLICT_IDS );
+        }
+
+//        @SuppressWarnings( "unchecked" )
+//        Map<String, Object> stats = (Map<String, Object>) context.get( 
TransformationContextKeys.STATS );
+//        long time1 = System.currentTimeMillis();
+
+        @SuppressWarnings( "unchecked" )
+        Collection<Collection<?>> conflictIdCycles =
+            (Collection<Collection<?>>) context.get( 
TransformationContextKeys.CYCLIC_CONFLICT_IDS );
+        if ( conflictIdCycles == null )
+        {
+            throw new RepositoryException( "conflict id cycles have not been 
identified" );
+        }
+
+        Map<?, ?> conflictIds = (Map<?, ?>) context.get( 
TransformationContextKeys.CONFLICT_IDS );
+        if ( conflictIds == null )
+        {
+            throw new RepositoryException( "conflict groups have not been 
identified" );
+        }
+
+        Map<Object, Collection<Object>> cyclicPredecessors = new 
HashMap<Object, Collection<Object>>();
+        for ( Collection<?> cycle : conflictIdCycles )
+        {
+            for ( Object conflictId : cycle )
+            {
+                Collection<Object> predecessors = cyclicPredecessors.get( 
conflictId );
+                if ( predecessors == null )
+                {
+                    predecessors = new HashSet<Object>();
+                    cyclicPredecessors.put( conflictId, predecessors );
+                }
+                predecessors.addAll( cycle );
+            }
+        }
+
+        State state = new State( node, conflictIds, sortedConflictIds.size(), 
context );
+        for ( Iterator<?> it = sortedConflictIds.iterator(); it.hasNext(); )
+        {
+            Object conflictId = it.next();
+
+            // reset data structures for next graph walk
+            state.prepare( conflictId, cyclicPredecessors.get( conflictId ) );
+
+            // find nodes with the current conflict id and while walking the 
graph (more deeply), nuke leftover losers
+            gatherConflictItems( node, state );
+
+            // now that we know the min depth of the parents, update depth of 
conflict items
+            state.finish();
+
+            // earlier runs might have nuked all parents of the current 
conflict id, so it might not exist anymore
+            if ( !state.items.isEmpty() )
+            {
+                ConflictContext ctx = state.conflictCtx;
+                state.versionSelector.selectVersion( ctx );
+                if ( ctx.winner == null )
+                {
+                    throw new RepositoryException( "conflict resolver did not 
select winner among " + state.items );
+                }
+                DependencyNode winner = ctx.winner.node;
+
+                state.scopeSelector.selectScope( ctx );
+                if ( state.verbose )
+                {
+                    winner.setData( NODE_DATA_ORIGINAL_SCOPE, 
winner.getDependency().getScope() );
+                }
+                winner.setScope( ctx.scope );
+
+                state.optionalitySelector.selectOptionality( ctx );
+                if ( state.verbose )
+                {
+                    winner.setData( NODE_DATA_ORIGINAL_OPTIONALITY, 
winner.getDependency().isOptional() );
+                }
+                winner.getDependency().setOptional( ctx.optional );
+//                winner.setOptional( ctx.optional );
+
+                removeLosers( state );
+            }
+
+            // record the winner so we can detect leftover losers during 
future graph walks
+            state.winner();
+
+            // in case of cycles, trigger final graph walk to ensure all 
leftover losers are gone
+            if ( !it.hasNext() && !conflictIdCycles.isEmpty() && 
state.conflictCtx.winner != null )
+            {
+                DependencyNode winner = state.conflictCtx.winner.node;
+                state.prepare( state, null );
+                gatherConflictItems( winner, state );
+            }
+        }
+
+//        if ( stats != null )
+//        {
+//            long time2 = System.currentTimeMillis();
+//            stats.put( "ConflictResolver.totalTime", time2 - time1 );
+//            stats.put( "ConflictResolver.conflictItemCount", 
state.totalConflictItems );
+//        }
+
+        return node;
+    }
+
+    private boolean gatherConflictItems( DependencyNode node, State state )
+        throws RepositoryException
+    {
+        Object conflictId = state.conflictIds.get( node );
+        if ( state.currentId.equals( conflictId ) )
+        {
+            // found it, add conflict item (if not already done earlier by 
another path)
+            state.add( node );
+            // we don't recurse here so we might miss losers beneath us, those 
will be nuked during future walks below
+        }
+        else if ( state.loser( node, conflictId ) )
+        {
+            // found a leftover loser (likely in a cycle) of an already 
processed conflict id, tell caller to nuke it
+            return false;
+        }
+        else if ( state.push( node, conflictId ) )
+        {
+            // found potential parent, no cycle and not visisted before with 
the same derived scope, so recurse
+            for ( Iterator<DependencyNode> it = node.getChildren().iterator(); 
it.hasNext(); )
+            {
+                DependencyNode child = it.next();
+                if ( !gatherConflictItems( child, state ) )
+                {
+                    it.remove();
+                }
+            }
+            state.pop();
+        }
+        return true;
+    }
+
+    private void removeLosers( State state )
+    {
+        ConflictItem winner = state.conflictCtx.winner;
+        List<DependencyNode> previousParent = null;
+        ListIterator<DependencyNode> childIt = null;
+        boolean conflictVisualized = false;
+        for ( ConflictItem item : state.items )
+        {
+            if ( item == winner )
+            {
+                continue;
+            }
+            if ( item.parent != previousParent )
+            {
+                childIt = item.parent.listIterator();
+                previousParent = item.parent;
+                conflictVisualized = false;
+            }
+            while ( childIt.hasNext() )
+            {
+                DependencyNode child = childIt.next();
+                if ( child == item.node )
+                {
+                    if ( state.verbose && !conflictVisualized && item.parent 
!= winner.parent )
+                    {
+                        conflictVisualized = true;
+                        DependencyNode loser = new DefaultDependencyNode( 
child );
+                        loser.setData( NODE_DATA_WINNER, winner.node );
+                        loser.setData( NODE_DATA_ORIGINAL_SCOPE, 
loser.getDependency().getScope() );
+                        loser.setData( NODE_DATA_ORIGINAL_OPTIONALITY, 
loser.getDependency().isOptional() );
+                        loser.setScope( item.getScopes().iterator().next() );
+//                        loser.setChildren( 
Collections.<DependencyNode>emptyList() );
+                        childIt.set( loser );
+                    }
+                    else
+                    {
+                        childIt.remove();
+                    }
+                    break;
+                }
+            }
+        }
+        // there might still be losers beneath the winner (e.g. in case of 
cycles)
+        // those will be nuked during future graph walks when we include the 
winner in the recursion
+    }
+
+    final class NodeInfo
+    {
+
+        /**
+         * The smallest depth at which the node was seen, used for "the" depth 
of its conflict items.
+         */
+        int minDepth;
+
+        /**
+         * The set of derived scopes the node was visited with, used to check 
whether an already seen node needs to be
+         * revisited again in context of another scope. To conserve memory, we 
start with {@code String} and update to
+         * {@code Set<String>} if needed.
+         */
+        Object derivedScopes;
+
+        /**
+         * The set of derived optionalities the node was visited with, used to 
check whether an already seen node needs
+         * to be revisited again in context of another optionality. To 
conserve memory, encoded as bit field (bit 0 ->
+         * optional=false, bit 1 -> optional=true).
+         */
+        int derivedOptionalities;
+
+        /**
+         * The conflict items which are immediate children of the node, used 
to easily update those conflict items after
+         * a new parent scope/optionality was encountered.
+         */
+        List<ConflictItem> children;
+
+        static final int CHANGE_SCOPE = 0x01;
+
+        static final int CHANGE_OPTIONAL = 0x02;
+
+        private static final int OPT_FALSE = 0x01;
+
+        private static final int OPT_TRUE = 0x02;
+
+        NodeInfo( int depth, String derivedScope, boolean optional )
+        {
+            minDepth = depth;
+            derivedScopes = derivedScope;
+            derivedOptionalities = optional ? OPT_TRUE : OPT_FALSE;
+        }
+
+        @SuppressWarnings( "unchecked" )
+        int update( int depth, String derivedScope, boolean optional )
+        {
+            if ( depth < minDepth )
+            {
+                minDepth = depth;
+            }
+            int changes;
+            if ( derivedScopes.equals( derivedScope ) )
+            {
+                changes = 0;
+            }
+            else if ( derivedScopes instanceof Collection )
+            {
+                changes = ( (Collection<String>) derivedScopes ).add( 
derivedScope ) ? CHANGE_SCOPE : 0;
+            }
+            else
+            {
+                Collection<String> scopes = new HashSet<String>();
+                scopes.add( (String) derivedScopes );
+                scopes.add( derivedScope );
+                derivedScopes = scopes;
+                changes = CHANGE_SCOPE;
+            }
+            int bit = optional ? OPT_TRUE : OPT_FALSE;
+            if ( ( derivedOptionalities & bit ) == 0 )
+            {
+                derivedOptionalities |= bit;
+                changes |= CHANGE_OPTIONAL;
+            }
+            return changes;
+        }
+
+        void add( ConflictItem item )
+        {
+            if ( children == null )
+            {
+                children = new ArrayList<ConflictItem>( 1 );
+            }
+            children.add( item );
+        }
+
+    }
+
+    final class State
+    {
+
+        /**
+         * The conflict id currently processed.
+         */
+        Object currentId;
+
+        /**
+         * Stats counter.
+         */
+        int totalConflictItems;
+
+        /**
+         * Flag whether we should keep losers in the graph to enable 
visualization/troubleshooting of conflicts.
+         */
+        final boolean verbose;
+
+        /**
+         * A mapping from conflict id to winner node, helps to recognize nodes 
that have their effective
+         * scope&optionality set or are leftovers from previous removals.
+         */
+        final Map<Object, DependencyNode> resolvedIds;
+
+        /**
+         * The set of conflict ids which could apply to ancestors of nodes 
with the current conflict id, used to avoid
+         * recursion early on. This is basically a superset of the key set of 
resolvedIds, the additional ids account
+         * for cyclic dependencies.
+         */
+        final Collection<Object> potentialAncestorIds;
+
+        /**
+         * The output from the conflict marker
+         */
+        final Map<?, ?> conflictIds;
+
+        /**
+         * The conflict items we have gathered so far for the current conflict 
id.
+         */
+        final List<ConflictItem> items;
+
+        /**
+         * The (conceptual) mapping from nodes to extra infos, technically 
keyed by the node's child list which better
+         * captures the identity of a node since we're basically concerned 
with effects towards children.
+         */
+        final Map<List<DependencyNode>, NodeInfo> infos;
+
+        /**
+         * The set of nodes on the DFS stack to detect cycles, technically 
keyed by the node's child list to match the
+         * dirty graph structure produced by the dependency collector for 
cycles.
+         */
+        final Map<List<DependencyNode>, Object> stack;
+
+        /**
+         * The stack of parent nodes.
+         */
+        final List<DependencyNode> parentNodes;
+
+        /**
+         * The stack of derived scopes for parent nodes.
+         */
+        final List<String> parentScopes;
+
+        /**
+         * The stack of derived optional flags for parent nodes.
+         */
+        final List<Boolean> parentOptionals;
+
+        /**
+         * The stack of node infos for parent nodes, may contain {@code null} 
which is used to disable creating new
+         * conflict items when visiting their parent again (conflict items are 
meant to be unique by parent-node combo).
+         */
+        final List<NodeInfo> parentInfos;
+
+        /**
+         * The conflict context passed to the version/scope/optionality 
selectors, updated as we move along rather than
+         * recreated to avoid tmp objects.
+         */
+        final ConflictContext conflictCtx;
+
+        /**
+         * The scope context passed to the scope deriver, updated as we move 
along rather than recreated to avoid tmp
+         * objects.
+         */
+        final ScopeContext scopeCtx;
+
+        /**
+         * The effective version selector, i.e. after initialization.
+         */
+        final VersionSelector versionSelector;
+
+        /**
+         * The effective scope selector, i.e. after initialization.
+         */
+        final ScopeSelector scopeSelector;
+
+        /**
+         * The effective scope deriver, i.e. after initialization.
+         */
+        final ScopeDeriver scopeDeriver;
+
+        /**
+         * The effective optionality selector, i.e. after initialization.
+         */
+        final OptionalitySelector optionalitySelector;
+
+        State( DependencyNode root, Map<?, ?> conflictIds, int conflictIdCount,
+               DependencyGraphTransformationContext context )
+            throws RepositoryException
+        {
+            this.conflictIds = conflictIds;
+            verbose = ConfigUtils.getBoolean( context.getSession(), false, 
CONFIG_PROP_VERBOSE );
+            potentialAncestorIds = new HashSet<Object>( conflictIdCount * 2 );
+            resolvedIds = new HashMap<Object, DependencyNode>( conflictIdCount 
* 2 );
+            items = new ArrayList<ConflictItem>( 256 );
+            infos = new IdentityHashMap<List<DependencyNode>, NodeInfo>( 64 );
+            stack = new IdentityHashMap<List<DependencyNode>, Object>( 64 );
+            parentNodes = new ArrayList<DependencyNode>( 64 );
+            parentScopes = new ArrayList<String>( 64 );
+            parentOptionals = new ArrayList<Boolean>( 64 );
+            parentInfos = new ArrayList<NodeInfo>( 64 );
+            conflictCtx = new ConflictContext( root, conflictIds, items );
+            scopeCtx = new ScopeContext( null, null );
+            versionSelector = 
ConflictResolver.this.versionSelector.getInstance( root, context );
+            scopeSelector = ConflictResolver.this.scopeSelector.getInstance( 
root, context );
+            scopeDeriver = ConflictResolver.this.scopeDeriver.getInstance( 
root, context );
+            optionalitySelector = 
ConflictResolver.this.optionalitySelector.getInstance( root, context );
+        }
+
+        void prepare( Object conflictId, Collection<Object> cyclicPredecessors 
)
+        {
+            currentId = conflictId;
+            conflictCtx.conflictId = conflictId;
+            conflictCtx.winner = null;
+            conflictCtx.scope = null;
+            conflictCtx.optional = null;
+            items.clear();
+            infos.clear();
+            if ( cyclicPredecessors != null )
+            {
+                potentialAncestorIds.addAll( cyclicPredecessors );
+            }
+        }
+
+        void finish()
+        {
+            List<DependencyNode> previousParent = null;
+            int previousDepth = 0;
+            totalConflictItems += items.size();
+            for ( int i = items.size() - 1; i >= 0; i-- )
+            {
+                ConflictItem item = items.get( i );
+                if ( item.parent == previousParent )
+                {
+                    item.depth = previousDepth;
+                }
+                else if ( item.parent != null )
+                {
+                    previousParent = item.parent;
+                    NodeInfo info = infos.get( previousParent );
+                    previousDepth = info.minDepth + 1;
+                    item.depth = previousDepth;
+                }
+            }
+            potentialAncestorIds.add( currentId );
+        }
+
+        void winner()
+        {
+            resolvedIds.put( currentId, ( conflictCtx.winner != null ) ? 
conflictCtx.winner.node : null );
+        }
+
+        boolean loser( DependencyNode node, Object conflictId )
+        {
+            DependencyNode winner = resolvedIds.get( conflictId );
+            return winner != null && winner != node;
+        }
+
+        boolean push( DependencyNode node, Object conflictId )
+            throws RepositoryException
+        {
+            if ( conflictId == null )
+            {
+                if ( node.getDependency() != null )
+                {
+                    if ( node.getData().get( NODE_DATA_WINNER ) != null )
+                    {
+                        return false;
+                    }
+                    throw new RepositoryException( "missing conflict id for 
node " + node );
+                }
+            }
+            else if ( !potentialAncestorIds.contains( conflictId ) )
+            {
+                return false;
+            }
+
+            List<DependencyNode> graphNode = node.getChildren();
+            if ( stack.put( graphNode, Boolean.TRUE ) != null )
+            {
+                return false;
+            }
+
+            int depth = depth();
+            String scope = deriveScope( node, conflictId );
+            boolean optional = deriveOptional( node, conflictId );
+            NodeInfo info = infos.get( graphNode );
+            if ( info == null )
+            {
+                info = new NodeInfo( depth, scope, optional );
+                infos.put( graphNode, info );
+                parentInfos.add( info );
+                parentNodes.add( node );
+                parentScopes.add( scope );
+                parentOptionals.add( optional );
+            }
+            else
+            {
+                int changes = info.update( depth, scope, optional );
+                if ( changes == 0 )
+                {
+                    stack.remove( graphNode );
+                    return false;
+                }
+                parentInfos.add( null ); // disable creating new conflict 
items, we update the existing ones below
+                parentNodes.add( node );
+                parentScopes.add( scope );
+                parentOptionals.add( optional );
+                if ( info.children != null )
+                {
+                    if ( ( changes & NodeInfo.CHANGE_SCOPE ) != 0 )
+                    {
+                        for ( int i = info.children.size() - 1; i >= 0; i-- )
+                        {
+                            ConflictItem item = info.children.get( i );
+                            String childScope = deriveScope( item.node, null );
+                            item.addScope( childScope );
+                        }
+                    }
+                    if ( ( changes & NodeInfo.CHANGE_OPTIONAL ) != 0 )
+                    {
+                        for ( int i = info.children.size() - 1; i >= 0; i-- )
+                        {
+                            ConflictItem item = info.children.get( i );
+                            boolean childOptional = deriveOptional( item.node, 
null );
+                            item.addOptional( childOptional );
+                        }
+                    }
+                }
+            }
+
+            return true;
+        }
+
+        void pop()
+        {
+            int last = parentInfos.size() - 1;
+            parentInfos.remove( last );
+            parentScopes.remove( last );
+            parentOptionals.remove( last );
+            DependencyNode node = parentNodes.remove( last );
+            stack.remove( node.getChildren() );
+        }
+
+        void add( DependencyNode node )
+            throws RepositoryException
+        {
+            DependencyNode parent = parent();
+            if ( parent == null )
+            {
+                ConflictItem item = newConflictItem( parent, node );
+                items.add( item );
+            }
+            else
+            {
+                NodeInfo info = parentInfos.get( parentInfos.size() - 1 );
+                if ( info != null )
+                {
+                    ConflictItem item = newConflictItem( parent, node );
+                    info.add( item );
+                    items.add( item );
+                }
+            }
+        }
+
+        private ConflictItem newConflictItem( DependencyNode parent, 
DependencyNode node )
+            throws RepositoryException
+        {
+            return new ConflictItem( parent, node, deriveScope( node, null ), 
deriveOptional( node, null ) );
+        }
+
+        private int depth()
+        {
+            return parentNodes.size();
+        }
+
+        private DependencyNode parent()
+        {
+            int size = parentNodes.size();
+            return ( size <= 0 ) ? null : parentNodes.get( size - 1 );
+        }
+
+        private String deriveScope( DependencyNode node, Object conflictId )
+            throws RepositoryException
+        {
+            if ( node.getPremanagedScope() != null
+                || ( conflictId != null && resolvedIds.containsKey( conflictId 
) ) )
+//            if ( ( node.getManagedBits() & DependencyNode.MANAGED_SCOPE ) != 0
+//                || ( conflictId != null && resolvedIds.containsKey( 
conflictId ) ) )
+            {
+                return scope( node.getDependency() );
+            }
+
+            int depth = parentNodes.size();
+            scopes( depth, node.getDependency() );
+            if ( depth > 0 )
+            {
+                scopeDeriver.deriveScope( scopeCtx );
+            }
+            return scopeCtx.derivedScope;
+        }
+
+        private void scopes( int parent, Dependency child )
+        {
+            scopeCtx.parentScope = ( parent > 0 ) ? parentScopes.get( parent - 
1 ) : null;
+            scopeCtx.derivedScope = scope( child );
+            scopeCtx.childScope = scopeCtx.derivedScope;
+        }
+
+        private String scope( Dependency dependency )
+        {
+            return ( dependency != null ) ? dependency.getScope() : null;
+        }
+
+        private boolean deriveOptional( DependencyNode node, Object conflictId 
)
+        {
+            Dependency dep = node.getDependency();
+            boolean optional = ( dep != null ) ? dep.isOptional() : false;
+            if ( optional
+                || ( node.getData().get( NODE_DATA_ORIGINAL_OPTIONALITY ) != 
null
+                    && ( (Boolean) node.getData().get( 
NODE_DATA_ORIGINAL_OPTIONALITY ) ).booleanValue() )
+                || ( conflictId != null && resolvedIds.containsKey( conflictId 
) ) )
+//            if ( optional || ( node.getManagedBits() & 
DependencyNode.MANAGED_OPTIONAL ) != 0
+//                || ( conflictId != null && resolvedIds.containsKey( 
conflictId ) ) )
+            {
+                return optional;
+            }
+            int depth = parentNodes.size();
+            return ( depth > 0 ) ? parentOptionals.get( depth - 1 ) : false;
+        }
+
+    }
+
+    /**
+     * A context used to hold information that is relevant for deriving the 
scope of a child dependency.
+     * 
+     * @see ScopeDeriver
+     * @noinstantiate This class is not intended to be instantiated by clients 
in production code, the constructor may
+     *                change without notice and only exists to enable unit 
testing.
+     */
+    public static final class ScopeContext
+    {
+
+        String parentScope;
+
+        String childScope;
+
+        String derivedScope;
+
+        /**
+         * Creates a new scope context with the specified properties.
+         * 
+         * @param parentScope The scope of the parent dependency, may be 
{@code null}.
+         * @param childScope The scope of the child dependency, may be {@code 
null}.
+         * @noreference This class is not intended to be instantiated by 
clients in production code, the constructor may
+         *              change without notice and only exists to enable unit 
testing.
+         */
+        public ScopeContext( String parentScope, String childScope )
+        {
+            this.parentScope = ( parentScope != null ) ? parentScope : "";
+            derivedScope = ( childScope != null ) ? childScope : "";
+            this.childScope = derivedScope;
+        }
+
+        /**
+         * Gets the scope of the parent dependency. This is usually the scope 
that was derived by earlier invocations of
+         * the scope deriver.
+         * 
+         * @return The scope of the parent dependency, never {@code null}.
+         */
+        public String getParentScope()
+        {
+            return parentScope;
+        }
+
+        /**
+         * Gets the original scope of the child dependency. This is the scope 
that was declared in the artifact
+         * descriptor of the parent dependency.
+         * 
+         * @return The original scope of the child dependency, never {@code 
null}.
+         */
+        public String getChildScope()
+        {
+            return childScope;
+        }
+
+        /**
+         * Gets the derived scope of the child dependency. This is initially 
equal to {@link #getChildScope()} until the
+         * scope deriver makes changes.
+         * 
+         * @return The derived scope of the child dependency, never {@code 
null}.
+         */
+        public String getDerivedScope()
+        {
+            return derivedScope;
+        }
+
+        /**
+         * Sets the derived scope of the child dependency.
+         * 
+         * @param derivedScope The derived scope of the dependency, may be 
{@code null}.
+         */
+        public void setDerivedScope( String derivedScope )
+        {
+            this.derivedScope = ( derivedScope != null ) ? derivedScope : "";
+        }
+
+    }
+
+    /**
+     * A conflicting dependency.
+     * 
+     * @noinstantiate This class is not intended to be instantiated by clients 
in production code, the constructor may
+     *                change without notice and only exists to enable unit 
testing.
+     */
+    public static final class ConflictItem
+    {
+
+        // nodes can share child lists, we care about the unique owner of a 
child node which is the child list
+        final List<DependencyNode> parent;
+
+        // only for debugging/toString() to help identify the parent node(s)
+        final Artifact artifact;
+
+        final DependencyNode node;
+
+        int depth;
+
+        // we start with String and update to Set<String> if needed
+        Object scopes;
+
+        // bit field of OPTIONAL_FALSE and OPTIONAL_TRUE
+        int optionalities;
+
+        /**
+         * Bit flag indicating whether one or more paths consider the 
dependency non-optional.
+         */
+        public static final int OPTIONAL_FALSE = 0x01;
+
+        /**
+         * Bit flag indicating whether one or more paths consider the 
dependency optional.
+         */
+        public static final int OPTIONAL_TRUE = 0x02;
+
+        ConflictItem( DependencyNode parent, DependencyNode node, String 
scope, boolean optional )
+        {
+            if ( parent != null )
+            {
+                this.parent = parent.getChildren();
+                this.artifact = parent.getDependency().getArtifact();
+//                this.artifact = parent.getArtifact();
+            }
+            else
+            {
+                this.parent = null;
+                this.artifact = null;
+            }
+            this.node = node;
+            this.scopes = scope;
+            this.optionalities = optional ? OPTIONAL_TRUE : OPTIONAL_FALSE;
+        }
+
+        /**
+         * Creates a new conflict item with the specified properties.
+         * 
+         * @param parent The parent node of the conflicting dependency, may be 
{@code null}.
+         * @param node The conflicting dependency, must not be {@code null}.
+         * @param depth The zero-based depth of the conflicting dependency.
+         * @param optionalities The optionalities the dependency was 
encountered with, encoded as a bit field consisting
+         *            of {@link ConflictResolver.ConflictItem#OPTIONAL_TRUE} 
and
+         *            {@link ConflictResolver.ConflictItem#OPTIONAL_FALSE}.
+         * @param scopes The derived scopes of the conflicting dependency, 
must not be {@code null}.
+         * @noreference This class is not intended to be instantiated by 
clients in production code, the constructor may
+         *              change without notice and only exists to enable unit 
testing.
+         */
+        public ConflictItem( DependencyNode parent, DependencyNode node, int 
depth, int optionalities,
+                             String... scopes )
+        {
+            this.parent = ( parent != null ) ? parent.getChildren() : null;
+            this.artifact = ( parent != null ) ? 
parent.getDependency().getArtifact() : null;
+//            this.artifact = ( parent != null ) ? parent.getArtifact() : null;
+            this.node = node;
+            this.depth = depth;
+            this.optionalities = optionalities;
+            this.scopes = Arrays.asList( scopes );
+        }
+
+        /**
+         * Determines whether the specified conflict item is a sibling of this 
item.
+         * 
+         * @param item The other conflict item, must not be {@code null}.
+         * @return {@code true} if the given item has the same parent as this 
item, {@code false} otherwise.
+         */
+        public boolean isSibling( ConflictItem item )
+        {
+            return parent == item.parent;
+        }
+
+        /**
+         * Gets the dependency node involved in the conflict.
+         * 
+         * @return The involved dependency node, never {@code null}.
+         */
+        public DependencyNode getNode()
+        {
+            return node;
+        }
+
+        /**
+         * Gets the dependency involved in the conflict, short for {@code 
getNode.getDependency()}.
+         * 
+         * @return The involved dependency, never {@code null}.
+         */
+        public Dependency getDependency()
+        {
+            return node.getDependency();
+        }
+
+        /**
+         * Gets the zero-based depth at which the conflicting node occurs in 
the graph. As such, the depth denotes the
+         * number of parent nodes. If actually multiple paths lead to the 
node, the return value denotes the smallest
+         * possible depth.
+         * 
+         * @return The zero-based depth of the node in the graph.
+         */
+        public int getDepth()
+        {
+            return depth;
+        }
+
+        /**
+         * Gets the derived scopes of the dependency. In general, the same 
dependency node could be reached via
+         * different paths and each path might result in a different derived 
scope.
+         * 
+         * @see ScopeDeriver
+         * @return The (read-only) set of derived scopes of the dependency, 
never {@code null}.
+         */
+        @SuppressWarnings( "unchecked" )
+        public Collection<String> getScopes()
+        {
+            if ( scopes instanceof String )
+            {
+                return Collections.singleton( (String) scopes );
+            }
+            return (Collection<String>) scopes;
+        }
+
+        @SuppressWarnings( "unchecked" )
+        void addScope( String scope )
+        {
+            if ( scopes instanceof Collection )
+            {
+                ( (Collection<String>) scopes ).add( scope );
+            }
+            else if ( !scopes.equals( scope ) )
+            {
+                Collection<Object> set = new HashSet<Object>();
+                set.add( scopes );
+                set.add( scope );
+                scopes = set;
+            }
+        }
+
+        /**
+         * Gets the derived optionalities of the dependency. In general, the 
same dependency node could be reached via
+         * different paths and each path might result in a different derived 
optionality.
+         * 
+         * @return A bit field consisting of {@link 
ConflictResolver.ConflictItem#OPTIONAL_FALSE} and/or
+         *         {@link ConflictResolver.ConflictItem#OPTIONAL_TRUE} 
indicating the derived optionalities the
+         *         dependency was encountered with.
+         */
+        public int getOptionalities()
+        {
+            return optionalities;
+        }
+
+        void addOptional( boolean optional )
+        {
+            optionalities |= optional ? OPTIONAL_TRUE : OPTIONAL_FALSE;
+        }
+
+        @Override
+        public String toString()
+        {
+            return node + " @ " + depth + " < " + artifact;
+        }
+
+    }
+
+    /**
+     * A context used to hold information that is relevant for resolving 
version and scope conflicts.
+     * 
+     * @see VersionSelector
+     * @see ScopeSelector
+     * @noinstantiate This class is not intended to be instantiated by clients 
in production code, the constructor may
+     *                change without notice and only exists to enable unit 
testing.
+     */
+    public static final class ConflictContext
+    {
+
+        final DependencyNode root;
+
+        final Map<?, ?> conflictIds;
+
+        final Collection<ConflictItem> items;
+
+        Object conflictId;
+
+        ConflictItem winner;
+
+        String scope;
+
+        Boolean optional;
+
+        ConflictContext( DependencyNode root, Map<?, ?> conflictIds, 
Collection<ConflictItem> items )
+        {
+            this.root = root;
+            this.conflictIds = conflictIds;
+            this.items = Collections.unmodifiableCollection( items );
+        }
+
+        /**
+         * Creates a new conflict context.
+         * 
+         * @param root The root node of the dependency graph, must not be 
{@code null}.
+         * @param conflictId The conflict id for the set of conflicting 
dependencies in this context, must not be
+         *            {@code null}.
+         * @param conflictIds The mapping from dependency node to conflict id, 
must not be {@code null}.
+         * @param items The conflict items in this context, must not be {@code 
null}.
+         * @noreference This class is not intended to be instantiated by 
clients in production code, the constructor may
+         *              change without notice and only exists to enable unit 
testing.
+         */
+        public ConflictContext( DependencyNode root, Object conflictId, 
Map<DependencyNode, Object> conflictIds,
+                                Collection<ConflictItem> items )
+        {
+            this( root, conflictIds, items );
+            this.conflictId = conflictId;
+        }
+
+        /**
+         * Gets the root node of the dependency graph being transformed.
+         * 
+         * @return The root node of the dependeny graph, never {@code null}.
+         */
+        public DependencyNode getRoot()
+        {
+            return root;
+        }
+
+        /**
+         * Determines whether the specified dependency node belongs to this 
conflict context.
+         * 
+         * @param node The dependency node to check, must not be {@code null}.
+         * @return {@code true} if the given node belongs to this conflict 
context, {@code false} otherwise.
+         */
+        public boolean isIncluded( DependencyNode node )
+        {
+            return conflictId.equals( conflictIds.get( node ) );
+        }
+
+        /**
+         * Gets the collection of conflict items in this context.
+         * 
+         * @return The (read-only) collection of conflict items in this 
context, never {@code null}.
+         */
+        public Collection<ConflictItem> getItems()
+        {
+            return items;
+        }
+
+        /**
+         * Gets the conflict item which has been selected as the winner among 
the conflicting dependencies.
+         * 
+         * @return The winning conflict item or {@code null} if not set yet.
+         */
+        public ConflictItem getWinner()
+        {
+            return winner;
+        }
+
+        /**
+         * Sets the conflict item which has been selected as the winner among 
the conflicting dependencies.
+         * 
+         * @param winner The winning conflict item, may be {@code null}.
+         */
+        public void setWinner( ConflictItem winner )
+        {
+            this.winner = winner;
+        }
+
+        /**
+         * Gets the effective scope of the winning dependency.
+         * 
+         * @return The effective scope of the winning dependency or {@code 
null} if none.
+         */
+        public String getScope()
+        {
+            return scope;
+        }
+
+        /**
+         * Sets the effective scope of the winning dependency.
+         * 
+         * @param scope The effective scope, may be {@code null}.
+         */
+        public void setScope( String scope )
+        {
+            this.scope = scope;
+        }
+
+        /**
+         * Gets the effective optional flag of the winning dependency.
+         * 
+         * @return The effective optional flag or {@code null} if none.
+         */
+        public Boolean getOptional()
+        {
+            return optional;
+        }
+
+        /**
+         * Sets the effective optional flag of the winning dependency.
+         * 
+         * @param optional The effective optional flag, may be {@code null}.
+         */
+        public void setOptional( Boolean optional )
+        {
+            this.optional = optional;
+        }
+
+        @Override
+        public String toString()
+        {
+            return winner + " @ " + scope + " < " + items;
+        }
+
+    }
+
+    /**
+     * An extension point of {@link ConflictResolver} that determines the 
winner among conflicting dependencies. The
+     * winning node (and its children) will be retained in the dependency 
graph, the other nodes will get removed. The
+     * version selector does not need to deal with potential scope conflicts, 
these will be addressed afterwards by the
+     * {@link ScopeSelector}. Implementations must be stateless.
+     */
+    public abstract static class VersionSelector
+    {
+
+        /**
+         * Retrieves the version selector for use during the specified graph 
transformation. The conflict resolver calls
+         * this method once per
+         * {@link ConflictResolver#transformGraph(DependencyNode, 
DependencyGraphTransformationContext)} invocation to
+         * allow implementations to prepare any auxiliary data that is needed 
for their operation. Given that
+         * implementations need to be stateless, a new instance needs to be 
returned to hold such auxiliary data. The
+         * default implementation simply returns the current instance which is 
appropriate for implementations which do
+         * not require auxiliary data.
+         * 
+         * @param root The root node of the (possibly cyclic!) graph to 
transform, must not be {@code null}.
+         * @param context The graph transformation context, must not be {@code 
null}.
+         * @return The scope deriver to use for the given graph 
transformation, never {@code null}.
+         * @throws RepositoryException If the instance could not be retrieved.
+         */
+        public VersionSelector getInstance( DependencyNode root, 
DependencyGraphTransformationContext context )
+            throws RepositoryException
+        {
+            return this;
+        }
+
+        /**
+         * Determines the winning node among conflicting dependencies. 
Implementations will usually iterate
+         * {@link ConflictContext#getItems()}, inspect {@link 
ConflictItem#getNode()} and eventually call
+         * {@link ConflictContext#setWinner(ConflictResolver.ConflictItem)} to 
deliver the winner. Failure to select a
+         * winner will automatically fail the entire conflict resolution.
+         * 
+         * @param context The conflict context, must not be {@code null}.
+         * @throws RepositoryException If the version selection failed.
+         */
+        public abstract void selectVersion( ConflictContext context )
+            throws RepositoryException;
+
+    }
+
+    /**
+     * An extension point of {@link ConflictResolver} that determines the 
effective scope of a dependency from a
+     * potentially conflicting set of {@link ScopeDeriver derived scopes}. The 
scope selector gets invoked after the
+     * {@link VersionSelector} has picked the winning node. Implementations 
must be stateless.
+     */
+    public abstract static class ScopeSelector
+    {
+
+        /**
+         * Retrieves the scope selector for use during the specified graph 
transformation. The conflict resolver calls
+         * this method once per
+         * {@link ConflictResolver#transformGraph(DependencyNode, 
DependencyGraphTransformationContext)} invocation to
+         * allow implementations to prepare any auxiliary data that is needed 
for their operation. Given that
+         * implementations need to be stateless, a new instance needs to be 
returned to hold such auxiliary data. The
+         * default implementation simply returns the current instance which is 
appropriate for implementations which do
+         * not require auxiliary data.
+         * 
+         * @param root The root node of the (possibly cyclic!) graph to 
transform, must not be {@code null}.
+         * @param context The graph transformation context, must not be {@code 
null}.
+         * @return The scope selector to use for the given graph 
transformation, never {@code null}.
+         * @throws RepositoryException If the instance could not be retrieved.
+         */
+        public ScopeSelector getInstance( DependencyNode root, 
DependencyGraphTransformationContext context )
+            throws RepositoryException
+        {
+            return this;
+        }
+
+        /**
+         * Determines the effective scope of the dependency given by {@link 
ConflictContext#getWinner()}.
+         * Implementations will usually iterate {@link 
ConflictContext#getItems()}, inspect
+         * {@link ConflictItem#getScopes()} and eventually call {@link 
ConflictContext#setScope(String)} to deliver the
+         * effective scope.
+         * 
+         * @param context The conflict context, must not be {@code null}.
+         * @throws RepositoryException If the scope selection failed.
+         */
+        public abstract void selectScope( ConflictContext context )
+            throws RepositoryException;
+
+    }
+
+    /**
+     * An extension point of {@link ConflictResolver} that determines the 
scope of a dependency in relation to the scope
+     * of its parent. Implementations must be stateless.
+     */
+    public abstract static class ScopeDeriver
+    {
+
+        /**
+         * Retrieves the scope deriver for use during the specified graph 
transformation. The conflict resolver calls
+         * this method once per
+         * {@link ConflictResolver#transformGraph(DependencyNode, 
DependencyGraphTransformationContext)} invocation to
+         * allow implementations to prepare any auxiliary data that is needed 
for their operation. Given that
+         * implementations need to be stateless, a new instance needs to be 
returned to hold such auxiliary data. The
+         * default implementation simply returns the current instance which is 
appropriate for implementations which do
+         * not require auxiliary data.
+         * 
+         * @param root The root node of the (possibly cyclic!) graph to 
transform, must not be {@code null}.
+         * @param context The graph transformation context, must not be {@code 
null}.
+         * @return The scope deriver to use for the given graph 
transformation, never {@code null}.
+         * @throws RepositoryException If the instance could not be retrieved.
+         */
+        public ScopeDeriver getInstance( DependencyNode root, 
DependencyGraphTransformationContext context )
+            throws RepositoryException
+        {
+            return this;
+        }
+
+        /**
+         * Determines the scope of a dependency in relation to the scope of 
its parent. Implementors need to call
+         * {@link ScopeContext#setDerivedScope(String)} to deliver the result 
of their calculation. If said method is
+         * not invoked, the conflict resolver will assume the scope of the 
child dependency remains unchanged.
+         * 
+         * @param context The scope context, must not be {@code null}.
+         * @throws RepositoryException If the scope deriviation failed.
+         */
+        public abstract void deriveScope( ScopeContext context )
+            throws RepositoryException;
+
+    }
+
+    /**
+     * An extension point of {@link ConflictResolver} that determines the 
effective optional flag of a dependency from a
+     * potentially conflicting set of derived optionalities. The optionality 
selector gets invoked after the
+     * {@link VersionSelector} has picked the winning node. Implementations 
must be stateless.
+     */
+    public abstract static class OptionalitySelector
+    {
+
+        /**
+         * Retrieves the optionality selector for use during the specified 
graph transformation. The conflict resolver
+         * calls this method once per
+         * {@link ConflictResolver#transformGraph(DependencyNode, 
DependencyGraphTransformationContext)} invocation to
+         * allow implementations to prepare any auxiliary data that is needed 
for their operation. Given that
+         * implementations need to be stateless, a new instance needs to be 
returned to hold such auxiliary data. The
+         * default implementation simply returns the current instance which is 
appropriate for implementations which do
+         * not require auxiliary data.
+         * 
+         * @param root The root node of the (possibly cyclic!) graph to 
transform, must not be {@code null}.
+         * @param context The graph transformation context, must not be {@code 
null}.
+         * @return The optionality selector to use for the given graph 
transformation, never {@code null}.
+         * @throws RepositoryException If the instance could not be retrieved.
+         */
+        public OptionalitySelector getInstance( DependencyNode root, 
DependencyGraphTransformationContext context )
+            throws RepositoryException
+        {
+            return this;
+        }
+
+        /**
+         * Determines the effective optional flag of the dependency given by 
{@link ConflictContext#getWinner()}.
+         * Implementations will usually iterate {@link 
ConflictContext#getItems()}, inspect
+         * {@link ConflictItem#getOptionalities()} and eventually call {@link 
ConflictContext#setOptional(Boolean)} to
+         * deliver the effective optional flag.
+         * 
+         * @param context The conflict context, must not be {@code null}.
+         * @throws RepositoryException If the optionality selection failed.
+         */
+        public abstract void selectOptionality( ConflictContext context )
+            throws RepositoryException;
+
+    }
+
+}
diff --git 
a/src/main/java/org/apache/maven/shared/dependency/graph/internal/maven30/ExclusionDependencySelector.java
 
b/src/main/java/org/apache/maven/shared/dependency/graph/internal/maven30/ExclusionDependencySelector.java
new file mode 100644
index 0000000..36f2bd7
--- /dev/null
+++ 
b/src/main/java/org/apache/maven/shared/dependency/graph/internal/maven30/ExclusionDependencySelector.java
@@ -0,0 +1,228 @@
+package org.apache.maven.shared.dependency.graph.internal.maven30;
+
+/*
+ * 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.Arrays;
+import java.util.Collection;
+import java.util.Comparator;
+import java.util.TreeSet;
+
+import org.sonatype.aether.artifact.Artifact;
+import org.sonatype.aether.collection.DependencyCollectionContext;
+import org.sonatype.aether.collection.DependencySelector;
+import org.sonatype.aether.graph.Dependency;
+import org.sonatype.aether.graph.Exclusion;
+
+/**
+ * This class is a copy of their homonymous in the Eclipse Aether library, 
adapted to work with Sonatype Aether.
+ * 
+ * @author Gabriel Belingueres
+ * @since 3.0.2
+ */
+public class ExclusionDependencySelector
+    implements DependencySelector
+{
+
+    // sorted and dupe-free array, faster to iterate than LinkedHashSet
+    private final Exclusion[] exclusions;
+
+    private int hashCode;
+
+    /**
+     * Creates a new selector without any exclusions.
+     */
+    public ExclusionDependencySelector()
+    {
+        this.exclusions = new Exclusion[0];
+    }
+
+    /**
+     * Creates a new selector with the specified exclusions.
+     * 
+     * @param exclusions The exclusions, may be {@code null}.
+     */
+    public ExclusionDependencySelector( Collection<Exclusion> exclusions )
+    {
+        if ( exclusions != null && !exclusions.isEmpty() )
+        {
+            TreeSet<Exclusion> sorted = new TreeSet<Exclusion>( 
ExclusionComparator.INSTANCE );
+            sorted.addAll( exclusions );
+            this.exclusions = sorted.toArray( new Exclusion[sorted.size()] );
+        }
+        else
+        {
+            this.exclusions = new Exclusion[0];
+        }
+    }
+
+    private ExclusionDependencySelector( Exclusion[] exclusions )
+    {
+        this.exclusions = exclusions;
+    }
+
+    public boolean selectDependency( Dependency dependency )
+    {
+        Artifact artifact = dependency.getArtifact();
+        for ( Exclusion exclusion : exclusions )
+        {
+            if ( matches( exclusion, artifact ) )
+            {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    private boolean matches( Exclusion exclusion, Artifact artifact )
+    {
+        if ( !matches( exclusion.getArtifactId(), artifact.getArtifactId() ) )
+        {
+            return false;
+        }
+        if ( !matches( exclusion.getGroupId(), artifact.getGroupId() ) )
+        {
+            return false;
+        }
+        if ( !matches( exclusion.getExtension(), artifact.getExtension() ) )
+        {
+            return false;
+        }
+        if ( !matches( exclusion.getClassifier(), artifact.getClassifier() ) )
+        {
+            return false;
+        }
+        return true;
+    }
+
+    private boolean matches( String pattern, String value )
+    {
+        return "*".equals( pattern ) || pattern.equals( value );
+    }
+
+    public DependencySelector deriveChildSelector( DependencyCollectionContext 
context )
+    {
+        Dependency dependency = context.getDependency();
+        Collection<Exclusion> exclusions = ( dependency != null ) ? 
dependency.getExclusions() : null;
+        if ( exclusions == null || exclusions.isEmpty() )
+        {
+            return this;
+        }
+
+        Exclusion[] merged = this.exclusions;
+        int count = merged.length;
+        for ( Exclusion exclusion : exclusions )
+        {
+            int index = Arrays.binarySearch( merged, exclusion, 
ExclusionComparator.INSTANCE );
+            if ( index < 0 )
+            {
+                index = -( index + 1 );
+                if ( count >= merged.length )
+                {
+                    Exclusion[] tmp = new Exclusion[merged.length + 
exclusions.size()];
+                    System.arraycopy( merged, 0, tmp, 0, index );
+                    tmp[index] = exclusion;
+                    System.arraycopy( merged, index, tmp, index + 1, count - 
index );
+                    merged = tmp;
+                }
+                else
+                {
+                    System.arraycopy( merged, index, merged, index + 1, count 
- index );
+                    merged[index] = exclusion;
+                }
+                count++;
+            }
+        }
+        if ( merged == this.exclusions )
+        {
+            return this;
+        }
+        if ( merged.length != count )
+        {
+            Exclusion[] tmp = new Exclusion[count];
+            System.arraycopy( merged, 0, tmp, 0, count );
+            merged = tmp;
+        }
+
+        return new ExclusionDependencySelector( merged );
+    }
+
+    @Override
+    public boolean equals( Object obj )
+    {
+        if ( this == obj )
+        {
+            return true;
+        }
+        else if ( null == obj || !getClass().equals( obj.getClass() ) )
+        {
+            return false;
+        }
+
+        ExclusionDependencySelector that = (ExclusionDependencySelector) obj;
+        return Arrays.equals( exclusions, that.exclusions );
+    }
+
+    @Override
+    public int hashCode()
+    {
+        if ( hashCode == 0 )
+        {
+            int hash = getClass().hashCode();
+            hash = hash * 31 + Arrays.hashCode( exclusions );
+            hashCode = hash;
+        }
+        return hashCode;
+    }
+
+    private static class ExclusionComparator
+        implements Comparator<Exclusion>
+    {
+
+        static final ExclusionComparator INSTANCE = new ExclusionComparator();
+
+        public int compare( Exclusion e1, Exclusion e2 )
+        {
+            if ( e1 == null )
+            {
+                return ( e2 == null ) ? 0 : 1;
+            }
+            else if ( e2 == null )
+            {
+                return -1;
+            }
+            int rel = e1.getArtifactId().compareTo( e2.getArtifactId() );
+            if ( rel == 0 )
+            {
+                rel = e1.getGroupId().compareTo( e2.getGroupId() );
+                if ( rel == 0 )
+                {
+                    rel = e1.getExtension().compareTo( e2.getExtension() );
+                    if ( rel == 0 )
+                    {
+                        rel = e1.getClassifier().compareTo( e2.getClassifier() 
);
+                    }
+                }
+            }
+            return rel;
+        }
+
+    }
+
+}
diff --git 
a/src/main/java/org/apache/maven/shared/dependency/graph/internal/maven30/JavaScopeDeriver.java
 
b/src/main/java/org/apache/maven/shared/dependency/graph/internal/maven30/JavaScopeDeriver.java
new file mode 100644
index 0000000..73367bf
--- /dev/null
+++ 
b/src/main/java/org/apache/maven/shared/dependency/graph/internal/maven30/JavaScopeDeriver.java
@@ -0,0 +1,72 @@
+package org.apache.maven.shared.dependency.graph.internal.maven30;
+
+/*
+ * 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.apache.maven.shared.dependency.graph.internal.maven30.ConflictResolver.ScopeContext;
+import 
org.apache.maven.shared.dependency.graph.internal.maven30.ConflictResolver.ScopeDeriver;
+import org.sonatype.aether.RepositoryException;
+import org.sonatype.aether.util.artifact.JavaScopes;
+
+/**
+ * This class is a copy of their homonymous in the Eclipse Aether library, 
adapted to work with Sonatype Aether.
+ * 
+ * @author Gabriel Belingueres
+ * @since 3.0.2
+ */
+public class JavaScopeDeriver
+    extends ScopeDeriver
+{
+
+    @Override
+    public void deriveScope( ScopeContext context )
+        throws RepositoryException
+    {
+        context.setDerivedScope( getDerivedScope( context.getParentScope(), 
context.getChildScope() ) );
+    }
+
+    private String getDerivedScope( String parentScope, String childScope )
+    {
+        String derivedScope;
+
+        if ( JavaScopes.SYSTEM.equals( childScope ) || JavaScopes.TEST.equals( 
childScope ) )
+        {
+            derivedScope = childScope;
+        }
+        else if ( parentScope == null || parentScope.length() <= 0 || 
JavaScopes.COMPILE.equals( parentScope ) )
+        {
+            derivedScope = childScope;
+        }
+        else if ( JavaScopes.TEST.equals( parentScope ) || 
JavaScopes.RUNTIME.equals( parentScope ) )
+        {
+            derivedScope = parentScope;
+        }
+        else if ( JavaScopes.SYSTEM.equals( parentScope ) || 
JavaScopes.PROVIDED.equals( parentScope ) )
+        {
+            derivedScope = JavaScopes.PROVIDED;
+        }
+        else
+        {
+            derivedScope = JavaScopes.RUNTIME;
+        }
+
+        return derivedScope;
+    }
+
+}
diff --git 
a/src/main/java/org/apache/maven/shared/dependency/graph/internal/maven30/JavaScopeSelector.java
 
b/src/main/java/org/apache/maven/shared/dependency/graph/internal/maven30/JavaScopeSelector.java
new file mode 100644
index 0000000..6eeea83
--- /dev/null
+++ 
b/src/main/java/org/apache/maven/shared/dependency/graph/internal/maven30/JavaScopeSelector.java
@@ -0,0 +1,102 @@
+package org.apache.maven.shared.dependency.graph.internal.maven30;
+
+/*
+ * 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.Collection;
+import java.util.HashSet;
+import java.util.Set;
+
+import 
org.apache.maven.shared.dependency.graph.internal.maven30.ConflictResolver.ConflictContext;
+import 
org.apache.maven.shared.dependency.graph.internal.maven30.ConflictResolver.ConflictItem;
+import 
org.apache.maven.shared.dependency.graph.internal.maven30.ConflictResolver.ScopeSelector;
+import org.sonatype.aether.RepositoryException;
+import org.sonatype.aether.util.artifact.JavaScopes;
+
+/**
+ * This class is a copy of their homonymous in the Eclipse Aether library, 
adapted to work with Sonatype Aether.
+ * 
+ * @author Gabriel Belingueres
+ * @since 3.0.2
+ */
+public final class JavaScopeSelector
+    extends ScopeSelector
+{
+
+    @Override
+    public void selectScope( ConflictContext context )
+        throws RepositoryException
+    {
+        String scope = context.getWinner().getDependency().getScope();
+        if ( !JavaScopes.SYSTEM.equals( scope ) )
+        {
+            scope = chooseEffectiveScope( context.getItems() );
+        }
+        context.setScope( scope );
+    }
+
+    private String chooseEffectiveScope( Collection<ConflictItem> items )
+    {
+        Set<String> scopes = new HashSet<String>();
+        for ( ConflictItem item : items )
+        {
+            if ( item.getDepth() <= 1 )
+            {
+                return item.getDependency().getScope();
+            }
+            scopes.addAll( item.getScopes() );
+        }
+        return chooseEffectiveScope( scopes );
+    }
+
+    private String chooseEffectiveScope( Set<String> scopes )
+    {
+        if ( scopes.size() > 1 )
+        {
+            scopes.remove( JavaScopes.SYSTEM );
+        }
+
+        String effectiveScope = "";
+
+        if ( scopes.size() == 1 )
+        {
+            effectiveScope = scopes.iterator().next();
+        }
+        else if ( scopes.contains( JavaScopes.COMPILE ) )
+        {
+            effectiveScope = JavaScopes.COMPILE;
+        }
+        else if ( scopes.contains( JavaScopes.RUNTIME ) )
+        {
+            effectiveScope = JavaScopes.RUNTIME;
+        }
+        else if ( scopes.contains( JavaScopes.PROVIDED ) )
+        {
+            effectiveScope = JavaScopes.PROVIDED;
+        }
+        else if ( scopes.contains( JavaScopes.TEST ) )
+        {
+            effectiveScope = JavaScopes.TEST;
+        }
+
+        return effectiveScope;
+    }
+
+
+}
diff --git 
a/src/main/java/org/apache/maven/shared/dependency/graph/internal/maven30/Maven3DirectScopeDependencySelector.java
 
b/src/main/java/org/apache/maven/shared/dependency/graph/internal/maven30/Maven3DirectScopeDependencySelector.java
new file mode 100644
index 0000000..e66b80b
--- /dev/null
+++ 
b/src/main/java/org/apache/maven/shared/dependency/graph/internal/maven30/Maven3DirectScopeDependencySelector.java
@@ -0,0 +1,132 @@
+package org.apache.maven.shared.dependency.graph.internal.maven30;
+
+/*
+ * 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.sonatype.aether.collection.DependencyCollectionContext;
+import org.sonatype.aether.collection.DependencySelector;
+import org.sonatype.aether.graph.Dependency;
+
+/**
+ * A dependency selector that excludes dependencies of an specific Scope which 
occur beyond level one of the dependency
+ * graph.
+ * 
+ * @see {@link Dependency#getScope()}
+ * @author Gabriel Belingueres
+ * @since 3.0.2
+ */
+public class Maven3DirectScopeDependencySelector
+    implements DependencySelector
+{
+
+    private final String scope;
+
+    private final int depth;
+
+    public Maven3DirectScopeDependencySelector( String scope )
+    {
+        this( scope, 0 );
+    }
+
+    private Maven3DirectScopeDependencySelector( String scope, int depth )
+    {
+        if ( scope == null )
+        {
+            throw new IllegalArgumentException( "scope is null!" );
+        }
+        this.scope = scope;
+        this.depth = depth;
+    }
+
+    /**
+     * Decides whether the specified dependency should be included in the 
dependency graph.
+     * 
+     * @param dependency The dependency to check, must not be {@code null}.
+     * @return {@code false} if the dependency should be excluded from the 
children of the current node, {@code true}
+     *         otherwise.
+     */
+    @Override
+    public boolean selectDependency( Dependency dependency )
+    {
+        return depth < 2 || !scope.equals( dependency.getScope() );
+    }
+
+    /**
+     * Derives a dependency selector for the specified collection context. 
When calculating the child selector,
+     * implementors are strongly advised to simply return the current instance 
if nothing changed to help save memory.
+     * 
+     * @param context The dependency collection context, must not be {@code 
null}.
+     * @return The dependency selector for the target node, must not be {@code 
null}.
+     */
+    @Override
+    public DependencySelector deriveChildSelector( DependencyCollectionContext 
context )
+    {
+        if ( depth >= 2 )
+        {
+            return this;
+        }
+
+        return new Maven3DirectScopeDependencySelector( scope, depth + 1 );
+    }
+
+    @Override
+    public int hashCode()
+    {
+        final int prime = 31;
+        int result = 1;
+        result = prime * result + depth;
+        result = prime * result + ( ( scope == null ) ? 0 : scope.hashCode() );
+        return result;
+    }
+
+    @Override
+    public boolean equals( Object obj )
+    {
+        if ( this == obj )
+        {
+            return true;
+        }
+        if ( obj == null )
+        {
+            return false;
+        }
+        if ( getClass() != obj.getClass() )
+        {
+            return false;
+        }
+        Maven3DirectScopeDependencySelector other = 
(Maven3DirectScopeDependencySelector) obj;
+        if ( depth != other.depth )
+        {
+            return false;
+        }
+        if ( scope == null )
+        {
+            if ( other.scope != null )
+            {
+                return false;
+            }
+        }
+        else if ( !scope.equals( other.scope ) )
+        {
+            return false;
+        }
+        return true;
+    }
+
+}
diff --git 
a/src/main/java/org/apache/maven/shared/dependency/graph/internal/maven30/NearestVersionSelector.java
 
b/src/main/java/org/apache/maven/shared/dependency/graph/internal/maven30/NearestVersionSelector.java
new file mode 100644
index 0000000..8d2bc93
--- /dev/null
+++ 
b/src/main/java/org/apache/maven/shared/dependency/graph/internal/maven30/NearestVersionSelector.java
@@ -0,0 +1,183 @@
+package org.apache.maven.shared.dependency.graph.internal.maven30;
+
+/*
+ * 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 java.util.Collection;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
+
+import 
org.apache.maven.shared.dependency.graph.internal.maven30.ConflictResolver.ConflictContext;
+import 
org.apache.maven.shared.dependency.graph.internal.maven30.ConflictResolver.ConflictItem;
+import 
org.apache.maven.shared.dependency.graph.internal.maven30.ConflictResolver.VersionSelector;
+import org.sonatype.aether.RepositoryException;
+import org.sonatype.aether.collection.UnsolvableVersionConflictException;
+import org.sonatype.aether.graph.DependencyFilter;
+import org.sonatype.aether.graph.DependencyNode;
+import org.sonatype.aether.util.graph.PathRecordingDependencyVisitor;
+import org.sonatype.aether.version.Version;
+import org.sonatype.aether.version.VersionConstraint;
+
+/**
+ * This class is a copy of their homonymous in the Eclipse Aether library, 
adapted to work with Sonatype Aether.
+ * 
+ * @author Gabriel Belingueres
+ * @since 3.0.2
+ */
+public final class NearestVersionSelector
+    extends VersionSelector
+{
+
+    @Override
+    public void selectVersion( ConflictContext context )
+        throws RepositoryException
+    {
+        ConflictGroup group = new ConflictGroup();
+        for ( ConflictItem item : context.getItems() )
+        {
+            DependencyNode node = item.getNode();
+            VersionConstraint constraint = node.getVersionConstraint();
+
+            boolean backtrack = false;
+            boolean hardConstraint = !constraint.getRanges().isEmpty();
+//            boolean hardConstraint = constraint.getRange() != null;
+
+            if ( hardConstraint )
+            {
+                if ( group.constraints.add( constraint ) )
+                {
+                    if ( group.winner != null && !constraint.containsVersion( 
group.winner.getNode().getVersion() ) )
+                    {
+                        backtrack = true;
+                    }
+                }
+            }
+
+            if ( isAcceptable( group, node.getVersion() ) )
+            {
+                group.candidates.add( item );
+
+                if ( backtrack )
+                {
+                    backtrack( group, context );
+                }
+                else if ( group.winner == null || isNearer( item, group.winner 
) )
+                {
+                    group.winner = item;
+                }
+            }
+            else if ( backtrack )
+            {
+                backtrack( group, context );
+            }
+        }
+        context.setWinner( group.winner );
+    }
+
+    private void backtrack( ConflictGroup group, ConflictContext context )
+        throws UnsolvableVersionConflictException
+    {
+        group.winner = null;
+
+        for ( Iterator<ConflictItem> it = group.candidates.iterator(); 
it.hasNext(); )
+        {
+            ConflictItem candidate = it.next();
+
+            if ( !isAcceptable( group, candidate.getNode().getVersion() ) )
+            {
+                it.remove();
+            }
+            else if ( group.winner == null || isNearer( candidate, 
group.winner ) )
+            {
+                group.winner = candidate;
+            }
+        }
+
+        if ( group.winner == null )
+        {
+            throw newFailure( context );
+        }
+    }
+
+    private boolean isAcceptable( ConflictGroup group, Version version )
+    {
+        for ( VersionConstraint constraint : group.constraints )
+        {
+            if ( !constraint.containsVersion( version ) )
+            {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    private boolean isNearer( ConflictItem item1, ConflictItem item2 )
+    {
+        if ( item1.isSibling( item2 ) )
+        {
+            return item1.getNode().getVersion().compareTo( 
item2.getNode().getVersion() ) > 0;
+        }
+        else
+        {
+            return item1.getDepth() < item2.getDepth();
+        }
+    }
+
+    private UnsolvableVersionConflictException newFailure( final 
ConflictContext context )
+    {
+        DependencyFilter filter = new DependencyFilter()
+        {
+            public boolean accept( DependencyNode node, List<DependencyNode> 
parents )
+            {
+                return context.isIncluded( node );
+            }
+        };
+        PathRecordingDependencyVisitor visitor = new 
PathRecordingDependencyVisitor( filter );
+        context.getRoot().accept( visitor );
+        return new UnsolvableVersionConflictException( visitor.getPaths(), 
context.conflictId );
+//        return new UnsolvableVersionConflictException( visitor.getPaths() );
+    }
+
+    static final class ConflictGroup
+    {
+
+        final Collection<VersionConstraint> constraints;
+
+        final Collection<ConflictItem> candidates;
+
+        ConflictItem winner;
+
+        ConflictGroup()
+        {
+            constraints = new HashSet<VersionConstraint>();
+            candidates = new ArrayList<ConflictItem>( 64 );
+        }
+
+        @Override
+        public String toString()
+        {
+            return String.valueOf( winner );
+        }
+
+    }
+
+
+}
diff --git 
a/src/main/java/org/apache/maven/shared/dependency/graph/internal/maven30/SimpleOptionalitySelector.java
 
b/src/main/java/org/apache/maven/shared/dependency/graph/internal/maven30/SimpleOptionalitySelector.java
new file mode 100644
index 0000000..ffd6a63
--- /dev/null
+++ 
b/src/main/java/org/apache/maven/shared/dependency/graph/internal/maven30/SimpleOptionalitySelector.java
@@ -0,0 +1,64 @@
+package org.apache.maven.shared.dependency.graph.internal.maven30;
+
+/*
+ * 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.Collection;
+
+import 
org.apache.maven.shared.dependency.graph.internal.maven30.ConflictResolver.ConflictContext;
+import 
org.apache.maven.shared.dependency.graph.internal.maven30.ConflictResolver.ConflictItem;
+import 
org.apache.maven.shared.dependency.graph.internal.maven30.ConflictResolver.OptionalitySelector;
+import org.sonatype.aether.RepositoryException;
+
+/**
+ * This class is a copy of their homonymous in the Eclipse Aether library, 
adapted to work with Sonatype Aether.
+ * 
+ * @author Gabriel Belingueres
+ * @since 3.0.2
+ */
+public class SimpleOptionalitySelector
+    extends OptionalitySelector
+{
+
+    @Override
+    public void selectOptionality( ConflictContext context )
+        throws RepositoryException
+    {
+        boolean optional = chooseEffectiveOptionality( context.getItems() );
+        context.setOptional( optional );
+    }
+
+    private boolean chooseEffectiveOptionality( Collection<ConflictItem> items 
)
+    {
+        boolean optional = true;
+        for ( ConflictItem item : items )
+        {
+            if ( item.getDepth() <= 1 )
+            {
+                return item.getDependency().isOptional();
+            }
+            if ( ( item.getOptionalities() & ConflictItem.OPTIONAL_FALSE ) != 
0 )
+            {
+                optional = false;
+            }
+        }
+        return optional;
+    }
+
+}
\ No newline at end of file
diff --git 
a/src/main/java/org/apache/maven/shared/dependency/graph/internal/maven31/Maven31DirectScopeDependencySelector.java
 
b/src/main/java/org/apache/maven/shared/dependency/graph/internal/maven31/Maven31DirectScopeDependencySelector.java
new file mode 100644
index 0000000..dbc71c5
--- /dev/null
+++ 
b/src/main/java/org/apache/maven/shared/dependency/graph/internal/maven31/Maven31DirectScopeDependencySelector.java
@@ -0,0 +1,132 @@
+package org.apache.maven.shared.dependency.graph.internal.maven31;
+
+/*
+ * 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.collection.DependencyCollectionContext;
+import org.eclipse.aether.collection.DependencySelector;
+import org.eclipse.aether.graph.Dependency;
+
+/**
+ * A dependency selector that excludes dependencies of an specific Scope which 
occur beyond level one of the dependency
+ * graph.
+ * 
+ * @see {@link Dependency#getScope()}
+ * @author Gabriel Belingueres
+ * @since 3.0.2
+ */
+public class Maven31DirectScopeDependencySelector
+    implements DependencySelector
+{
+
+    private final String scope;
+
+    private final int depth;
+
+    public Maven31DirectScopeDependencySelector( String scope )
+    {
+        this( scope, 0 );
+    }
+
+    private Maven31DirectScopeDependencySelector( String scope, int depth )
+    {
+        if ( scope == null )
+        {
+            throw new IllegalArgumentException( "scope is null!" );
+        }
+        this.scope = scope;
+        this.depth = depth;
+    }
+
+    /**
+     * Decides whether the specified dependency should be included in the 
dependency graph.
+     * 
+     * @param dependency The dependency to check, must not be {@code null}.
+     * @return {@code false} if the dependency should be excluded from the 
children of the current node, {@code true}
+     *         otherwise.
+     */
+    @Override
+    public boolean selectDependency( Dependency dependency )
+    {
+        return depth < 2 || !scope.equals( dependency.getScope() );
+    }
+
+    /**
+     * Derives a dependency selector for the specified collection context. 
When calculating the child selector,
+     * implementors are strongly advised to simply return the current instance 
if nothing changed to help save memory.
+     * 
+     * @param context The dependency collection context, must not be {@code 
null}.
+     * @return The dependency selector for the target node, must not be {@code 
null}.
+     */
+    @Override
+    public DependencySelector deriveChildSelector( DependencyCollectionContext 
context )
+    {
+        if ( depth >= 2 )
+        {
+            return this;
+        }
+
+        return new Maven31DirectScopeDependencySelector( scope, depth + 1 );
+    }
+
+    @Override
+    public int hashCode()
+    {
+        final int prime = 31;
+        int result = 1;
+        result = prime * result + depth;
+        result = prime * result + ( ( scope == null ) ? 0 : scope.hashCode() );
+        return result;
+    }
+
+    @Override
+    public boolean equals( Object obj )
+    {
+        if ( this == obj )
+        {
+            return true;
+        }
+        if ( obj == null )
+        {
+            return false;
+        }
+        if ( getClass() != obj.getClass() )
+        {
+            return false;
+        }
+        Maven31DirectScopeDependencySelector other = 
(Maven31DirectScopeDependencySelector) obj;
+        if ( depth != other.depth )
+        {
+            return false;
+        }
+        if ( scope == null )
+        {
+            if ( other.scope != null )
+            {
+                return false;
+            }
+        }
+        else if ( !scope.equals( other.scope ) )
+        {
+            return false;
+        }
+        return true;
+    }
+
+}

Reply via email to