From f24c6e9855f7d20d1b925e840ad06d2d39a76e06 Mon Sep 17 00:00:00 2001 From: Tamas Cservenak Date: Fri, 14 Nov 2025 14:51:37 +0100 Subject: [PATCH 01/13] Bug: fix prefix filter discovery Two major issues: one was that it tried to fix the Maven issue of remote repository uniqueness (globally), and that attempt was in fact wrong, fix is elsewhere. The reason it tried to do this is in fact to circumvent locking issues #1644. The other issue was filter lifecycle, manager did it wrongly: the bug caused that one session was used to acquire filters and same filter got used for potentially other (maybe even reconfigured) session. This overlook forced filters to implement hoops and loops (to prevent recursion), but also prevented any third party code to have saying in filter operation. Changes: * fix manager to make sure filters and session are aligned * fix/simplify prefix filter recursion prevention * introduce public and documented way to inhibit prefix discovery --- .../DefaultRemoteRepositoryFilterManager.java | 8 +- .../PrefixesRemoteRepositoryFilterSource.java | 97 +++++++++---------- .../util/repository/RepositoryIdHelper.java | 4 + src/site/markdown/configuration.md | 1 + 4 files changed, 56 insertions(+), 54 deletions(-) diff --git a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/filter/DefaultRemoteRepositoryFilterManager.java b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/filter/DefaultRemoteRepositoryFilterManager.java index 276153ce4..ee63ce831 100644 --- a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/filter/DefaultRemoteRepositoryFilterManager.java +++ b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/filter/DefaultRemoteRepositoryFilterManager.java @@ -24,6 +24,7 @@ import java.util.Collections; import java.util.HashMap; +import java.util.IdentityHashMap; import java.util.Map; import java.util.stream.Collectors; @@ -48,18 +49,18 @@ @Singleton @Named public final class DefaultRemoteRepositoryFilterManager implements RemoteRepositoryFilterManager { - private static final String INSTANCE_KEY = DefaultRemoteRepositoryFilterManager.class.getName() + ".instance"; - private final Map sources; + private final Map usedFilters; @Inject public DefaultRemoteRepositoryFilterManager(Map sources) { this.sources = requireNonNull(sources); + this.usedFilters = Collections.synchronizedMap(new IdentityHashMap<>()); } @Override public RemoteRepositoryFilter getRemoteRepositoryFilter(RepositorySystemSession session) { - return (RemoteRepositoryFilter) session.getData().computeIfAbsent(INSTANCE_KEY, () -> { + return usedFilters.computeIfAbsent(session, k -> { HashMap filters = new HashMap<>(); for (Map.Entry entry : sources.entrySet()) { RemoteRepositoryFilter filter = entry.getValue().getRemoteRepositoryFilter(session); @@ -68,6 +69,7 @@ public RemoteRepositoryFilter getRemoteRepositoryFilter(RepositorySystemSession } } if (!filters.isEmpty()) { + session.addOnSessionEndedHandler(() -> usedFilters.remove(session)); return new Participants(filters); } else { return null; diff --git a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/filter/PrefixesRemoteRepositoryFilterSource.java b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/filter/PrefixesRemoteRepositoryFilterSource.java index 34698d310..ba630bdf5 100644 --- a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/filter/PrefixesRemoteRepositoryFilterSource.java +++ b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/filter/PrefixesRemoteRepositoryFilterSource.java @@ -25,7 +25,6 @@ import java.nio.file.Files; import java.nio.file.Path; import java.util.Collections; -import java.util.Objects; import java.util.concurrent.ConcurrentHashMap; import java.util.function.Supplier; @@ -119,6 +118,23 @@ public final class PrefixesRemoteRepositoryFilterSource extends RemoteRepository public static final boolean DEFAULT_ENABLED = true; + /** + * Configuration to skip the Prefixes filter for given request. This configuration is evaluated and if {@code true} + * the prefixes remote filter will not kick in. Main use case is by filter itself, to prevent recursion during + * discovery of remote prefixes file, but this also allows other components to control prefix filter discovery, while + * leaving configuration like {@link #CONFIG_PROP_ENABLED} still show the "real state". + * + * @since 2.0.14 + * @configurationSource {@link RepositorySystemSession#getConfigProperties()} + * @configurationType {@link java.lang.Boolean} + * @configurationRepoIdSuffix Yes + * @configurationDefaultValue {@link #DEFAULT_SKIPPED} + */ + public static final String CONFIG_PROP_SKIPPED = + RemoteRepositoryFilterSourceSupport.CONFIG_PROPS_PREFIX + NAME + ".skipped"; + + public static final boolean DEFAULT_SKIPPED = false; + /** * The basedir where to store filter files. If path is relative, it is resolved from local repository root. * @@ -146,8 +162,6 @@ public final class PrefixesRemoteRepositoryFilterSource extends RemoteRepository private final ConcurrentHashMap layouts; - private final ConcurrentHashMap ongoingUpdates; - @Inject public PrefixesRemoteRepositoryFilterSource( Supplier metadataResolver, @@ -158,20 +172,26 @@ public PrefixesRemoteRepositoryFilterSource( this.repositoryLayoutProvider = requireNonNull(repositoryLayoutProvider); this.prefixes = new ConcurrentHashMap<>(); this.layouts = new ConcurrentHashMap<>(); - this.ongoingUpdates = new ConcurrentHashMap<>(); } @Override protected boolean isEnabled(RepositorySystemSession session) { - return ConfigUtils.getBoolean(session, DEFAULT_ENABLED, CONFIG_PROP_ENABLED); + return ConfigUtils.getBoolean(session, DEFAULT_ENABLED, CONFIG_PROP_ENABLED) + && !ConfigUtils.getBoolean(session, DEFAULT_SKIPPED, CONFIG_PROP_SKIPPED); } private boolean isRepositoryFilteringEnabled(RepositorySystemSession session, RemoteRepository remoteRepository) { if (isEnabled(session)) { return ConfigUtils.getBoolean( - session, - ConfigUtils.getBoolean(session, true, CONFIG_PROP_ENABLED + ".*"), - CONFIG_PROP_ENABLED + "." + remoteRepository.getId()); + session, + DEFAULT_ENABLED, + CONFIG_PROP_ENABLED + "." + remoteRepository.getId(), + CONFIG_PROP_ENABLED + ".*") + && !ConfigUtils.getBoolean( + session, + DEFAULT_SKIPPED, + CONFIG_PROP_SKIPPED + "." + remoteRepository.getId(), + CONFIG_PROP_SKIPPED + ".*"); } return false; } @@ -201,22 +221,7 @@ private RepositoryLayout cacheLayout(RepositorySystemSession session, RemoteRepo private PrefixTree cachePrefixTree( RepositorySystemSession session, Path basedir, RemoteRepository remoteRepository) { - return ongoingUpdatesGuard( - remoteRepository, - () -> prefixes.computeIfAbsent( - remoteRepository, r -> loadPrefixTree(session, basedir, remoteRepository)), - () -> PrefixTree.SENTINEL); - } - - private T ongoingUpdatesGuard(RemoteRepository remoteRepository, Supplier unblocked, Supplier blocked) { - if (!remoteRepository.isBlocked() && null == ongoingUpdates.putIfAbsent(remoteRepository, Boolean.TRUE)) { - try { - return unblocked.get(); - } finally { - ongoingUpdates.remove(remoteRepository); - } - } - return blocked.get(); + return prefixes.computeIfAbsent(remoteRepository, r -> loadPrefixTree(session, basedir, remoteRepository)); } private PrefixTree loadPrefixTree( @@ -282,35 +287,23 @@ private Path resolvePrefixesFromRemoteRepository( RemoteRepository prepared = rm.aggregateRepositories( session, Collections.emptyList(), Collections.singletonList(remoteRepository), true) .get(0); - // make it unique - RemoteRepository unique = new RemoteRepository.Builder(prepared) - .setId(RepositoryIdHelper.remoteRepositoryUniqueId(remoteRepository)) - .build(); // supplier for path - Supplier supplier = () -> { - MetadataRequest request = - new MetadataRequest(new DefaultMetadata(PREFIX_FILE_PATH, Metadata.Nature.RELEASE_OR_SNAPSHOT)); - // use unique repository; this will result in prefix (repository metadata) cached under unique id - request.setRepository(unique); - request.setDeleteLocalCopyIfMissing(true); - request.setFavorLocalRepository(true); - MetadataResult result = mr.resolveMetadata( - new DefaultRepositorySystemSession(session).setTransferListener(null), - Collections.singleton(request)) - .get(0); - if (result.isResolved()) { - return result.getMetadata().getPath(); - } else { - return null; - } - }; - - // prevent recursive calls; but we need extra work if not dealing with Central (as in that case outer call - // shields us) - if (Objects.equals(prepared.getId(), unique.getId())) { - return supplier.get(); + MetadataRequest request = + new MetadataRequest(new DefaultMetadata(PREFIX_FILE_PATH, Metadata.Nature.RELEASE_OR_SNAPSHOT)); + // use unique repository; this will result in prefix (repository metadata) cached under unique id + request.setRepository(prepared); + request.setDeleteLocalCopyIfMissing(true); + request.setFavorLocalRepository(true); + MetadataResult result = mr.resolveMetadata( + new DefaultRepositorySystemSession(session) + .setTransferListener(null) + .setConfigProperty(CONFIG_PROP_SKIPPED, Boolean.TRUE.toString()), + Collections.singleton(request)) + .get(0); + if (result.isResolved()) { + return result.getMetadata().getPath(); } else { - return ongoingUpdatesGuard(unique, supplier, () -> null); + return null; } } return null; @@ -362,4 +355,6 @@ private Result acceptPrefix(RemoteRepository remoteRepository, String path) { private static final RemoteRepositoryFilter.Result NOT_PRESENT_RESULT = new SimpleResult(true, "Prefix file not present"); + + private static final RemoteRepositoryFilter.Result SKIPPED_RESULT = new SimpleResult(true, "Prefix filter skipped"); } diff --git a/maven-resolver-util/src/main/java/org/eclipse/aether/util/repository/RepositoryIdHelper.java b/maven-resolver-util/src/main/java/org/eclipse/aether/util/repository/RepositoryIdHelper.java index edcf42e8e..47bae1b5e 100644 --- a/maven-resolver-util/src/main/java/org/eclipse/aether/util/repository/RepositoryIdHelper.java +++ b/maven-resolver-util/src/main/java/org/eclipse/aether/util/repository/RepositoryIdHelper.java @@ -75,7 +75,11 @@ private RepositoryIdHelper() {} * different string id. The checksum and update policies are not participating in key creation. *

* This method is costly, so should be invoked sparingly, or cache results if needed. + * + * @deprecated Do not use this method, as it totally disconnects repositories used in session. This method + * MAY be used under some special circumstances, but NOT within a Resolver (and Maven) session. */ + @Deprecated public static String remoteRepositoryUniqueId(RemoteRepository repository) { if (CENTRAL_DIRECT_ONLY.test(repository)) { return CENTRAL_REPOSITORY_ID; diff --git a/src/site/markdown/configuration.md b/src/site/markdown/configuration.md index e30b9316e..615a7121d 100644 --- a/src/site/markdown/configuration.md +++ b/src/site/markdown/configuration.md @@ -103,6 +103,7 @@ To modify this file, edit the template and regenerate. | `"aether.remoteRepositoryFilter.groupId.record"` | `Boolean` | Should filter go into "record" mode (and collect encountered artifacts)? | `false` | 1.9.0 | No | Session Configuration | | `"aether.remoteRepositoryFilter.prefixes"` | `Boolean` | Configuration to enable the Prefixes filter (enabled by default). Can be fine-tuned per repository using repository ID suffixes. Important: For this filter to take effect, configuration files must be available. Without configuration files, the enabled filter remains dormant and does not interfere with resolution. Configuration File Resolution:

  1. User-provided files: Checked first from directory specified by #CONFIG_PROP_BASEDIR (defaults to $LOCAL_REPO/.remoteRepositoryFilters )
  2. Auto-discovery: If not found, attempts to download from remote repository and cache locally
File Naming: prefixes-$(repository.id).txt Recommended Setup (Auto-Discovery with Override Capability): Start with auto-discovery, but prepare for project-specific overrides. Add to .mvn/maven.config :
 -Daether.remoteRepositoryFilter.prefixes=true -Daether.remoteRepositoryFilter.prefixes.basedir=${session.rootDirectory}/.mvn/rrf/ 
Initial setup: Don't provide any files - rely on auto-discovery as repositories are accessed. Override when needed: Create prefixes-myrepoId.txt files in .mvn/rrf/ and commit to version control. Caching: Auto-discovered prefix files are cached in the local repository with unique IDs (using RepositoryIdHelper#remoteRepositoryUniqueId(RemoteRepository) ) to prevent conflicts that could cause build failures. | `true` | 1.9.0 | Yes | Session Configuration | | `"aether.remoteRepositoryFilter.prefixes.basedir"` | `String` | The basedir where to store filter files. If path is relative, it is resolved from local repository root. | `".remoteRepositoryFilters"` | 1.9.0 | No | Session Configuration | +| `"aether.remoteRepositoryFilter.prefixes.skipped"` | `Boolean` | Configuration to skip the Prefixes filter for given request. This configuration is evaluated and if true the prefixes remote filter will not kick in. Main use case is by filter itself, to prevent recursion during discovery of remote prefixes file, but this also allows other components to control prefix filter discovery, while leaving configuration like #CONFIG_PROP_ENABLED still show the "real state". | `false` | 2.0.14 | Yes | Session Configuration | | `"aether.snapshotFilter"` | `Boolean` | The key in the repository session's RepositorySystemSession#getConfigProperties() configurationproperties used to store a Boolean flag whether this filter should be forced to ban snapshots. By default, snapshots are only filtered if the root artifact is not a snapshot. | `false` | | No | Session Configuration | | `"aether.syncContext.named.basedir.locksDir"` | `String` | The location of the directory toi use for locks. If relative path, it is resolved from the local repository root. | `".locks"` | 1.9.0 | No | Session Configuration | | `"aether.syncContext.named.discriminating.discriminator"` | `String` | Configuration property to pass in discriminator, if needed. If not present, it is auto-calculated. | - | 1.7.0 | No | Session Configuration | From f8666c1d4498ccb2654c5c793749e34ba93dfe8a Mon Sep 17 00:00:00 2001 From: Tamas Cservenak Date: Fri, 14 Nov 2025 15:10:21 +0100 Subject: [PATCH 02/13] Do not deprecate, is useful, but document it. --- .../aether/util/repository/RepositoryIdHelper.java | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/maven-resolver-util/src/main/java/org/eclipse/aether/util/repository/RepositoryIdHelper.java b/maven-resolver-util/src/main/java/org/eclipse/aether/util/repository/RepositoryIdHelper.java index 47bae1b5e..1db086b59 100644 --- a/maven-resolver-util/src/main/java/org/eclipse/aether/util/repository/RepositoryIdHelper.java +++ b/maven-resolver-util/src/main/java/org/eclipse/aether/util/repository/RepositoryIdHelper.java @@ -75,11 +75,12 @@ private RepositoryIdHelper() {} * different string id. The checksum and update policies are not participating in key creation. *

* This method is costly, so should be invoked sparingly, or cache results if needed. - * - * @deprecated Do not use this method, as it totally disconnects repositories used in session. This method - * MAY be used under some special circumstances, but NOT within a Resolver (and Maven) session. + *

+ * Important:Do not use this method, or at least do consider when do you want to use it, as it + * totally disconnects repositories used in session. This method may be used under some special circumstances + * (ie reporting), but must not be used within Resolver (and Maven) session for "usual" resolution and + * deployment use cases. */ - @Deprecated public static String remoteRepositoryUniqueId(RemoteRepository repository) { if (CENTRAL_DIRECT_ONLY.test(repository)) { return CENTRAL_REPOSITORY_ID; From d392216b7b6a53ab47de52ddd951b23443068219 Mon Sep 17 00:00:00 2001 From: Tamas Cservenak Date: Fri, 14 Nov 2025 15:11:27 +0100 Subject: [PATCH 03/13] Remove method reference, is untrue. --- .../impl/filter/PrefixesRemoteRepositoryFilterSource.java | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/filter/PrefixesRemoteRepositoryFilterSource.java b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/filter/PrefixesRemoteRepositoryFilterSource.java index ba630bdf5..c0225529c 100644 --- a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/filter/PrefixesRemoteRepositoryFilterSource.java +++ b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/filter/PrefixesRemoteRepositoryFilterSource.java @@ -105,9 +105,7 @@ public final class PrefixesRemoteRepositoryFilterSource extends RemoteRepository * Initial setup: Don't provide any files - rely on auto-discovery as repositories are accessed. * Override when needed: Create {@code prefixes-myrepoId.txt} files in {@code .mvn/rrf/} and * commit to version control. - * Caching: Auto-discovered prefix files are cached in the local repository with unique IDs - * (using {@link RepositoryIdHelper#remoteRepositoryUniqueId(RemoteRepository)}) to prevent conflicts that - * could cause build failures. + * Caching: Auto-discovered prefix files are cached in the local repository. * * @configurationSource {@link RepositorySystemSession#getConfigProperties()} * @configurationType {@link java.lang.Boolean} From f13ad90f09f8baa370145bb7fe2aa3f1324e1d09 Mon Sep 17 00:00:00 2001 From: Tamas Cservenak Date: Fri, 14 Nov 2025 15:12:30 +0100 Subject: [PATCH 04/13] Typo --- .../org/eclipse/aether/util/repository/RepositoryIdHelper.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/maven-resolver-util/src/main/java/org/eclipse/aether/util/repository/RepositoryIdHelper.java b/maven-resolver-util/src/main/java/org/eclipse/aether/util/repository/RepositoryIdHelper.java index 1db086b59..5c8bbbbcd 100644 --- a/maven-resolver-util/src/main/java/org/eclipse/aether/util/repository/RepositoryIdHelper.java +++ b/maven-resolver-util/src/main/java/org/eclipse/aether/util/repository/RepositoryIdHelper.java @@ -78,7 +78,7 @@ private RepositoryIdHelper() {} *

* Important:Do not use this method, or at least do consider when do you want to use it, as it * totally disconnects repositories used in session. This method may be used under some special circumstances - * (ie reporting), but must not be used within Resolver (and Maven) session for "usual" resolution and + * (ie reporting), but must not be used within Resolver (and Maven) session for "usual" resolution and * deployment use cases. */ public static String remoteRepositoryUniqueId(RemoteRepository repository) { From 2c0cb937bbea95d006c4ab97cf6534c1b6f9a6d9 Mon Sep 17 00:00:00 2001 From: Tamas Cservenak Date: Fri, 14 Nov 2025 15:47:31 +0100 Subject: [PATCH 05/13] Simplify, tidy up Signed-off-by: Tamas Cservenak --- .../filter/DefaultRemoteRepositoryFilterManager.java | 10 +++++----- .../filter/PrefixesRemoteRepositoryFilterSource.java | 2 -- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/filter/DefaultRemoteRepositoryFilterManager.java b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/filter/DefaultRemoteRepositoryFilterManager.java index ee63ce831..42f406416 100644 --- a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/filter/DefaultRemoteRepositoryFilterManager.java +++ b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/filter/DefaultRemoteRepositoryFilterManager.java @@ -24,7 +24,6 @@ import java.util.Collections; import java.util.HashMap; -import java.util.IdentityHashMap; import java.util.Map; import java.util.stream.Collectors; @@ -49,18 +48,20 @@ @Singleton @Named public final class DefaultRemoteRepositoryFilterManager implements RemoteRepositoryFilterManager { + private static final String INSTANCE_KEY = DefaultRemoteRepositoryFilterManager.class.getName() + ".instance"; + private final Map sources; - private final Map usedFilters; @Inject public DefaultRemoteRepositoryFilterManager(Map sources) { this.sources = requireNonNull(sources); - this.usedFilters = Collections.synchronizedMap(new IdentityHashMap<>()); } @Override public RemoteRepositoryFilter getRemoteRepositoryFilter(RepositorySystemSession session) { - return usedFilters.computeIfAbsent(session, k -> { + // use session specific key to distinguish between "derived" sessions + String instanceSpecificKey = INSTANCE_KEY + "." + session.hashCode(); + return (RemoteRepositoryFilter) session.getData().computeIfAbsent(instanceSpecificKey, () -> { HashMap filters = new HashMap<>(); for (Map.Entry entry : sources.entrySet()) { RemoteRepositoryFilter filter = entry.getValue().getRemoteRepositoryFilter(session); @@ -69,7 +70,6 @@ public RemoteRepositoryFilter getRemoteRepositoryFilter(RepositorySystemSession } } if (!filters.isEmpty()) { - session.addOnSessionEndedHandler(() -> usedFilters.remove(session)); return new Participants(filters); } else { return null; diff --git a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/filter/PrefixesRemoteRepositoryFilterSource.java b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/filter/PrefixesRemoteRepositoryFilterSource.java index c0225529c..fb0f819e4 100644 --- a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/filter/PrefixesRemoteRepositoryFilterSource.java +++ b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/filter/PrefixesRemoteRepositoryFilterSource.java @@ -353,6 +353,4 @@ private Result acceptPrefix(RemoteRepository remoteRepository, String path) { private static final RemoteRepositoryFilter.Result NOT_PRESENT_RESULT = new SimpleResult(true, "Prefix file not present"); - - private static final RemoteRepositoryFilter.Result SKIPPED_RESULT = new SimpleResult(true, "Prefix filter skipped"); } From 2f47e801b7d27b1bf8f302b6d293e36af850e2bf Mon Sep 17 00:00:00 2001 From: Tamas Cservenak Date: Mon, 17 Nov 2025 14:14:03 +0100 Subject: [PATCH 06/13] Adjust groupId too for skip --- .../GroupIdRemoteRepositoryFilterSource.java | 30 ++++++++++++++++--- 1 file changed, 26 insertions(+), 4 deletions(-) diff --git a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/filter/GroupIdRemoteRepositoryFilterSource.java b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/filter/GroupIdRemoteRepositoryFilterSource.java index 92585d35f..963dcc642 100644 --- a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/filter/GroupIdRemoteRepositoryFilterSource.java +++ b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/filter/GroupIdRemoteRepositoryFilterSource.java @@ -109,6 +109,21 @@ public final class GroupIdRemoteRepositoryFilterSource extends RemoteRepositoryF public static final boolean DEFAULT_ENABLED = true; + /** + * Configuration to skip the GroupId filter for given request. This configuration is evaluated and if {@code true} + * the GroupId remote filter will not kick in. + * + * @since 2.0.14 + * @configurationSource {@link RepositorySystemSession#getConfigProperties()} + * @configurationType {@link java.lang.Boolean} + * @configurationRepoIdSuffix Yes + * @configurationDefaultValue {@link #DEFAULT_SKIPPED} + */ + public static final String CONFIG_PROP_SKIPPED = + RemoteRepositoryFilterSourceSupport.CONFIG_PROPS_PREFIX + NAME + ".skipped"; + + public static final boolean DEFAULT_SKIPPED = false; + /** * The basedir where to store filter files. If path is relative, it is resolved from local repository root. * @@ -160,15 +175,22 @@ public GroupIdRemoteRepositoryFilterSource( @Override protected boolean isEnabled(RepositorySystemSession session) { - return ConfigUtils.getBoolean(session, DEFAULT_ENABLED, CONFIG_PROP_ENABLED); + return ConfigUtils.getBoolean(session, DEFAULT_ENABLED, CONFIG_PROP_ENABLED) + && !ConfigUtils.getBoolean(session, DEFAULT_SKIPPED, CONFIG_PROP_SKIPPED); } private boolean isRepositoryFilteringEnabled(RepositorySystemSession session, RemoteRepository remoteRepository) { if (isEnabled(session)) { return ConfigUtils.getBoolean( - session, - ConfigUtils.getBoolean(session, true, CONFIG_PROP_ENABLED + ".*"), - CONFIG_PROP_ENABLED + "." + remoteRepository.getId()); + session, + DEFAULT_ENABLED, + CONFIG_PROP_ENABLED + "." + remoteRepository.getId(), + CONFIG_PROP_ENABLED + ".*") + && !ConfigUtils.getBoolean( + session, + DEFAULT_SKIPPED, + CONFIG_PROP_SKIPPED + "." + remoteRepository.getId(), + CONFIG_PROP_SKIPPED + ".*"); } return false; } From 71e290b5a8d29132a3d4320319aca77fe98a7453 Mon Sep 17 00:00:00 2001 From: Tamas Cservenak Date: Mon, 17 Nov 2025 14:16:26 +0100 Subject: [PATCH 07/13] Update doco --- src/site/markdown/configuration.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/site/markdown/configuration.md b/src/site/markdown/configuration.md index 615a7121d..52ae97cfb 100644 --- a/src/site/markdown/configuration.md +++ b/src/site/markdown/configuration.md @@ -101,7 +101,8 @@ To modify this file, edit the template and regenerate. | `"aether.remoteRepositoryFilter.groupId"` | `Boolean` | Configuration to enable the GroupId filter (enabled by default). Can be fine-tuned per repository using repository ID suffixes. Important: For this filter to take effect, you must provide configuration files. Without configuration files, the enabled filter remains dormant and does not interfere with resolution. Configuration Files:

  • Location: Directory specified by #CONFIG_PROP_BASEDIR (defaults to $LOCAL_REPO/.remoteRepositoryFilters )
  • Naming: groupId-$(repository.id).txt
  • Content: One groupId per line to allow/block from the repository
Recommended Setup (Per-Project): Use project-specific configuration to avoid repository ID clashes. Add to .mvn/maven.config :
 -Daether.remoteRepositoryFilter.groupId=true -Daether.remoteRepositoryFilter.groupId.basedir=${session.rootDirectory}/.mvn/rrf/ 
Then create groupId-myrepoId.txt files in the .mvn/rrf/ directory and commit them to version control. | `true` | 1.9.0 | Yes | Session Configuration | | `"aether.remoteRepositoryFilter.groupId.basedir"` | `String` | The basedir where to store filter files. If path is relative, it is resolved from local repository root. | `".remoteRepositoryFilters"` | 1.9.0 | No | Session Configuration | | `"aether.remoteRepositoryFilter.groupId.record"` | `Boolean` | Should filter go into "record" mode (and collect encountered artifacts)? | `false` | 1.9.0 | No | Session Configuration | -| `"aether.remoteRepositoryFilter.prefixes"` | `Boolean` | Configuration to enable the Prefixes filter (enabled by default). Can be fine-tuned per repository using repository ID suffixes. Important: For this filter to take effect, configuration files must be available. Without configuration files, the enabled filter remains dormant and does not interfere with resolution. Configuration File Resolution:
  1. User-provided files: Checked first from directory specified by #CONFIG_PROP_BASEDIR (defaults to $LOCAL_REPO/.remoteRepositoryFilters )
  2. Auto-discovery: If not found, attempts to download from remote repository and cache locally
File Naming: prefixes-$(repository.id).txt Recommended Setup (Auto-Discovery with Override Capability): Start with auto-discovery, but prepare for project-specific overrides. Add to .mvn/maven.config :
 -Daether.remoteRepositoryFilter.prefixes=true -Daether.remoteRepositoryFilter.prefixes.basedir=${session.rootDirectory}/.mvn/rrf/ 
Initial setup: Don't provide any files - rely on auto-discovery as repositories are accessed. Override when needed: Create prefixes-myrepoId.txt files in .mvn/rrf/ and commit to version control. Caching: Auto-discovered prefix files are cached in the local repository with unique IDs (using RepositoryIdHelper#remoteRepositoryUniqueId(RemoteRepository) ) to prevent conflicts that could cause build failures. | `true` | 1.9.0 | Yes | Session Configuration | +| `"aether.remoteRepositoryFilter.groupId.skipped"` | `Boolean` | Configuration to skip the GroupId filter for given request. This configuration is evaluated and if true the GroupId remote filter will not kick in. | `false` | 2.0.14 | Yes | Session Configuration | +| `"aether.remoteRepositoryFilter.prefixes"` | `Boolean` | Configuration to enable the Prefixes filter (enabled by default). Can be fine-tuned per repository using repository ID suffixes. Important: For this filter to take effect, configuration files must be available. Without configuration files, the enabled filter remains dormant and does not interfere with resolution. Configuration File Resolution:
  1. User-provided files: Checked first from directory specified by #CONFIG_PROP_BASEDIR (defaults to $LOCAL_REPO/.remoteRepositoryFilters )
  2. Auto-discovery: If not found, attempts to download from remote repository and cache locally
File Naming: prefixes-$(repository.id).txt Recommended Setup (Auto-Discovery with Override Capability): Start with auto-discovery, but prepare for project-specific overrides. Add to .mvn/maven.config :
 -Daether.remoteRepositoryFilter.prefixes=true -Daether.remoteRepositoryFilter.prefixes.basedir=${session.rootDirectory}/.mvn/rrf/ 
Initial setup: Don't provide any files - rely on auto-discovery as repositories are accessed. Override when needed: Create prefixes-myrepoId.txt files in .mvn/rrf/ and commit to version control. Caching: Auto-discovered prefix files are cached in the local repository. | `true` | 1.9.0 | Yes | Session Configuration | | `"aether.remoteRepositoryFilter.prefixes.basedir"` | `String` | The basedir where to store filter files. If path is relative, it is resolved from local repository root. | `".remoteRepositoryFilters"` | 1.9.0 | No | Session Configuration | | `"aether.remoteRepositoryFilter.prefixes.skipped"` | `Boolean` | Configuration to skip the Prefixes filter for given request. This configuration is evaluated and if true the prefixes remote filter will not kick in. Main use case is by filter itself, to prevent recursion during discovery of remote prefixes file, but this also allows other components to control prefix filter discovery, while leaving configuration like #CONFIG_PROP_ENABLED still show the "real state". | `false` | 2.0.14 | Yes | Session Configuration | | `"aether.snapshotFilter"` | `Boolean` | The key in the repository session's RepositorySystemSession#getConfigProperties() configurationproperties used to store a Boolean flag whether this filter should be forced to ban snapshots. By default, snapshots are only filtered if the root artifact is not a snapshot. | `false` | | No | Session Configuration | From cc657ddb1d085a4ee82f323c0579beb1eb08399d Mon Sep 17 00:00:00 2001 From: Tamas Cservenak Date: Mon, 17 Nov 2025 14:33:49 +0100 Subject: [PATCH 08/13] Normalize reposes --- .../PrefixesRemoteRepositoryFilterSource.java | 41 +++++++++++++------ 1 file changed, 29 insertions(+), 12 deletions(-) diff --git a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/filter/PrefixesRemoteRepositoryFilterSource.java b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/filter/PrefixesRemoteRepositoryFilterSource.java index fb0f819e4..17694a346 100644 --- a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/filter/PrefixesRemoteRepositoryFilterSource.java +++ b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/filter/PrefixesRemoteRepositoryFilterSource.java @@ -25,6 +25,7 @@ import java.nio.file.Files; import java.nio.file.Path; import java.util.Collections; +import java.util.List; import java.util.concurrent.ConcurrentHashMap; import java.util.function.Supplier; @@ -202,6 +203,22 @@ public RemoteRepositoryFilter getRemoteRepositoryFilter(RepositorySystemSession return null; } + /** + * We use remote repositories as keys, but they may fly in as "bare" or as "equipped" (w/ auth and proxy) if caller + * used {@link org.eclipse.aether.RepositorySystem#newResolutionRepositories(RepositorySystemSession, List)} beforehand. + * The hash/equalTo method factors in all these as well, but from our perspective, they do not matter. So we make all + * key remote repositories back to "bare". + */ + private RemoteRepository normalizeRemoteRepository( + RepositorySystemSession session, RemoteRepository remoteRepository) { + return new RemoteRepository.Builder(remoteRepository) + .setProxy(null) + .setAuthentication(null) + .setMirroredRepositories(null) + .setRepositoryManager(false) + .build(); + } + /** * Caches layout instances for remote repository. In case of unknown layout it returns {@code null}. * @@ -318,35 +335,35 @@ private PrefixesFilter(RepositorySystemSession session, Path basedir) { @Override public Result acceptArtifact(RemoteRepository remoteRepository, Artifact artifact) { - RepositoryLayout repositoryLayout = cacheLayout(session, remoteRepository); + RemoteRepository repository = normalizeRemoteRepository(session, remoteRepository); + RepositoryLayout repositoryLayout = cacheLayout(session, repository); if (repositoryLayout == null) { - return new SimpleResult(true, "Unsupported layout: " + remoteRepository); + return new SimpleResult(true, "Unsupported layout: " + repository); } return acceptPrefix( - remoteRepository, - repositoryLayout.getLocation(artifact, false).getPath()); + repository, repositoryLayout.getLocation(artifact, false).getPath()); } @Override public Result acceptMetadata(RemoteRepository remoteRepository, Metadata metadata) { - RepositoryLayout repositoryLayout = cacheLayout(session, remoteRepository); + RemoteRepository repository = normalizeRemoteRepository(session, remoteRepository); + RepositoryLayout repositoryLayout = cacheLayout(session, repository); if (repositoryLayout == null) { - return new SimpleResult(true, "Unsupported layout: " + remoteRepository); + return new SimpleResult(true, "Unsupported layout: " + repository); } return acceptPrefix( - remoteRepository, - repositoryLayout.getLocation(metadata, false).getPath()); + repository, repositoryLayout.getLocation(metadata, false).getPath()); } - private Result acceptPrefix(RemoteRepository remoteRepository, String path) { - PrefixTree prefixTree = cachePrefixTree(session, basedir, remoteRepository); + private Result acceptPrefix(RemoteRepository repository, String path) { + PrefixTree prefixTree = cachePrefixTree(session, basedir, repository); if (PrefixTree.SENTINEL == prefixTree) { return NOT_PRESENT_RESULT; } if (prefixTree.acceptedPath(path)) { - return new SimpleResult(true, "Path " + path + " allowed from " + remoteRepository); + return new SimpleResult(true, "Path " + path + " allowed from " + repository); } else { - return new SimpleResult(false, "Prefix " + path + " NOT allowed from " + remoteRepository); + return new SimpleResult(false, "Path " + path + " NOT allowed from " + repository); } } } From f3303f5125fa1ba4960aabbe2b059be182311b1d Mon Sep 17 00:00:00 2001 From: Tamas Cservenak Date: Mon, 17 Nov 2025 15:12:35 +0100 Subject: [PATCH 09/13] Add more config --- .../PrefixesRemoteRepositoryFilterSource.java | 54 ++++++++++++++----- ...fixesRemoteRepositoryFilterSourceTest.java | 42 ++++++++++++++- ...moteRepositoryFilterSourceTestSupport.java | 10 ++-- src/site/markdown/configuration.md | 1 + 4 files changed, 87 insertions(+), 20 deletions(-) diff --git a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/filter/PrefixesRemoteRepositoryFilterSource.java b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/filter/PrefixesRemoteRepositoryFilterSource.java index 17694a346..f1c7fbfed 100644 --- a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/filter/PrefixesRemoteRepositoryFilterSource.java +++ b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/filter/PrefixesRemoteRepositoryFilterSource.java @@ -134,6 +134,23 @@ public final class PrefixesRemoteRepositoryFilterSource extends RemoteRepository public static final boolean DEFAULT_SKIPPED = false; + /** + * Configuration to allow Prefixes filter to auto-discover prefixes from mirrored repositories as well. For this to + * work Maven should be aware that given remote repository is mirror and is usually backed by MRM. Given + * multiple MRM implementations messes up prefixes file, is better to just skip these. In other case, one may use + * {@link #CONFIG_PROP_ENABLED} with repository ID suffix. + * + * @since 2.0.14 + * @configurationSource {@link RepositorySystemSession#getConfigProperties()} + * @configurationType {@link java.lang.Boolean} + * @configurationRepoIdSuffix Yes + * @configurationDefaultValue {@link #DEFAULT_USE_MIRRORED_REPOSITORIES} + */ + public static final String CONFIG_PROP_USE_MIRRORED_REPOSITORIES = + RemoteRepositoryFilterSourceSupport.CONFIG_PROPS_PREFIX + NAME + ".useMirroredRepositories"; + + public static final boolean DEFAULT_USE_MIRRORED_REPOSITORIES = false; + /** * The basedir where to store filter files. If path is relative, it is resolved from local repository root. * @@ -236,7 +253,8 @@ private RepositoryLayout cacheLayout(RepositorySystemSession session, RemoteRepo private PrefixTree cachePrefixTree( RepositorySystemSession session, Path basedir, RemoteRepository remoteRepository) { - return prefixes.computeIfAbsent(remoteRepository, r -> loadPrefixTree(session, basedir, remoteRepository)); + RemoteRepository normalized = normalizeRemoteRepository(session, remoteRepository); + return prefixes.computeIfAbsent(normalized, r -> loadPrefixTree(session, basedir, remoteRepository)); } private PrefixTree loadPrefixTree( @@ -245,8 +263,12 @@ private PrefixTree loadPrefixTree( String origin = "user-provided"; Path filePath = resolvePrefixesFromLocalConfiguration(session, baseDir, remoteRepository); if (filePath == null) { - origin = "auto-discovered"; - filePath = resolvePrefixesFromRemoteRepository(session, remoteRepository); + if (!shouldAttemptResolvePrefixesForRemoteRepository(session, remoteRepository)) { + origin = "unsupported"; + } else { + origin = "auto-discovered"; + filePath = resolvePrefixesFromRemoteRepository(session, remoteRepository); + } } if (filePath != null) { PrefixesSource prefixesSource = PrefixesSource.of(remoteRepository, filePath); @@ -293,6 +315,13 @@ private Path resolvePrefixesFromLocalConfiguration( } } + private boolean shouldAttemptResolvePrefixesForRemoteRepository( + RepositorySystemSession session, RemoteRepository remoteRepository) { + return remoteRepository.getMirroredRepositories().isEmpty() + || ConfigUtils.getBoolean( + session, DEFAULT_USE_MIRRORED_REPOSITORIES, CONFIG_PROP_USE_MIRRORED_REPOSITORIES); + } + private Path resolvePrefixesFromRemoteRepository( RepositorySystemSession session, RemoteRepository remoteRepository) { MetadataResolver mr = metadataResolver.get(); @@ -302,10 +331,9 @@ private Path resolvePrefixesFromRemoteRepository( RemoteRepository prepared = rm.aggregateRepositories( session, Collections.emptyList(), Collections.singletonList(remoteRepository), true) .get(0); - // supplier for path + // retrieve prefix as metadata from repository MetadataRequest request = new MetadataRequest(new DefaultMetadata(PREFIX_FILE_PATH, Metadata.Nature.RELEASE_OR_SNAPSHOT)); - // use unique repository; this will result in prefix (repository metadata) cached under unique id request.setRepository(prepared); request.setDeleteLocalCopyIfMissing(true); request.setFavorLocalRepository(true); @@ -335,24 +363,24 @@ private PrefixesFilter(RepositorySystemSession session, Path basedir) { @Override public Result acceptArtifact(RemoteRepository remoteRepository, Artifact artifact) { - RemoteRepository repository = normalizeRemoteRepository(session, remoteRepository); - RepositoryLayout repositoryLayout = cacheLayout(session, repository); + RepositoryLayout repositoryLayout = cacheLayout(session, remoteRepository); if (repositoryLayout == null) { - return new SimpleResult(true, "Unsupported layout: " + repository); + return new SimpleResult(true, "Unsupported layout: " + remoteRepository); } return acceptPrefix( - repository, repositoryLayout.getLocation(artifact, false).getPath()); + remoteRepository, + repositoryLayout.getLocation(artifact, false).getPath()); } @Override public Result acceptMetadata(RemoteRepository remoteRepository, Metadata metadata) { - RemoteRepository repository = normalizeRemoteRepository(session, remoteRepository); - RepositoryLayout repositoryLayout = cacheLayout(session, repository); + RepositoryLayout repositoryLayout = cacheLayout(session, remoteRepository); if (repositoryLayout == null) { - return new SimpleResult(true, "Unsupported layout: " + repository); + return new SimpleResult(true, "Unsupported layout: " + remoteRepository); } return acceptPrefix( - repository, repositoryLayout.getLocation(metadata, false).getPath()); + remoteRepository, + repositoryLayout.getLocation(metadata, false).getPath()); } private Result acceptPrefix(RemoteRepository repository, String path) { diff --git a/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/filter/PrefixesRemoteRepositoryFilterSourceTest.java b/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/filter/PrefixesRemoteRepositoryFilterSourceTest.java index 4db77039b..7d7c21782 100644 --- a/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/filter/PrefixesRemoteRepositoryFilterSourceTest.java +++ b/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/filter/PrefixesRemoteRepositoryFilterSourceTest.java @@ -25,6 +25,7 @@ import java.nio.file.Path; import java.util.Collection; import java.util.Collections; +import java.util.List; import org.eclipse.aether.DefaultRepositorySystemSession; import org.eclipse.aether.RepositorySystemSession; @@ -35,11 +36,17 @@ import org.eclipse.aether.internal.impl.DefaultRepositoryLayoutProvider; import org.eclipse.aether.internal.impl.Maven2RepositoryLayoutFactory; import org.eclipse.aether.repository.RemoteRepository; +import org.eclipse.aether.repository.RepositoryPolicy; import org.eclipse.aether.resolution.MetadataRequest; import org.eclipse.aether.resolution.MetadataResult; +import org.eclipse.aether.spi.connector.filter.RemoteRepositoryFilter; import org.eclipse.aether.spi.connector.filter.RemoteRepositoryFilterSource; +import org.junit.jupiter.api.Test; import static org.eclipse.aether.internal.impl.checksum.Checksums.checksumsSelector; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @@ -54,9 +61,24 @@ protected RemoteRepositoryFilterSource getRemoteRepositoryFilterSource( // in test we do not resolve; just reply failed resolution MetadataResult failed = new MetadataResult(new MetadataRequest()); MetadataResolver metadataResolver = mock(MetadataResolver.class); - RemoteRepositoryManager remoteRepositoryManager = mock(RemoteRepositoryManager.class); + RemoteRepositoryManager remoteRepositoryManager = new RemoteRepositoryManager() { + @Override + public List aggregateRepositories( + RepositorySystemSession session, + List dominantRepositories, + List recessiveRepositories, + boolean recessiveIsRaw) { + return recessiveRepositories; + } + + @Override + public RepositoryPolicy getPolicy( + RepositorySystemSession session, RemoteRepository repository, boolean releases, boolean snapshots) { + throw new UnsupportedOperationException("not implemented"); + } + }; when(metadataResolver.resolveMetadata(any(RepositorySystemSession.class), any(Collection.class))) - .thenReturn(Collections.singletonList(failed)); + .thenThrow(new IllegalStateException("should not enter here")); DefaultRepositoryLayoutProvider layoutProvider = new DefaultRepositoryLayoutProvider(Collections.singletonMap( Maven2RepositoryLayoutFactory.NAME, new Maven2RepositoryLayoutFactory( @@ -91,4 +113,20 @@ protected void allowArtifact( throw new UncheckedIOException(e); } } + + @Test + void notAcceptedArtifactFromMirror() { + RemoteRepository mirror = new RemoteRepository.Builder("mirror", "default", "https://irrelevant.com") + .addMirroredRepository(remoteRepository) + .build(); + enableSource(session, true); + + RemoteRepositoryFilter filter = subject.getRemoteRepositoryFilter(session); + assertNotNull(filter); + + RemoteRepositoryFilter.Result result = filter.acceptArtifact(mirror, acceptedArtifact); + + assertTrue(result.isAccepted()); + assertEquals("Prefix file not present", result.reasoning()); + } } diff --git a/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/filter/RemoteRepositoryFilterSourceTestSupport.java b/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/filter/RemoteRepositoryFilterSourceTestSupport.java index 9a149585f..c364aac3b 100644 --- a/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/filter/RemoteRepositoryFilterSourceTestSupport.java +++ b/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/filter/RemoteRepositoryFilterSourceTestSupport.java @@ -34,15 +34,15 @@ * UT helper for {@link RemoteRepositoryFilterSource} UTs. */ public abstract class RemoteRepositoryFilterSourceTestSupport { - private final Artifact acceptedArtifact = new DefaultArtifact("org.one:aid:1.0"); + protected final Artifact acceptedArtifact = new DefaultArtifact("org.one:aid:1.0"); - private final Artifact notAcceptedArtifact = new DefaultArtifact("org.two:aid:1.0"); + protected final Artifact notAcceptedArtifact = new DefaultArtifact("org.two:aid:1.0"); - private DefaultRepositorySystemSession session; + protected DefaultRepositorySystemSession session; - private RemoteRepository remoteRepository; + protected RemoteRepository remoteRepository; - private RemoteRepositoryFilterSource subject; + protected RemoteRepositoryFilterSource subject; @BeforeEach void setup() { diff --git a/src/site/markdown/configuration.md b/src/site/markdown/configuration.md index 52ae97cfb..c07d5ac76 100644 --- a/src/site/markdown/configuration.md +++ b/src/site/markdown/configuration.md @@ -105,6 +105,7 @@ To modify this file, edit the template and regenerate. | `"aether.remoteRepositoryFilter.prefixes"` | `Boolean` | Configuration to enable the Prefixes filter (enabled by default). Can be fine-tuned per repository using repository ID suffixes. Important: For this filter to take effect, configuration files must be available. Without configuration files, the enabled filter remains dormant and does not interfere with resolution. Configuration File Resolution:
  1. User-provided files: Checked first from directory specified by #CONFIG_PROP_BASEDIR (defaults to $LOCAL_REPO/.remoteRepositoryFilters )
  2. Auto-discovery: If not found, attempts to download from remote repository and cache locally
File Naming: prefixes-$(repository.id).txt Recommended Setup (Auto-Discovery with Override Capability): Start with auto-discovery, but prepare for project-specific overrides. Add to .mvn/maven.config :
 -Daether.remoteRepositoryFilter.prefixes=true -Daether.remoteRepositoryFilter.prefixes.basedir=${session.rootDirectory}/.mvn/rrf/ 
Initial setup: Don't provide any files - rely on auto-discovery as repositories are accessed. Override when needed: Create prefixes-myrepoId.txt files in .mvn/rrf/ and commit to version control. Caching: Auto-discovered prefix files are cached in the local repository. | `true` | 1.9.0 | Yes | Session Configuration | | `"aether.remoteRepositoryFilter.prefixes.basedir"` | `String` | The basedir where to store filter files. If path is relative, it is resolved from local repository root. | `".remoteRepositoryFilters"` | 1.9.0 | No | Session Configuration | | `"aether.remoteRepositoryFilter.prefixes.skipped"` | `Boolean` | Configuration to skip the Prefixes filter for given request. This configuration is evaluated and if true the prefixes remote filter will not kick in. Main use case is by filter itself, to prevent recursion during discovery of remote prefixes file, but this also allows other components to control prefix filter discovery, while leaving configuration like #CONFIG_PROP_ENABLED still show the "real state". | `false` | 2.0.14 | Yes | Session Configuration | +| `"aether.remoteRepositoryFilter.prefixes.useMirroredRepositories"` | `Boolean` | Configuration to allow Prefixes filter to auto-discover prefixes from mirrored repositories as well. For this to work Maven should be aware that given remote repository is mirror and is usually backed by MRM. Given multiple MRM implementations messes up prefixes file, is better to just skip these. In other case, one may use #CONFIG_PROP_ENABLED with repository ID suffix. | `false` | 2.0.14 | Yes | Session Configuration | | `"aether.snapshotFilter"` | `Boolean` | The key in the repository session's RepositorySystemSession#getConfigProperties() configurationproperties used to store a Boolean flag whether this filter should be forced to ban snapshots. By default, snapshots are only filtered if the root artifact is not a snapshot. | `false` | | No | Session Configuration | | `"aether.syncContext.named.basedir.locksDir"` | `String` | The location of the directory toi use for locks. If relative path, it is resolved from the local repository root. | `".locks"` | 1.9.0 | No | Session Configuration | | `"aether.syncContext.named.discriminating.discriminator"` | `String` | Configuration property to pass in discriminator, if needed. If not present, it is auto-calculated. | - | 1.7.0 | No | Session Configuration | From 2726c727a89df1e746c65297971343959fe124a0 Mon Sep 17 00:00:00 2001 From: Tamas Cservenak Date: Mon, 17 Nov 2025 15:15:36 +0100 Subject: [PATCH 10/13] More and TODO --- .../impl/filter/PrefixesRemoteRepositoryFilterSource.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/filter/PrefixesRemoteRepositoryFilterSource.java b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/filter/PrefixesRemoteRepositoryFilterSource.java index f1c7fbfed..6470dcee5 100644 --- a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/filter/PrefixesRemoteRepositoryFilterSource.java +++ b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/filter/PrefixesRemoteRepositoryFilterSource.java @@ -263,7 +263,7 @@ private PrefixTree loadPrefixTree( String origin = "user-provided"; Path filePath = resolvePrefixesFromLocalConfiguration(session, baseDir, remoteRepository); if (filePath == null) { - if (!shouldAttemptResolvePrefixesForRemoteRepository(session, remoteRepository)) { + if (!supportedResolvePrefixesForRemoteRepository(session, remoteRepository)) { origin = "unsupported"; } else { origin = "auto-discovered"; @@ -315,8 +315,9 @@ private Path resolvePrefixesFromLocalConfiguration( } } - private boolean shouldAttemptResolvePrefixesForRemoteRepository( + private boolean supportedResolvePrefixesForRemoteRepository( RepositorySystemSession session, RemoteRepository remoteRepository) { + // TODO: RemoteRepository.isRepositoryManager() is still unused in Maven; once used, factor it in return remoteRepository.getMirroredRepositories().isEmpty() || ConfigUtils.getBoolean( session, DEFAULT_USE_MIRRORED_REPOSITORIES, CONFIG_PROP_USE_MIRRORED_REPOSITORIES); From 7eae084108adbe6fad3fe88e667d54698e1d3935 Mon Sep 17 00:00:00 2001 From: Tamas Cservenak Date: Mon, 17 Nov 2025 17:03:31 +0100 Subject: [PATCH 11/13] Normalize in both Whenever RemoteRepository is used as a key in map, it has to be normalized, as otherwise things may fall apart. --- .../GroupIdRemoteRepositoryFilterSource.java | 10 ++++---- .../PrefixesRemoteRepositoryFilterSource.java | 24 ++++--------------- .../RemoteRepositoryFilterSourceSupport.java | 18 ++++++++++++++ 3 files changed, 28 insertions(+), 24 deletions(-) diff --git a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/filter/GroupIdRemoteRepositoryFilterSource.java b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/filter/GroupIdRemoteRepositoryFilterSource.java index 963dcc642..844616bcf 100644 --- a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/filter/GroupIdRemoteRepositoryFilterSource.java +++ b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/filter/GroupIdRemoteRepositoryFilterSource.java @@ -215,10 +215,11 @@ public void postProcess(RepositorySystemSession session, List ar if (isRepositoryFilteringEnabled(session, remoteRepository)) { ruleFile(session, remoteRepository); // populate it; needed for save String line = "=" + artifactResult.getArtifact().getGroupId(); + RemoteRepository normalized = normalizeRemoteRepository(session, remoteRepository); recordedRules - .computeIfAbsent(remoteRepository, k -> new TreeSet<>()) + .computeIfAbsent(normalized, k -> new TreeSet<>()) .add(line); - rules.compute(remoteRepository, (k, v) -> { + rules.compute(normalized, (k, v) -> { if (v == null || v == GroupTree.SENTINEL) { v = new GroupTree(""); } @@ -232,7 +233,7 @@ public void postProcess(RepositorySystemSession session, List ar } private Path ruleFile(RepositorySystemSession session, RemoteRepository remoteRepository) { - return ruleFiles.computeIfAbsent(remoteRepository, r -> getBasedir( + return ruleFiles.computeIfAbsent(normalizeRemoteRepository(session, remoteRepository), r -> getBasedir( session, LOCAL_REPO_PREFIX_DIR, CONFIG_PROP_BASEDIR, false) .resolve(GROUP_ID_FILE_PREFIX + RepositoryIdHelper.cachedIdToPathSegment(session).apply(remoteRepository) @@ -240,7 +241,8 @@ private Path ruleFile(RepositorySystemSession session, RemoteRepository remoteRe } private GroupTree cacheRules(RepositorySystemSession session, RemoteRepository remoteRepository) { - return rules.computeIfAbsent(remoteRepository, r -> loadRepositoryRules(session, r)); + return rules.computeIfAbsent( + normalizeRemoteRepository(session, remoteRepository), r -> loadRepositoryRules(session, r)); } private GroupTree loadRepositoryRules(RepositorySystemSession session, RemoteRepository remoteRepository) { diff --git a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/filter/PrefixesRemoteRepositoryFilterSource.java b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/filter/PrefixesRemoteRepositoryFilterSource.java index 6470dcee5..e79792183 100644 --- a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/filter/PrefixesRemoteRepositoryFilterSource.java +++ b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/filter/PrefixesRemoteRepositoryFilterSource.java @@ -25,7 +25,6 @@ import java.nio.file.Files; import java.nio.file.Path; import java.util.Collections; -import java.util.List; import java.util.concurrent.ConcurrentHashMap; import java.util.function.Supplier; @@ -220,29 +219,13 @@ public RemoteRepositoryFilter getRemoteRepositoryFilter(RepositorySystemSession return null; } - /** - * We use remote repositories as keys, but they may fly in as "bare" or as "equipped" (w/ auth and proxy) if caller - * used {@link org.eclipse.aether.RepositorySystem#newResolutionRepositories(RepositorySystemSession, List)} beforehand. - * The hash/equalTo method factors in all these as well, but from our perspective, they do not matter. So we make all - * key remote repositories back to "bare". - */ - private RemoteRepository normalizeRemoteRepository( - RepositorySystemSession session, RemoteRepository remoteRepository) { - return new RemoteRepository.Builder(remoteRepository) - .setProxy(null) - .setAuthentication(null) - .setMirroredRepositories(null) - .setRepositoryManager(false) - .build(); - } - /** * Caches layout instances for remote repository. In case of unknown layout it returns {@code null}. * * @return the layout instance of {@code null} if layout not supported. */ private RepositoryLayout cacheLayout(RepositorySystemSession session, RemoteRepository remoteRepository) { - return layouts.computeIfAbsent(remoteRepository, r -> { + return layouts.computeIfAbsent(normalizeRemoteRepository(session, remoteRepository), r -> { try { return repositoryLayoutProvider.newRepositoryLayout(session, remoteRepository); } catch (NoRepositoryLayoutException e) { @@ -253,8 +236,9 @@ private RepositoryLayout cacheLayout(RepositorySystemSession session, RemoteRepo private PrefixTree cachePrefixTree( RepositorySystemSession session, Path basedir, RemoteRepository remoteRepository) { - RemoteRepository normalized = normalizeRemoteRepository(session, remoteRepository); - return prefixes.computeIfAbsent(normalized, r -> loadPrefixTree(session, basedir, remoteRepository)); + return prefixes.computeIfAbsent( + normalizeRemoteRepository(session, remoteRepository), + r -> loadPrefixTree(session, basedir, remoteRepository)); } private PrefixTree loadPrefixTree( diff --git a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/filter/RemoteRepositoryFilterSourceSupport.java b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/filter/RemoteRepositoryFilterSourceSupport.java index 5d1744920..5507dc6cf 100644 --- a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/filter/RemoteRepositoryFilterSourceSupport.java +++ b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/filter/RemoteRepositoryFilterSourceSupport.java @@ -21,9 +21,11 @@ import java.io.IOException; import java.io.UncheckedIOException; import java.nio.file.Path; +import java.util.List; import org.eclipse.aether.ConfigurationProperties; import org.eclipse.aether.RepositorySystemSession; +import org.eclipse.aether.repository.RemoteRepository; import org.eclipse.aether.spi.connector.filter.RemoteRepositoryFilter; import org.eclipse.aether.spi.connector.filter.RemoteRepositoryFilterSource; import org.eclipse.aether.util.DirectoryUtils; @@ -76,6 +78,22 @@ protected Path getBasedir( } } + /** + * We use remote repositories as keys, but they may fly in as "bare" or as "equipped" (w/ auth and proxy) if caller + * used {@link org.eclipse.aether.RepositorySystem#newResolutionRepositories(RepositorySystemSession, List)} beforehand. + * The hash/equalTo method factors in all these as well, but from our perspective, they do not matter. So we make all + * key remote repositories back to "bare". + */ + protected RemoteRepository normalizeRemoteRepository( + RepositorySystemSession session, RemoteRepository remoteRepository) { + return new RemoteRepository.Builder(remoteRepository) + .setProxy(null) + .setAuthentication(null) + .setMirroredRepositories(null) + .setRepositoryManager(false) + .build(); + } + /** * Simple {@link RemoteRepositoryFilter.Result} immutable implementation. */ From 490b1ca8da958d41c82a24c304ecdbe26fbaa780 Mon Sep 17 00:00:00 2001 From: Tamas Cservenak Date: Wed, 19 Nov 2025 12:33:58 +0100 Subject: [PATCH 12/13] Work out the TODO --- .../PrefixesRemoteRepositoryFilterSource.java | 30 ++++++++++++++++--- src/site/markdown/configuration.md | 1 + 2 files changed, 27 insertions(+), 4 deletions(-) diff --git a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/filter/PrefixesRemoteRepositoryFilterSource.java b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/filter/PrefixesRemoteRepositoryFilterSource.java index e79792183..806cfef96 100644 --- a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/filter/PrefixesRemoteRepositoryFilterSource.java +++ b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/filter/PrefixesRemoteRepositoryFilterSource.java @@ -150,6 +150,24 @@ public final class PrefixesRemoteRepositoryFilterSource extends RemoteRepository public static final boolean DEFAULT_USE_MIRRORED_REPOSITORIES = false; + /** + * Configuration to allow Prefixes filter to auto-discover prefixes from repository managers as well. For this to + * work Maven should be aware that given remote repository is backed by repository manager. + * Given multiple MRM implementations messes up prefixes file, is better to just skip these. In other case, one may use + * {@link #CONFIG_PROP_ENABLED} with repository ID suffix. + * Note: as of today, nothing sets this on remote repositories, but is added for future. + * + * @since 2.0.14 + * @configurationSource {@link RepositorySystemSession#getConfigProperties()} + * @configurationType {@link java.lang.Boolean} + * @configurationRepoIdSuffix Yes + * @configurationDefaultValue {@link #DEFAULT_USE_REPOSITORY_MANAGERS} + */ + public static final String CONFIG_PROP_USE_REPOSITORY_MANAGERS = + RemoteRepositoryFilterSourceSupport.CONFIG_PROPS_PREFIX + NAME + ".useRepositoryManagers"; + + public static final boolean DEFAULT_USE_REPOSITORY_MANAGERS = false; + /** * The basedir where to store filter files. If path is relative, it is resolved from local repository root. * @@ -301,10 +319,14 @@ private Path resolvePrefixesFromLocalConfiguration( private boolean supportedResolvePrefixesForRemoteRepository( RepositorySystemSession session, RemoteRepository remoteRepository) { - // TODO: RemoteRepository.isRepositoryManager() is still unused in Maven; once used, factor it in - return remoteRepository.getMirroredRepositories().isEmpty() - || ConfigUtils.getBoolean( - session, DEFAULT_USE_MIRRORED_REPOSITORIES, CONFIG_PROP_USE_MIRRORED_REPOSITORIES); + if (remoteRepository.isRepositoryManager()) { + return ConfigUtils.getBoolean( + session, DEFAULT_USE_REPOSITORY_MANAGERS, CONFIG_PROP_USE_REPOSITORY_MANAGERS); + } else { + return remoteRepository.getMirroredRepositories().isEmpty() + || ConfigUtils.getBoolean( + session, DEFAULT_USE_MIRRORED_REPOSITORIES, CONFIG_PROP_USE_MIRRORED_REPOSITORIES); + } } private Path resolvePrefixesFromRemoteRepository( diff --git a/src/site/markdown/configuration.md b/src/site/markdown/configuration.md index c07d5ac76..7e1d84040 100644 --- a/src/site/markdown/configuration.md +++ b/src/site/markdown/configuration.md @@ -106,6 +106,7 @@ To modify this file, edit the template and regenerate. | `"aether.remoteRepositoryFilter.prefixes.basedir"` | `String` | The basedir where to store filter files. If path is relative, it is resolved from local repository root. | `".remoteRepositoryFilters"` | 1.9.0 | No | Session Configuration | | `"aether.remoteRepositoryFilter.prefixes.skipped"` | `Boolean` | Configuration to skip the Prefixes filter for given request. This configuration is evaluated and if true the prefixes remote filter will not kick in. Main use case is by filter itself, to prevent recursion during discovery of remote prefixes file, but this also allows other components to control prefix filter discovery, while leaving configuration like #CONFIG_PROP_ENABLED still show the "real state". | `false` | 2.0.14 | Yes | Session Configuration | | `"aether.remoteRepositoryFilter.prefixes.useMirroredRepositories"` | `Boolean` | Configuration to allow Prefixes filter to auto-discover prefixes from mirrored repositories as well. For this to work Maven should be aware that given remote repository is mirror and is usually backed by MRM. Given multiple MRM implementations messes up prefixes file, is better to just skip these. In other case, one may use #CONFIG_PROP_ENABLED with repository ID suffix. | `false` | 2.0.14 | Yes | Session Configuration | +| `"aether.remoteRepositoryFilter.prefixes.useRepositoryManagers"` | `Boolean` | Configuration to allow Prefixes filter to auto-discover prefixes from repository managers as well. For this to work Maven should be aware that given remote repository is backed by repository manager. Given multiple MRM implementations messes up prefixes file, is better to just skip these. In other case, one may use #CONFIG_PROP_ENABLED with repository ID suffix. Note: as of today, nothing sets this on remote repositories, but is added for future. | `false` | 2.0.14 | Yes | Session Configuration | | `"aether.snapshotFilter"` | `Boolean` | The key in the repository session's RepositorySystemSession#getConfigProperties() configurationproperties used to store a Boolean flag whether this filter should be forced to ban snapshots. By default, snapshots are only filtered if the root artifact is not a snapshot. | `false` | | No | Session Configuration | | `"aether.syncContext.named.basedir.locksDir"` | `String` | The location of the directory toi use for locks. If relative path, it is resolved from the local repository root. | `".locks"` | 1.9.0 | No | Session Configuration | | `"aether.syncContext.named.discriminating.discriminator"` | `String` | Configuration property to pass in discriminator, if needed. If not present, it is auto-calculated. | - | 1.7.0 | No | Session Configuration | From 353751b945c66b4d2872ce3fd915ccf43a2a321d Mon Sep 17 00:00:00 2001 From: Tamas Cservenak Date: Thu, 20 Nov 2025 12:51:07 +0100 Subject: [PATCH 13/13] Add more detailed javadoc --- .../impl/filter/RemoteRepositoryFilterSourceSupport.java | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/filter/RemoteRepositoryFilterSourceSupport.java b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/filter/RemoteRepositoryFilterSourceSupport.java index 5507dc6cf..bb4c1e3ff 100644 --- a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/filter/RemoteRepositoryFilterSourceSupport.java +++ b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/filter/RemoteRepositoryFilterSourceSupport.java @@ -83,6 +83,13 @@ protected Path getBasedir( * used {@link org.eclipse.aether.RepositorySystem#newResolutionRepositories(RepositorySystemSession, List)} beforehand. * The hash/equalTo method factors in all these as well, but from our perspective, they do not matter. So we make all * key remote repositories back to "bare". + * Ignored properties of normalized repositories: + *
    + *
  • proxy - is environment dependent
  • + *
  • authentication - is environment and/or user dependent
  • + *
  • mirrored repositories - is environment dependent (within same session does not change)
  • + *
  • repository manager - is environment dependent (within same session does not change)
  • + *
*/ protected RemoteRepository normalizeRemoteRepository( RepositorySystemSession session, RemoteRepository remoteRepository) {