You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@maven.apache.org by rf...@apache.org on 2021/05/22 12:04:30 UTC
[maven-dependency-tree] branch master updated: [MSHARED-788] Add
functionality to collect raw dependencies in Maven 3+
This is an automated email from the ASF dual-hosted git repository.
rfscholte pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/maven-dependency-tree.git
The following commit(s) were added to refs/heads/master by this push:
new 7e225ab [MSHARED-788] Add functionality to collect raw dependencies in Maven 3+
7e225ab is described below
commit 7e225abf11faf1a0114d735b9aa2ca9f0669c694
Author: Gabriel Belingueres <be...@gmail.com>
AuthorDate: Sat May 22 14:04:15 2021 +0200
[MSHARED-788] Add functionality to collect raw dependencies in Maven 3+
Signed-off-by: rfscholte <rf...@apache.org>
---
.../graph/DependencyCollectorBuilder.java | 50 +
.../graph/DependencyCollectorBuilderException.java | 54 +
.../DefaultDependencyCollectorBuilder.java | 116 ++
.../graph/internal/ExceptionHandler.java | 30 +
.../shared/dependency/graph/internal/Invoker.java | 62 +-
.../Maven31DependencyCollectorBuilder.java | 310 +++++
.../internal/Maven31DependencyGraphBuilder.java | 20 +-
.../internal/Maven3DependencyCollectorBuilder.java | 316 +++++
.../internal/Maven3DependencyGraphBuilder.java | 2 +-
.../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 ++
18 files changed, 3522 insertions(+), 39 deletions(-)
diff --git a/src/main/java/org/apache/maven/shared/dependency/graph/DependencyCollectorBuilder.java b/src/main/java/org/apache/maven/shared/dependency/graph/DependencyCollectorBuilder.java
new file mode 100644
index 0000000..43cfb40
--- /dev/null
+++ b/src/main/java/org/apache/maven/shared/dependency/graph/DependencyCollectorBuilder.java
@@ -0,0 +1,50 @@
+package org.apache.maven.shared.dependency.graph;
+
+/*
+ * 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.artifact.repository.ArtifactRepository;
+import org.apache.maven.artifact.resolver.filter.ArtifactFilter;
+import org.apache.maven.project.ProjectBuildingRequest;
+
+/**
+ * Maven project dependency raw dependency collector API, providing an abstraction layer against Maven 3 and Maven 3.1+
+ * particular Aether implementations.
+ *
+ * @author Gabriel Belingueres
+ * @since 3.1.0
+ */
+public interface DependencyCollectorBuilder
+{
+
+ /**
+ * collect the project's raw dependency graph, with information to allow the API client to reason on its own about
+ * dependencies.
+ *
+ * @param localRepository the local repository.
+ * @param buildingRequest the request with the project to process its dependencies.
+ * @param filter an artifact filter if not all dependencies are required (can be <code>null</code>)
+ * @return the raw dependency tree
+ * @throws DependencyGraphBuilderException if some of the dependencies could not be collected.
+ */
+ DependencyNode collectDependencyGraph( ArtifactRepository localRepository, ProjectBuildingRequest buildingRequest,
+ ArtifactFilter filter )
+ throws DependencyCollectorBuilderException;
+
+}
diff --git a/src/main/java/org/apache/maven/shared/dependency/graph/DependencyCollectorBuilderException.java b/src/main/java/org/apache/maven/shared/dependency/graph/DependencyCollectorBuilderException.java
new file mode 100644
index 0000000..866213d
--- /dev/null
+++ b/src/main/java/org/apache/maven/shared/dependency/graph/DependencyCollectorBuilderException.java
@@ -0,0 +1,54 @@
+package org.apache.maven.shared.dependency.graph;
+
+/*
+ * 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.
+ */
+
+/**
+ * Indicates an issue with the DependencyCollectorBuilder
+ *
+ * @author Robert Scholte
+ * @since 3.1.0
+ */
+public class DependencyCollectorBuilderException
+ extends Exception
+{
+ /**
+ *
+ */
+ private static final long serialVersionUID = 1305852327231950979L;
+
+ // constructors -----------------------------------------------------------
+
+ /**
+ * @param message Message indicating why dependency graph could not be resolved.
+ */
+ public DependencyCollectorBuilderException( String message )
+ {
+ super( message );
+ }
+
+ /**
+ * @param message Message indicating why dependency graph could not be resolved.
+ * @param cause Throwable indicating at which point the graph failed to be resolved.
+ */
+ public DependencyCollectorBuilderException( String message, Throwable cause )
+ {
+ super( message, cause );
+ }
+}
diff --git a/src/main/java/org/apache/maven/shared/dependency/graph/internal/DefaultDependencyCollectorBuilder.java b/src/main/java/org/apache/maven/shared/dependency/graph/internal/DefaultDependencyCollectorBuilder.java
new file mode 100644
index 0000000..e29cc9d
--- /dev/null
+++ b/src/main/java/org/apache/maven/shared/dependency/graph/internal/DefaultDependencyCollectorBuilder.java
@@ -0,0 +1,116 @@
+package org.apache.maven.shared.dependency.graph.internal;
+
+/*
+ * 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.artifact.repository.ArtifactRepository;
+import org.apache.maven.artifact.resolver.filter.ArtifactFilter;
+import org.apache.maven.project.MavenProject;
+import org.apache.maven.project.ProjectBuildingRequest;
+import org.apache.maven.shared.dependency.graph.DependencyCollectorBuilder;
+import org.apache.maven.shared.dependency.graph.DependencyCollectorBuilderException;
+import org.apache.maven.shared.dependency.graph.DependencyNode;
+import org.codehaus.plexus.PlexusConstants;
+import org.codehaus.plexus.PlexusContainer;
+import org.codehaus.plexus.component.annotations.Component;
+import org.codehaus.plexus.component.repository.exception.ComponentLookupException;
+import org.codehaus.plexus.context.Context;
+import org.codehaus.plexus.context.ContextException;
+import org.codehaus.plexus.logging.AbstractLogEnabled;
+import org.codehaus.plexus.personality.plexus.lifecycle.phase.Contextualizable;
+
+/**
+ * Default project dependency raw dependency collector API, providing an abstraction layer against Maven 3 and Maven
+ * 3.1+ particular Aether implementations.
+ *
+ * @author Gabriel Belingueres
+ * @since 3.1.0
+ */
+@Component( role = DependencyCollectorBuilder.class )
+public class DefaultDependencyCollectorBuilder
+ extends AbstractLogEnabled
+ implements DependencyCollectorBuilder, Contextualizable
+{
+ protected PlexusContainer container;
+
+ @Override
+ public DependencyNode collectDependencyGraph( ArtifactRepository localRepository,
+ ProjectBuildingRequest buildingRequest, ArtifactFilter filter )
+ throws DependencyCollectorBuilderException
+ {
+ try
+ {
+ String hint = isMaven31() ? "maven31" : "maven3";
+
+ DependencyCollectorBuilder effectiveGraphBuilder =
+ (DependencyCollectorBuilder) container.lookup( DependencyCollectorBuilder.class.getCanonicalName(),
+ hint );
+
+ if ( getLogger().isDebugEnabled() )
+ {
+ MavenProject project = buildingRequest.getProject();
+
+ getLogger().debug( "building " + hint + " RAW dependency tree for " + project.getId() + " with "
+ + effectiveGraphBuilder.getClass().getSimpleName() );
+ }
+
+ return effectiveGraphBuilder.collectDependencyGraph( localRepository, buildingRequest, filter );
+ }
+ catch ( ComponentLookupException e )
+ {
+ throw new DependencyCollectorBuilderException( e.getMessage(), e );
+ }
+ }
+
+ /**
+ * @return true if the current Maven version is Maven 3.1.
+ */
+ protected static boolean isMaven31()
+ {
+ return canFindCoreClass( "org.eclipse.aether.artifact.Artifact" ); // Maven 3.1 specific
+ }
+
+ private static boolean canFindCoreClass( String className )
+ {
+ try
+ {
+ Thread.currentThread().getContextClassLoader().loadClass( className );
+
+ return true;
+ }
+ catch ( ClassNotFoundException e )
+ {
+ return false;
+ }
+ }
+
+ /**
+ * Injects the Plexus content.
+ *
+ * @param context Plexus context to inject.
+ * @throws ContextException if the PlexusContainer could not be located.
+ */
+ @Override
+ public void contextualize( Context context )
+ throws ContextException
+ {
+ container = (PlexusContainer) context.get( PlexusConstants.PLEXUS_KEY );
+ }
+
+}
diff --git a/src/main/java/org/apache/maven/shared/dependency/graph/internal/ExceptionHandler.java b/src/main/java/org/apache/maven/shared/dependency/graph/internal/ExceptionHandler.java
new file mode 100644
index 0000000..2dd86cc
--- /dev/null
+++ b/src/main/java/org/apache/maven/shared/dependency/graph/internal/ExceptionHandler.java
@@ -0,0 +1,30 @@
+package org.apache.maven.shared.dependency.graph.internal;
+
+/*
+ * 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.
+ */
+
+/**
+ *
+ *
+ * @param <T>
+ */
+interface ExceptionHandler<T extends Exception>
+{
+ T create( String message, Exception exception );
+}
diff --git a/src/main/java/org/apache/maven/shared/dependency/graph/internal/Invoker.java b/src/main/java/org/apache/maven/shared/dependency/graph/internal/Invoker.java
index f591973..8cc8851 100644
--- a/src/main/java/org/apache/maven/shared/dependency/graph/internal/Invoker.java
+++ b/src/main/java/org/apache/maven/shared/dependency/graph/internal/Invoker.java
@@ -19,8 +19,6 @@ package org.apache.maven.shared.dependency.graph.internal;
* under the License.
*/
-import java.lang.reflect.InvocationTargetException;
-
import org.apache.maven.shared.dependency.graph.DependencyGraphBuilderException;
/**
@@ -33,34 +31,27 @@ final class Invoker
// do not instantiate
}
- public static Object invoke( Object object, String method )
- throws DependencyGraphBuilderException
+ static <T extends Exception> Object invoke( Object object, String method, ExceptionHandler<T> exceptionHandler )
+ throws T
{
- return invoke( object.getClass(), object, method );
+ return invoke( object.getClass(), object, method, exceptionHandler );
}
- public static Object invoke( Class<?> objectClazz, Object object, String method )
- throws DependencyGraphBuilderException
+ static <T extends Exception> Object invoke( Class<?> objectClazz, Object object, String method,
+ ExceptionHandler<T> exceptionHandler )
+ throws T
{
try
{
return objectClazz.getMethod( method ).invoke( object );
}
- catch ( IllegalAccessException e )
- {
- throw new DependencyGraphBuilderException( e.getMessage(), e );
- }
- catch ( InvocationTargetException e )
+ catch ( ReflectiveOperationException e )
{
- throw new DependencyGraphBuilderException( e.getMessage(), e );
- }
- catch ( NoSuchMethodException e )
- {
- throw new DependencyGraphBuilderException( e.getMessage(), e );
+ throw exceptionHandler.create( e.getMessage(), e );
}
}
- public static Object invoke( Object object, String method, Class<?> clazz, Object arg )
+ static Object invoke( Object object, String method, Class<?> clazz, Object arg )
throws DependencyGraphBuilderException
{
try
@@ -68,38 +59,39 @@ final class Invoker
final Class<?> objectClazz = object.getClass();
return objectClazz.getMethod( method, clazz ).invoke( object, arg );
}
- catch ( IllegalAccessException e )
- {
- throw new DependencyGraphBuilderException( e.getMessage(), e );
- }
- catch ( InvocationTargetException e )
- {
- throw new DependencyGraphBuilderException( e.getMessage(), e );
- }
- catch ( NoSuchMethodException e )
+ catch ( ReflectiveOperationException e )
{
throw new DependencyGraphBuilderException( e.getMessage(), e );
}
}
- public static Object invoke( Class<?> objectClazz, String staticMethod, Class<?> argClazz, Object arg )
- throws DependencyGraphBuilderException
+ static <T extends Exception> Object invoke( Class<?> objectClazz, String staticMethod,
+ Class<?> argClazz, Object arg,
+ ExceptionHandler<T> exceptionHandler )
+ throws T
{
try
{
return objectClazz.getMethod( staticMethod, argClazz ).invoke( null, arg );
}
- catch ( IllegalAccessException e )
+ catch ( ReflectiveOperationException e )
{
- throw new DependencyGraphBuilderException( e.getMessage(), e );
+ throw exceptionHandler.create( e.getMessage(), e );
}
- catch ( InvocationTargetException e )
+ }
+
+ static <T extends Exception> Object invoke( Class<?> objectClazz, String staticMethod, Class<?> argClazz1,
+ Class<?> argClazz2, Object arg1, Object arg2,
+ ExceptionHandler<T> exceptionHandler )
+ throws T
+ {
+ try
{
- throw new DependencyGraphBuilderException( e.getMessage(), e );
+ return objectClazz.getMethod( staticMethod, argClazz1, argClazz2 ).invoke( null, arg1, arg2 );
}
- catch ( NoSuchMethodException e )
+ catch ( ReflectiveOperationException e )
{
- throw new DependencyGraphBuilderException( e.getMessage(), e );
+ throw exceptionHandler.create( e.getMessage(), e );
}
}
}
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
new file mode 100644
index 0000000..15c173d
--- /dev/null
+++ b/src/main/java/org/apache/maven/shared/dependency/graph/internal/Maven31DependencyCollectorBuilder.java
@@ -0,0 +1,310 @@
+package org.apache.maven.shared.dependency.graph.internal;
+
+/*
+ * 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.Collections;
+import java.util.List;
+
+import org.apache.maven.RepositoryUtils;
+import org.apache.maven.artifact.Artifact;
+import org.apache.maven.artifact.repository.ArtifactRepository;
+import org.apache.maven.artifact.resolver.filter.ArtifactFilter;
+import org.apache.maven.model.Dependency;
+import org.apache.maven.project.MavenProject;
+import org.apache.maven.project.ProjectBuildingRequest;
+import org.apache.maven.shared.dependency.graph.DependencyCollectorBuilder;
+import org.apache.maven.shared.dependency.graph.DependencyCollectorBuilderException;
+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;
+import org.eclipse.aether.DefaultRepositorySystemSession;
+import org.eclipse.aether.RepositorySystem;
+import org.eclipse.aether.artifact.ArtifactTypeRegistry;
+import org.eclipse.aether.collection.CollectRequest;
+import org.eclipse.aether.collection.CollectResult;
+import org.eclipse.aether.collection.DependencyCollectionException;
+import org.eclipse.aether.collection.DependencyGraphTransformer;
+import org.eclipse.aether.collection.DependencySelector;
+import org.eclipse.aether.graph.DependencyVisitor;
+import org.eclipse.aether.graph.Exclusion;
+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.transformer.ConflictResolver;
+import org.eclipse.aether.util.graph.transformer.JavaScopeDeriver;
+import org.eclipse.aether.util.graph.transformer.JavaScopeSelector;
+import org.eclipse.aether.util.graph.transformer.NearestVersionSelector;
+import org.eclipse.aether.util.graph.transformer.SimpleOptionalitySelector;
+import org.eclipse.aether.util.graph.visitor.TreeDependencyVisitor;
+import org.eclipse.aether.version.VersionConstraint;
+
+/**
+ * Project dependency raw dependency collector API, abstracting Maven 3.1+'s Aether implementation.
+ *
+ * @author Gabriel Belingueres
+ * @since 3.1.0
+ */
+@Component( role = DependencyCollectorBuilder.class, hint = "maven31" )
+public class Maven31DependencyCollectorBuilder
+ extends AbstractLogEnabled
+ implements DependencyCollectorBuilder
+{
+ @Requirement
+ private RepositorySystem repositorySystem;
+
+ private final ExceptionHandler<DependencyCollectorBuilderException> exceptionHandler;
+
+ public Maven31DependencyCollectorBuilder()
+ {
+ this.exceptionHandler = new ExceptionHandler<DependencyCollectorBuilderException>()
+ {
+ @Override
+ public DependencyCollectorBuilderException create( String message, Exception exception )
+ {
+ return new DependencyCollectorBuilderException( message, exception );
+ }
+ };
+ }
+
+ @Override
+ public DependencyNode collectDependencyGraph( ArtifactRepository localRepository,
+ ProjectBuildingRequest buildingRequest, ArtifactFilter filter )
+ throws DependencyCollectorBuilderException
+ {
+ DefaultRepositorySystemSession session = null;
+ try
+ {
+ MavenProject project = buildingRequest.getProject();
+
+ Artifact projectArtifact = project.getArtifact();
+ List<ArtifactRepository> remoteArtifactRepositories = project.getRemoteArtifactRepositories();
+
+ DefaultRepositorySystemSession repositorySession =
+ (DefaultRepositorySystemSession) Invoker.invoke( buildingRequest, "getRepositorySession",
+ exceptionHandler );
+
+ session = new DefaultRepositorySystemSession( repositorySession );
+
+ DependencyGraphTransformer transformer =
+ new ConflictResolver( new NearestVersionSelector(), new JavaScopeSelector(),
+ new SimpleOptionalitySelector(), new JavaScopeDeriver() );
+ session.setDependencyGraphTransformer( transformer );
+
+ DependencySelector depFilter =
+ new AndDependencySelector( new Maven31DirectScopeDependencySelector( JavaScopes.TEST ),
+ new OptionalDependencySelector(),
+ new ExclusionDependencySelector() );
+ session.setDependencySelector( depFilter );
+
+ session.setConfigProperty( ConflictResolver.CONFIG_PROP_VERBOSE, true );
+ session.setConfigProperty( DependencyManagerUtils.CONFIG_PROP_VERBOSE, true );
+
+ org.eclipse.aether.artifact.Artifact aetherArtifact =
+ (org.eclipse.aether.artifact.Artifact) Invoker.invoke( RepositoryUtils.class, "toArtifact",
+ Artifact.class, projectArtifact,
+ exceptionHandler );
+
+ @SuppressWarnings( "unchecked" )
+ List<org.eclipse.aether.repository.RemoteRepository> aetherRepos =
+ (List<org.eclipse.aether.repository.RemoteRepository>) Invoker.invoke( RepositoryUtils.class, "toRepos",
+ List.class,
+ remoteArtifactRepositories,
+ exceptionHandler );
+
+ CollectRequest collectRequest = new CollectRequest();
+ collectRequest.setRoot( new org.eclipse.aether.graph.Dependency( aetherArtifact, "" ) );
+ collectRequest.setRepositories( aetherRepos );
+
+ org.eclipse.aether.artifact.ArtifactTypeRegistry stereotypes = session.getArtifactTypeRegistry();
+ collectDependencyList( collectRequest, project, stereotypes );
+ collectManagedDependencyList( collectRequest, project, stereotypes );
+
+ CollectResult collectResult = repositorySystem.collectDependencies( session, collectRequest );
+
+ org.eclipse.aether.graph.DependencyNode rootNode = collectResult.getRoot();
+
+ if ( getLogger().isDebugEnabled() )
+ {
+ logTree( rootNode );
+ }
+
+ return buildDependencyNode( null, rootNode, projectArtifact, filter );
+ }
+ catch ( DependencyCollectionException e )
+ {
+ throw new DependencyCollectorBuilderException( "Could not collect dependencies: " + e.getResult(), e );
+ }
+ finally
+ {
+ if ( session != null )
+ {
+ session.setReadOnly();
+ }
+ }
+ }
+
+ private void logTree( org.eclipse.aether.graph.DependencyNode rootNode )
+ {
+ // print the node tree with its associated data Map
+ rootNode.accept( new TreeDependencyVisitor( new DependencyVisitor()
+ {
+ String indent = "";
+
+ @Override
+ public boolean visitEnter( org.eclipse.aether.graph.DependencyNode dependencyNode )
+ {
+ getLogger().debug( indent + "Aether node: " + dependencyNode + " data map: "
+ + dependencyNode.getData() );
+ indent += " ";
+ return true;
+ }
+
+ @Override
+ public boolean visitLeave( org.eclipse.aether.graph.DependencyNode dependencyNode )
+ {
+ indent = indent.substring( 0, indent.length() - 4 );
+ return true;
+ }
+ } ) );
+ }
+
+ private void collectManagedDependencyList( CollectRequest collectRequest, MavenProject project,
+ ArtifactTypeRegistry stereotypes )
+ throws DependencyCollectorBuilderException
+ {
+ if ( project.getDependencyManagement() != null )
+ {
+ for ( Dependency dependency : project.getDependencyManagement().getDependencies() )
+ {
+ org.eclipse.aether.graph.Dependency aetherDep = toAetherDependency( stereotypes, dependency );
+ collectRequest.addManagedDependency( aetherDep );
+ }
+ }
+ }
+
+ private void collectDependencyList( CollectRequest collectRequest, MavenProject project,
+ org.eclipse.aether.artifact.ArtifactTypeRegistry stereotypes )
+ throws DependencyCollectorBuilderException
+ {
+ for ( Dependency dependency : project.getDependencies() )
+ {
+ org.eclipse.aether.graph.Dependency aetherDep = toAetherDependency( stereotypes, dependency );
+ collectRequest.addDependency( aetherDep );
+ }
+ }
+
+ // CHECKSTYLE_OFF: LineLength
+ private org.eclipse.aether.graph.Dependency toAetherDependency( org.eclipse.aether.artifact.ArtifactTypeRegistry stereotypes,
+ Dependency dependency )
+ throws DependencyCollectorBuilderException
+ {
+ org.eclipse.aether.graph.Dependency aetherDep =
+ (org.eclipse.aether.graph.Dependency) Invoker.invoke( RepositoryUtils.class, "toDependency",
+ Dependency.class,
+ org.eclipse.aether.artifact.ArtifactTypeRegistry.class,
+ dependency, stereotypes, exceptionHandler );
+ return aetherDep;
+ }
+ // CHECKSTYLE_ON: LineLength
+
+ private Artifact getDependencyArtifact( org.eclipse.aether.graph.Dependency dep )
+ {
+ org.eclipse.aether.artifact.Artifact artifact = dep.getArtifact();
+
+ try
+ {
+ Artifact mavenArtifact =
+ (Artifact) Invoker.invoke( RepositoryUtils.class, "toArtifact",
+ org.eclipse.aether.artifact.Artifact.class, artifact, exceptionHandler );
+
+ mavenArtifact.setScope( dep.getScope() );
+ mavenArtifact.setOptional( dep.isOptional() );
+
+ return mavenArtifact;
+ }
+ catch ( DependencyCollectorBuilderException e )
+ {
+ // ReflectionException should not happen
+ throw new RuntimeException( e.getMessage(), e );
+ }
+ }
+
+ private DependencyNode buildDependencyNode( DependencyNode parent, org.eclipse.aether.graph.DependencyNode node,
+ Artifact artifact, ArtifactFilter filter )
+ {
+ String premanagedVersion = DependencyManagerUtils.getPremanagedVersion( node );
+ String premanagedScope = DependencyManagerUtils.getPremanagedScope( node );
+
+ Boolean optional = null;
+ if ( node.getDependency() != null )
+ {
+ optional = node.getDependency().isOptional();
+ }
+
+ List<org.apache.maven.model.Exclusion> exclusions = null;
+ if ( node.getDependency() != null )
+ {
+ exclusions = new ArrayList<>( node.getDependency().getExclusions().size() );
+ for ( Exclusion exclusion : node.getDependency().getExclusions() )
+ {
+ org.apache.maven.model.Exclusion modelExclusion = new org.apache.maven.model.Exclusion();
+ modelExclusion.setGroupId( exclusion.getGroupId() );
+ modelExclusion.setArtifactId( exclusion.getArtifactId() );
+ exclusions.add( modelExclusion );
+ }
+ }
+
+ DefaultDependencyNode current =
+ new DefaultDependencyNode( parent, artifact, premanagedVersion, premanagedScope,
+ getVersionSelectedFromRange( node.getVersionConstraint() ), optional,
+ exclusions );
+
+ List<DependencyNode> nodes = new ArrayList<DependencyNode>( node.getChildren().size() );
+ for ( org.eclipse.aether.graph.DependencyNode child : node.getChildren() )
+ {
+ Artifact childArtifact = getDependencyArtifact( child.getDependency() );
+
+ if ( ( filter == null ) || filter.include( childArtifact ) )
+ {
+ nodes.add( buildDependencyNode( current, child, childArtifact, filter ) );
+ }
+ }
+
+ current.setChildren( Collections.unmodifiableList( nodes ) );
+
+ return current;
+ }
+
+ private String getVersionSelectedFromRange( VersionConstraint constraint )
+ {
+ if ( ( constraint == null ) || ( constraint.getVersion() != null ) )
+ {
+ return null;
+ }
+
+ return constraint.getRange().toString();
+ }
+
+}
diff --git a/src/main/java/org/apache/maven/shared/dependency/graph/internal/Maven31DependencyGraphBuilder.java b/src/main/java/org/apache/maven/shared/dependency/graph/internal/Maven31DependencyGraphBuilder.java
index 5a53dcb..c1f9ab4 100644
--- a/src/main/java/org/apache/maven/shared/dependency/graph/internal/Maven31DependencyGraphBuilder.java
+++ b/src/main/java/org/apache/maven/shared/dependency/graph/internal/Maven31DependencyGraphBuilder.java
@@ -62,6 +62,20 @@ public class Maven31DependencyGraphBuilder
@Requirement
private ProjectDependenciesResolver resolver;
+ private final ExceptionHandler<DependencyGraphBuilderException> exceptionHandler;
+
+ public Maven31DependencyGraphBuilder()
+ {
+ this.exceptionHandler = new ExceptionHandler<DependencyGraphBuilderException>()
+ {
+ @Override
+ public DependencyGraphBuilderException create( String message, Exception exception )
+ {
+ return new DependencyGraphBuilderException( message, exception );
+ }
+ };
+ }
+
/**
* Builds the dependency graph for Maven 3.1+.
*
@@ -95,7 +109,7 @@ public class Maven31DependencyGraphBuilder
MavenProject project = buildingRequest.getProject();
RepositorySystemSession session =
- (RepositorySystemSession) Invoker.invoke( buildingRequest, "getRepositorySession" );
+ (RepositorySystemSession) Invoker.invoke( buildingRequest, "getRepositorySession", exceptionHandler );
/*
* if ( Boolean.TRUE != ( (Boolean) session.getConfigProperties().get(
@@ -111,7 +125,7 @@ public class Maven31DependencyGraphBuilder
final DependencyResolutionResult result = resolveDependencies( request, reactorProjects );
org.eclipse.aether.graph.DependencyNode graph =
(org.eclipse.aether.graph.DependencyNode) Invoker.invoke( DependencyResolutionResult.class, result,
- "getDependencyGraph" );
+ "getDependencyGraph", exceptionHandler );
return buildDependencyNode( null, graph, project.getArtifact(), filter );
}
@@ -189,7 +203,7 @@ public class Maven31DependencyGraphBuilder
try
{
Artifact mavenArtifact = (Artifact) Invoker.invoke( RepositoryUtils.class, "toArtifact",
- org.eclipse.aether.artifact.Artifact.class, artifact );
+ org.eclipse.aether.artifact.Artifact.class, artifact, exceptionHandler );
mavenArtifact.setScope( dep.getScope() );
mavenArtifact.setOptional( dep.isOptional() );
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
new file mode 100644
index 0000000..24c0d65
--- /dev/null
+++ b/src/main/java/org/apache/maven/shared/dependency/graph/internal/Maven3DependencyCollectorBuilder.java
@@ -0,0 +1,316 @@
+package org.apache.maven.shared.dependency.graph.internal;
+
+/*
+ * 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.Collections;
+import java.util.List;
+
+import org.apache.maven.RepositoryUtils;
+import org.apache.maven.artifact.Artifact;
+import org.apache.maven.artifact.repository.ArtifactRepository;
+import org.apache.maven.artifact.resolver.filter.ArtifactFilter;
+import org.apache.maven.model.Dependency;
+import org.apache.maven.project.MavenProject;
+import org.apache.maven.project.ProjectBuildingRequest;
+import org.apache.maven.shared.dependency.graph.DependencyCollectorBuilder;
+import org.apache.maven.shared.dependency.graph.DependencyCollectorBuilderException;
+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;
+import org.sonatype.aether.RepositorySystem;
+import org.sonatype.aether.RepositorySystemSession;
+import org.sonatype.aether.artifact.ArtifactTypeRegistry;
+import org.sonatype.aether.collection.CollectRequest;
+import org.sonatype.aether.collection.CollectResult;
+import org.sonatype.aether.collection.DependencyCollectionException;
+import org.sonatype.aether.collection.DependencyGraphTransformer;
+import org.sonatype.aether.collection.DependencySelector;
+import org.sonatype.aether.graph.DependencyVisitor;
+import org.sonatype.aether.graph.Exclusion;
+import org.sonatype.aether.util.DefaultRepositorySystemSession;
+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.version.VersionConstraint;
+
+/**
+ * Project dependency raw dependency collector API, abstracting Maven 3's Aether implementation.
+ *
+ * @author Gabriel Belingueres
+ * @since 3.1.0
+ */
+@Component( role = DependencyCollectorBuilder.class, hint = "maven3" )
+public class Maven3DependencyCollectorBuilder
+ extends AbstractLogEnabled
+ implements DependencyCollectorBuilder
+{
+ @Requirement
+ private RepositorySystem repositorySystem;
+
+ private final ExceptionHandler<DependencyCollectorBuilderException> exceptionHandler;
+
+ public Maven3DependencyCollectorBuilder()
+ {
+ this.exceptionHandler = new ExceptionHandler<DependencyCollectorBuilderException>()
+ {
+ @Override
+ public DependencyCollectorBuilderException create( String message, Exception exception )
+ {
+ return new DependencyCollectorBuilderException( message, exception );
+ }
+ };
+ }
+
+ @Override
+ public DependencyNode collectDependencyGraph( ArtifactRepository localRepository,
+ ProjectBuildingRequest buildingRequest, ArtifactFilter filter )
+ throws DependencyCollectorBuilderException
+ {
+ try
+ {
+ MavenProject project = buildingRequest.getProject();
+
+ Artifact projectArtifact = project.getArtifact();
+ List<ArtifactRepository> remoteArtifactRepositories = project.getRemoteArtifactRepositories();
+
+ // throws ClassCastException (classloading issues?)
+ // DefaultRepositorySystemSession repositorySystemSession =
+ // (DefaultRepositorySystemSession) Invoker.invoke( buildingRequest, "getRepositorySession" );
+ RepositorySystemSession repositorySystemSession = buildingRequest.getRepositorySession();
+
+ DefaultRepositorySystemSession session = new DefaultRepositorySystemSession( repositorySystemSession );
+
+ DependencyGraphTransformer transformer =
+ new ConflictResolver( new NearestVersionSelector(), new JavaScopeSelector(),
+ new SimpleOptionalitySelector(), new JavaScopeDeriver() );
+ session.setDependencyGraphTransformer( transformer );
+
+ DependencySelector depFilter =
+ 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,
+ exceptionHandler );
+
+ @SuppressWarnings( "unchecked" )
+ List<org.sonatype.aether.repository.RemoteRepository> aetherRepos =
+ (List<org.sonatype.aether.repository.RemoteRepository>) Invoker.invoke( RepositoryUtils.class,
+ "toRepos", List.class,
+ remoteArtifactRepositories,
+ exceptionHandler );
+
+ CollectRequest collectRequest = new CollectRequest();
+ collectRequest.setRoot( new org.sonatype.aether.graph.Dependency( aetherArtifact, "" ) );
+ collectRequest.setRepositories( aetherRepos );
+
+ org.sonatype.aether.artifact.ArtifactTypeRegistry stereotypes = session.getArtifactTypeRegistry();
+ collectDependencyList( collectRequest, project, stereotypes );
+ collectManagedDependencyList( collectRequest, project, stereotypes );
+
+ CollectResult collectResult = repositorySystem.collectDependencies( session, collectRequest );
+
+ org.sonatype.aether.graph.DependencyNode rootNode = collectResult.getRoot();
+
+ if ( getLogger().isDebugEnabled() )
+ {
+ logTree( rootNode );
+ }
+
+ return buildDependencyNode( null, rootNode, projectArtifact, filter );
+ }
+ catch ( DependencyCollectionException e )
+ {
+ throw new DependencyCollectorBuilderException( "Could not collect dependencies: " + e.getResult(), e );
+ }
+ }
+
+ private void logTree( org.sonatype.aether.graph.DependencyNode rootNode )
+ {
+ // print the node tree with its associated data Map
+ rootNode.accept( new TreeDependencyVisitor( new DependencyVisitor()
+ {
+ String indent = "";
+
+ @Override
+ public boolean visitEnter( org.sonatype.aether.graph.DependencyNode dependencyNode )
+ {
+ StringBuilder sb = new StringBuilder();
+ sb.append( indent ).append( "Aether node: " ).append( dependencyNode );
+ if ( !dependencyNode.getData().isEmpty() )
+ {
+ sb.append( "data map: " ).append( dependencyNode.getData() );
+ }
+ if ( dependencyNode.getPremanagedVersion() != null && !dependencyNode.getPremanagedVersion().isEmpty() )
+ {
+ sb.append( "Premanaged.version: " ).append( dependencyNode.getPremanagedVersion() );
+ }
+ if ( dependencyNode.getPremanagedScope() != null && !dependencyNode.getPremanagedScope().isEmpty() )
+ {
+ sb.append( "Premanaged.scope: " ).append( dependencyNode.getPremanagedScope() );
+ }
+ getLogger().debug( sb.toString() );
+ indent += " ";
+ return true;
+ }
+
+ @Override
+ public boolean visitLeave( org.sonatype.aether.graph.DependencyNode dependencyNode )
+ {
+ indent = indent.substring( 0, indent.length() - 4 );
+ return true;
+ }
+ } ) );
+ }
+
+ private void collectManagedDependencyList( CollectRequest collectRequest, MavenProject project,
+ ArtifactTypeRegistry stereotypes )
+ throws DependencyCollectorBuilderException
+ {
+ if ( project.getDependencyManagement() != null )
+ {
+ for ( Dependency dependency : project.getDependencyManagement().getDependencies() )
+ {
+ org.sonatype.aether.graph.Dependency aetherDep = toAetherDependency( stereotypes, dependency );
+ collectRequest.addManagedDependency( aetherDep );
+ }
+ }
+ }
+
+ private void collectDependencyList( CollectRequest collectRequest, MavenProject project,
+ org.sonatype.aether.artifact.ArtifactTypeRegistry stereotypes )
+ throws DependencyCollectorBuilderException
+ {
+ for ( Dependency dependency : project.getDependencies() )
+ {
+ org.sonatype.aether.graph.Dependency aetherDep = toAetherDependency( stereotypes, dependency );
+ collectRequest.addDependency( aetherDep );
+ }
+ }
+
+ // CHECKSTYLE_OFF: LineLength
+ private org.sonatype.aether.graph.Dependency toAetherDependency( org.sonatype.aether.artifact.ArtifactTypeRegistry stereotypes,
+ Dependency dependency )
+ throws DependencyCollectorBuilderException
+ {
+ org.sonatype.aether.graph.Dependency aetherDep =
+ (org.sonatype.aether.graph.Dependency) Invoker.invoke( RepositoryUtils.class, "toDependency",
+ Dependency.class,
+ org.sonatype.aether.artifact.ArtifactTypeRegistry.class,
+ dependency, stereotypes, exceptionHandler );
+ return aetherDep;
+ }
+ // CHECKSTYLE_ON: LineLength
+
+ private Artifact getDependencyArtifact( org.sonatype.aether.graph.Dependency dep )
+ {
+ org.sonatype.aether.artifact.Artifact artifact = dep.getArtifact();
+
+ try
+ {
+ Artifact mavenArtifact =
+ (Artifact) Invoker.invoke( RepositoryUtils.class, "toArtifact",
+ org.sonatype.aether.artifact.Artifact.class, artifact, exceptionHandler );
+
+ mavenArtifact.setScope( dep.getScope() );
+ mavenArtifact.setOptional( dep.isOptional() );
+
+ return mavenArtifact;
+ }
+ catch ( DependencyCollectorBuilderException e )
+ {
+ // ReflectionException should not happen
+ throw new RuntimeException( e.getMessage(), e );
+ }
+ }
+
+ private DependencyNode buildDependencyNode( DependencyNode parent, org.sonatype.aether.graph.DependencyNode node,
+ Artifact artifact, ArtifactFilter filter )
+ {
+ String premanagedVersion = node.getPremanagedVersion();
+ String premanagedScope = node.getPremanagedScope();
+
+ Boolean optional = null;
+ if ( node.getDependency() != null )
+ {
+ optional = node.getDependency().isOptional();
+ }
+
+ List<org.apache.maven.model.Exclusion> exclusions = null;
+ if ( node.getDependency() != null )
+ {
+ exclusions = new ArrayList<>( node.getDependency().getExclusions().size() );
+ for ( Exclusion exclusion : node.getDependency().getExclusions() )
+ {
+ org.apache.maven.model.Exclusion modelExclusion = new org.apache.maven.model.Exclusion();
+ modelExclusion.setGroupId( exclusion.getGroupId() );
+ modelExclusion.setArtifactId( exclusion.getArtifactId() );
+ exclusions.add( modelExclusion );
+ }
+ }
+
+ DefaultDependencyNode current =
+ new DefaultDependencyNode( parent, artifact, premanagedVersion, premanagedScope,
+ getVersionSelectedFromRange( node.getVersionConstraint() ), optional,
+ exclusions );
+
+ List<DependencyNode> nodes = new ArrayList<DependencyNode>( node.getChildren().size() );
+ for ( org.sonatype.aether.graph.DependencyNode child : node.getChildren() )
+ {
+ Artifact childArtifact = getDependencyArtifact( child.getDependency() );
+
+ if ( ( filter == null ) || filter.include( childArtifact ) )
+ {
+ nodes.add( buildDependencyNode( current, child, childArtifact, filter ) );
+ }
+ }
+
+ current.setChildren( Collections.unmodifiableList( nodes ) );
+
+ return current;
+ }
+
+ private String getVersionSelectedFromRange( VersionConstraint constraint )
+ {
+ if ( ( constraint == null ) || ( constraint.getVersion() != null ) || ( constraint.getRanges().isEmpty() ) )
+ {
+ return null;
+ }
+
+ return constraint.getRanges().iterator().next().toString();
+ }
+
+}
diff --git a/src/main/java/org/apache/maven/shared/dependency/graph/internal/Maven3DependencyGraphBuilder.java b/src/main/java/org/apache/maven/shared/dependency/graph/internal/Maven3DependencyGraphBuilder.java
index 2a2e58b..39e2a35 100644
--- a/src/main/java/org/apache/maven/shared/dependency/graph/internal/Maven3DependencyGraphBuilder.java
+++ b/src/main/java/org/apache/maven/shared/dependency/graph/internal/Maven3DependencyGraphBuilder.java
@@ -130,7 +130,7 @@ public class Maven3DependencyGraphBuilder
List<Dependency> reactorDeps = getReactorDependencies( reactorProjects, result.getUnresolvedDependencies() );
result.getUnresolvedDependencies().removeAll( reactorDeps );
- Invoker.invoke( result.getResolvedDependencies(), "addAll", Collection.class, reactorDeps );
+ result.getResolvedDependencies().addAll( reactorDeps );
if ( !result.getUnresolvedDependencies().isEmpty() )
{
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..d686842
--- /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.1.0
+ */
+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..abbb36e
--- /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.1.0
+ */
+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..e9555f9
--- /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.1.0
+ */
+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..d35abf0
--- /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.1.0
+ */
+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..9a810e7
--- /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.1.0
+ */
+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..4776e6d
--- /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.1.0
+ */
+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..c793b96
--- /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.1.0
+ */
+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..d63df0b
--- /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.1.0
+ */
+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..95ee25c
--- /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.1.0
+ */
+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;
+ }
+
+}