Skip to content

Commit 9aecb15

Browse files
authored
Feat: Version range filtering (#11936)
Port version range filtering from Maven 4. Fixes #11886
1 parent 6fc2248 commit 9aecb15

2 files changed

Lines changed: 382 additions & 0 deletions

File tree

maven-core/src/main/java/org/apache/maven/internal/aether/DefaultRepositorySystemSessionFactory.java

Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@
2929
import java.util.LinkedHashMap;
3030
import java.util.List;
3131
import java.util.Map;
32+
import java.util.function.Function;
33+
import java.util.function.Predicate;
3234
import java.util.stream.Collectors;
3335

3436
import org.apache.maven.RepositoryUtils;
@@ -51,21 +53,36 @@
5153
import org.codehaus.plexus.logging.Logger;
5254
import org.codehaus.plexus.util.xml.Xpp3Dom;
5355
import org.eclipse.aether.ConfigurationProperties;
56+
import org.eclipse.aether.RepositoryException;
5457
import org.eclipse.aether.RepositorySystem;
5558
import org.eclipse.aether.RepositorySystemSession;
59+
import org.eclipse.aether.artifact.Artifact;
60+
import org.eclipse.aether.artifact.DefaultArtifact;
61+
import org.eclipse.aether.collection.DependencyCollectionContext;
62+
import org.eclipse.aether.collection.VersionFilter;
5663
import org.eclipse.aether.repository.LocalRepository;
5764
import org.eclipse.aether.repository.LocalRepositoryManager;
5865
import org.eclipse.aether.repository.RepositoryPolicy;
5966
import org.eclipse.aether.repository.WorkspaceReader;
6067
import org.eclipse.aether.resolution.ResolutionErrorPolicy;
6168
import org.eclipse.aether.util.ConfigUtils;
69+
import org.eclipse.aether.util.graph.version.ChainedVersionFilter;
70+
import org.eclipse.aether.util.graph.version.ContextualSnapshotVersionFilter;
71+
import org.eclipse.aether.util.graph.version.HighestVersionFilter;
72+
import org.eclipse.aether.util.graph.version.LowestVersionFilter;
73+
import org.eclipse.aether.util.graph.version.PredicateVersionFilter;
74+
import org.eclipse.aether.util.graph.version.SnapshotVersionFilter;
6275
import org.eclipse.aether.util.listener.ChainedRepositoryListener;
6376
import org.eclipse.aether.util.repository.AuthenticationBuilder;
6477
import org.eclipse.aether.util.repository.ChainedLocalRepositoryManager;
6578
import org.eclipse.aether.util.repository.DefaultAuthenticationSelector;
6679
import org.eclipse.aether.util.repository.DefaultMirrorSelector;
6780
import org.eclipse.aether.util.repository.DefaultProxySelector;
6881
import org.eclipse.aether.util.repository.SimpleResolutionErrorPolicy;
82+
import org.eclipse.aether.util.version.GenericVersionScheme;
83+
import org.eclipse.aether.version.InvalidVersionSpecificationException;
84+
import org.eclipse.aether.version.Version;
85+
import org.eclipse.aether.version.VersionConstraint;
6986
import org.eclipse.sisu.Nullable;
7087

7188
/**
@@ -110,6 +127,28 @@ public class DefaultRepositorySystemSessionFactory {
110127
*/
111128
private static final String MAVEN_REPO_LOCAL_RECORD_REVERSE_TREE = "maven.repo.local.recordReverseTree";
112129

130+
/**
131+
* User property for version filter expression used in session, applied to resolving ranges: a semicolon separated
132+
* list of filters to apply. By default, no version filter is applied (like in Maven 3).
133+
* <br/>
134+
* Supported filters:
135+
* <ul>
136+
* <li>{@code "h"} or {@code "h(num[@G[:A]])"} - highest version or top list of highest ones filter</li>
137+
* <li>{@code "l"} or {@code "l(num[@G[:A]])"} - lowest version or bottom list of lowest ones filter</li>
138+
* <li>{@code "s"} - contextual snapshot filter</li>
139+
* <li>{@code "ns"} - unconditional snapshot filter (no snapshots selected from ranges)</li>
140+
* <li>{@code "e(G:A:V)"} - predicate filter (excludes G:A:V from range, if hit, V can be version constraint)</li>
141+
* <li>{@code "i(G:A:V)"} - predicate filter (includes G:A:V from range, if hit, V can be version constraint)</li>
142+
* </ul>
143+
* Example filter expression: <code>"h(5);s;e(org.foo:bar:1)</code> will cause: ranges are filtered for "top 5" (instead
144+
* full range), snapshots are banned if root project is not a snapshot, and if range for <code>org.foo:bar</code> is
145+
* being processed, version 1 is omitted. Value in this property builds
146+
* <code>org.eclipse.aether.collection.VersionFilter</code> instance.
147+
*
148+
* @since 3.10.0
149+
*/
150+
private static final String MAVEN_VERSION_FILTER = "maven.session.versionFilter";
151+
113152
private static final String MAVEN_RESOLVER_TRANSPORT_KEY = "maven.resolver.transport";
114153

115154
private static final String MAVEN_RESOLVER_TRANSPORT_DEFAULT = "default";
@@ -154,6 +193,8 @@ public class DefaultRepositorySystemSessionFactory {
154193
@Inject
155194
private RuntimeInformation runtimeInformation;
156195

196+
private final GenericVersionScheme versionScheme = new GenericVersionScheme();
197+
157198
@SuppressWarnings("checkstyle:methodlength")
158199
public RepositorySystemSession.SessionBuilder newRepositorySession(MavenExecutionRequest request) {
159200
RepositorySystemSession.SessionBuilder mainSessionBuilder = MavenRepositorySystemUtils.newSession(repoSystem);
@@ -208,6 +249,11 @@ public RepositorySystemSession.SessionBuilder newRepositorySession(MavenExecutio
208249
}
209250
}
210251

252+
VersionFilter versionFilter = buildVersionFilter((String) configProps.get(MAVEN_VERSION_FILTER));
253+
if (versionFilter != null) {
254+
mainSessionBuilder.setVersionFilter(versionFilter);
255+
}
256+
211257
DefaultMirrorSelector mirrorSelector = new DefaultMirrorSelector();
212258
for (Mirror mirror : request.getMirrors()) {
213259
mirrorSelector.add(
@@ -438,6 +484,114 @@ public static Path resolve(String string) {
438484
}
439485
}
440486

487+
/**
488+
* Visible for testing.
489+
*/
490+
VersionFilter buildVersionFilter(String filterExpression) {
491+
ArrayList<VersionFilter> filters = new ArrayList<>();
492+
if (filterExpression != null) {
493+
List<String> expressions = Arrays.stream(filterExpression.split(";"))
494+
.filter(s -> !s.trim().isEmpty())
495+
.collect(Collectors.toList());
496+
for (String expression : expressions) {
497+
if ("h".equals(expression)) {
498+
filters.add(new HighestVersionFilter());
499+
} else if ("l".equals(expression)) {
500+
filters.add(new LowestVersionFilter());
501+
} else if ((expression.startsWith("h(") || expression.startsWith("l(")) && expression.endsWith(")")) {
502+
Function<Integer, VersionFilter> filterSupplier =
503+
n -> expression.startsWith("h(") ? new HighestVersionFilter(n) : new LowestVersionFilter(n);
504+
String inner = expression.substring(2, expression.length() - 1);
505+
int num;
506+
String g;
507+
String a;
508+
if (inner.contains("@")) {
509+
num = Integer.parseInt(inner.substring(0, inner.indexOf('@')));
510+
String remainder = inner.substring(inner.indexOf('@') + 1);
511+
if (remainder.contains(":")) {
512+
g = remainder.substring(0, remainder.indexOf(':'));
513+
a = remainder.substring(remainder.indexOf(':') + 1);
514+
} else {
515+
g = remainder;
516+
a = null;
517+
}
518+
} else {
519+
num = Integer.parseInt(inner);
520+
g = null;
521+
a = null;
522+
}
523+
if (g == null) {
524+
filters.add(filterSupplier.apply(num));
525+
} else {
526+
VersionFilter versionFilter = filterSupplier.apply(num);
527+
filters.add(new VersionFilter() {
528+
@Override
529+
public void filterVersions(VersionFilterContext context) throws RepositoryException {
530+
Artifact dependencyArtifact =
531+
context.getDependency().getArtifact();
532+
if (g.equals(dependencyArtifact.getGroupId())
533+
&& (a == null || a.equals(dependencyArtifact.getArtifactId()))) {
534+
versionFilter.filterVersions(context);
535+
}
536+
}
537+
538+
@Override
539+
public VersionFilter deriveChildFilter(DependencyCollectionContext context) {
540+
return this;
541+
}
542+
});
543+
}
544+
} else if ("s".equals(expression)) {
545+
filters.add(new ContextualSnapshotVersionFilter());
546+
} else if ("ns".equals(expression)) {
547+
filters.add(new SnapshotVersionFilter());
548+
} else if ((expression.startsWith("e(") || (expression.startsWith("i("))) && expression.endsWith(")")) {
549+
Artifact artifact = new DefaultArtifact(expression.substring(2, expression.length() - 1));
550+
VersionConstraint versionConstraint = parseVersionConstraint(artifact.getVersion());
551+
Predicate<Artifact> predicate = a -> {
552+
if (artifact.getGroupId().equals(a.getGroupId())
553+
&& artifact.getArtifactId().equals(a.getArtifactId())) {
554+
if (expression.startsWith("e(")) {
555+
// exclude
556+
return !versionConstraint.containsVersion(parseVersion(a.getVersion()));
557+
} else {
558+
// include
559+
return versionConstraint.containsVersion(parseVersion(a.getVersion()));
560+
}
561+
}
562+
return true;
563+
};
564+
filters.add(new PredicateVersionFilter(predicate));
565+
} else {
566+
throw new IllegalArgumentException("Unsupported filter expression: " + expression);
567+
}
568+
}
569+
}
570+
if (filters.isEmpty()) {
571+
return null;
572+
} else if (filters.size() == 1) {
573+
return filters.get(0);
574+
} else {
575+
return ChainedVersionFilter.newInstance(filters);
576+
}
577+
}
578+
579+
private Version parseVersion(String spec) {
580+
try {
581+
return versionScheme.parseVersion(spec);
582+
} catch (InvalidVersionSpecificationException e) {
583+
throw new RuntimeException(e);
584+
}
585+
}
586+
587+
private VersionConstraint parseVersionConstraint(String spec) {
588+
try {
589+
return versionScheme.parseVersionConstraint(spec);
590+
} catch (InvalidVersionSpecificationException e) {
591+
throw new RuntimeException(e);
592+
}
593+
}
594+
441595
private Map<?, ?> getPropertiesFromRequestedProfiles(MavenExecutionRequest request) {
442596

443597
List<String> activeProfileId = request.getActiveProfiles();

0 commit comments

Comments
 (0)