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