From a952c9ee10a066f9670585dd0cc438224e1738b7 Mon Sep 17 00:00:00 2001 From: Tamas Cservenak Date: Thu, 11 Jan 2024 18:47:28 +0100 Subject: [PATCH] [MNG-7960] Artifact collection filtering (#1353) Make Maven more friendly (and tunable) when ranges are being used. There new options are merely affecting range processing, allowing different narrowing strategies. Relies on latest Resolver alpha-6 features. https://issues.apache.org/jira/browse/MNG-7960 --- ...DefaultRepositorySystemSessionFactory.java | 107 +++++++++++++++++- ...ultRepositorySystemSessionFactoryTest.java | 98 +++++++++++++--- 2 files changed, 190 insertions(+), 15 deletions(-) diff --git a/maven-core/src/main/java/org/apache/maven/internal/aether/DefaultRepositorySystemSessionFactory.java b/maven-core/src/main/java/org/apache/maven/internal/aether/DefaultRepositorySystemSessionFactory.java index 9491788da7..add1a08f2e 100644 --- a/maven-core/src/main/java/org/apache/maven/internal/aether/DefaultRepositorySystemSessionFactory.java +++ b/maven-core/src/main/java/org/apache/maven/internal/aether/DefaultRepositorySystemSessionFactory.java @@ -30,6 +30,7 @@ import java.util.HashSet; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.function.Predicate; import java.util.stream.Collectors; import org.apache.maven.RepositoryUtils; @@ -59,6 +60,9 @@ import org.eclipse.aether.RepositoryListener; import org.eclipse.aether.RepositorySystem; import org.eclipse.aether.RepositorySystemSession; import org.eclipse.aether.RepositorySystemSession.SessionBuilder; +import org.eclipse.aether.artifact.Artifact; +import org.eclipse.aether.artifact.DefaultArtifact; +import org.eclipse.aether.collection.VersionFilter; import org.eclipse.aether.repository.AuthenticationContext; import org.eclipse.aether.repository.AuthenticationSelector; import org.eclipse.aether.repository.ProxySelector; @@ -67,6 +71,7 @@ import org.eclipse.aether.repository.RepositoryPolicy; import org.eclipse.aether.repository.WorkspaceReader; import org.eclipse.aether.resolution.ResolutionErrorPolicy; import org.eclipse.aether.util.graph.manager.ClassicDependencyManager; +import org.eclipse.aether.util.graph.version.*; import org.eclipse.aether.util.listener.ChainedRepositoryListener; import org.eclipse.aether.util.repository.AuthenticationBuilder; import org.eclipse.aether.util.repository.ChainedLocalRepositoryManager; @@ -75,6 +80,10 @@ import org.eclipse.aether.util.repository.DefaultMirrorSelector; import org.eclipse.aether.util.repository.DefaultProxySelector; import org.eclipse.aether.util.repository.SimpleArtifactDescriptorPolicy; import org.eclipse.aether.util.repository.SimpleResolutionErrorPolicy; +import org.eclipse.aether.version.InvalidVersionSpecificationException; +import org.eclipse.aether.version.Version; +import org.eclipse.aether.version.VersionRange; +import org.eclipse.aether.version.VersionScheme; import org.eclipse.sisu.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -84,6 +93,25 @@ import org.slf4j.LoggerFactory; */ @Named public class DefaultRepositorySystemSessionFactory { + /** + * User property for version filters expression, a semicolon separated list of filters to apply. By default, no version + * filter is applied (like in Maven 3). + *

+ * Supported filters: + *

+ * Example filter expression: {@code "h(5);s;e(org.foo:bar:1)} will cause: ranges are filtered for "top 5" (instead + * full range), snapshots are banned if root project is not a snapshot, and if range for {@code org.foo:bar} is + * being processed, version 1 is omitted. + * + * @since 4.0.0 + */ + private static final String MAVEN_VERSION_FILTERS = "maven.versionFilters"; + /** * User property for chained LRM: list of "tail" local repository paths (separated by comma), to be used with * {@link ChainedLocalRepositoryManager}. @@ -162,6 +190,8 @@ public class DefaultRepositorySystemSessionFactory { private final TypeRegistry typeRegistry; + private final VersionScheme versionScheme; + @SuppressWarnings("checkstyle:ParameterNumber") @Inject public DefaultRepositorySystemSessionFactory( @@ -171,7 +201,8 @@ public class DefaultRepositorySystemSessionFactory { SettingsDecrypter settingsDecrypter, EventSpyDispatcher eventSpyDispatcher, RuntimeInformation runtimeInformation, - TypeRegistry typeRegistry) { + TypeRegistry typeRegistry, + VersionScheme versionScheme) { this.artifactHandlerManager = artifactHandlerManager; this.repoSystem = repoSystem; this.workspaceRepository = workspaceRepository; @@ -179,6 +210,7 @@ public class DefaultRepositorySystemSessionFactory { this.eventSpyDispatcher = eventSpyDispatcher; this.runtimeInformation = runtimeInformation; this.typeRegistry = typeRegistry; + this.versionScheme = versionScheme; } @Deprecated @@ -222,6 +254,11 @@ public class DefaultRepositorySystemSessionFactory { session.setArtifactDescriptorPolicy(new SimpleArtifactDescriptorPolicy( request.isIgnoreMissingArtifactDescriptor(), request.isIgnoreInvalidArtifactDescriptor())); + VersionFilter versionFilter = buildVersionFilter((String) configProps.get(MAVEN_VERSION_FILTERS)); + if (versionFilter != null) { + session.setVersionFilter(versionFilter); + } + session.setArtifactTypeRegistry(RepositoryUtils.newArtifactTypeRegistry(artifactHandlerManager)); session.setWorkspaceReader( @@ -426,6 +463,72 @@ public class DefaultRepositorySystemSessionFactory { return session; } + private VersionFilter buildVersionFilter(String filterExpression) { + ArrayList filters = new ArrayList<>(); + if (filterExpression != null) { + List expressions = Arrays.stream(filterExpression.split(";")) + .filter(s -> s != null && !s.trim().isEmpty()) + .collect(Collectors.toList()); + for (String expression : expressions) { + if ("h".equals(expression)) { + filters.add(new HighestVersionFilter()); + } else if (expression.startsWith("h(") && expression.endsWith(")")) { + int num = Integer.parseInt(expression.substring(2, expression.length() - 1)); + filters.add(new HighestVersionFilter(num)); + } else if ("l".equals(expression)) { + filters.add(new LowestVersionFilter()); + } else if (expression.startsWith("l(") && expression.endsWith(")")) { + int num = Integer.parseInt(expression.substring(2, expression.length() - 1)); + filters.add(new LowestVersionFilter(num)); + } else if ("s".equals(expression)) { + filters.add(new ContextualSnapshotVersionFilter()); + } else if (expression.startsWith("e(") && expression.endsWith(")")) { + Artifact artifact = new DefaultArtifact(expression.substring(2, expression.length() - 1)); + VersionRange versionRange = + artifact.getVersion().contains(",") ? parseVersionRange(artifact.getVersion()) : null; + Predicate predicate = a -> { + if (artifact.getGroupId().equals(a.getGroupId()) + && artifact.getArtifactId().equals(a.getArtifactId())) { + if (versionRange != null) { + Version v = parseVersion(a.getVersion()); + return !versionRange.containsVersion(v); + } else { + return !artifact.getVersion().equals(a.getVersion()); + } + } + return true; + }; + filters.add(new PredicateVersionFilter(predicate)); + } else { + throw new IllegalArgumentException("Unsupported filter expression: " + expression); + } + } + } + if (filters.isEmpty()) { + return null; + } else if (filters.size() == 1) { + return filters.get(0); + } else { + return ChainedVersionFilter.newInstance(filters); + } + } + + private Version parseVersion(String spec) { + try { + return versionScheme.parseVersion(spec); + } catch (InvalidVersionSpecificationException e) { + throw new RuntimeException(e); + } + } + + private VersionRange parseVersionRange(String spec) { + try { + return versionScheme.parseVersionRange(spec); + } catch (InvalidVersionSpecificationException e) { + throw new RuntimeException(e); + } + } + private Map getPropertiesFromRequestedProfiles(MavenExecutionRequest request) { HashSet activeProfileId = new HashSet<>(request.getProfileActivation().getRequiredActiveProfileIds()); @@ -508,7 +611,7 @@ public class DefaultRepositorySystemSessionFactory { return null; } - public void injectAuthentication(AuthenticationSelector selector, List repositories) { + private void injectAuthentication(AuthenticationSelector selector, List repositories) { if (repositories != null && selector != null) { for (ArtifactRepository repository : repositories) { repository.setAuthentication(getAuthentication(selector, repository)); diff --git a/maven-core/src/test/java/org/apache/maven/internal/aether/DefaultRepositorySystemSessionFactoryTest.java b/maven-core/src/test/java/org/apache/maven/internal/aether/DefaultRepositorySystemSessionFactoryTest.java index f3e359310e..0f982ca34d 100644 --- a/maven-core/src/test/java/org/apache/maven/internal/aether/DefaultRepositorySystemSessionFactoryTest.java +++ b/maven-core/src/test/java/org/apache/maven/internal/aether/DefaultRepositorySystemSessionFactoryTest.java @@ -41,14 +41,14 @@ import org.codehaus.plexus.configuration.PlexusConfiguration; import org.codehaus.plexus.testing.PlexusTest; import org.codehaus.plexus.util.xml.Xpp3Dom; import org.eclipse.aether.ConfigurationProperties; +import org.eclipse.aether.collection.VersionFilter; import org.eclipse.aether.repository.RepositoryPolicy; +import org.eclipse.aether.util.graph.version.*; +import org.eclipse.aether.version.VersionScheme; import org.junit.jupiter.api.Test; import static org.codehaus.plexus.testing.PlexusExtension.getBasedir; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertNull; -import static org.junit.jupiter.api.Assertions.assertThrowsExactly; +import static org.junit.jupiter.api.Assertions.*; /** * UT for {@link DefaultRepositorySystemSessionFactory}. @@ -76,6 +76,9 @@ public class DefaultRepositorySystemSessionFactoryTest { @Inject protected DefaultTypeRegistry defaultTypeRegistry; + @Inject + protected VersionScheme versionScheme; + @Test void isNoSnapshotUpdatesTest() throws InvalidRepositoryException { DefaultRepositorySystemSessionFactory systemSessionFactory = new DefaultRepositorySystemSessionFactory( @@ -85,7 +88,8 @@ public class DefaultRepositorySystemSessionFactoryTest { settingsDecrypter, eventSpyDispatcher, information, - defaultTypeRegistry); + defaultTypeRegistry, + versionScheme); MavenExecutionRequest request = new DefaultMavenExecutionRequest(); request.setLocalRepository(getLocalRepository()); @@ -108,7 +112,8 @@ public class DefaultRepositorySystemSessionFactoryTest { settingsDecrypter, eventSpyDispatcher, information, - defaultTypeRegistry); + defaultTypeRegistry, + versionScheme); MavenExecutionRequest request = new DefaultMavenExecutionRequest(); request.setLocalRepository(getLocalRepository()); @@ -143,7 +148,8 @@ public class DefaultRepositorySystemSessionFactoryTest { settingsDecrypter, eventSpyDispatcher, information, - defaultTypeRegistry); + defaultTypeRegistry, + versionScheme); PlexusConfiguration plexusConfiguration = (PlexusConfiguration) systemSessionFactory .newRepositorySession(request) @@ -186,7 +192,8 @@ public class DefaultRepositorySystemSessionFactoryTest { settingsDecrypter, eventSpyDispatcher, information, - defaultTypeRegistry); + defaultTypeRegistry, + versionScheme); Map headers = (Map) systemSessionFactory .newRepositorySession(request) @@ -223,7 +230,8 @@ public class DefaultRepositorySystemSessionFactoryTest { settingsDecrypter, eventSpyDispatcher, information, - defaultTypeRegistry); + defaultTypeRegistry, + versionScheme); int connectionTimeout = (Integer) systemSessionFactory .newRepositorySession(request) @@ -264,7 +272,8 @@ public class DefaultRepositorySystemSessionFactoryTest { settingsDecrypter, eventSpyDispatcher, information, - defaultTypeRegistry); + defaultTypeRegistry, + versionScheme); int connectionTimeout = (Integer) systemSessionFactory .newRepositorySession(request) @@ -299,7 +308,8 @@ public class DefaultRepositorySystemSessionFactoryTest { settingsDecrypter, eventSpyDispatcher, information, - defaultTypeRegistry); + defaultTypeRegistry, + versionScheme); int requestTimeout = (Integer) systemSessionFactory .newRepositorySession(request) @@ -340,7 +350,8 @@ public class DefaultRepositorySystemSessionFactoryTest { settingsDecrypter, eventSpyDispatcher, information, - defaultTypeRegistry); + defaultTypeRegistry, + versionScheme); int requestTimeout = (Integer) systemSessionFactory .newRepositorySession(request) @@ -358,7 +369,8 @@ public class DefaultRepositorySystemSessionFactoryTest { settingsDecrypter, eventSpyDispatcher, information, - defaultTypeRegistry); + defaultTypeRegistry, + versionScheme); MavenExecutionRequest request = new DefaultMavenExecutionRequest(); request.setLocalRepository(getLocalRepository()); @@ -395,6 +407,66 @@ public class DefaultRepositorySystemSessionFactoryTest { properties.remove("maven.resolver.transport"); } + @Test + void versionFilteringTest() throws InvalidRepositoryException { + DefaultRepositorySystemSessionFactory systemSessionFactory = new DefaultRepositorySystemSessionFactory( + artifactHandlerManager, + aetherRepositorySystem, + null, + settingsDecrypter, + eventSpyDispatcher, + information, + defaultTypeRegistry, + versionScheme); + + MavenExecutionRequest request = new DefaultMavenExecutionRequest(); + request.setLocalRepository(getLocalRepository()); + + VersionFilter versionFilter; + + // single one + request.getUserProperties().put("maven.versionFilters", "s"); + versionFilter = systemSessionFactory.newRepositorySession(request).getVersionFilter(); + assertNotNull(versionFilter); + assertTrue(versionFilter instanceof ContextualSnapshotVersionFilter); + + request.getUserProperties().put("maven.versionFilters", "h"); + versionFilter = systemSessionFactory.newRepositorySession(request).getVersionFilter(); + assertNotNull(versionFilter); + assertTrue(versionFilter instanceof HighestVersionFilter); + + request.getUserProperties().put("maven.versionFilters", "h(5)"); + versionFilter = systemSessionFactory.newRepositorySession(request).getVersionFilter(); + assertNotNull(versionFilter); + assertTrue(versionFilter instanceof HighestVersionFilter); + + request.getUserProperties().put("maven.versionFilters", "l"); + versionFilter = systemSessionFactory.newRepositorySession(request).getVersionFilter(); + assertNotNull(versionFilter); + assertTrue(versionFilter instanceof LowestVersionFilter); + + request.getUserProperties().put("maven.versionFilters", "l(5)"); + versionFilter = systemSessionFactory.newRepositorySession(request).getVersionFilter(); + assertNotNull(versionFilter); + assertTrue(versionFilter instanceof LowestVersionFilter); + + request.getUserProperties().put("maven.versionFilters", "e(g:a:v)"); + versionFilter = systemSessionFactory.newRepositorySession(request).getVersionFilter(); + assertNotNull(versionFilter); + assertTrue(versionFilter instanceof PredicateVersionFilter); + + request.getUserProperties().put("maven.versionFilters", "e(g:a:[1,2])"); + versionFilter = systemSessionFactory.newRepositorySession(request).getVersionFilter(); + assertNotNull(versionFilter); + assertTrue(versionFilter instanceof PredicateVersionFilter); + + // chained + request.getUserProperties().put("maven.versionFilters", "h(5);s;e(org.foo:bar:1)"); + versionFilter = systemSessionFactory.newRepositorySession(request).getVersionFilter(); + assertNotNull(versionFilter); + assertTrue(versionFilter instanceof ChainedVersionFilter); + } + protected ArtifactRepository getLocalRepository() throws InvalidRepositoryException { File repoDir = new File(getBasedir(), "target/local-repo").getAbsoluteFile();