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 <belingue...@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 <rfscho...@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; + } + +}