This is an automated email from the ASF dual-hosted git repository. cstamas pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/maven-resolver.git
The following commit(s) were added to refs/heads/master by this push: new 267d2883 [MRESOLVER-345] Conflict resolution in verbose mode is sensitive to version ordering (#271) 267d2883 is described below commit 267d2883374ebea3143a8caa55580b0847a16814 Author: Tamas Cservenak <ta...@cservenak.net> AuthorDate: Fri Mar 24 13:52:39 2023 +0100 [MRESOLVER-345] Conflict resolution in verbose mode is sensitive to version ordering (#271) Our two collector implementations produce slightly different dirty trees when in transitive dependencies ranges are in use. The new BF produces (w/ and w/o skipper) produces this dirty tree: ``` menforcer426:aid:ext:1 compile +- menforcer426:bid:ext:1 compile | +- menforcer426:cid:ext:3 compile | +- menforcer426:cid:ext:2 compile | \- menforcer426:cid:ext:1 compile +- menforcer426:cid:ext:3 compile +- menforcer426:cid:ext:2 compile \- menforcer426:cid:ext:1 compile ``` The "old" DF produces this one: ``` menforcer426:aid:ext:1 compile +- menforcer426:bid:ext:1 compile | +- menforcer426:cid:ext:1 compile | +- menforcer426:cid:ext:2 compile | \- menforcer426:cid:ext:3 compile +- menforcer426:cid:ext:1 compile +- menforcer426:cid:ext:2 compile \- menforcer426:cid:ext:3 compile ``` Spot the difference: the two dirty trees are "semantically" (or content-wise?) equal/same, except for the artifact ordering, where there was a version range (collector in this case discovers available versions that "fits" range and created nodes for them, one for each version that lies within version constraint). DF collector relies and provides "metadata based" ordering (as metadata contained the versions), while BF explicitly sorts them in descending order (for internal optimization [...] But, Conflict resolver in verbose mode for two inputs above produces different outputs. For DF with "divergence found", while for BF "no divergence found" (correctly). Overall changes in this PR: * most are test and test related resources * cosmetic changes like javadoc typos, adding override etc * added reusable `DependencyGraphDumper` * key changes are in `ConflictResolver` covered by `ConflictResolverTest` UT changes * overall fix is to make `ConflictResolver` insensitive to input dirty tree version ordering, make sure output is same for "semantically" (or content-wise?) same inputs. How tested this: * built/installed this PR * built maven-3.9.x with this resolver * ran maven IT suite -- OK * ran https://github.com/apache/maven-enforcer/pull/259 w/ built maven (so fixed resolver is used). This PR contains only one reproducer IT that fails with any released Maven version -- OK w/ maven built as above. Just realized how enforcer ITs are good source of inspiration for resolver use cases, so many if not all of new tests are actually inspired by enforcer ITs. --- https://issues.apache.org/jira/browse/MRESOLVER-345 --- .../maven/resolver/examples/AllResolverDemos.java | 1 + ...chy.java => DependencyHierarchyWithRanges.java} | 30 ++- .../resolver/examples/GetDependencyHierarchy.java | 3 +- .../maven/resolver/examples/GetDependencyTree.java | 3 +- .../maven/resolver/examples/resolver/Resolver.java | 15 +- .../maven/resolver/examples/util/Booter.java | 3 + .../resolver/demo/mresolver345/a/1.0/a-1.0.pom | 40 ++++ .../resolver/demo/mresolver345/b/1.0/b-1.0.pom | 35 +++ .../resolver/demo/mresolver345/c/1.0/c-1.0.pom | 27 +++ .../resolver/demo/mresolver345/c/2.0/c-2.0.pom | 27 +++ .../resolver/demo/mresolver345/c/3.0/c-3.0.pom | 27 +++ .../demo/mresolver345/c/maven-metadata.xml | 33 +++ .../DependencyCollectorDelegateTestSupport.java | 17 ++ .../impl/collect/bf/BfDependencyCollectorTest.java | 11 +- .../impl/collect/df/DfDependencyCollectorTest.java | 11 +- .../transitiveDepsUseRangesDirtyTreeResult_BF.txt | 8 + .../transitiveDepsUseRangesDirtyTreeResult_DF.txt | 8 + .../transitiveDepsUseRangesDirtyTree_aid_1.ini | 3 + .../transitiveDepsUseRangesDirtyTree_bid_1.ini | 2 + .../transitiveDepsUseRangesDirtyTree_cid_1.ini | 1 + .../transitiveDepsUseRangesDirtyTree_cid_2.ini | 1 + .../transitiveDepsUseRangesDirtyTree_cid_3.ini | 1 + .../util/graph/transformer/ConflictResolver.java | 164 ++++++++++--- .../AbstractDepthFirstNodeListGenerator.java | 2 + .../graph/visitor/CloningDependencyVisitor.java | 2 + .../util/graph/visitor/DependencyGraphDumper.java | 45 ++-- .../graph/visitor/FilteringDependencyVisitor.java | 4 +- .../visitor/PathRecordingDependencyVisitor.java | 2 + .../graph/visitor/PostorderNodeListGenerator.java | 6 +- .../graph/visitor/PreorderNodeListGenerator.java | 2 +- .../util/graph/visitor/TreeDependencyVisitor.java | 2 + .../graph/transformer/ConflictResolverTest.java | 265 +++++++++++++++++++++ 32 files changed, 717 insertions(+), 84 deletions(-) diff --git a/maven-resolver-demos/maven-resolver-demo-snippets/src/main/java/org/apache/maven/resolver/examples/AllResolverDemos.java b/maven-resolver-demos/maven-resolver-demo-snippets/src/main/java/org/apache/maven/resolver/examples/AllResolverDemos.java index 1e5bab32..f947f854 100644 --- a/maven-resolver-demos/maven-resolver-demo-snippets/src/main/java/org/apache/maven/resolver/examples/AllResolverDemos.java +++ b/maven-resolver-demos/maven-resolver-demo-snippets/src/main/java/org/apache/maven/resolver/examples/AllResolverDemos.java @@ -35,6 +35,7 @@ public class AllResolverDemos { GetDirectDependencies.main(args); GetDependencyTree.main(args); GetDependencyHierarchy.main(args); + DependencyHierarchyWithRanges.main(args); ResolveArtifact.main(args); ResolveTransitiveDependencies.main(args); ReverseDependencyTree.main(args); diff --git a/maven-resolver-demos/maven-resolver-demo-snippets/src/main/java/org/apache/maven/resolver/examples/GetDependencyHierarchy.java b/maven-resolver-demos/maven-resolver-demo-snippets/src/main/java/org/apache/maven/resolver/examples/DependencyHierarchyWithRanges.java similarity index 71% copy from maven-resolver-demos/maven-resolver-demo-snippets/src/main/java/org/apache/maven/resolver/examples/GetDependencyHierarchy.java copy to maven-resolver-demos/maven-resolver-demo-snippets/src/main/java/org/apache/maven/resolver/examples/DependencyHierarchyWithRanges.java index 3d6441f5..19820f59 100644 --- a/maven-resolver-demos/maven-resolver-demo-snippets/src/main/java/org/apache/maven/resolver/examples/GetDependencyHierarchy.java +++ b/maven-resolver-demos/maven-resolver-demo-snippets/src/main/java/org/apache/maven/resolver/examples/DependencyHierarchyWithRanges.java @@ -18,45 +18,57 @@ */ package org.apache.maven.resolver.examples; +import java.io.File; +import java.util.Collections; + import org.apache.maven.resolver.examples.util.Booter; -import org.apache.maven.resolver.examples.util.ConsoleDependencyGraphDumper; import org.eclipse.aether.DefaultRepositorySystemSession; import org.eclipse.aether.RepositorySystem; import org.eclipse.aether.artifact.Artifact; import org.eclipse.aether.artifact.DefaultArtifact; import org.eclipse.aether.collection.CollectRequest; import org.eclipse.aether.collection.CollectResult; +import org.eclipse.aether.repository.RemoteRepository; +import org.eclipse.aether.repository.RepositoryPolicy; 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.ConflictResolver; /** - * Visualizes the transitive dependencies of an artifact similar to m2e's dependency hierarchy view. + * Visualizes the transitive dependencies of an artifact similar to m2e's dependency hierarchy view. Artifact in this + * test is not "plain" one as is original "demo" {@link GetDependencyHierarchy}, but specially crafted for case + * described in MRESOLVER-345. + * + * @see <a href="https://issues.apache.org/jira/browse/MRESOLVER-345">MRESOLVER-345</a> */ -public class GetDependencyHierarchy { +public class DependencyHierarchyWithRanges { /** * Main. - * @param args - * @throws Exception */ public static void main(String[] args) throws Exception { System.out.println("------------------------------------------------------------"); - System.out.println(GetDependencyHierarchy.class.getSimpleName()); + System.out.println(DependencyHierarchyWithRanges.class.getSimpleName()); RepositorySystem system = Booter.newRepositorySystem(Booter.selectFactory(args)); DefaultRepositorySystemSession session = Booter.newRepositorySystemSession(system); + session.setChecksumPolicy(RepositoryPolicy.CHECKSUM_POLICY_IGNORE); // to not bother with checksums session.setConfigProperty(ConflictResolver.CONFIG_PROP_VERBOSE, true); session.setConfigProperty(DependencyManagerUtils.CONFIG_PROP_VERBOSE, true); - Artifact artifact = new DefaultArtifact("org.apache.maven:maven-resolver-provider:3.6.1"); + // this artifact is in "remote" repository in src/main/remote-repository + Artifact artifact = new DefaultArtifact("org.apache.maven.resolver.demo.mresolver345:a:1.0"); + + File remoteRepoBasedir = new File("src/main/remote-repository"); ArtifactDescriptorRequest descriptorRequest = new ArtifactDescriptorRequest(); descriptorRequest.setArtifact(artifact); - descriptorRequest.setRepositories(Booter.newRepositories(system, session)); + descriptorRequest.setRepositories(Collections.singletonList(new RemoteRepository.Builder( + "remote", "default", remoteRepoBasedir.toURI().toASCIIString()) + .build())); ArtifactDescriptorResult descriptorResult = system.readArtifactDescriptor(session, descriptorRequest); CollectRequest collectRequest = new CollectRequest(); @@ -67,6 +79,6 @@ public class GetDependencyHierarchy { CollectResult collectResult = system.collectDependencies(session, collectRequest); - collectResult.getRoot().accept(new ConsoleDependencyGraphDumper()); + collectResult.getRoot().accept(Booter.DUMPER_SOUT); } } diff --git a/maven-resolver-demos/maven-resolver-demo-snippets/src/main/java/org/apache/maven/resolver/examples/GetDependencyHierarchy.java b/maven-resolver-demos/maven-resolver-demo-snippets/src/main/java/org/apache/maven/resolver/examples/GetDependencyHierarchy.java index 3d6441f5..3a4504d7 100644 --- a/maven-resolver-demos/maven-resolver-demo-snippets/src/main/java/org/apache/maven/resolver/examples/GetDependencyHierarchy.java +++ b/maven-resolver-demos/maven-resolver-demo-snippets/src/main/java/org/apache/maven/resolver/examples/GetDependencyHierarchy.java @@ -19,7 +19,6 @@ package org.apache.maven.resolver.examples; import org.apache.maven.resolver.examples.util.Booter; -import org.apache.maven.resolver.examples.util.ConsoleDependencyGraphDumper; import org.eclipse.aether.DefaultRepositorySystemSession; import org.eclipse.aether.RepositorySystem; import org.eclipse.aether.artifact.Artifact; @@ -67,6 +66,6 @@ public class GetDependencyHierarchy { CollectResult collectResult = system.collectDependencies(session, collectRequest); - collectResult.getRoot().accept(new ConsoleDependencyGraphDumper()); + collectResult.getRoot().accept(Booter.DUMPER_SOUT); } } diff --git a/maven-resolver-demos/maven-resolver-demo-snippets/src/main/java/org/apache/maven/resolver/examples/GetDependencyTree.java b/maven-resolver-demos/maven-resolver-demo-snippets/src/main/java/org/apache/maven/resolver/examples/GetDependencyTree.java index e47129cb..77042e40 100644 --- a/maven-resolver-demos/maven-resolver-demo-snippets/src/main/java/org/apache/maven/resolver/examples/GetDependencyTree.java +++ b/maven-resolver-demos/maven-resolver-demo-snippets/src/main/java/org/apache/maven/resolver/examples/GetDependencyTree.java @@ -19,7 +19,6 @@ package org.apache.maven.resolver.examples; import org.apache.maven.resolver.examples.util.Booter; -import org.apache.maven.resolver.examples.util.ConsoleDependencyGraphDumper; import org.eclipse.aether.RepositorySystem; import org.eclipse.aether.RepositorySystemSession; import org.eclipse.aether.artifact.Artifact; @@ -54,6 +53,6 @@ public class GetDependencyTree { CollectResult collectResult = system.collectDependencies(session, collectRequest); - collectResult.getRoot().accept(new ConsoleDependencyGraphDumper()); + collectResult.getRoot().accept(Booter.DUMPER_SOUT); } } diff --git a/maven-resolver-demos/maven-resolver-demo-snippets/src/main/java/org/apache/maven/resolver/examples/resolver/Resolver.java b/maven-resolver-demos/maven-resolver-demo-snippets/src/main/java/org/apache/maven/resolver/examples/resolver/Resolver.java index a5a01f76..bf1d3795 100644 --- a/maven-resolver-demos/maven-resolver-demo-snippets/src/main/java/org/apache/maven/resolver/examples/resolver/Resolver.java +++ b/maven-resolver-demos/maven-resolver-demo-snippets/src/main/java/org/apache/maven/resolver/examples/resolver/Resolver.java @@ -20,9 +20,10 @@ package org.apache.maven.resolver.examples.resolver; import java.io.ByteArrayOutputStream; import java.io.PrintStream; +import java.io.UnsupportedEncodingException; +import java.nio.charset.StandardCharsets; import org.apache.maven.resolver.examples.util.Booter; -import org.apache.maven.resolver.examples.util.ConsoleDependencyGraphDumper; import org.eclipse.aether.DefaultRepositorySystemSession; import org.eclipse.aether.RepositorySystem; import org.eclipse.aether.RepositorySystemSession; @@ -40,6 +41,7 @@ import org.eclipse.aether.repository.LocalRepository; import org.eclipse.aether.repository.RemoteRepository; import org.eclipse.aether.resolution.DependencyRequest; import org.eclipse.aether.resolution.DependencyResolutionException; +import org.eclipse.aether.util.graph.visitor.DependencyGraphDumper; import org.eclipse.aether.util.graph.visitor.PreorderNodeListGenerator; import org.eclipse.aether.util.repository.AuthenticationBuilder; @@ -121,8 +123,13 @@ public class Resolver { } private void displayTree(DependencyNode node, StringBuilder sb) { - ByteArrayOutputStream os = new ByteArrayOutputStream(1024); - node.accept(new ConsoleDependencyGraphDumper(new PrintStream(os))); - sb.append(os.toString()); + try { + ByteArrayOutputStream os = new ByteArrayOutputStream(1024); + PrintStream ps = new PrintStream(os, true, StandardCharsets.UTF_8.name()); + node.accept(new DependencyGraphDumper(ps::println)); + sb.append(os); + } catch (UnsupportedEncodingException e) { + throw new RuntimeException(e); + } } } diff --git a/maven-resolver-demos/maven-resolver-demo-snippets/src/main/java/org/apache/maven/resolver/examples/util/Booter.java b/maven-resolver-demos/maven-resolver-demo-snippets/src/main/java/org/apache/maven/resolver/examples/util/Booter.java index 93ade9c4..2744650b 100644 --- a/maven-resolver-demos/maven-resolver-demo-snippets/src/main/java/org/apache/maven/resolver/examples/util/Booter.java +++ b/maven-resolver-demos/maven-resolver-demo-snippets/src/main/java/org/apache/maven/resolver/examples/util/Booter.java @@ -28,6 +28,7 @@ import org.eclipse.aether.RepositorySystem; import org.eclipse.aether.RepositorySystemSession; import org.eclipse.aether.repository.LocalRepository; import org.eclipse.aether.repository.RemoteRepository; +import org.eclipse.aether.util.graph.visitor.DependencyGraphDumper; /** * A helper to boot the repository system and a repository system session. @@ -39,6 +40,8 @@ public class Booter { public static final String SISU = "sisu"; + public static final DependencyGraphDumper DUMPER_SOUT = new DependencyGraphDumper(System.out::println); + public static String selectFactory(String[] args) { if (args == null || args.length == 0) { return SERVICE_LOCATOR; diff --git a/maven-resolver-demos/maven-resolver-demo-snippets/src/main/remote-repository/org/apache/maven/resolver/demo/mresolver345/a/1.0/a-1.0.pom b/maven-resolver-demos/maven-resolver-demo-snippets/src/main/remote-repository/org/apache/maven/resolver/demo/mresolver345/a/1.0/a-1.0.pom new file mode 100644 index 00000000..918e0eb1 --- /dev/null +++ b/maven-resolver-demos/maven-resolver-demo-snippets/src/main/remote-repository/org/apache/maven/resolver/demo/mresolver345/a/1.0/a-1.0.pom @@ -0,0 +1,40 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + 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. +--> +<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> + <modelVersion>4.0.0</modelVersion> + + <groupId>org.apache.maven.resolver.demo.mresolver345</groupId> + <artifactId>a</artifactId> + <version>1.0</version> + + <dependencies> + <dependency> + <groupId>org.apache.maven.resolver.demo.mresolver345</groupId> + <artifactId>b</artifactId> + <version>1.0</version> + </dependency> + <dependency> + <groupId>org.apache.maven.resolver.demo.mresolver345</groupId> + <artifactId>c</artifactId> + <version>[1.0, 3.0]</version> + </dependency> + </dependencies> + +</project> diff --git a/maven-resolver-demos/maven-resolver-demo-snippets/src/main/remote-repository/org/apache/maven/resolver/demo/mresolver345/b/1.0/b-1.0.pom b/maven-resolver-demos/maven-resolver-demo-snippets/src/main/remote-repository/org/apache/maven/resolver/demo/mresolver345/b/1.0/b-1.0.pom new file mode 100644 index 00000000..02805ea9 --- /dev/null +++ b/maven-resolver-demos/maven-resolver-demo-snippets/src/main/remote-repository/org/apache/maven/resolver/demo/mresolver345/b/1.0/b-1.0.pom @@ -0,0 +1,35 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + 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. +--> +<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> + <modelVersion>4.0.0</modelVersion> + + <groupId>org.apache.maven.resolver.demo.mresolver345</groupId> + <artifactId>b</artifactId> + <version>1.0</version> + + <dependencies> + <dependency> + <groupId>org.apache.maven.resolver.demo.mresolver345</groupId> + <artifactId>c</artifactId> + <version>[1.0, 3.0]</version> + </dependency> + </dependencies> + +</project> diff --git a/maven-resolver-demos/maven-resolver-demo-snippets/src/main/remote-repository/org/apache/maven/resolver/demo/mresolver345/c/1.0/c-1.0.pom b/maven-resolver-demos/maven-resolver-demo-snippets/src/main/remote-repository/org/apache/maven/resolver/demo/mresolver345/c/1.0/c-1.0.pom new file mode 100644 index 00000000..a875fe07 --- /dev/null +++ b/maven-resolver-demos/maven-resolver-demo-snippets/src/main/remote-repository/org/apache/maven/resolver/demo/mresolver345/c/1.0/c-1.0.pom @@ -0,0 +1,27 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + 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. +--> +<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> + <modelVersion>4.0.0</modelVersion> + + <groupId>org.apache.maven.resolver.demo.mresolver345</groupId> + <artifactId>c</artifactId> + <version>1.0</version> + +</project> diff --git a/maven-resolver-demos/maven-resolver-demo-snippets/src/main/remote-repository/org/apache/maven/resolver/demo/mresolver345/c/2.0/c-2.0.pom b/maven-resolver-demos/maven-resolver-demo-snippets/src/main/remote-repository/org/apache/maven/resolver/demo/mresolver345/c/2.0/c-2.0.pom new file mode 100644 index 00000000..9f961ae0 --- /dev/null +++ b/maven-resolver-demos/maven-resolver-demo-snippets/src/main/remote-repository/org/apache/maven/resolver/demo/mresolver345/c/2.0/c-2.0.pom @@ -0,0 +1,27 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + 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. +--> +<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> + <modelVersion>4.0.0</modelVersion> + + <groupId>org.apache.maven.resolver.demo.mresolver345</groupId> + <artifactId>c</artifactId> + <version>2.0</version> + +</project> diff --git a/maven-resolver-demos/maven-resolver-demo-snippets/src/main/remote-repository/org/apache/maven/resolver/demo/mresolver345/c/3.0/c-3.0.pom b/maven-resolver-demos/maven-resolver-demo-snippets/src/main/remote-repository/org/apache/maven/resolver/demo/mresolver345/c/3.0/c-3.0.pom new file mode 100644 index 00000000..44f9af58 --- /dev/null +++ b/maven-resolver-demos/maven-resolver-demo-snippets/src/main/remote-repository/org/apache/maven/resolver/demo/mresolver345/c/3.0/c-3.0.pom @@ -0,0 +1,27 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + 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. +--> +<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> + <modelVersion>4.0.0</modelVersion> + + <groupId>org.apache.maven.resolver.demo.mresolver345</groupId> + <artifactId>c</artifactId> + <version>3.0</version> + +</project> diff --git a/maven-resolver-demos/maven-resolver-demo-snippets/src/main/remote-repository/org/apache/maven/resolver/demo/mresolver345/c/maven-metadata.xml b/maven-resolver-demos/maven-resolver-demo-snippets/src/main/remote-repository/org/apache/maven/resolver/demo/mresolver345/c/maven-metadata.xml new file mode 100644 index 00000000..7d3618d7 --- /dev/null +++ b/maven-resolver-demos/maven-resolver-demo-snippets/src/main/remote-repository/org/apache/maven/resolver/demo/mresolver345/c/maven-metadata.xml @@ -0,0 +1,33 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + 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. +--> +<metadata> + <groupId>org.apache.maven.resolver.demo.mresolver345</groupId> + <artifactId>c</artifactId> + <versioning> + <latest>3.0</latest> + <release>3.0</release> + <versions> + <version>1.0</version> + <version>2.0</version> + <version>3.0</version> + </versions> + <lastUpdated>20230320074804</lastUpdated> + </versioning> +</metadata> diff --git a/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/collect/DependencyCollectorDelegateTestSupport.java b/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/collect/DependencyCollectorDelegateTestSupport.java index 5cf4d7f2..89a2291e 100644 --- a/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/collect/DependencyCollectorDelegateTestSupport.java +++ b/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/collect/DependencyCollectorDelegateTestSupport.java @@ -497,6 +497,23 @@ public abstract class DependencyCollectorDelegateTestSupport { assertEqualSubtree(expectedTree, toDependencyResult(result.getRoot(), "compile", null)); } + @Test + public void testTransitiveDepsUseRangesDirtyTree() throws DependencyCollectionException, IOException { + // Note: DF depends on version order (ultimately the order of versions as returned by VersionRangeResolver + // that in case of Maven, means order as in maven-metadata.xml + // BF on the other hand explicitly sorts versions from range in descending order + // + // Hence, the "dirty tree" of two will not match. + DependencyNode root = parser.parseResource(getTransitiveDepsUseRangesDirtyTreeResource()); + Dependency dependency = root.getDependency(); + CollectRequest request = new CollectRequest(dependency, singletonList(repository)); + + CollectResult result = collector.collectDependencies(session, request); + assertEqualSubtree(root, result.getRoot()); + } + + protected abstract String getTransitiveDepsUseRangesDirtyTreeResource(); + private DependencyNode toDependencyResult( final DependencyNode root, final String rootScope, final Boolean optional) { // Make the root artifact resultion result a dependency resolution result for the subtree check. diff --git a/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/collect/bf/BfDependencyCollectorTest.java b/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/collect/bf/BfDependencyCollectorTest.java index 353030fd..870d2ef6 100644 --- a/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/collect/bf/BfDependencyCollectorTest.java +++ b/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/collect/bf/BfDependencyCollectorTest.java @@ -59,10 +59,13 @@ public class BfDependencyCollectorTest extends DependencyCollectorDelegateTestSu protected void setupCollector() { session.setConfigProperty(BfDependencyCollector.CONFIG_PROP_SKIPPER, useSkipper); - collector = new BfDependencyCollector(); - collector.setArtifactDescriptorReader(newReader("")); - collector.setVersionRangeResolver(new StubVersionRangeResolver()); - collector.setRemoteRepositoryManager(new StubRemoteRepositoryManager()); + collector = new BfDependencyCollector( + new StubRemoteRepositoryManager(), newReader(""), new StubVersionRangeResolver()); + } + + @Override + protected String getTransitiveDepsUseRangesDirtyTreeResource() { + return "transitiveDepsUseRangesDirtyTreeResult_BF.txt"; } private Dependency newDep(String coords, String scope, Collection<Exclusion> exclusions) { diff --git a/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/collect/df/DfDependencyCollectorTest.java b/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/collect/df/DfDependencyCollectorTest.java index f5e808b6..f07fdc33 100644 --- a/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/collect/df/DfDependencyCollectorTest.java +++ b/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/collect/df/DfDependencyCollectorTest.java @@ -28,9 +28,12 @@ import org.eclipse.aether.internal.impl.collect.DependencyCollectorDelegateTestS public class DfDependencyCollectorTest extends DependencyCollectorDelegateTestSupport { @Override protected void setupCollector() { - collector = new DfDependencyCollector(); - collector.setArtifactDescriptorReader(newReader("")); - collector.setVersionRangeResolver(new StubVersionRangeResolver()); - collector.setRemoteRepositoryManager(new StubRemoteRepositoryManager()); + collector = new DfDependencyCollector( + new StubRemoteRepositoryManager(), newReader(""), new StubVersionRangeResolver()); + } + + @Override + protected String getTransitiveDepsUseRangesDirtyTreeResource() { + return "transitiveDepsUseRangesDirtyTreeResult_DF.txt"; } } diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/transitiveDepsUseRangesDirtyTreeResult_BF.txt b/maven-resolver-impl/src/test/resources/artifact-descriptions/transitiveDepsUseRangesDirtyTreeResult_BF.txt new file mode 100644 index 00000000..b1711a4b --- /dev/null +++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/transitiveDepsUseRangesDirtyTreeResult_BF.txt @@ -0,0 +1,8 @@ +transitiveDepsUseRangesDirtyTree:aid:ext:1 compile ++- transitiveDepsUseRangesDirtyTree:bid:ext:1 compile +| +- transitiveDepsUseRangesDirtyTree:cid:ext:3 compile +| +- transitiveDepsUseRangesDirtyTree:cid:ext:2 compile +| \- transitiveDepsUseRangesDirtyTree:cid:ext:1 compile ++- transitiveDepsUseRangesDirtyTree:cid:ext:3 compile ++- transitiveDepsUseRangesDirtyTree:cid:ext:2 compile +\- transitiveDepsUseRangesDirtyTree:cid:ext:1 compile diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/transitiveDepsUseRangesDirtyTreeResult_DF.txt b/maven-resolver-impl/src/test/resources/artifact-descriptions/transitiveDepsUseRangesDirtyTreeResult_DF.txt new file mode 100644 index 00000000..ef148ca9 --- /dev/null +++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/transitiveDepsUseRangesDirtyTreeResult_DF.txt @@ -0,0 +1,8 @@ +transitiveDepsUseRangesDirtyTree:aid:ext:1 compile ++- transitiveDepsUseRangesDirtyTree:bid:ext:1 compile +| +- transitiveDepsUseRangesDirtyTree:cid:ext:1 compile +| +- transitiveDepsUseRangesDirtyTree:cid:ext:2 compile +| \- transitiveDepsUseRangesDirtyTree:cid:ext:3 compile ++- transitiveDepsUseRangesDirtyTree:cid:ext:1 compile ++- transitiveDepsUseRangesDirtyTree:cid:ext:2 compile +\- transitiveDepsUseRangesDirtyTree:cid:ext:3 compile diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/transitiveDepsUseRangesDirtyTree_aid_1.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/transitiveDepsUseRangesDirtyTree_aid_1.ini new file mode 100644 index 00000000..ed282a9c --- /dev/null +++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/transitiveDepsUseRangesDirtyTree_aid_1.ini @@ -0,0 +1,3 @@ +[dependencies] +transitiveDepsUseRangesDirtyTree:bid:ext:1 +transitiveDepsUseRangesDirtyTree:cid:ext:[1,3] diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/transitiveDepsUseRangesDirtyTree_bid_1.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/transitiveDepsUseRangesDirtyTree_bid_1.ini new file mode 100644 index 00000000..e9a5d76b --- /dev/null +++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/transitiveDepsUseRangesDirtyTree_bid_1.ini @@ -0,0 +1,2 @@ +[dependencies] +transitiveDepsUseRangesDirtyTree:cid:ext:[1,3] diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/transitiveDepsUseRangesDirtyTree_cid_1.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/transitiveDepsUseRangesDirtyTree_cid_1.ini new file mode 100644 index 00000000..61a252c2 --- /dev/null +++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/transitiveDepsUseRangesDirtyTree_cid_1.ini @@ -0,0 +1 @@ +[dependencies] diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/transitiveDepsUseRangesDirtyTree_cid_2.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/transitiveDepsUseRangesDirtyTree_cid_2.ini new file mode 100644 index 00000000..61a252c2 --- /dev/null +++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/transitiveDepsUseRangesDirtyTree_cid_2.ini @@ -0,0 +1 @@ +[dependencies] diff --git a/maven-resolver-impl/src/test/resources/artifact-descriptions/transitiveDepsUseRangesDirtyTree_cid_3.ini b/maven-resolver-impl/src/test/resources/artifact-descriptions/transitiveDepsUseRangesDirtyTree_cid_3.ini new file mode 100644 index 00000000..61a252c2 --- /dev/null +++ b/maven-resolver-impl/src/test/resources/artifact-descriptions/transitiveDepsUseRangesDirtyTree_cid_3.ini @@ -0,0 +1 @@ +[dependencies] diff --git a/maven-resolver-util/src/main/java/org/eclipse/aether/util/graph/transformer/ConflictResolver.java b/maven-resolver-util/src/main/java/org/eclipse/aether/util/graph/transformer/ConflictResolver.java index 7289c9bc..084f2d66 100644 --- a/maven-resolver-util/src/main/java/org/eclipse/aether/util/graph/transformer/ConflictResolver.java +++ b/maven-resolver-util/src/main/java/org/eclipse/aether/util/graph/transformer/ConflictResolver.java @@ -18,26 +18,17 @@ */ package org.eclipse.aether.util.graph.transformer; -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 java.util.*; import org.eclipse.aether.RepositoryException; +import org.eclipse.aether.RepositorySystemSession; import org.eclipse.aether.artifact.Artifact; import org.eclipse.aether.collection.DependencyGraphTransformationContext; import org.eclipse.aether.collection.DependencyGraphTransformer; import org.eclipse.aether.graph.DefaultDependencyNode; import org.eclipse.aether.graph.Dependency; import org.eclipse.aether.graph.DependencyNode; -import org.eclipse.aether.util.ConfigUtils; +import org.eclipse.aether.util.artifact.ArtifactIdUtils; import static java.util.Objects.requireNonNull; @@ -66,9 +57,66 @@ public final class ConflictResolver implements DependencyGraphTransformer { /** * The key in the repository session's {@link org.eclipse.aether.RepositorySystemSession#getConfigProperties() * configuration properties} used to store a {@link Boolean} flag controlling the transformer's verbose mode. + * Accepted values are {@link Boolean} type, {@link String} type (where "true" would be interpreted as {@code true} + * or {@link Verbosity} enum instances. */ public static final String CONFIG_PROP_VERBOSE = "aether.conflictResolver.verbose"; + /** + * The enum representing verbosity levels of conflict resolver. + * + * @since 1.9.8 + */ + public enum Verbosity { + /** + * Verbosity level to be used in all "common" resolving use cases (ie. dependencies to build class path). The + * {@link ConflictResolver} in this mode will trim down the graph to the barest minimum: will not leave + * any conflicting node in place, hence no conflicts will be present in transformed graph either. + */ + NONE, + + /** + * Verbosity level to be used in "analyze" resolving use cases (ie. dependency convergence calculations). The + * {@link ConflictResolver} in this mode will remove any redundant collected nodes, in turn it will leave one + * with recorded conflicting information. This mode corresponds to "classic verbose" mode when + * {@link #CONFIG_PROP_VERBOSE} was set to {@code true}. Obviously, the resulting dependency tree is not + * suitable for artifact resolution unless a filter is employed to exclude the duplicate dependencies. + */ + STANDARD, + + /** + * Verbosity level to be used in "analyze" resolving use cases (ie. dependency convergence calculations). The + * {@link ConflictResolver} in this mode will not remove any collected node, in turn it will record on all + * eliminated nodes the conflicting information. Obviously, the resulting dependency tree is not suitable + * for artifact resolution unless a filter is employed to exclude the duplicate dependencies. + */ + FULL + } + + /** + * Helper method that uses {@link RepositorySystemSession} and {@link #CONFIG_PROP_VERBOSE} key to figure out + * current {@link Verbosity}: if {@link Boolean} or {@code String} found, returns {@link Verbosity#STANDARD} + * or {@link Verbosity#NONE}, depending on value (string is parsed with {@link Boolean#parseBoolean(String)} + * for {@code true} or {@code false} correspondingly. This is to retain "existing" behavior, where the config + * key accepted only these values. + * Since 1.9.8 release, this key may contain {@link Verbosity} enum instance as well, in which case that instance + * is returned. + * This method never returns {@code null}. + */ + private static Verbosity getVerbosity(RepositorySystemSession session) { + final Object verbosityValue = session.getConfigProperties().get(CONFIG_PROP_VERBOSE); + if (verbosityValue instanceof Boolean) { + return (Boolean) verbosityValue ? Verbosity.STANDARD : Verbosity.NONE; + } else if (verbosityValue instanceof String) { + return Boolean.parseBoolean(verbosityValue.toString()) ? Verbosity.STANDARD : Verbosity.NONE; + } else if (verbosityValue instanceof Verbosity) { + return (Verbosity) verbosityValue; + } else if (verbosityValue != null) { + throw new IllegalArgumentException("Unsupported Verbosity configuration: " + verbosityValue); + } + return Verbosity.NONE; + } + /** * 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. @@ -173,14 +221,14 @@ public final class ConflictResolver implements DependencyGraphTransformer { DependencyNode winner = ctx.winner.node; state.scopeSelector.selectScope(ctx); - if (state.verbose) { + if (Verbosity.NONE != state.verbosity) { winner.setData( NODE_DATA_ORIGINAL_SCOPE, winner.getDependency().getScope()); } winner.setScope(ctx.scope); state.optionalitySelector.selectOptionality(ctx); - if (state.verbose) { + if (Verbosity.NONE != state.verbosity) { winner.setData( NODE_DATA_ORIGINAL_OPTIONALITY, winner.getDependency().isOptional()); @@ -232,11 +280,12 @@ public final class ConflictResolver implements DependencyGraphTransformer { return true; } - private void removeLosers(State state) { + private static void removeLosers(State state) { ConflictItem winner = state.conflictCtx.winner; + String winnerArtifactId = ArtifactIdUtils.toId(winner.node.getArtifact()); List<DependencyNode> previousParent = null; ListIterator<DependencyNode> childIt = null; - boolean conflictVisualized = false; + HashSet<String> toRemoveIds = new HashSet<>(); for (ConflictItem item : state.items) { if (item == winner) { continue; @@ -244,30 +293,78 @@ public final class ConflictResolver implements DependencyGraphTransformer { 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 { + // NONE: just remove it and done + if (Verbosity.NONE == state.verbosity) { childIt.remove(); + break; + } + + // STANDARD: doing extra bookkeeping to select "which nodes to remove" + if (Verbosity.STANDARD == state.verbosity) { + String childArtifactId = ArtifactIdUtils.toId(child.getArtifact()); + // if two IDs are equal, it means "there is nearest", not conflict per se. + // In that case we do NOT allow this child to be removed (but remove others) + // and this keeps us safe from iteration (and in general, version) ordering + // as we explicitly leave out ID that is "nearest found" state. + // + // This tackles version ranges mostly, where ranges are turned into list of + // several nodes in collector (as many were discovered, ie. from metadata), and + // old code would just "mark" the first hit as conflict, and remove the rest, + // even if rest could contain "more suitable" version, that is not conflicting/diverging. + // This resulted in verbose mode transformed tree, that was misrepresenting things + // for dependency convergence calculations: it represented state like parent node + // depends on "wrong" version (diverge), while "right" version was present (but removed) + // as well, as it was contained in parents version range. + if (!Objects.equals(winnerArtifactId, childArtifactId)) { + toRemoveIds.add(childArtifactId); + } } + + // FULL: just record the facts + 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.emptyList()); + childIt.set(loser); + item.node = loser; break; } } } + + // 2nd pass to apply "standard" verbosity: leaving only 1 loser, but with care + if (Verbosity.STANDARD == state.verbosity && !toRemoveIds.isEmpty()) { + previousParent = null; + for (ConflictItem item : state.items) { + if (item == winner) { + continue; + } + if (item.parent != previousParent) { + childIt = item.parent.listIterator(); + previousParent = item.parent; + } + while (childIt.hasNext()) { + DependencyNode child = childIt.next(); + if (child == item.node) { + String childArtifactId = ArtifactIdUtils.toId(child.getArtifact()); + if (toRemoveIds.contains(childArtifactId) && item.parent.size() > 1) { + 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 } @@ -361,7 +458,7 @@ public final class ConflictResolver implements DependencyGraphTransformer { /** * Flag whether we should keep losers in the graph to enable visualization/troubleshooting of conflicts. */ - final boolean verbose; + final Verbosity verbosity; /** * A mapping from conflict id to winner node, helps to recognize nodes that have their effective @@ -458,7 +555,7 @@ public final class ConflictResolver implements DependencyGraphTransformer { DependencyGraphTransformationContext context) throws RepositoryException { this.conflictIds = conflictIds; - verbose = ConfigUtils.getBoolean(context.getSession(), false, CONFIG_PROP_VERBOSE); + this.verbosity = getVerbosity(context.getSession()); potentialAncestorIds = new HashSet<>(conflictIdCount * 2); resolvedIds = new HashMap<>(conflictIdCount * 2); items = new ArrayList<>(256); @@ -734,7 +831,8 @@ public final class ConflictResolver implements DependencyGraphTransformer { // only for debugging/toString() to help identify the parent node(s) final Artifact artifact; - final DependencyNode node; + // is mutable as removeLosers will mutate it (if Verbosity==STANDARD) + DependencyNode node; int depth; diff --git a/maven-resolver-util/src/main/java/org/eclipse/aether/util/graph/visitor/AbstractDepthFirstNodeListGenerator.java b/maven-resolver-util/src/main/java/org/eclipse/aether/util/graph/visitor/AbstractDepthFirstNodeListGenerator.java index 97b3342a..6293dda0 100644 --- a/maven-resolver-util/src/main/java/org/eclipse/aether/util/graph/visitor/AbstractDepthFirstNodeListGenerator.java +++ b/maven-resolver-util/src/main/java/org/eclipse/aether/util/graph/visitor/AbstractDepthFirstNodeListGenerator.java @@ -156,7 +156,9 @@ abstract class AbstractDepthFirstNodeListGenerator implements DependencyVisitor return visitedNodes.put(node, Boolean.TRUE) == null; } + @Override public abstract boolean visitEnter(DependencyNode node); + @Override public abstract boolean visitLeave(DependencyNode node); } diff --git a/maven-resolver-util/src/main/java/org/eclipse/aether/util/graph/visitor/CloningDependencyVisitor.java b/maven-resolver-util/src/main/java/org/eclipse/aether/util/graph/visitor/CloningDependencyVisitor.java index 0da5649f..ab1406e8 100644 --- a/maven-resolver-util/src/main/java/org/eclipse/aether/util/graph/visitor/CloningDependencyVisitor.java +++ b/maven-resolver-util/src/main/java/org/eclipse/aether/util/graph/visitor/CloningDependencyVisitor.java @@ -66,6 +66,7 @@ public class CloningDependencyVisitor implements DependencyVisitor { return new DefaultDependencyNode(node); } + @Override public final boolean visitEnter(DependencyNode node) { boolean recurse = true; @@ -90,6 +91,7 @@ public class CloningDependencyVisitor implements DependencyVisitor { return recurse; } + @Override public final boolean visitLeave(DependencyNode node) { parents.pop(); diff --git a/maven-resolver-demos/maven-resolver-demo-snippets/src/main/java/org/apache/maven/resolver/examples/util/ConsoleDependencyGraphDumper.java b/maven-resolver-util/src/main/java/org/eclipse/aether/util/graph/visitor/DependencyGraphDumper.java similarity index 75% rename from maven-resolver-demos/maven-resolver-demo-snippets/src/main/java/org/apache/maven/resolver/examples/util/ConsoleDependencyGraphDumper.java rename to maven-resolver-util/src/main/java/org/eclipse/aether/util/graph/visitor/DependencyGraphDumper.java index e94ae2c3..60a12727 100644 --- a/maven-resolver-demos/maven-resolver-demo-snippets/src/main/java/org/apache/maven/resolver/examples/util/ConsoleDependencyGraphDumper.java +++ b/maven-resolver-util/src/main/java/org/eclipse/aether/util/graph/visitor/DependencyGraphDumper.java @@ -16,12 +16,12 @@ * specific language governing permissions and limitations * under the License. */ -package org.apache.maven.resolver.examples.util; +package org.eclipse.aether.util.graph.visitor; -import java.io.PrintStream; import java.util.ArrayList; import java.util.Iterator; import java.util.List; +import java.util.function.Consumer; import org.eclipse.aether.artifact.Artifact; import org.eclipse.aether.graph.Dependency; @@ -31,25 +31,27 @@ import org.eclipse.aether.util.artifact.ArtifactIdUtils; import org.eclipse.aether.util.graph.manager.DependencyManagerUtils; import org.eclipse.aether.util.graph.transformer.ConflictResolver; +import static java.util.Objects.requireNonNull; + /** - * A dependency visitor that dumps the graph to the console. + * A dependency visitor that dumps the graph to any {@link Consumer<String>}. Meant for diagnostic and testing, as + * it may output the graph to standard output, error or even some logging interface. + * + * @since 1.9.8 */ -public class ConsoleDependencyGraphDumper implements DependencyVisitor { +public class DependencyGraphDumper implements DependencyVisitor { - private final PrintStream out; + private final Consumer<String> consumer; private final List<ChildInfo> childInfos = new ArrayList<>(); - public ConsoleDependencyGraphDumper() { - this(null); - } - - public ConsoleDependencyGraphDumper(PrintStream out) { - this.out = (out != null) ? out : System.out; + public DependencyGraphDumper(Consumer<String> consumer) { + this.consumer = requireNonNull(consumer); } + @Override public boolean visitEnter(DependencyNode node) { - out.println(formatIndentation() + formatNode(node)); + consumer.accept(formatIndentation() + formatNode(node)); childInfos.add(new ChildInfo(node.getChildren().size())); return true; } @@ -84,19 +86,24 @@ public class ConsoleDependencyGraphDumper implements DependencyVisitor { buffer.append(" (scope managed from ").append(premanaged).append(")"); } DependencyNode winner = (DependencyNode) node.getData().get(ConflictResolver.NODE_DATA_WINNER); - if (winner != null && !ArtifactIdUtils.equalsId(a, winner.getArtifact())) { - Artifact w = winner.getArtifact(); - buffer.append(" (conflicts with "); - if (ArtifactIdUtils.toVersionlessId(a).equals(ArtifactIdUtils.toVersionlessId(w))) { - buffer.append(w.getVersion()); + if (winner != null) { + if (ArtifactIdUtils.equalsId(a, winner.getArtifact())) { + buffer.append(" (nearer exists)"); } else { - buffer.append(w); + Artifact w = winner.getArtifact(); + buffer.append(" (conflicts with "); + if (ArtifactIdUtils.toVersionlessId(a).equals(ArtifactIdUtils.toVersionlessId(w))) { + buffer.append(w.getVersion()); + } else { + buffer.append(w); + } + buffer.append(")"); } - buffer.append(")"); } return buffer.toString(); } + @Override public boolean visitLeave(DependencyNode node) { if (!childInfos.isEmpty()) { childInfos.remove(childInfos.size() - 1); diff --git a/maven-resolver-util/src/main/java/org/eclipse/aether/util/graph/visitor/FilteringDependencyVisitor.java b/maven-resolver-util/src/main/java/org/eclipse/aether/util/graph/visitor/FilteringDependencyVisitor.java index 16a85102..202c2598 100644 --- a/maven-resolver-util/src/main/java/org/eclipse/aether/util/graph/visitor/FilteringDependencyVisitor.java +++ b/maven-resolver-util/src/main/java/org/eclipse/aether/util/graph/visitor/FilteringDependencyVisitor.java @@ -26,7 +26,7 @@ import static java.util.Objects.requireNonNull; /** * A dependency visitor that delegates to another visitor if nodes match a filter. Note that in case of a mismatching - * node, the children of that node are still visisted and presented to the filter. + * node, the children of that node are still visited and presented to the filter. */ public final class FilteringDependencyVisitor implements DependencyVisitor { @@ -69,6 +69,7 @@ public final class FilteringDependencyVisitor implements DependencyVisitor { return filter; } + @Override public boolean visitEnter(DependencyNode node) { boolean accept = filter == null || filter.accept(node, parents); @@ -83,6 +84,7 @@ public final class FilteringDependencyVisitor implements DependencyVisitor { } } + @Override public boolean visitLeave(DependencyNode node) { parents.pop(); diff --git a/maven-resolver-util/src/main/java/org/eclipse/aether/util/graph/visitor/PathRecordingDependencyVisitor.java b/maven-resolver-util/src/main/java/org/eclipse/aether/util/graph/visitor/PathRecordingDependencyVisitor.java index 11932385..d0576ca9 100644 --- a/maven-resolver-util/src/main/java/org/eclipse/aether/util/graph/visitor/PathRecordingDependencyVisitor.java +++ b/maven-resolver-util/src/main/java/org/eclipse/aether/util/graph/visitor/PathRecordingDependencyVisitor.java @@ -85,6 +85,7 @@ public final class PathRecordingDependencyVisitor implements DependencyVisitor { return paths; } + @Override public boolean visitEnter(DependencyNode node) { boolean accept = filter == null || filter.accept(node, parents); @@ -106,6 +107,7 @@ public final class PathRecordingDependencyVisitor implements DependencyVisitor { return !hasDuplicateNodeInParent; } + @Override public boolean visitLeave(DependencyNode node) { parents.pop(); diff --git a/maven-resolver-util/src/main/java/org/eclipse/aether/util/graph/visitor/PostorderNodeListGenerator.java b/maven-resolver-util/src/main/java/org/eclipse/aether/util/graph/visitor/PostorderNodeListGenerator.java index b8ba0dae..09dad67c 100644 --- a/maven-resolver-util/src/main/java/org/eclipse/aether/util/graph/visitor/PostorderNodeListGenerator.java +++ b/maven-resolver-util/src/main/java/org/eclipse/aether/util/graph/visitor/PostorderNodeListGenerator.java @@ -42,11 +42,7 @@ public final class PostorderNodeListGenerator extends AbstractDepthFirstNodeList visits.push(visited); - if (visited) { - return false; - } - - return true; + return !visited; } @Override diff --git a/maven-resolver-util/src/main/java/org/eclipse/aether/util/graph/visitor/PreorderNodeListGenerator.java b/maven-resolver-util/src/main/java/org/eclipse/aether/util/graph/visitor/PreorderNodeListGenerator.java index f8370b1a..07841049 100644 --- a/maven-resolver-util/src/main/java/org/eclipse/aether/util/graph/visitor/PreorderNodeListGenerator.java +++ b/maven-resolver-util/src/main/java/org/eclipse/aether/util/graph/visitor/PreorderNodeListGenerator.java @@ -21,7 +21,7 @@ package org.eclipse.aether.util.graph.visitor; import org.eclipse.aether.graph.DependencyNode; /** - * Generates a sequence of dependency nodes from a dependeny graph by traversing the graph in preorder. This visitor + * Generates a sequence of dependency nodes from a dependency graph by traversing the graph in preorder. This visitor * visits each node exactly once regardless how many paths within the dependency graph lead to the node such that the * resulting node sequence is free of duplicates. */ diff --git a/maven-resolver-util/src/main/java/org/eclipse/aether/util/graph/visitor/TreeDependencyVisitor.java b/maven-resolver-util/src/main/java/org/eclipse/aether/util/graph/visitor/TreeDependencyVisitor.java index 94cc382e..44b3759c 100644 --- a/maven-resolver-util/src/main/java/org/eclipse/aether/util/graph/visitor/TreeDependencyVisitor.java +++ b/maven-resolver-util/src/main/java/org/eclipse/aether/util/graph/visitor/TreeDependencyVisitor.java @@ -50,6 +50,7 @@ public final class TreeDependencyVisitor implements DependencyVisitor { visits = new Stack<>(); } + @Override public boolean visitEnter(DependencyNode node) { boolean visited = visitedNodes.put(node, Boolean.TRUE) != null; @@ -62,6 +63,7 @@ public final class TreeDependencyVisitor implements DependencyVisitor { return visitor.visitEnter(node); } + @Override public boolean visitLeave(DependencyNode node) { Boolean visited = visits.pop(); diff --git a/maven-resolver-util/src/test/java/org/eclipse/aether/util/graph/transformer/ConflictResolverTest.java b/maven-resolver-util/src/test/java/org/eclipse/aether/util/graph/transformer/ConflictResolverTest.java index 01b28800..0486e669 100644 --- a/maven-resolver-util/src/test/java/org/eclipse/aether/util/graph/transformer/ConflictResolverTest.java +++ b/maven-resolver-util/src/test/java/org/eclipse/aether/util/graph/transformer/ConflictResolverTest.java @@ -22,6 +22,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.List; +import org.eclipse.aether.DefaultRepositorySystemSession; import org.eclipse.aether.RepositoryException; import org.eclipse.aether.artifact.DefaultArtifact; import org.eclipse.aether.graph.DefaultDependencyNode; @@ -30,9 +31,13 @@ import org.eclipse.aether.graph.DependencyNode; import org.eclipse.aether.internal.test.util.TestUtils; import org.eclipse.aether.internal.test.util.TestVersion; import org.eclipse.aether.internal.test.util.TestVersionConstraint; +import org.eclipse.aether.util.graph.visitor.DependencyGraphDumper; import org.junit.Test; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNotSame; +import static org.junit.Assert.assertNull; import static org.junit.Assert.assertSame; import static org.junit.Assert.assertTrue; @@ -77,6 +82,266 @@ public class ConflictResolverTest { assertSame(baz1Node, fooNode.getChildren().get(1)); } + @Test + public void versionClashForkedStandardVerbose() throws RepositoryException { + + // root -> impl1 -> api:1 + // |----> impl2 -> api:2 + DependencyNode root = makeDependencyNode("some-group", "root", "1.0"); + DependencyNode impl1 = makeDependencyNode("some-group", "impl1", "1.0"); + DependencyNode impl2 = makeDependencyNode("some-group", "impl2", "1.0"); + DependencyNode api1 = makeDependencyNode("some-group", "api", "1.1"); + DependencyNode api2 = makeDependencyNode("some-group", "api", "1.0"); + + root.setChildren(mutableList(impl1, impl2)); + impl1.setChildren(mutableList(api1)); + impl2.setChildren(mutableList(api2)); + + DependencyNode transformedNode = versionRangeClash(root, ConflictResolver.Verbosity.STANDARD); + + assertSame(root, transformedNode); + assertEquals(2, root.getChildren().size()); + assertSame(impl1, root.getChildren().get(0)); + assertSame(impl2, root.getChildren().get(1)); + assertEquals(1, impl1.getChildren().size()); + assertSame(api1, impl1.getChildren().get(0)); + assertEquals(1, impl2.getChildren().size()); + assertConflictedButSameAsOriginal(api2, impl2.getChildren().get(0)); + } + + @Test + public void versionRangeClashAscOrder() throws RepositoryException { + // A -> B -> C[1..2] + // \--> C[1..2] + DependencyNode a = makeDependencyNode("some-group", "a", "1.0"); + DependencyNode b = makeDependencyNode("some-group", "b", "1.0"); + DependencyNode c1 = makeDependencyNode("some-group", "c", "1.0"); + DependencyNode c2 = makeDependencyNode("some-group", "c", "2.0"); + a.setChildren(mutableList(b, c1, c2)); + b.setChildren(mutableList(c1, c2)); + + DependencyNode ta = versionRangeClash(a, ConflictResolver.Verbosity.NONE); + + assertSame(a, ta); + assertEquals(2, a.getChildren().size()); + assertSame(b, a.getChildren().get(0)); + assertSame(c2, a.getChildren().get(1)); + assertEquals(0, b.getChildren().size()); + } + + @Test + public void versionRangeClashAscOrderStandardVerbose() throws RepositoryException { + // A -> B -> C[1..2] + // \--> C[1..2] + DependencyNode a = makeDependencyNode("some-group", "a", "1.0"); + DependencyNode b = makeDependencyNode("some-group", "b", "1.0"); + DependencyNode c1 = makeDependencyNode("some-group", "c", "1.0"); + DependencyNode c2 = makeDependencyNode("some-group", "c", "2.0"); + a.setChildren(mutableList(b, c1, c2)); + b.setChildren(mutableList(c1, c2)); + + DependencyNode ta = versionRangeClash(a, ConflictResolver.Verbosity.STANDARD); + + assertSame(a, ta); + assertEquals(2, a.getChildren().size()); + assertSame(b, a.getChildren().get(0)); + assertSame(c2, a.getChildren().get(1)); + assertEquals(1, b.getChildren().size()); + assertConflictedButSameAsOriginal(c2, b.getChildren().get(0)); + } + + @Test + public void versionRangeClashAscOrderFullVerbose() throws RepositoryException { + // A -> B -> C[1..2] + // \--> C[1..2] + DependencyNode a = makeDependencyNode("some-group", "a", "1.0"); + DependencyNode b = makeDependencyNode("some-group", "b", "1.0"); + DependencyNode c1 = makeDependencyNode("some-group", "c", "1.0"); + DependencyNode c2 = makeDependencyNode("some-group", "c", "2.0"); + a.setChildren(mutableList(b, c1, c2)); + b.setChildren(mutableList(c1, c2)); + + DependencyNode ta = versionRangeClash(a, ConflictResolver.Verbosity.FULL); + + assertSame(a, ta); + assertEquals(3, a.getChildren().size()); + assertSame(b, a.getChildren().get(0)); + assertConflictedButSameAsOriginal(c1, a.getChildren().get(1)); + assertSame(c2, a.getChildren().get(2)); + assertEquals(2, b.getChildren().size()); + assertConflictedButSameAsOriginal(c1, b.getChildren().get(0)); + assertConflictedButSameAsOriginal(c2, b.getChildren().get(1)); + } + + @Test + public void versionRangeClashDescOrder() throws RepositoryException { + // A -> B -> C[1..2] + // \--> C[1..2] + DependencyNode a = makeDependencyNode("some-group", "a", "1.0"); + DependencyNode b = makeDependencyNode("some-group", "b", "1.0"); + DependencyNode c1 = makeDependencyNode("some-group", "c", "1.0"); + DependencyNode c2 = makeDependencyNode("some-group", "c", "2.0"); + a.setChildren(mutableList(b, c2, c1)); + b.setChildren(mutableList(c2, c1)); + + DependencyNode ta = versionRangeClash(a, ConflictResolver.Verbosity.NONE); + + assertSame(a, ta); + assertEquals(2, a.getChildren().size()); + assertSame(b, a.getChildren().get(0)); + assertSame(c2, a.getChildren().get(1)); + assertEquals(0, b.getChildren().size()); + } + + @Test + public void versionRangeClashDescOrderStandardVerbose() throws RepositoryException { + // A -> B -> C[1..2] + // \--> C[1..2] + DependencyNode a = makeDependencyNode("some-group", "a", "1.0"); + DependencyNode b = makeDependencyNode("some-group", "b", "1.0"); + DependencyNode c1 = makeDependencyNode("some-group", "c", "1.0"); + DependencyNode c2 = makeDependencyNode("some-group", "c", "2.0"); + a.setChildren(mutableList(b, c2, c1)); + b.setChildren(mutableList(c2, c1)); + + DependencyNode ta = versionRangeClash(a, ConflictResolver.Verbosity.STANDARD); + + assertSame(a, ta); + assertEquals(2, a.getChildren().size()); + assertSame(b, a.getChildren().get(0)); + assertSame(c2, a.getChildren().get(1)); + assertEquals(1, b.getChildren().size()); + assertConflictedButSameAsOriginal(c2, b.getChildren().get(0)); + } + + @Test + public void versionRangeClashDescOrderFullVerbose() throws RepositoryException { + // A -> B -> C[1..2] + // \--> C[1..2] + DependencyNode a = makeDependencyNode("some-group", "a", "1.0"); + DependencyNode b = makeDependencyNode("some-group", "b", "1.0"); + DependencyNode c1 = makeDependencyNode("some-group", "c", "1.0"); + DependencyNode c2 = makeDependencyNode("some-group", "c", "2.0"); + a.setChildren(mutableList(b, c2, c1)); + b.setChildren(mutableList(c2, c1)); + + DependencyNode ta = versionRangeClash(a, ConflictResolver.Verbosity.FULL); + + assertSame(a, ta); + assertEquals(3, a.getChildren().size()); + assertSame(b, a.getChildren().get(0)); + assertSame(c2, a.getChildren().get(1)); + assertConflictedButSameAsOriginal(c1, a.getChildren().get(2)); + assertEquals(2, b.getChildren().size()); + assertConflictedButSameAsOriginal(c2, b.getChildren().get(0)); + assertConflictedButSameAsOriginal(c1, b.getChildren().get(1)); + } + + @Test + public void versionRangeClashMixedOrder() throws RepositoryException { + // A -> B -> C[1..2] + // \--> C[1..2] + DependencyNode a = makeDependencyNode("some-group", "a", "1.0"); + DependencyNode b = makeDependencyNode("some-group", "b", "1.0"); + DependencyNode c1 = makeDependencyNode("some-group", "c", "1.0"); + DependencyNode c2 = makeDependencyNode("some-group", "c", "2.0"); + a.setChildren(mutableList(b, c2, c1)); + b.setChildren(mutableList(c1, c2)); + + DependencyNode ta = versionRangeClash(a, ConflictResolver.Verbosity.NONE); + + assertSame(a, ta); + assertEquals(2, a.getChildren().size()); + assertSame(b, a.getChildren().get(0)); + assertSame(c2, a.getChildren().get(1)); + assertEquals(0, b.getChildren().size()); + } + + @Test + public void versionRangeClashMixedOrderStandardVerbose() throws RepositoryException { + // A -> B -> C[1..2] + // \--> C[1..2] + DependencyNode a = makeDependencyNode("some-group", "a", "1.0"); + DependencyNode b = makeDependencyNode("some-group", "b", "1.0"); + DependencyNode c1 = makeDependencyNode("some-group", "c", "1.0"); + DependencyNode c2 = makeDependencyNode("some-group", "c", "2.0"); + a.setChildren(mutableList(b, c2, c1)); + b.setChildren(mutableList(c1, c2)); + + DependencyNode ta = versionRangeClash(a, ConflictResolver.Verbosity.STANDARD); + + assertSame(a, ta); + assertEquals(2, a.getChildren().size()); + assertSame(b, a.getChildren().get(0)); + assertSame(c2, a.getChildren().get(1)); + assertEquals(1, b.getChildren().size()); + assertConflictedButSameAsOriginal(c2, b.getChildren().get(0)); + } + + @Test + public void versionRangeClashMixedOrderFullVerbose() throws RepositoryException { + // A -> B -> C[1..2] + // \--> C[1..2] + DependencyNode a = makeDependencyNode("some-group", "a", "1.0"); + DependencyNode b = makeDependencyNode("some-group", "b", "1.0"); + DependencyNode c1 = makeDependencyNode("some-group", "c", "1.0"); + DependencyNode c2 = makeDependencyNode("some-group", "c", "2.0"); + a.setChildren(mutableList(b, c2, c1)); + b.setChildren(mutableList(c1, c2)); + + DependencyNode ta = versionRangeClash(a, ConflictResolver.Verbosity.FULL); + + assertSame(a, ta); + assertEquals(3, a.getChildren().size()); + assertSame(b, a.getChildren().get(0)); + assertSame(c2, a.getChildren().get(1)); + assertConflictedButSameAsOriginal(c1, a.getChildren().get(2)); + assertEquals(2, b.getChildren().size()); + assertConflictedButSameAsOriginal(c1, b.getChildren().get(0)); + assertConflictedButSameAsOriginal(c2, b.getChildren().get(1)); + } + + /** + * Conflict resolver in case of conflict replaces {@link DependencyNode} instances with copies to keep them + * stateful on different levels of graph and records conflict data. This method assert that two nodes do represent + * same dependency (same GAV, scope, optionality), but that original is not conflicted while current is. + */ + private void assertConflictedButSameAsOriginal(DependencyNode original, DependencyNode current) { + assertNotSame(original, current); + assertEquals( + original.getDependency().getArtifact(), current.getDependency().getArtifact()); + assertEquals( + original.getDependency().getScope(), current.getDependency().getScope()); + assertEquals( + original.getDependency().getOptional(), current.getDependency().getOptional()); + assertNull(original.getData().get(ConflictResolver.NODE_DATA_WINNER)); + assertNotNull(current.getData().get(ConflictResolver.NODE_DATA_WINNER)); + } + + private static final DependencyGraphDumper DUMPER_SOUT = new DependencyGraphDumper(System.out::println); + + /** + * Performs a verbose conflict resolution on passed in root. + */ + private DependencyNode versionRangeClash(DependencyNode root, ConflictResolver.Verbosity verbosity) + throws RepositoryException { + ConflictResolver resolver = makeDefaultResolver(); + + System.out.println(); + System.out.println("Input node:"); + root.accept(DUMPER_SOUT); + + DefaultRepositorySystemSession session = TestUtils.newSession(); + session.setConfigProperty(ConflictResolver.CONFIG_PROP_VERBOSE, verbosity); + DependencyNode transformedRoot = resolver.transformGraph(root, TestUtils.newTransformationContext(session)); + + System.out.println(); + System.out.println("Transformed node:"); + transformedRoot.accept(DUMPER_SOUT); + + return transformedRoot; + } + @Test public void derivedScopeChange() throws RepositoryException { ConflictResolver resolver = makeDefaultResolver();