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"));
+ }
}