diff --git a/src/it/projects/tree-excluded/expected-included-survives.txt b/src/it/projects/tree-excluded/expected-included-survives.txt new file mode 100644 index 000000000..b61e0a3ba --- /dev/null +++ b/src/it/projects/tree-excluded/expected-included-survives.txt @@ -0,0 +1,4 @@ +org.apache.maven.its.dependency:test:jar:1.0-SNAPSHOT +\- org.apache.maven:maven-project:jar:2.0.6:compile + \- org.apache.maven:maven-artifact-manager:jar:2.0.6:compile + \- org.apache.maven.wagon:wagon-provider-api:jar:1.0-beta-2:compile diff --git a/src/it/projects/tree-excluded/expected-v4.txt b/src/it/projects/tree-excluded/expected-v4.txt index 82cd95252..ff6840584 100644 --- a/src/it/projects/tree-excluded/expected-v4.txt +++ b/src/it/projects/tree-excluded/expected-v4.txt @@ -7,7 +7,4 @@ org.apache.maven.its.dependency:test:jar:1.0-SNAPSHOT | +- org.apache.maven:maven-repository-metadata:jar:2.0.6:compile | \- org.apache.maven.wagon:wagon-provider-api:jar:1.0-beta-2:compile +- org.apache.maven:maven-plugin-registry:jar:2.0.6:compile - +- org.apache.maven:maven-artifact:jar:2.0.6:compile - \- org.codehaus.plexus:plexus-container-default:jar:1.0-alpha-9-stable-1:compile - +- junit:junit:jar:3.8.1:compile - \- classworlds:classworlds:jar:1.1:compile + \- org.apache.maven:maven-artifact:jar:2.0.6:compile diff --git a/src/it/projects/tree-excluded/expected.txt b/src/it/projects/tree-excluded/expected.txt index 38be1dd0d..ff6840584 100644 --- a/src/it/projects/tree-excluded/expected.txt +++ b/src/it/projects/tree-excluded/expected.txt @@ -7,7 +7,4 @@ org.apache.maven.its.dependency:test:jar:1.0-SNAPSHOT | +- org.apache.maven:maven-repository-metadata:jar:2.0.6:compile | \- org.apache.maven.wagon:wagon-provider-api:jar:1.0-beta-2:compile +- org.apache.maven:maven-plugin-registry:jar:2.0.6:compile - +- org.apache.maven:maven-artifact:jar:2.0.6:compile - \- org.codehaus.plexus:plexus-container-default:jar:1.0-alpha-9-stable-1:compile - +- junit:junit:jar:3.8.1:compile - \- classworlds:classworlds:jar:1.1-alpha-2:compile + \- org.apache.maven:maven-artifact:jar:2.0.6:compile diff --git a/src/it/projects/tree-excluded/invoker.properties b/src/it/projects/tree-excluded/invoker.properties index afb73d253..476a9d5ed 100644 --- a/src/it/projects/tree-excluded/invoker.properties +++ b/src/it/projects/tree-excluded/invoker.properties @@ -15,4 +15,11 @@ # specific language governing permissions and limitations # under the License. -invoker.goals = ${project.groupId}:${project.artifactId}:${project.version}:tree +invoker.goals.1 = ${project.groupId}:${project.artifactId}:${project.version}:tree +invoker.userPropertiesFile.1 = test.properties + +invoker.goals.2 = ${project.groupId}:${project.artifactId}:${project.version}:tree +invoker.userPropertiesFile.2 = test-included-excluded.properties + +invoker.goals.3 = ${project.groupId}:${project.artifactId}:${project.version}:tree +invoker.userPropertiesFile.3 = test-included-survives.properties diff --git a/src/it/projects/tree-excluded/test-included-excluded.properties b/src/it/projects/tree-excluded/test-included-excluded.properties new file mode 100644 index 000000000..fad197e38 --- /dev/null +++ b/src/it/projects/tree-excluded/test-included-excluded.properties @@ -0,0 +1,20 @@ +# 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. + +outputFile = target/included-excluded.txt +includes = junit:junit +excludes = org.codehaus.plexus:* diff --git a/src/it/projects/tree-excluded/test-included-survives.properties b/src/it/projects/tree-excluded/test-included-survives.properties new file mode 100644 index 000000000..63e2b17dc --- /dev/null +++ b/src/it/projects/tree-excluded/test-included-survives.properties @@ -0,0 +1,20 @@ +# 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. + +outputFile = target/included-survives.txt +includes = :wagon* +excludes = org.codehaus.plexus:* diff --git a/src/it/projects/tree-excluded/verify.groovy b/src/it/projects/tree-excluded/verify.groovy index e9d8d8135..6908ce3fd 100644 --- a/src/it/projects/tree-excluded/verify.groovy +++ b/src/it/projects/tree-excluded/verify.groovy @@ -25,4 +25,9 @@ def expected = mavenVersion.startsWith('4.') ? "expected-v4.txt" : "expected.txt assertThat(new File(basedir, "target/tree.txt")) .hasSameTextualContentAs(new File(basedir, expected)) +assertThat(new File(basedir, "target/included-excluded.txt").text).isEmpty() + +assertThat(new File(basedir, "target/included-survives.txt")) + .hasSameTextualContentAs(new File(basedir, "expected-included-survives.txt")) + return true diff --git a/src/main/java/org/apache/maven/plugins/dependency/tree/PruningDependencyNodeVisitor.java b/src/main/java/org/apache/maven/plugins/dependency/tree/PruningDependencyNodeVisitor.java new file mode 100644 index 000000000..380d2873e --- /dev/null +++ b/src/main/java/org/apache/maven/plugins/dependency/tree/PruningDependencyNodeVisitor.java @@ -0,0 +1,54 @@ +/* + * 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.plugins.dependency.tree; + +import java.util.ArrayDeque; +import java.util.Deque; + +import org.apache.maven.shared.dependency.graph.DependencyNode; +import org.apache.maven.shared.dependency.graph.filter.DependencyNodeFilter; +import org.apache.maven.shared.dependency.graph.traversal.DependencyNodeVisitor; + +/** + * A dependency node visitor that delegates accepted nodes and prunes rejected nodes and their descendants. + */ +final class PruningDependencyNodeVisitor implements DependencyNodeVisitor { + private final DependencyNodeVisitor visitor; + + private final DependencyNodeFilter filter; + + private final Deque acceptedNodes = new ArrayDeque<>(); + + PruningDependencyNodeVisitor(DependencyNodeVisitor visitor, DependencyNodeFilter filter) { + this.visitor = visitor; + this.filter = filter; + } + + @Override + public boolean visit(DependencyNode node) { + boolean accepted = filter.accept(node); + acceptedNodes.push(accepted); + return accepted && visitor.visit(node); + } + + @Override + public boolean endVisit(DependencyNode node) { + return !acceptedNodes.pop() || visitor.endVisit(node); + } +} diff --git a/src/main/java/org/apache/maven/plugins/dependency/tree/TreeMojo.java b/src/main/java/org/apache/maven/plugins/dependency/tree/TreeMojo.java index a2e7d8e84..cbb0af336 100644 --- a/src/main/java/org/apache/maven/plugins/dependency/tree/TreeMojo.java +++ b/src/main/java/org/apache/maven/plugins/dependency/tree/TreeMojo.java @@ -24,7 +24,6 @@ import java.io.IOException; import java.io.StringWriter; import java.io.Writer; -import java.util.ArrayList; import java.util.List; import java.util.Objects; @@ -49,7 +48,6 @@ import org.apache.maven.shared.dependency.graph.DependencyGraphBuilderException; import org.apache.maven.shared.dependency.graph.DependencyNode; import org.apache.maven.shared.dependency.graph.filter.AncestorOrSelfDependencyNodeFilter; -import org.apache.maven.shared.dependency.graph.filter.AndDependencyNodeFilter; import org.apache.maven.shared.dependency.graph.filter.ArtifactDependencyNodeFilter; import org.apache.maven.shared.dependency.graph.filter.DependencyNodeFilter; import org.apache.maven.shared.dependency.graph.traversal.CollectingDependencyNodeVisitor; @@ -153,6 +151,8 @@ public class TreeMojo extends AbstractMojo { * For example, org.apache.* will match all artifacts whose group id starts with * org.apache., and :::*-SNAPSHOT will match all snapshot artifacts. *

+ * Paths leading to included artifacts are retained. If an artifact is also beneath a subtree matched by + * {@link #excludes}, the exclusion takes precedence and the artifact is not included. * * @see StrictPatternIncludesArtifactFilter * @since 2.0-alpha-6 @@ -174,6 +174,8 @@ public class TreeMojo extends AbstractMojo { * For example, org.apache.* will match all artifacts whose group id starts with * org.apache., and :::*-SNAPSHOT will match all snapshot artifacts. *

+ * A matching artifact and its entire dependency subtree are removed from the serialized dependency tree. + * Exclusions are applied before {@link #includes} and take precedence. * * @see StrictPatternExcludesArtifactFilter * @since 2.0-alpha-6 @@ -333,11 +335,16 @@ private String serializeDependencyTree(DependencyNode theRootNode) { // TODO: remove the need for this when the serializer can calculate last nodes from visitor calls only visitor = new BuildingDependencyNodeVisitor(visitor); - DependencyNodeFilter filter = createDependencyNodeFilter(); + DependencyNodeFilter includesFilter = createIncludesDependencyNodeFilter(); + DependencyNodeFilter excludesFilter = createExcludesDependencyNodeFilter(); - if (filter != null) { + if (includesFilter != null) { CollectingDependencyNodeVisitor collectingVisitor = new CollectingDependencyNodeVisitor(); - DependencyNodeVisitor firstPassVisitor = new FilteringDependencyNodeVisitor(collectingVisitor, filter); + DependencyNodeVisitor firstPassVisitor = + new FilteringDependencyNodeVisitor(collectingVisitor, includesFilter); + if (excludesFilter != null) { + firstPassVisitor = new PruningDependencyNodeVisitor(firstPassVisitor, excludesFilter); + } theRootNode.accept(firstPassVisitor); DependencyNodeFilter secondPassFilter = @@ -345,6 +352,10 @@ private String serializeDependencyTree(DependencyNode theRootNode) { visitor = new FilteringDependencyNodeVisitor(visitor, secondPassFilter); } + if (excludesFilter != null) { + visitor = new PruningDependencyNodeVisitor(visitor, excludesFilter); + } + theRootNode.accept(visitor); return writer.toString(); @@ -397,27 +408,25 @@ private GraphTokens toGraphTokens(String theTokens) { * * @return the dependency node filter, or null if none required */ - private DependencyNodeFilter createDependencyNodeFilter() { - List filters = new ArrayList<>(); - - // filter includes + private DependencyNodeFilter createIncludesDependencyNodeFilter() { if (includes != null && !includes.isEmpty()) { - getLog().debug("+ Filtering dependency tree by artifact include patterns: " + includes); ArtifactFilter artifactFilter = new StrictPatternIncludesArtifactFilter(includes); - filters.add(new ArtifactDependencyNodeFilter(artifactFilter)); + return new ArtifactDependencyNodeFilter(artifactFilter); } - // filter excludes - if (excludes != null && !excludes.isEmpty()) { + return null; + } + private DependencyNodeFilter createExcludesDependencyNodeFilter() { + if (excludes != null && !excludes.isEmpty()) { getLog().debug("+ Filtering dependency tree by artifact exclude patterns: " + excludes); ArtifactFilter artifactFilter = new StrictPatternExcludesArtifactFilter(excludes); - filters.add(new ArtifactDependencyNodeFilter(artifactFilter)); + return new ArtifactDependencyNodeFilter(artifactFilter); } - return filters.isEmpty() ? null : new AndDependencyNodeFilter(filters); + return null; } } diff --git a/src/site/markdown/examples/filtering-the-dependency-tree.md b/src/site/markdown/examples/filtering-the-dependency-tree.md index 73be34d7d..91b14441f 100644 --- a/src/site/markdown/examples/filtering-the-dependency-tree.md +++ b/src/site/markdown/examples/filtering-the-dependency-tree.md @@ -64,6 +64,9 @@ The dependency tree can also be filtered to remove specific dependencies. For ex mvn dependency:tree -Dexcludes=org.codehaus.plexus ``` +A dependency matching an exclude pattern and its entire dependency subtree are removed from the serialized tree. +This affects only the displayed tree; it does not change the project's dependency resolution. + ## Specifying multiple patterns Multiple patterns can be specified when filtering the dependency tree by separating the patterns with commas. For example, to exclude Maven and Plexus dependencies from the tree, we can execute the following: @@ -74,8 +77,12 @@ mvn dependency:tree -Dexcludes=org.apache.maven*,org.codehaus.plexus ## Including and excluding dependencies from the tree -Both include and exclude patterns and be specified together to filter the dependency tree. For example, to locate all non-snapshot Plexus dependencies in the tree, we can execute the following: +Both include and exclude patterns can be specified together to filter the dependency tree. For example, to locate all non-snapshot Plexus dependencies in the tree, we can execute the following: ```shell mvn dependency:tree -Dincludes=org.codehaus.plexus -Dexcludes=:::*-SNAPSHOT ``` + +Excludes are applied first and take precedence over includes. Includes then select matching dependencies from the +remaining tree and retain the paths leading to those dependencies. An include therefore cannot restore a dependency +beneath an excluded subtree. diff --git a/src/test/java/org/apache/maven/plugins/dependency/tree/PruningDependencyNodeVisitorTest.java b/src/test/java/org/apache/maven/plugins/dependency/tree/PruningDependencyNodeVisitorTest.java new file mode 100644 index 000000000..2231d7c5f --- /dev/null +++ b/src/test/java/org/apache/maven/plugins/dependency/tree/PruningDependencyNodeVisitorTest.java @@ -0,0 +1,85 @@ +/* + * 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.plugins.dependency.tree; + +import java.util.Arrays; +import java.util.Collections; +import java.util.concurrent.atomic.AtomicInteger; + +import org.apache.maven.artifact.Artifact; +import org.apache.maven.shared.dependency.graph.DependencyNode; +import org.apache.maven.shared.dependency.graph.filter.DependencyNodeFilter; +import org.apache.maven.shared.dependency.graph.internal.DefaultDependencyNode; +import org.apache.maven.shared.dependency.graph.traversal.CollectingDependencyNodeVisitor; +import org.apache.maven.shared.dependency.graph.traversal.DependencyNodeVisitor; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.mock; + +class PruningDependencyNodeVisitorTest { + @Test + void evaluatesFilterOnlyWhenStartingNodeVisit() { + DefaultDependencyNode root = newNode(null); + root.setChildren(Collections.emptyList()); + AtomicInteger filterInvocations = new AtomicInteger(); + AtomicInteger endVisits = new AtomicInteger(); + DependencyNodeVisitor visitor = new DependencyNodeVisitor() { + @Override + public boolean visit(DependencyNode node) { + return true; + } + + @Override + public boolean endVisit(DependencyNode node) { + endVisits.incrementAndGet(); + return true; + } + }; + + root.accept(new PruningDependencyNodeVisitor(visitor, node -> filterInvocations.incrementAndGet() == 1)); + + assertEquals(1, filterInvocations.get()); + assertEquals(1, endVisits.get()); + } + + @Test + void prunesRejectedSubtreeAndContinuesWithSiblings() { + DefaultDependencyNode root = newNode(null); + DefaultDependencyNode rejected = newNode(root); + DefaultDependencyNode rejectedChild = newNode(rejected); + DefaultDependencyNode sibling = newNode(root); + + root.setChildren(Arrays.asList(rejected, sibling)); + rejected.setChildren(Collections.singletonList(rejectedChild)); + rejectedChild.setChildren(Collections.emptyList()); + sibling.setChildren(Collections.emptyList()); + + CollectingDependencyNodeVisitor collectingVisitor = new CollectingDependencyNodeVisitor(); + DependencyNodeFilter filter = node -> node != rejected; + + root.accept(new PruningDependencyNodeVisitor(collectingVisitor, filter)); + + assertEquals(Arrays.asList(root, sibling), collectingVisitor.getNodes()); + } + + private DefaultDependencyNode newNode(DependencyNode parent) { + return new DefaultDependencyNode(parent, mock(Artifact.class), null, null, null); + } +}