diff --git a/maven-resolver-api/src/main/java/org/eclipse/aether/collection/UnsolvableVersionConflictException.java b/maven-resolver-api/src/main/java/org/eclipse/aether/collection/UnsolvableVersionConflictException.java index 445f9339a..10d26a25a 100644 --- a/maven-resolver-api/src/main/java/org/eclipse/aether/collection/UnsolvableVersionConflictException.java +++ b/maven-resolver-api/src/main/java/org/eclipse/aether/collection/UnsolvableVersionConflictException.java @@ -29,6 +29,8 @@ import org.eclipse.aether.graph.DependencyNode; import org.eclipse.aether.version.VersionConstraint; +import static java.util.Objects.requireNonNull; + /** * Thrown in case of an unsolvable conflict between different version constraints for a dependency. */ @@ -42,9 +44,25 @@ public class UnsolvableVersionConflictException extends RepositoryException { * Creates a new exception with the specified paths to conflicting nodes in the dependency graph. * * @param paths The paths to the dependency nodes that participate in the version conflict, may be {@code null}. + * @deprecated Use {@link #UnsolvableVersionConflictException(String, Collection)} instead. */ + @Deprecated public UnsolvableVersionConflictException(Collection extends List extends DependencyNode>> paths) { - super("Could not resolve version conflict among " + toPaths(paths)); + this("Unsolvable hard constraint combination", paths); + } + + /** + * Creates a new exception with the specified paths to conflicting nodes in the dependency graph. + * + * @param message The strategy that throw the bucket in, must not be {@code null}. Should provide concise message + * why this exception was thrown. + * @param paths The paths to the dependency nodes that participate in the version conflict, may be {@code null}. + * + * @since 2.0.0 + */ + public UnsolvableVersionConflictException( + String message, Collection extends List extends DependencyNode>> paths) { + super(requireNonNull(message, "message") + "; Could not resolve version conflict among " + toPaths(paths)); if (paths == null) { this.paths = Collections.emptyList(); this.versions = Collections.emptyList(); diff --git a/maven-resolver-demos/maven-resolver-demo-snippets/src/main/java/org/apache/maven/resolver/examples/GetDependencyHierarchyWithConflicts.java b/maven-resolver-demos/maven-resolver-demo-snippets/src/main/java/org/apache/maven/resolver/examples/GetDependencyHierarchyWithConflicts.java new file mode 100644 index 000000000..aacf00bb3 --- /dev/null +++ b/maven-resolver-demos/maven-resolver-demo-snippets/src/main/java/org/apache/maven/resolver/examples/GetDependencyHierarchyWithConflicts.java @@ -0,0 +1,143 @@ +/* + * 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. + */ +package org.apache.maven.resolver.examples; + +import org.apache.maven.resolver.examples.util.Booter; +import org.eclipse.aether.RepositorySystem; +import org.eclipse.aether.RepositorySystemSession.CloseableSession; +import org.eclipse.aether.RepositorySystemSession.SessionBuilder; +import org.eclipse.aether.artifact.Artifact; +import org.eclipse.aether.artifact.DefaultArtifact; +import org.eclipse.aether.collection.CollectRequest; +import org.eclipse.aether.collection.UnsolvableVersionConflictException; +import org.eclipse.aether.resolution.ArtifactDescriptorRequest; +import org.eclipse.aether.resolution.ArtifactDescriptorResult; +import org.eclipse.aether.util.graph.manager.DependencyManagerUtils; +import org.eclipse.aether.util.graph.transformer.ChainedDependencyGraphTransformer; +import org.eclipse.aether.util.graph.transformer.ConfigurableVersionSelector; +import org.eclipse.aether.util.graph.transformer.ConflictResolver; +import org.eclipse.aether.util.graph.transformer.JavaDependencyContextRefiner; +import org.eclipse.aether.util.graph.transformer.JavaScopeDeriver; +import org.eclipse.aether.util.graph.transformer.JavaScopeSelector; +import org.eclipse.aether.util.graph.transformer.SimpleOptionalitySelector; + +/** + * Visualizes the transitive dependencies of an artifact similar to m2e's dependency hierarchy view. + */ +public class GetDependencyHierarchyWithConflicts { + + /** + * Main. + * @param args + * @throws Exception + */ + public static void main(String[] args) throws Exception { + System.out.println("------------------------------------------------------------"); + System.out.println(GetDependencyHierarchyWithConflicts.class.getSimpleName()); + + // incompatible versions: two incompatible versions present in graph + try (RepositorySystem system = Booter.newRepositorySystem(Booter.selectFactory(args))) { + SessionBuilder sessionBuilder = Booter.newRepositorySystemSession(system); + sessionBuilder.setConfigProperty(ConflictResolver.CONFIG_PROP_VERBOSE, true); + sessionBuilder.setConfigProperty(DependencyManagerUtils.CONFIG_PROP_VERBOSE, true); + try (CloseableSession session = sessionBuilder + .setDependencyGraphTransformer(new ChainedDependencyGraphTransformer( + new ConflictResolver( + new ConfigurableVersionSelector( + new ConfigurableVersionSelector.MajorVersionConvergence( + new ConfigurableVersionSelector.Nearest())), + new JavaScopeSelector(), + new SimpleOptionalitySelector(), + new JavaScopeDeriver()), + new JavaDependencyContextRefiner())) + .build()) { + Artifact artifact = new DefaultArtifact("org.apache.maven.shared:maven-dependency-tree:3.0.1"); + + ArtifactDescriptorRequest descriptorRequest = new ArtifactDescriptorRequest(); + descriptorRequest.setArtifact(artifact); + descriptorRequest.setRepositories(Booter.newRepositories(system, session)); + ArtifactDescriptorResult descriptorResult = system.readArtifactDescriptor(session, descriptorRequest); + + CollectRequest collectRequest = new CollectRequest(); + collectRequest.setRootArtifact(descriptorResult.getArtifact()); + collectRequest.setDependencies(descriptorResult.getDependencies()); + collectRequest.setManagedDependencies(descriptorResult.getManagedDependencies()); + collectRequest.setRepositories(descriptorRequest.getRepositories()); + + system.collectDependencies(session, collectRequest); + throw new IllegalStateException("should fail"); + } + } catch (Exception e) { + e.printStackTrace(); + if (e.getCause() instanceof UnsolvableVersionConflictException) { + String cause = e.getCause().getMessage(); + if (!cause.contains( + "Incompatible versions for org.apache.maven:maven-core, incompatible versions: [2.0], all versions [2.0, 3.0.4]")) { + throw new IllegalStateException("should fail due incompatible versions"); + } + } else { + throw new IllegalStateException("should fail due incompatible versions"); + } + } + + // dependency divergence: multiple versions of same GA present in graph + try (RepositorySystem system = Booter.newRepositorySystem(Booter.selectFactory(args))) { + SessionBuilder sessionBuilder = Booter.newRepositorySystemSession(system); + sessionBuilder.setConfigProperty(ConflictResolver.CONFIG_PROP_VERBOSE, true); + sessionBuilder.setConfigProperty(DependencyManagerUtils.CONFIG_PROP_VERBOSE, true); + try (CloseableSession session = sessionBuilder + .setDependencyGraphTransformer(new ChainedDependencyGraphTransformer( + new ConflictResolver( + new ConfigurableVersionSelector(new ConfigurableVersionSelector.VersionConvergence( + new ConfigurableVersionSelector.Nearest())), + new JavaScopeSelector(), + new SimpleOptionalitySelector(), + new JavaScopeDeriver()), + new JavaDependencyContextRefiner())) + .build()) { + Artifact artifact = new DefaultArtifact("org.apache.maven.shared:maven-dependency-tree:3.1.0"); + + ArtifactDescriptorRequest descriptorRequest = new ArtifactDescriptorRequest(); + descriptorRequest.setArtifact(artifact); + descriptorRequest.setRepositories(Booter.newRepositories(system, session)); + ArtifactDescriptorResult descriptorResult = system.readArtifactDescriptor(session, descriptorRequest); + + CollectRequest collectRequest = new CollectRequest(); + collectRequest.setRootArtifact(descriptorResult.getArtifact()); + collectRequest.setDependencies(descriptorResult.getDependencies()); + collectRequest.setManagedDependencies(descriptorResult.getManagedDependencies()); + collectRequest.setRepositories(descriptorRequest.getRepositories()); + + system.collectDependencies(session, collectRequest); + throw new IllegalStateException("should fail"); + } + } catch (Exception e) { + e.printStackTrace(); + if (e.getCause() instanceof UnsolvableVersionConflictException) { + String cause = e.getCause().getMessage(); + if (!cause.contains( + "Convergence violated for org.codehaus.plexus:plexus-utils, versions present: [2.1, 1.5.5, 2.0.6]")) { + throw new IllegalStateException("should fail due convergence violation"); + } + } else { + throw new IllegalStateException("should fail due convergence violation"); + } + } + } +} diff --git a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/scope/ScopeManagerImpl.java b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/scope/ScopeManagerImpl.java index 485b7aac3..eec86246c 100644 --- a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/scope/ScopeManagerImpl.java +++ b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/scope/ScopeManagerImpl.java @@ -49,8 +49,8 @@ import org.eclipse.aether.util.graph.selector.AndDependencySelector; import org.eclipse.aether.util.graph.selector.ExclusionDependencySelector; import org.eclipse.aether.util.graph.transformer.ChainedDependencyGraphTransformer; +import org.eclipse.aether.util.graph.transformer.ConfigurableVersionSelector; import org.eclipse.aether.util.graph.transformer.ConflictResolver; -import org.eclipse.aether.util.graph.transformer.NearestVersionSelector; import org.eclipse.aether.util.graph.transformer.SimpleOptionalitySelector; import org.eclipse.aether.util.graph.visitor.CloningDependencyVisitor; import org.eclipse.aether.util.graph.visitor.FilteringDependencyVisitor; @@ -157,7 +157,7 @@ public DependencySelector getDependencySelector(ResolutionScope resolutionScope) public DependencyGraphTransformer getDependencyGraphTransformer(ResolutionScope resolutionScope) { return new ChainedDependencyGraphTransformer( new ConflictResolver( - new NearestVersionSelector(), new ManagedScopeSelector(this), + new ConfigurableVersionSelector(), new ManagedScopeSelector(this), new SimpleOptionalitySelector(), new ManagedScopeDeriver(this)), new ManagedDependencyContextRefiner(this)); } diff --git a/maven-resolver-supplier-mvn3/src/main/java/org/eclipse/aether/supplier/SessionBuilderSupplier.java b/maven-resolver-supplier-mvn3/src/main/java/org/eclipse/aether/supplier/SessionBuilderSupplier.java index 89fb5f3ca..882edef62 100644 --- a/maven-resolver-supplier-mvn3/src/main/java/org/eclipse/aether/supplier/SessionBuilderSupplier.java +++ b/maven-resolver-supplier-mvn3/src/main/java/org/eclipse/aether/supplier/SessionBuilderSupplier.java @@ -39,11 +39,11 @@ import org.eclipse.aether.util.graph.selector.AndDependencySelector; import org.eclipse.aether.util.graph.selector.ExclusionDependencySelector; import org.eclipse.aether.util.graph.transformer.ChainedDependencyGraphTransformer; +import org.eclipse.aether.util.graph.transformer.ConfigurableVersionSelector; import org.eclipse.aether.util.graph.transformer.ConflictResolver; import org.eclipse.aether.util.graph.transformer.JavaDependencyContextRefiner; 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.traverser.FatArtifactTraverser; import org.eclipse.aether.util.repository.SimpleArtifactDescriptorPolicy; @@ -69,6 +69,7 @@ public SessionBuilderSupplier(RepositorySystem repositorySystem) { } protected void configureSessionBuilder(SessionBuilder session) { + session.setSystemProperties(System.getProperties()); session.setDependencyTraverser(getDependencyTraverser()); session.setDependencyManager(getDependencyManager()); session.setDependencySelector(getDependencySelector()); @@ -95,7 +96,7 @@ protected DependencySelector getDependencySelector() { protected DependencyGraphTransformer getDependencyGraphTransformer() { return new ChainedDependencyGraphTransformer( new ConflictResolver( - new NearestVersionSelector(), new JavaScopeSelector(), + new ConfigurableVersionSelector(), new JavaScopeSelector(), new SimpleOptionalitySelector(), new JavaScopeDeriver()), new JavaDependencyContextRefiner()); } diff --git a/maven-resolver-util/src/main/java/org/eclipse/aether/util/graph/transformer/ConfigurableVersionSelector.java b/maven-resolver-util/src/main/java/org/eclipse/aether/util/graph/transformer/ConfigurableVersionSelector.java new file mode 100644 index 000000000..9ad0e0551 --- /dev/null +++ b/maven-resolver-util/src/main/java/org/eclipse/aether/util/graph/transformer/ConfigurableVersionSelector.java @@ -0,0 +1,316 @@ +/* + * 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. + */ +package org.eclipse.aether.util.graph.transformer; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashSet; +import java.util.Iterator; +import java.util.Objects; +import java.util.Set; +import java.util.stream.Collectors; + +import org.eclipse.aether.RepositoryException; +import org.eclipse.aether.collection.UnsolvableVersionConflictException; +import org.eclipse.aether.graph.DependencyFilter; +import org.eclipse.aether.graph.DependencyNode; +import org.eclipse.aether.util.graph.transformer.ConflictResolver.ConflictContext; +import org.eclipse.aether.util.graph.transformer.ConflictResolver.ConflictItem; +import org.eclipse.aether.util.graph.transformer.ConflictResolver.VersionSelector; +import org.eclipse.aether.util.graph.visitor.PathRecordingDependencyVisitor; +import org.eclipse.aether.util.graph.visitor.TreeDependencyVisitor; +import org.eclipse.aether.version.Version; +import org.eclipse.aether.version.VersionConstraint; + +import static java.util.Objects.requireNonNull; + +/** + * A configurable version selector for use with {@link ConflictResolver} that resolves version conflicts using a + * specified strategy. If there is no single node that satisfies all encountered version ranges, the selector will fail. + * Based on configuration, this selector may fail for other reasons as well. + * + * @since 2.0.0 + */ +public class ConfigurableVersionSelector extends VersionSelector { + /** + * The strategy how "winner" is being selected. + */ + public interface SelectionStrategy { + /** + * Invoked for every "candidate" when winner is already set (very first candidate is set as winner). + *
+ * This method should determine is candidate "better" or not and should replace current winner. This method + * is invoked whenever {@code candidate} is "considered" (fits any constraint in effect, if any). + */ + boolean isBetter(ConflictItem candidate, ConflictItem winner); + /** + * Method invoked at version selection end, just before version selector returns. Note: {@code winner} may + * be {@code null}, while the rest of parameters cannot. The parameter {@code candidates} contains all the + * "considered candidates", dependencies that fulfil all constraints, if present. The {@code context} on the + * other hand contains all items participating in conflict. + *
+ * This method by default just returns the passed in {@code winner}, but can do much more.
+ */
+ default ConflictItem winnerSelected(
+ ConflictItem winner, Collection
+ * This is the "classic" Maven strategy.
+ */
+ public static class Nearest implements SelectionStrategy {
+ @Override
+ public boolean isBetter(ConflictItem candidate, ConflictItem winner) {
+ if (candidate.isSibling(winner)) {
+ return candidate
+ .getNode()
+ .getVersion()
+ .compareTo(winner.getNode().getVersion())
+ > 0;
+ } else {
+ return candidate.getDepth() < winner.getDepth();
+ }
+ }
+ }
+
+ /**
+ * Selection strategy that selects "highest" version.
+ */
+ public static class Highest implements SelectionStrategy {
+ @Override
+ public boolean isBetter(ConflictItem candidate, ConflictItem winner) {
+ return candidate.getNode().getVersion().compareTo(winner.getNode().getVersion()) > 0;
+ }
+ }
+
+ /**
+ * Example selection strategy (used in tests and demos), is not recommended to be used in production.
+ *
+ * Selection strategy that delegates to another selection strategy, and at the end enforces dependency convergence
+ * among candidates.
+ */
+ public static class VersionConvergence implements SelectionStrategy {
+ private final SelectionStrategy delegate;
+
+ public VersionConvergence(SelectionStrategy delegate) {
+ this.delegate = requireNonNull(delegate, "delegate");
+ }
+
+ @Override
+ public boolean isBetter(ConflictItem candidate, ConflictItem winner) {
+ return delegate.isBetter(candidate, winner);
+ }
+
+ @Override
+ public ConflictItem winnerSelected(
+ ConflictItem winner, Collection
+ * Selection strategy that delegates to another selection strategy, and at end enforces aligned "major versions"
+ * among candidates.
+ */
+ public static class MajorVersionConvergence implements SelectionStrategy {
+ private final SelectionStrategy delegate;
+
+ public MajorVersionConvergence(SelectionStrategy delegate) {
+ this.delegate = requireNonNull(delegate, "delegate");
+ }
+
+ @Override
+ public boolean isBetter(ConflictItem candidate, ConflictItem winner) {
+ return delegate.isBetter(candidate, winner);
+ }
+
+ @Override
+ public ConflictItem winnerSelected(
+ ConflictItem winner, Collection