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 afe56ab39 Feat: Pull in version filter builder (#1867)
afe56ab39 is described below
commit afe56ab396d6ba3183e3356845a787fb4eacfb94
Author: Tamas Cservenak <[email protected]>
AuthorDate: Mon May 11 23:08:37 2026 +0200
Feat: Pull in version filter builder (#1867)
Version builder filter should be shared across all Resolver users, hence it
makes sense to have it in Resolver (instead of 2 copies in Maven 3.10 and 4).
---
.../aether/collection/VersionFilterBuilder.java | 47 ++++
.../impl/collect/DefaultVersionFilterBuilder.java | 176 +++++++++++++
.../collect/DefaultVersionFilterBuilderTest.java | 276 +++++++++++++++++++++
3 files changed, 499 insertions(+)
diff --git
a/maven-resolver-api/src/main/java/org/eclipse/aether/collection/VersionFilterBuilder.java
b/maven-resolver-api/src/main/java/org/eclipse/aether/collection/VersionFilterBuilder.java
new file mode 100644
index 000000000..19d89d3b3
--- /dev/null
+++
b/maven-resolver-api/src/main/java/org/eclipse/aether/collection/VersionFilterBuilder.java
@@ -0,0 +1,47 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.eclipse.aether.collection;
+
+import java.util.Optional;
+import java.util.function.Function;
+
+import org.eclipse.aether.version.VersionConstraint;
+
+/**
+ * Builds {@link VersionFilter} instances out of input expression string.
+ *
+ * @since 2.0.18
+ */
+public interface VersionFilterBuilder {
+ /**
+ * Config property for version filter suppression. Presence of this key
will suppress filter created by this builder.
+ * This key is not meant for users, but to programmatically signal
filtering suppression.
+ */
+ String VERSION_FILTER_SUPPRESSED = "aether.versionFilter.suppressed";
+
+ /**
+ * Builds a version filter based on the given filter expression.
+ *
+ * @param filterExpression a string containing filter expressions, may be
{@code null}.
+ * @param versionConstraintParser version constraint parts to be used
during parsing, must not be {@code null}.
+ * @return optional version filter, never {@code null}.
+ */
+ Optional<VersionFilter> buildVersionFilter(
+ String filterExpression, Function<String, VersionConstraint>
versionConstraintParser);
+}
diff --git
a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/collect/DefaultVersionFilterBuilder.java
b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/collect/DefaultVersionFilterBuilder.java
new file mode 100644
index 000000000..59577df0d
--- /dev/null
+++
b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/collect/DefaultVersionFilterBuilder.java
@@ -0,0 +1,176 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.eclipse.aether.internal.impl.collect;
+
+import javax.inject.Named;
+import javax.inject.Singleton;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Optional;
+import java.util.function.Function;
+import java.util.function.Predicate;
+import java.util.stream.Collectors;
+
+import org.eclipse.aether.artifact.Artifact;
+import org.eclipse.aether.collection.VersionFilter;
+import org.eclipse.aether.collection.VersionFilterBuilder;
+import org.eclipse.aether.util.ConfigUtils;
+import org.eclipse.aether.util.graph.version.ChainedVersionFilter;
+import
org.eclipse.aether.util.graph.version.ContextPredicateDelegatingVersionFilter;
+import org.eclipse.aether.util.graph.version.ContextualSnapshotVersionFilter;
+import org.eclipse.aether.util.graph.version.GenericQualifiersVersionFilter;
+import org.eclipse.aether.util.graph.version.HighestVersionFilter;
+import org.eclipse.aether.util.graph.version.LowestVersionFilter;
+import org.eclipse.aether.util.graph.version.ReleaseVersionFilter;
+import org.eclipse.aether.util.graph.version.SnapshotVersionFilter;
+import org.eclipse.aether.util.graph.version.VersionPredicateVersionFilter;
+import org.eclipse.aether.version.VersionConstraint;
+
+import static java.util.Objects.requireNonNull;
+
+/**
+ * Builds {@link VersionFilter} instances out of input expression string.
+ *
+ * Expression is a semicolon separated list of filters to apply. By default,
no version filter is applied (like in Maven 3).
+ * <br/>
+ * Supported filters:
+ * <ul>
+ * <li>{@code "s"} - contextual snapshot filter (project version decides
are snapshots allowed or not)</li>
+ * <li>{@code "nosnapshot"} - unconditional snapshot filter (no snapshot
versions selected from ranges)</li>
+ * <li>{@code "norelease"} - unconditional release filter (no release
versions selected from ranges)</li>
+ * <li>{@code "nopreview"} - unconditional preview filter (no preview
versions selected from ranges)</li>
+ * <li>{@code "noprerelease"} - unconditional pre-release filter (no
preview and rc/cr versions selected from ranges)</li>
+ * <li>{@code "noqualifier"} - unconditional any-qualifier filter (no
version with any qualifier selected from ranges)</li>
+ * <li>{@code "h"} (shorthand of {@code h(1)}) or {@code "h(num)"} -
highest N version (based on version ordering)</li>
+ * <li>{@code "l"} (shorthand of {@code l(1)}) or {@code "l(num)"} -
lowest N version (based on version ordering)</li>
+ * <li>{@code "e(V)"} - exclusion filter (excludes versions matching V
version constraint)</li>
+ * <li>{@code "i(V)"} - inclusion filter (includes versions matching V
version constraint)</li>
+ * </ul>
+ * Every filter expression may have "scope" applied, in form of {@code
@G[:A]}. Presence of "scope" narrows the
+ * application of filter to given G or G:A.
+ * <p>
+ * In case of multiple "similar" rule scopes, user should enlist rules from
"most specific" to "least specific".
+ * <p>
+ * Example filter expression: <code>"h(5);s;e(1)@org.foo:bar</code> will cause:
+ * <ul>
+ * <li>ranges are filtered for "top 5" (instead of full range)</li>
+ * <li>snapshots are banned if root project is not a snapshot</li>
+ * <li>if range for <code>org.foo:bar</code> is being processed, version 1
is omitted</li>
+ * </ul>
+ * Values in this property builds
<code>org.eclipse.aether.collection.VersionFilter</code> instance.
+ *
+ * @since 2.0.18
+ */
+@Singleton
+@Named
+public class DefaultVersionFilterBuilder implements VersionFilterBuilder {
+ /**
+ * Builds a version filter based on the given filter expression.
+ *
+ * @param filterExpression a string containing filter expressions, may be
{@code null}.
+ * @param versionConstraintParser version constraint parts to be used
during parsing, must not be {@code null}.
+ * @return optional version filter, never {@code null}.
+ */
+ @Override
+ public Optional<VersionFilter> buildVersionFilter(
+ String filterExpression, Function<String, VersionConstraint>
versionConstraintParser) {
+ requireNonNull(versionConstraintParser);
+ ArrayList<VersionFilter> filters = new ArrayList<>();
+ if (filterExpression != null) {
+ List<String> expressions =
Arrays.stream(filterExpression.split(";"))
+ .filter(s -> !s.trim().isEmpty())
+ .collect(Collectors.toList());
+ for (String expression : expressions) {
+ Predicate<Artifact> scopePredicate;
+ VersionFilter filter;
+ if (expression.contains("@")) {
+ String remainder =
expression.substring(expression.indexOf('@') + 1);
+ if (remainder.contains(":")) {
+ String g = remainder.substring(0,
remainder.indexOf(':'));
+ String a = remainder.substring(remainder.indexOf(':')
+ 1);
+ scopePredicate =
+ artifact -> g.equals(artifact.getGroupId()) &&
a.equals(artifact.getArtifactId());
+ } else {
+ scopePredicate = artifact ->
remainder.equals(artifact.getGroupId());
+ }
+ expression = expression.substring(0,
expression.indexOf('@'));
+ } else {
+ scopePredicate = null;
+ }
+ if ("s".equals(expression)) {
+ filter = new ContextualSnapshotVersionFilter();
+ } else if ("nosnapshot".equals(expression)) {
+ filter = new SnapshotVersionFilter();
+ } else if ("norelease".equals(expression)) {
+ filter = new ReleaseVersionFilter();
+ } else if ("nopreview".equals(expression)) {
+ filter =
GenericQualifiersVersionFilter.previewVersionFilter();
+ } else if ("noprerelease".equals(expression)) {
+ filter =
GenericQualifiersVersionFilter.preReleaseVersionFilter();
+ } else if ("noqualifier".equals(expression)) {
+ filter =
GenericQualifiersVersionFilter.anyQualifierVersionFilter();
+ } else if ("h".equals(expression)) {
+ filter = new HighestVersionFilter();
+ } else if ("l".equals(expression)) {
+ filter = new LowestVersionFilter();
+ } else if ((expression.startsWith("h(") ||
expression.startsWith("l(")) && expression.endsWith(")")) {
+ int num = Integer.parseInt(expression.substring(2,
expression.length() - 1));
+ if (expression.startsWith("h(")) {
+ filter = new HighestVersionFilter(num);
+ } else {
+ filter = new LowestVersionFilter(num);
+ }
+ } else if ((expression.startsWith("e(") ||
(expression.startsWith("i("))) && expression.endsWith(")")) {
+ VersionConstraint versionConstraint =
+
versionConstraintParser.apply(expression.substring(2, expression.length() - 1));
+ if (expression.startsWith("e(")) {
+ // exclude
+ filter = new VersionPredicateVersionFilter(v ->
!versionConstraint.containsVersion(v));
+ } else {
+ // include
+ filter = new
VersionPredicateVersionFilter(versionConstraint::containsVersion);
+ }
+ } else {
+ throw new IllegalArgumentException("Unsupported filter
expression: " + expression);
+ }
+
+ filters.add(contextPredicate(scopePredicate, filter));
+ }
+ }
+ if (filters.isEmpty()) {
+ return Optional.empty();
+ } else if (filters.size() == 1) {
+ return Optional.of(filters.get(0));
+ } else {
+ return Optional.of(ChainedVersionFilter.newInstance(filters));
+ }
+ }
+
+ private VersionFilter contextPredicate(Predicate<Artifact>
artifactPredicate, VersionFilter filter) {
+ Predicate<VersionFilter.VersionFilterContext> contextPredicate =
+ c -> !ConfigUtils.getBoolean(c.getSession(), false,
VERSION_FILTER_SUPPRESSED);
+ if (artifactPredicate != null) {
+ contextPredicate = contextPredicate.and(
+ c ->
artifactPredicate.test(c.getDependency().getArtifact()));
+ }
+ return new ContextPredicateDelegatingVersionFilter(contextPredicate,
filter);
+ }
+}
diff --git
a/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/collect/DefaultVersionFilterBuilderTest.java
b/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/collect/DefaultVersionFilterBuilderTest.java
new file mode 100644
index 000000000..13cdef5e5
--- /dev/null
+++
b/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/collect/DefaultVersionFilterBuilderTest.java
@@ -0,0 +1,276 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.eclipse.aether.internal.impl.collect;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.eclipse.aether.RepositoryException;
+import org.eclipse.aether.RepositorySystemSession;
+import org.eclipse.aether.artifact.DefaultArtifact;
+import org.eclipse.aether.collection.VersionFilter;
+import org.eclipse.aether.collection.VersionFilterBuilder;
+import org.eclipse.aether.graph.Dependency;
+import org.eclipse.aether.resolution.VersionRangeRequest;
+import org.eclipse.aether.resolution.VersionRangeResult;
+import
org.eclipse.aether.util.graph.version.ContextPredicateDelegatingVersionFilter;
+import org.eclipse.aether.util.version.GenericVersionScheme;
+import org.eclipse.aether.version.InvalidVersionSpecificationException;
+import org.eclipse.aether.version.Version;
+import org.eclipse.aether.version.VersionConstraint;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertInstanceOf;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+public class DefaultVersionFilterBuilderTest {
+ final GenericVersionScheme versionScheme = new GenericVersionScheme();
+ RepositorySystemSession session;
+ DefaultVersionFilterBuilder factory;
+ Map<String, Object> sessionConfigProperties;
+
+ @BeforeEach
+ public void prepare() {
+ session = mock(RepositorySystemSession.class);
+ sessionConfigProperties = new HashMap<>();
+
when(session.getConfigProperties()).thenReturn(sessionConfigProperties);
+ factory = new DefaultVersionFilterBuilder();
+ }
+
+ private Version version(String spec) {
+ try {
+ return versionScheme.parseVersion(spec);
+ } catch (InvalidVersionSpecificationException e) {
+ throw new RuntimeException(e); // never happens
+ }
+ }
+
+ private VersionConstraint versionConstraint(String spec) {
+ try {
+ return versionScheme.parseVersionConstraint(spec);
+ } catch (InvalidVersionSpecificationException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ @Test
+ public void versionFilterUnsupportedExpression() {
+ // null and empty string are OK
+ assertThrows(IllegalArgumentException.class, () ->
factory.buildVersionFilter("[*", this::versionConstraint));
+ assertThrows(
+ IllegalArgumentException.class, () ->
factory.buildVersionFilter("foobar", this::versionConstraint));
+ }
+
+ /**
+ * Simple assertions for {@code h} filter,
+ */
+ @Test
+ public void versionFilterHighest() {
+ VersionFilter vf;
+ vf = factory.buildVersionFilter("h",
this::versionConstraint).orElse(null);
+ assertNotNull(vf);
+ assertInstanceOf(ContextPredicateDelegatingVersionFilter.class, vf);
+
+ vf = factory.buildVersionFilter("h(5)",
this::versionConstraint).orElse(null);
+ assertNotNull(vf);
+ assertInstanceOf(ContextPredicateDelegatingVersionFilter.class, vf);
+
+ vf = factory.buildVersionFilter("h(1)@group",
this::versionConstraint).orElse(null);
+ assertNotNull(vf);
+ assertInstanceOf(ContextPredicateDelegatingVersionFilter.class, vf);
// this is wrapped instance
+ }
+
+ /**
+ * Creating {@code h(1@group)} and incoming artifact G does not match =>
not applied.
+ */
+ @Test
+ public void versionFilterSuppressed() throws RepositoryException {
+ VersionFilter vf;
+
+ vf = factory.buildVersionFilter("h(2)@group",
this::versionConstraint).orElse(null);
+ assertNotNull(vf);
+
+ List<Version> versions = Arrays.asList(version("1.0"), version("1.1"),
version("1.2"));
+
+ DefaultVersionFilterContext context = new
DefaultVersionFilterContext(session);
+ VersionRangeResult result =
+ new VersionRangeResult(new
VersionRangeRequest()).setVersions(new ArrayList<>(versions));
+ context.set(new Dependency(new DefaultArtifact("group:a:[1,)"), ""),
result);
+
+ vf.filterVersions(context);
+
+ // hit
+ assertEquals(2, context.get().size());
+ assertEquals(version("1.1"), context.get().get(0));
+ assertEquals(version("1.2"), context.get().get(1));
+
+
sessionConfigProperties.put(VersionFilterBuilder.VERSION_FILTER_SUPPRESSED,
Boolean.TRUE);
+
+ context = new DefaultVersionFilterContext(session);
+ context.set(new Dependency(new DefaultArtifact("group:a:[1,)"), ""),
result);
+
+ // suppressed
+ assertEquals(versions, context.get());
+ }
+
+ /**
+ * Creating {@code h(1@group)} and incoming artifact G does not match =>
not applied.
+ */
+ @Test
+ public void versionFilterHLFuncMiss() throws RepositoryException {
+ VersionFilter vf;
+
+ vf = factory.buildVersionFilter("h(2)@group",
this::versionConstraint).orElse(null);
+ assertNotNull(vf);
+
+ List<Version> versions = Arrays.asList(version("1.0"), version("1.1"),
version("1.2"));
+
+ DefaultVersionFilterContext context = new
DefaultVersionFilterContext(session);
+ VersionRangeResult result =
+ new VersionRangeResult(new
VersionRangeRequest()).setVersions(new ArrayList<>(versions));
+ context.set(new Dependency(new DefaultArtifact("g:a:[1,)"), ""),
result);
+
+ vf.filterVersions(context);
+
+ vf.filterVersions(context);
+ }
+
+ /**
+ * Creating {@code h(1@group)} and incoming artifact G does match =>
applied.
+ */
+ @Test
+ public void versionFilterHLFuncHit() throws RepositoryException {
+ VersionFilter vf;
+
+ vf = factory.buildVersionFilter("h(2)@group",
this::versionConstraint).orElse(null);
+ assertNotNull(vf);
+
+ List<Version> versions = Arrays.asList(version("1.0"), version("1.1"),
version("1.2"));
+
+ DefaultVersionFilterContext context = new
DefaultVersionFilterContext(session);
+ VersionRangeResult result =
+ new VersionRangeResult(new
VersionRangeRequest()).setVersions(new ArrayList<>(versions));
+ context.set(new Dependency(new DefaultArtifact("group:a:[1,)"), ""),
result);
+
+ vf.filterVersions(context);
+
+ assertEquals(2, context.get().size());
+ assertEquals(version("1.1"), context.get().get(0));
+ assertEquals(version("1.2"), context.get().get(1));
+ }
+
+ /**
+ * Creating {@code l(1@group)} and incoming artifact G does not match =>
not applied.
+ */
+ @Test
+ public void versionFilterLowestFuncMiss() throws RepositoryException {
+ VersionFilter vf;
+
+ vf = factory.buildVersionFilter("l(2)@group",
this::versionConstraint).orElse(null);
+ assertNotNull(vf);
+
+ List<Version> versions = Arrays.asList(version("1.0"), version("1.1"),
version("1.2"));
+
+ DefaultVersionFilterContext context = new
DefaultVersionFilterContext(session);
+ VersionRangeResult result =
+ new VersionRangeResult(new
VersionRangeRequest()).setVersions(new ArrayList<>(versions));
+ context.set(new Dependency(new DefaultArtifact("g:a:[1,)"), ""),
result);
+
+ vf.filterVersions(context);
+
+ assertEquals(versions, context.get());
+ }
+
+ /**
+ * Creating {@code l(1@group)} and incoming artifact G does match =>
applied.
+ */
+ @Test
+ public void versionFilterLowestFuncHit() throws RepositoryException {
+ VersionFilter vf;
+
+ vf = factory.buildVersionFilter("l(2)@group",
this::versionConstraint).orElse(null);
+ assertNotNull(vf);
+
+ List<Version> versions = Arrays.asList(version("1.0"), version("1.1"),
version("1.2"));
+
+ DefaultVersionFilterContext context = new
DefaultVersionFilterContext(session);
+ VersionRangeResult result =
+ new VersionRangeResult(new
VersionRangeRequest()).setVersions(new ArrayList<>(versions));
+ context.set(new Dependency(new DefaultArtifact("group:a:[1,)"), ""),
result);
+
+ vf.filterVersions(context);
+
+ assertEquals(2, context.get().size());
+ assertEquals(version("1.0"), context.get().get(0));
+ assertEquals(version("1.1"), context.get().get(1));
+ }
+
+ @Test
+ public void versionFilterExcludeFuncHit() throws RepositoryException {
+ VersionFilter vf;
+
+ vf = factory.buildVersionFilter("e([1.1,2.0))@group:a",
this::versionConstraint)
+ .orElse(null);
+ assertNotNull(vf);
+
+ List<Version> versions = Arrays.asList(version("1.0"), version("1.1"),
version("1.2"), version("2.0"));
+
+ DefaultVersionFilterContext context = new
DefaultVersionFilterContext(session);
+ VersionRangeResult result =
+ new VersionRangeResult(new
VersionRangeRequest()).setVersions(new ArrayList<>(versions));
+ context.set(new Dependency(new DefaultArtifact("group:a:[1,)"), ""),
result);
+
+ vf.filterVersions(context);
+
+ assertEquals(2, context.get().size());
+ assertEquals(version("1.0"), context.get().get(0));
+ assertEquals(version("2.0"), context.get().get(1));
+ }
+
+ @Test
+ public void versionFilterIncludeFuncHit() throws RepositoryException {
+ VersionFilter vf;
+
+ vf = factory.buildVersionFilter("i([1.1,),[2.0,))@group:a",
this::versionConstraint)
+ .orElse(null);
+ assertNotNull(vf);
+
+ List<Version> versions = Arrays.asList(version("1.0"), version("1.1"),
version("1.2"), version("2.0"));
+
+ DefaultVersionFilterContext context = new
DefaultVersionFilterContext(session);
+ VersionRangeResult result =
+ new VersionRangeResult(new
VersionRangeRequest()).setVersions(new ArrayList<>(versions));
+ context.set(new Dependency(new DefaultArtifact("group:a:[1,)"), ""),
result);
+
+ vf.filterVersions(context);
+
+ assertEquals(3, context.get().size());
+ assertEquals(version("1.1"), context.get().get(0));
+ assertEquals(version("1.2"), context.get().get(1));
+ assertEquals(version("2.0"), context.get().get(2));
+ }
+}