You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@maven.apache.org by sl...@apache.org on 2026/10/19 13:37:50 UTC

(maven-dependency-plugin) 03/04: [MDEP-574] Match include and exclude patterns on the artifact type

This is an automated email from the ASF dual-hosted git repository.

slachiewicz pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/maven-dependency-plugin.git

commit 7afd046b7504e4b5be4ac385a35e9aad1b8802b0
Author: Sylwester Lachiewicz <sl...@apache.org>
AuthorDate: Tue Oct 13 23:05:01 2026 +0200

    [MDEP-574] Match include and exclude patterns on the artifact type
    
    The Resolver pattern filters compare the third element with the file extension only, so a pattern naming a type such as test-jar never matched. Keep matching the extension, so existing patterns such as groupId:artifactId:jar still work.
---
 .../invoker.properties                             | 18 ++++++
 .../pom.xml                                        | 59 +++++++++++++++++
 .../setup.groovy                                   | 34 ++++++++++
 .../test.properties                                | 20 ++++++
 .../verify.groovy                                  | 27 ++++++++
 .../dependency/PurgeLocalRepositoryMojo.java       | 58 ++++++++++++++++-
 .../examples/purging-local-repository.md.vm        |  8 ++-
 .../dependency/PurgeLocalRepositoryMojoTest.java   | 73 ++++++++++++++++++++++
 8 files changed, 294 insertions(+), 3 deletions(-)

diff --git a/src/it/projects/purge-local-repository-include-custom-type/invoker.properties b/src/it/projects/purge-local-repository-include-custom-type/invoker.properties
new file mode 100644
index 00000000..3c1911e6
--- /dev/null
+++ b/src/it/projects/purge-local-repository-include-custom-type/invoker.properties
@@ -0,0 +1,18 @@
+# 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.
+
+invoker.goals = ${project.groupId}:${project.artifactId}:${project.version}:purge-local-repository
diff --git a/src/it/projects/purge-local-repository-include-custom-type/pom.xml b/src/it/projects/purge-local-repository-include-custom-type/pom.xml
new file mode 100644
index 00000000..0d40246e
--- /dev/null
+++ b/src/it/projects/purge-local-repository-include-custom-type/pom.xml
@@ -0,0 +1,59 @@
+<?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.its.dependency</groupId>
+  <artifactId>test</artifactId>
+  <version>1.0-SNAPSHOT</version>
+
+  <name>Test</name>
+  <description>
+    Test dependency:purge-local-repository with an include pattern that names an artifact type (MDEP-574)
+  </description>
+
+  <properties>
+    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+  </properties>
+
+  <dependencies>
+    <dependency>
+      <groupId>org.apache.maven.its.dependency</groupId>
+      <artifactId>typed</artifactId>
+      <version>1.0</version>
+      <type>test-jar</type>
+    </dependency>
+    <dependency>
+      <groupId>org.apache.maven.its.dependency</groupId>
+      <artifactId>typed</artifactId>
+      <version>1.0</version>
+    </dependency>
+    <dependency>
+      <groupId>org.apache.maven.its.dependency</groupId>
+      <artifactId>untouched</artifactId>
+      <version>1.0</version>
+      <type>test-jar</type>
+    </dependency>
+  </dependencies>
+
+</project>
diff --git a/src/it/projects/purge-local-repository-include-custom-type/setup.groovy b/src/it/projects/purge-local-repository-include-custom-type/setup.groovy
new file mode 100644
index 00000000..3ccff669
--- /dev/null
+++ b/src/it/projects/purge-local-repository-include-custom-type/setup.groovy
@@ -0,0 +1,34 @@
+/*
+ * 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.
+ */
+
+String groupPath = "org/apache/maven/its/dependency/"
+
+void createFile( String relativePath )
+{
+    File file = new File( localRepositoryPath, relativePath )
+    file.getParentFile().mkdirs()
+    file.createNewFile()
+}
+
+// the type test-jar has the file extension jar and the classifier tests
+createFile( groupPath + "typed/1.0/typed-1.0-tests.jar" )
+createFile( groupPath + "typed/1.0/typed-1.0.jar" )
+createFile( groupPath + "untouched/1.0/untouched-1.0-tests.jar" )
+
+return true
diff --git a/src/it/projects/purge-local-repository-include-custom-type/test.properties b/src/it/projects/purge-local-repository-include-custom-type/test.properties
new file mode 100644
index 00000000..daa19f69
--- /dev/null
+++ b/src/it/projects/purge-local-repository-include-custom-type/test.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.
+
+reResolve = false
+include = org.apache.maven.its.dependency:typed:test-jar
+resolutionFuzziness = file
diff --git a/src/it/projects/purge-local-repository-include-custom-type/verify.groovy b/src/it/projects/purge-local-repository-include-custom-type/verify.groovy
new file mode 100644
index 00000000..f1ea36ad
--- /dev/null
+++ b/src/it/projects/purge-local-repository-include-custom-type/verify.groovy
@@ -0,0 +1,27 @@
+/*
+ * 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.
+ */
+
+String groupPath = "org/apache/maven/its/dependency/"
+
+// only the test-jar of the included artifactId is purged: the plain jar has the extension jar, but not the type test-jar
+assert !new File( localRepositoryPath, groupPath + "typed/1.0/typed-1.0-tests.jar" ).exists() : "test-jar was not purged"
+assert new File( localRepositoryPath, groupPath + "typed/1.0/typed-1.0.jar" ).exists() : "jar was purged"
+assert new File( localRepositoryPath, groupPath + "untouched/1.0/untouched-1.0-tests.jar" ).exists() : "not included artifact was purged"
+
+return true
diff --git a/src/main/java/org/apache/maven/plugins/dependency/PurgeLocalRepositoryMojo.java b/src/main/java/org/apache/maven/plugins/dependency/PurgeLocalRepositoryMojo.java
index 4f9ccf90..0047bf0e 100644
--- a/src/main/java/org/apache/maven/plugins/dependency/PurgeLocalRepositoryMojo.java
+++ b/src/main/java/org/apache/maven/plugins/dependency/PurgeLocalRepositoryMojo.java
@@ -60,8 +60,12 @@ import org.apache.maven.shared.artifact.filter.resolve.transform.EclipseAetherFi
 import org.apache.maven.shared.utils.logging.MessageBuilder;
 import org.apache.maven.shared.utils.logging.MessageUtils;
 import org.codehaus.plexus.util.FileUtils;
+import org.eclipse.aether.artifact.ArtifactType;
 import org.eclipse.aether.artifact.ArtifactTypeRegistry;
+import org.eclipse.aether.artifact.DefaultArtifact;
+import org.eclipse.aether.graph.DefaultDependencyNode;
 import org.eclipse.aether.graph.DependencyFilter;
+import org.eclipse.aether.graph.DependencyNode;
 import org.eclipse.aether.resolution.ArtifactDescriptorException;
 import org.eclipse.aether.resolution.DependencyResolutionException;
 
@@ -267,6 +271,56 @@ public class PurgeLocalRepositoryMojo extends AbstractMojo {
         }
     }
 
+    /**
+     * Applies an include or exclude pattern filter to an artifact's type as well as to its file extension. The
+     * pattern filters of the Resolver match the third element of {@code groupId:artifactId:extension:version} against
+     * the file extension only, so a pattern naming a type such as {@code test-jar} would never match.
+     */
+    private class TypeOrExtensionFilter extends AbstractFilter {
+        private final DependencyFilter patternFilter;
+
+        private final boolean exclusion;
+
+        /**
+         * @param patternFilter a {@link PatternInclusionsFilter} or {@link PatternExclusionsFilter}
+         * @param exclusion whether the pattern filter is an exclusion, which accepts the artifacts that match none
+         */
+        TypeOrExtensionFilter(TransformableFilter patternFilter, boolean exclusion) {
+            this.patternFilter = patternFilter.transform(new EclipseAetherFilterTransformer());
+            this.exclusion = exclusion;
+        }
+
+        @Override
+        public boolean accept(Node node, List<Node> parents) {
+            Dependency dependency = node.getDependency();
+            if (dependency == null) {
+                return false;
+            }
+
+            String type = dependency.getType();
+            ArtifactType artifactType =
+                    session.getRepositorySession().getArtifactTypeRegistry().get(type);
+            String extension = artifactType != null ? artifactType.getExtension() : type;
+
+            Set<String> candidates = new LinkedHashSet<>(Arrays.asList(type, extension));
+            for (String candidate : candidates) {
+                org.eclipse.aether.artifact.Artifact artifact = new DefaultArtifact(
+                        dependency.getGroupId(),
+                        dependency.getArtifactId(),
+                        dependency.getClassifier(),
+                        candidate,
+                        dependency.getVersion());
+                DependencyNode candidateNode = new DefaultDependencyNode(
+                        new org.eclipse.aether.graph.Dependency(artifact, dependency.getScope()));
+                // an include is satisfied by one match, an exclude only by the absence of any match
+                if (patternFilter.accept(candidateNode, Collections.emptyList()) != exclusion) {
+                    return !exclusion;
+                }
+            }
+            return exclusion;
+        }
+    }
+
     /**
      * Includes only snapshot artifacts.
      */
@@ -444,14 +498,14 @@ public class PurgeLocalRepositoryMojo extends AbstractMojo {
             this.includes = parseIncludes(this.include);
         }
         if (this.includes != null) {
-            subFilters.add(new PatternInclusionsFilter(includes));
+            subFilters.add(new TypeOrExtensionFilter(new PatternInclusionsFilter(includes), false));
         }
 
         if (!(this.exclude == null || this.exclude.isEmpty())) {
             this.excludes = parseIncludes(this.exclude);
         }
         if (this.excludes != null) {
-            subFilters.add(new PatternExclusionsFilter(excludes));
+            subFilters.add(new TypeOrExtensionFilter(new PatternExclusionsFilter(excludes), true));
         }
 
         if (!actTransitively) {
diff --git a/src/site/markdown/examples/purging-local-repository.md.vm b/src/site/markdown/examples/purging-local-repository.md.vm
index 399c1d78..23ff2799 100644
--- a/src/site/markdown/examples/purging-local-repository.md.vm
+++ b/src/site/markdown/examples/purging-local-repository.md.vm
@@ -85,6 +85,12 @@ Specific dependencies from the current project dependency tree can be included o
 </project>
 ```
 
+Each include or exclude entry is a pattern of the form `groupId[:artifactId[:type[:version]]]`. Trailing elements can be left out, and `*` matches any sequence of characters in an element. The `type` element matches either the dependency's type, such as `test-jar` or `ejb-client`, or the file extension of its artifact, such as `jar`. For example, `org.example:lib:test-jar` selects the test jar of `org.example:lib` without selecting its main jar.
+
+```shell
+mvn dependency:purge-local-repository -Dinclude=org.example:lib:test-jar
+```
+
 The includes/excludes configuration can also be passed from the CLI using the "include" or "exclude" parameter.
 
 ```shell
@@ -109,7 +115,7 @@ mvn dependency:purge-local-repository -DmanualInclude=org.apache:apache
 Other purge configuration
 -------------------------
 
-To purge all the dependencies of a given groupId, use the `resolutionFuzziness` configuration parameter.
+To purge all the dependencies of a given groupId, use the `resolutionFuzziness` configuration parameter. Combined with `snapshotsOnly`, the `artifactId` and `groupId` values delete only the snapshot versions below the artifactId or groupId directory and keep its release versions.
 
 If neither `includes` nor `excludes` are specified, all the dependencies of the current project are purged.
 
diff --git a/src/test/java/org/apache/maven/plugins/dependency/PurgeLocalRepositoryMojoTest.java b/src/test/java/org/apache/maven/plugins/dependency/PurgeLocalRepositoryMojoTest.java
index 6f78e1fb..4f84e53f 100644
--- a/src/test/java/org/apache/maven/plugins/dependency/PurgeLocalRepositoryMojoTest.java
+++ b/src/test/java/org/apache/maven/plugins/dependency/PurgeLocalRepositoryMojoTest.java
@@ -37,18 +37,22 @@ import org.eclipse.aether.RepositorySystemSession;
 import org.eclipse.aether.artifact.Artifact;
 import org.eclipse.aether.artifact.DefaultArtifact;
 import org.eclipse.aether.artifact.DefaultArtifactType;
+import org.eclipse.aether.graph.DefaultDependencyNode;
+import org.eclipse.aether.graph.Dependency;
 import org.eclipse.aether.graph.DependencyFilter;
 import org.eclipse.aether.repository.RemoteRepository;
 import org.eclipse.aether.util.artifact.DefaultArtifactTypeRegistry;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.io.TempDir;
+import org.mockito.ArgumentCaptor;
 
 import static org.apache.maven.api.plugin.testing.MojoExtension.setVariableValueToObject;
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyList;
 import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
 @SuppressWarnings("deprecation")
@@ -199,4 +203,73 @@ class PurgeLocalRepositoryMojoTest {
         assertThat(localRepo.resolve(GROUP_PATH)).doesNotExist();
         assertThat(otherGroup).exists();
     }
+
+    @Test
+    void includeMatchesCustomArtifactType() throws Exception {
+        DependencyFilter filter = purgeFilterFor("include", "org.example:custom-lib:custom:1.0");
+
+        assertThat(filter.accept(node("custom-lib", "1.0", "custom"), Collections.emptyList()))
+                .as("type")
+                .isTrue();
+        assertThat(filter.accept(node("other-lib", "1.0", "custom"), Collections.emptyList()))
+                .as("other artifactId")
+                .isFalse();
+        assertThat(filter.accept(node("custom-lib", "1.0", "jar"), Collections.emptyList()))
+                .as("other type")
+                .isFalse();
+    }
+
+    @Test
+    void includeStillMatchesFileExtension() throws Exception {
+        DependencyFilter filter = purgeFilterFor("include", "org.example:custom-lib:cext:1.0");
+
+        assertThat(filter.accept(node("custom-lib", "1.0", "custom"), Collections.emptyList()))
+                .isTrue();
+    }
+
+    @Test
+    void includeSupportsWildcardsAroundCustomArtifactType() throws Exception {
+        DependencyFilter filter = purgeFilterFor("include", "org.example:custom-*:custom");
+
+        assertThat(filter.accept(node("custom-lib", "1.0", "custom"), Collections.emptyList()))
+                .isTrue();
+        assertThat(filter.accept(node("other-lib", "1.0", "custom"), Collections.emptyList()))
+                .isFalse();
+    }
+
+    @Test
+    void excludeMatchesCustomArtifactType() throws Exception {
+        DependencyFilter filter = purgeFilterFor("exclude", "org.example:custom-lib:custom");
+
+        assertThat(filter.accept(node("custom-lib", "1.0", "custom"), Collections.emptyList()))
+                .isFalse();
+        assertThat(filter.accept(node("custom-lib", "1.0", "jar"), Collections.emptyList()))
+                .isTrue();
+        assertThat(filter.accept(node("other-lib", "1.0", "custom"), Collections.emptyList()))
+                .isTrue();
+    }
+
+    private DependencyFilter purgeFilterFor(String parameter, String pattern) throws Exception {
+        setVariableValueToObject(mojo, parameter, pattern);
+        resolvesTo();
+
+        mojo.execute();
+
+        ArgumentCaptor<DependencyFilter> filter = ArgumentCaptor.forClass(DependencyFilter.class);
+        verify(resolverUtil)
+                .resolveDependenciesForArtifact(any(Artifact.class), anyList(), any(), anyList(), filter.capture());
+        return filter.getValue();
+    }
+
+    private static DefaultDependencyNode node(String artifactId, String version, String type) {
+        String extension = "custom".equals(type) ? "cext" : type;
+        Artifact artifact = new DefaultArtifact(
+                "org.example",
+                artifactId,
+                "",
+                extension,
+                version,
+                new DefaultArtifactType(type, extension, "", "java"));
+        return new DefaultDependencyNode(new Dependency(artifact, "compile"));
+    }
 }