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 79b6d5f2e Repository Key Function SPI (#1679)
79b6d5f2e is described below

commit 79b6d5f2ee1472a4aaf3224b7670f3c55b8dace2
Author: Tamas Cservenak <[email protected]>
AuthorDate: Mon Dec 1 17:31:39 2025 +0100

    Repository Key Function SPI (#1679)
    
    New opt-in **experimental** feature for enhanced local repository: ability 
to choose "repository key" function. Local repository (simple and enhanced) by 
default uses the `simple` key (historically) where other places all used `nid`. 
Have to note, that `simple` is technically equivalent to `nid` as 
`RemoteRepository.isRepositoryManager()` is not set anywhere in Maven.
    
    Added key functions:
    * `simple` -> the original code in LRM
    * `nid` -> `norm(id)` (path friendly)
    * `hurl` -> `sha1(url)`
    * `nid_hurl` -> `norm(id)-sha1(url)`
    * `gurk` -> `norm(id)-sha1(seed)` where "seed" is all config properties of 
repo
    * `ngurk` -> `norm(id)-sha1(seed)` where "seed" is all config sans mirror 
list (just the string "isMirrored" is added for mirrors)
    
    Introduced `RepositoryKeyFunction` type, that is used consistently in 
enhanced local repository (availability calculation), prefix composer (split 
repository prefix calculation) and remote repository manager (remote repository 
consolidation). This is new SPI, and impl provides default implementation with 
those above.
---
 .../eclipse/aether/ConfigurationProperties.java    |  31 ++-
 .../eclipse/aether/repository/LocalRepository.java |  18 +-
 .../aether/repository/RepositoryKeyFunction.java   |  40 +++
 .../aether/repository/WorkspaceRepository.java     |  12 +-
 .../maven/resolver/examples/resolver/Resolver.java |   3 +-
 .../resolver/examples/resolver/ResolverDemo.java   |  12 +-
 .../DefaultLocalPathPrefixComposerFactory.java     |  22 +-
 .../impl/DefaultRemoteRepositoryManager.java       |  31 ++-
 .../impl/DefaultRepositoryKeyFunctionFactory.java  |  88 ++++++
 .../impl/EnhancedLocalRepositoryManager.java       |   7 +-
 .../EnhancedLocalRepositoryManagerFactory.java     |  10 +-
 .../LocalPathPrefixComposerFactorySupport.java     |  18 +-
 .../impl/SimpleLocalRepositoryManager.java         |  44 +--
 .../impl/SimpleLocalRepositoryManagerFactory.java  |  14 +-
 .../FileTrustedChecksumsSourceSupport.java         |  44 ++-
 .../SparseDirectoryTrustedChecksumsSource.java     |  14 +-
 .../SummaryFileTrustedChecksumsSource.java         |  16 +-
 .../GroupIdRemoteRepositoryFilterSource.java       |  11 +-
 .../PrefixesRemoteRepositoryFilterSource.java      |   9 +-
 .../RemoteRepositoryFilterSourceSupport.java       |  38 +++
 .../DefaultLocalPathPrefixComposerFactoryTest.java |  12 +-
 .../impl/DefaultRemoteRepositoryManagerTest.java   |   6 +-
 .../internal/impl/DefaultRepositorySystemTest.java |   4 +-
 .../impl/EnhancedLocalRepositoryManagerTest.java   |   7 +-
 .../EnhancedSplitLocalRepositoryManagerTest.java   |   7 +-
 .../impl/SimpleLocalRepositoryManagerTest.java     |   4 +-
 .../SparseDirectoryTrustedChecksumsSourceTest.java |   5 +-
 .../SummaryFileTrustedChecksumsSourceTest.java     |   6 +-
 .../GroupIdRemoteRepositoryFilterSourceTest.java   |   5 +-
 .../PrefixesRemoteRepositoryFilterSourceTest.java  |   6 +-
 .../remoterepo/RepositoryKeyFunctionFactory.java   |  56 ++++
 .../aether/spi/remoterepo/package-info.java        |  23 ++
 .../aether/supplier/RepositorySystemSupplier.java  |  46 ++-
 .../aether/supplier/RepositorySystemSupplier.java  |  46 ++-
 .../aether/util/repository/RepositoryIdHelper.java | 309 ++++++++++++++-------
 .../util/repository/RepositoryIdHelperTest.java    | 110 +++++---
 src/site/markdown/configuration.md                 |   3 +
 src/site/markdown/repository-key-function.md       | 123 ++++++++
 src/site/site.xml                                  |   1 +
 39 files changed, 958 insertions(+), 303 deletions(-)

diff --git 
a/maven-resolver-api/src/main/java/org/eclipse/aether/ConfigurationProperties.java
 
b/maven-resolver-api/src/main/java/org/eclipse/aether/ConfigurationProperties.java
index 327815fdb..3945a7486 100644
--- 
a/maven-resolver-api/src/main/java/org/eclipse/aether/ConfigurationProperties.java
+++ 
b/maven-resolver-api/src/main/java/org/eclipse/aether/ConfigurationProperties.java
@@ -149,6 +149,13 @@ public final class ConfigurationProperties {
      */
     public static final String CACHED_PRIORITIES = PREFIX_PRIORITY + "cached";
 
+    /**
+     * The default caching of priority components if {@link 
#CACHED_PRIORITIES} isn't set. Default value is {@code true}.
+     *
+     * @since 2.0.0
+     */
+    public static final boolean DEFAULT_CACHED_PRIORITIES = true;
+
     /**
      * The priority to use for a certain extension class. {@code 
&lt;class&gt;} can either be the fully qualified
      * name or the simple name of a class. If the class name ends with Factory 
that suffix could optionally be left out.
@@ -171,13 +178,6 @@ public final class ConfigurationProperties {
      */
     public static final String CLASS_PRIORITIES = PREFIX_PRIORITY + "<class>";
 
-    /**
-     * The default caching of priority components if {@link 
#CACHED_PRIORITIES} isn't set. Default value is {@code true}.
-     *
-     * @since 2.0.0
-     */
-    public static final boolean DEFAULT_CACHED_PRIORITIES = true;
-
     /**
      * A flag indicating whether interaction with the user is allowed.
      *
@@ -560,6 +560,23 @@ public final class ConfigurationProperties {
     public static final String DEFAULT_REPOSITORY_SYSTEM_DEPENDENCY_VISITOR =
             REPOSITORY_SYSTEM_DEPENDENCY_VISITOR_LEVELORDER;
 
+    /**
+     * <b>Experimental:</b> Configuration for system-wide "repository key" 
function.
+     * Accepted and recommended values: "nid" (default), "nid_hurl" and 
"ngurk", while "simple" is Maven 3 legacy,
+     * technically equivalent to "nid". For complete description see enum
+     * {@code 
org.eclipse.aether.util.repository.RepositoryIdHelper.RepositoryKeyType} in 
utils. <em>Warning:</em>
+     * repository key function affects Resolver fundamentally and may have 
unexpected results! Only change this
+     * if you know what you are doing!
+     *
+     * @since 2.0.14
+     * @configurationSource {@link 
RepositorySystemSession#getConfigProperties()}
+     * @configurationType {@link java.lang.String}
+     * @configurationDefaultValue {@link 
#DEFAULT_REPOSITORY_SYSTEM_REPOSITORY_KEY_FUNCTION}
+     */
+    public static final String REPOSITORY_SYSTEM_REPOSITORY_KEY_FUNCTION = 
PREFIX_SYSTEM + "repositoryKeyFunction";
+
+    public static final String 
DEFAULT_REPOSITORY_SYSTEM_REPOSITORY_KEY_FUNCTION = "nid";
+
     /**
      * A flag indicating whether version scheme cache statistics should be 
printed on JVM shutdown.
      * This is useful for analyzing cache performance and effectiveness in 
development and testing scenarios.
diff --git 
a/maven-resolver-api/src/main/java/org/eclipse/aether/repository/LocalRepository.java
 
b/maven-resolver-api/src/main/java/org/eclipse/aether/repository/LocalRepository.java
index 44ae964b0..6c586c572 100644
--- 
a/maven-resolver-api/src/main/java/org/eclipse/aether/repository/LocalRepository.java
+++ 
b/maven-resolver-api/src/main/java/org/eclipse/aether/repository/LocalRepository.java
@@ -31,19 +31,25 @@ import java.util.Objects;
  * the repository.
  */
 public final class LocalRepository implements ArtifactRepository {
+    public static final String ID = "local";
 
     private final Path basePath;
 
     private final String type;
 
+    private final int hashCode;
+
     /**
      * Creates a new local repository with the specified base directory and 
unknown type.
      *
      * @param basedir The base directory of the repository, may be {@code 
null}.
+     * @deprecated Use {@link LocalRepository(Path)} instead.
      */
+    @Deprecated
     public LocalRepository(String basedir) {
         this.basePath = 
Paths.get(RepositoryUriUtils.toUri(basedir)).toAbsolutePath();
         this.type = "";
+        this.hashCode = Objects.hash(this.basePath, this.type);
     }
 
     /**
@@ -99,6 +105,7 @@ public final class LocalRepository implements 
ArtifactRepository {
     public LocalRepository(Path basePath, String type) {
         this.basePath = basePath;
         this.type = (type != null) ? type : "";
+        this.hashCode = Objects.hash(this.basePath, this.type);
     }
 
     @Override
@@ -108,7 +115,7 @@ public final class LocalRepository implements 
ArtifactRepository {
 
     @Override
     public String getId() {
-        return "local";
+        return ID;
     }
 
     /**
@@ -153,13 +160,6 @@ public final class LocalRepository implements 
ArtifactRepository {
 
     @Override
     public int hashCode() {
-        int hash = 17;
-        hash = hash * 31 + hash(basePath);
-        hash = hash * 31 + hash(type);
-        return hash;
-    }
-
-    private static int hash(Object obj) {
-        return obj != null ? obj.hashCode() : 0;
+        return hashCode;
     }
 }
diff --git 
a/maven-resolver-api/src/main/java/org/eclipse/aether/repository/RepositoryKeyFunction.java
 
b/maven-resolver-api/src/main/java/org/eclipse/aether/repository/RepositoryKeyFunction.java
new file mode 100644
index 000000000..e44f7cdc9
--- /dev/null
+++ 
b/maven-resolver-api/src/main/java/org/eclipse/aether/repository/RepositoryKeyFunction.java
@@ -0,0 +1,40 @@
+/*
+ * 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.repository;
+
+import java.util.function.BiFunction;
+
+/**
+ * The repository key function, it produces keys (strings) for given {@link 
RemoteRepository} instances.
+ *
+ * @since 2.0.14
+ */
+@FunctionalInterface
+public interface RepositoryKeyFunction extends BiFunction<RemoteRepository, 
String, String> {
+    /**
+     * Produces a string representing "repository key" for given {@link 
RemoteRepository} and
+     * optionally (maybe {@code null}) "context".
+     *
+     * @param repository The {@link RemoteRepository}, may not be {@code null}.
+     * @param context    The "context" string, or {@code null}.
+     * @return The "repository key" string, never {@code null}.
+     */
+    @Override
+    String apply(RemoteRepository repository, String context);
+}
diff --git 
a/maven-resolver-api/src/main/java/org/eclipse/aether/repository/WorkspaceRepository.java
 
b/maven-resolver-api/src/main/java/org/eclipse/aether/repository/WorkspaceRepository.java
index e128af209..9b0b44021 100644
--- 
a/maven-resolver-api/src/main/java/org/eclipse/aether/repository/WorkspaceRepository.java
+++ 
b/maven-resolver-api/src/main/java/org/eclipse/aether/repository/WorkspaceRepository.java
@@ -18,6 +18,7 @@
  */
 package org.eclipse.aether.repository;
 
+import java.util.Objects;
 import java.util.UUID;
 
 /**
@@ -27,11 +28,14 @@ import java.util.UUID;
  * the contained artifacts is handled by a {@link WorkspaceReader}.
  */
 public final class WorkspaceRepository implements ArtifactRepository {
+    public static final String ID = "workspace";
 
     private final String type;
 
     private final Object key;
 
+    private final int hashCode;
+
     /**
      * Creates a new workspace repository of type {@code "workspace"} and a 
random key.
      */
@@ -58,6 +62,7 @@ public final class WorkspaceRepository implements 
ArtifactRepository {
     public WorkspaceRepository(String type, Object key) {
         this.type = (type != null) ? type : "";
         this.key = (key != null) ? key : 
UUID.randomUUID().toString().replace("-", "");
+        this.hashCode = Objects.hash(type, key);
     }
 
     public String getContentType() {
@@ -65,7 +70,7 @@ public final class WorkspaceRepository implements 
ArtifactRepository {
     }
 
     public String getId() {
-        return "workspace";
+        return ID;
     }
 
     /**
@@ -99,9 +104,6 @@ public final class WorkspaceRepository implements 
ArtifactRepository {
 
     @Override
     public int hashCode() {
-        int hash = 17;
-        hash = hash * 31 + getKey().hashCode();
-        hash = hash * 31 + getContentType().hashCode();
-        return hash;
+        return hashCode;
     }
 }
diff --git 
a/maven-resolver-demos/maven-resolver-demo-snippets/src/main/java/org/apache/maven/resolver/examples/resolver/Resolver.java
 
b/maven-resolver-demos/maven-resolver-demo-snippets/src/main/java/org/apache/maven/resolver/examples/resolver/Resolver.java
index b44f4011b..8b5e308f7 100644
--- 
a/maven-resolver-demos/maven-resolver-demo-snippets/src/main/java/org/apache/maven/resolver/examples/resolver/Resolver.java
+++ 
b/maven-resolver-demos/maven-resolver-demo-snippets/src/main/java/org/apache/maven/resolver/examples/resolver/Resolver.java
@@ -21,6 +21,7 @@ package org.apache.maven.resolver.examples.resolver;
 import java.io.ByteArrayOutputStream;
 import java.io.PrintStream;
 import java.nio.charset.StandardCharsets;
+import java.nio.file.Path;
 
 import org.apache.maven.resolver.examples.util.Booter;
 import org.eclipse.aether.RepositorySystem;
@@ -55,7 +56,7 @@ public class Resolver {
 
     private final LocalRepository localRepository;
 
-    public Resolver(String[] args, String remoteRepository, String 
localRepository) {
+    public Resolver(String[] args, String remoteRepository, Path 
localRepository) {
         this.args = args;
         this.remoteRepository = remoteRepository;
         this.repositorySystem = 
Booter.newRepositorySystem(Booter.selectFactory(args));
diff --git 
a/maven-resolver-demos/maven-resolver-demo-snippets/src/main/java/org/apache/maven/resolver/examples/resolver/ResolverDemo.java
 
b/maven-resolver-demos/maven-resolver-demo-snippets/src/main/java/org/apache/maven/resolver/examples/resolver/ResolverDemo.java
index 7c0e70a46..eb8193bbd 100644
--- 
a/maven-resolver-demos/maven-resolver-demo-snippets/src/main/java/org/apache/maven/resolver/examples/resolver/ResolverDemo.java
+++ 
b/maven-resolver-demos/maven-resolver-demo-snippets/src/main/java/org/apache/maven/resolver/examples/resolver/ResolverDemo.java
@@ -19,6 +19,7 @@
 package org.apache.maven.resolver.examples.resolver;
 
 import java.io.File;
+import java.nio.file.Paths;
 import java.util.List;
 
 import org.eclipse.aether.artifact.Artifact;
@@ -37,7 +38,8 @@ public class ResolverDemo {
         
System.out.println("------------------------------------------------------------");
         System.out.println(ResolverDemo.class.getSimpleName());
 
-        Resolver resolver = new Resolver(args, 
"https://repo.maven.apache.org/maven2/";, "target/resolver-demo-repo");
+        Resolver resolver =
+                new Resolver(args, "https://repo.maven.apache.org/maven2/";, 
Paths.get("target/resolver-demo-repo"));
         ResolverResult result = resolver.resolve("junit", "junit", "4.13.2");
 
         System.out.println("Result:");
@@ -47,8 +49,8 @@ public class ResolverDemo {
     }
 
     public void resolve(String[] args) throws DependencyResolutionException {
-        Resolver resolver =
-                new Resolver(args, 
"http://localhost:8081/nexus/content/groups/public";, "target/aether-repo");
+        Resolver resolver = new Resolver(
+                args, "http://localhost:8081/nexus/content/groups/public";, 
Paths.get("target/aether-repo"));
 
         ResolverResult result = resolver.resolve("com.mycompany.app", 
"super-app", "1.0");
 
@@ -66,8 +68,8 @@ public class ResolverDemo {
     }
 
     public void installAndDeploy(String[] args) throws InstallationException, 
DeploymentException {
-        Resolver resolver =
-                new Resolver(args, 
"http://localhost:8081/nexus/content/groups/public";, "target/aether-repo");
+        Resolver resolver = new Resolver(
+                args, "http://localhost:8081/nexus/content/groups/public";, 
Paths.get("target/aether-repo"));
 
         Artifact artifact = new DefaultArtifact("com.mycompany.super", 
"super-core", "jar", "0.1-SNAPSHOT");
         artifact = artifact.setFile(new File("jar-from-whatever-process.jar"));
diff --git 
a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/DefaultLocalPathPrefixComposerFactory.java
 
b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/DefaultLocalPathPrefixComposerFactory.java
index 2a18ce049..022400667 100644
--- 
a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/DefaultLocalPathPrefixComposerFactory.java
+++ 
b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/DefaultLocalPathPrefixComposerFactory.java
@@ -18,14 +18,15 @@
  */
 package org.eclipse.aether.internal.impl;
 
+import javax.inject.Inject;
 import javax.inject.Named;
 import javax.inject.Singleton;
 
-import java.util.function.Function;
-
 import org.eclipse.aether.RepositorySystemSession;
-import org.eclipse.aether.repository.ArtifactRepository;
-import org.eclipse.aether.util.repository.RepositoryIdHelper;
+import org.eclipse.aether.repository.RepositoryKeyFunction;
+import org.eclipse.aether.spi.remoterepo.RepositoryKeyFunctionFactory;
+
+import static java.util.Objects.requireNonNull;
 
 /**
  * Default local path prefix composer factory: it fully reuses {@link 
LocalPathPrefixComposerFactorySupport} class
@@ -36,6 +37,13 @@ import org.eclipse.aether.util.repository.RepositoryIdHelper;
 @Singleton
 @Named
 public final class DefaultLocalPathPrefixComposerFactory extends 
LocalPathPrefixComposerFactorySupport {
+    private final RepositoryKeyFunctionFactory repositoryKeyFunctionFactory;
+
+    @Inject
+    public DefaultLocalPathPrefixComposerFactory(RepositoryKeyFunctionFactory 
repositoryKeyFunctionFactory) {
+        this.repositoryKeyFunctionFactory = 
requireNonNull(repositoryKeyFunctionFactory);
+    }
+
     @Override
     public LocalPathPrefixComposer createComposer(RepositorySystemSession 
session) {
         return new DefaultLocalPathPrefixComposer(
@@ -48,7 +56,7 @@ public final class DefaultLocalPathPrefixComposerFactory 
extends LocalPathPrefix
                 isSplitRemoteRepositoryLast(session),
                 getReleasesPrefix(session),
                 getSnapshotsPrefix(session),
-                RepositoryIdHelper.cachedIdToPathSegment(session));
+                
repositoryKeyFunctionFactory.systemRepositoryKeyFunction(session));
     }
 
     /**
@@ -66,7 +74,7 @@ public final class DefaultLocalPathPrefixComposerFactory 
extends LocalPathPrefix
                 boolean splitRemoteRepositoryLast,
                 String releasesPrefix,
                 String snapshotsPrefix,
-                Function<ArtifactRepository, String> idToPathSegmentFunction) {
+                RepositoryKeyFunction repositoryKeyFunction) {
             super(
                     split,
                     localPrefix,
@@ -77,7 +85,7 @@ public final class DefaultLocalPathPrefixComposerFactory 
extends LocalPathPrefix
                     splitRemoteRepositoryLast,
                     releasesPrefix,
                     snapshotsPrefix,
-                    idToPathSegmentFunction);
+                    repositoryKeyFunction);
         }
     }
 }
diff --git 
a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/DefaultRemoteRepositoryManager.java
 
b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/DefaultRemoteRepositoryManager.java
index 3ad72aade..fbe35695a 100644
--- 
a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/DefaultRemoteRepositoryManager.java
+++ 
b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/DefaultRemoteRepositoryManager.java
@@ -38,8 +38,10 @@ import org.eclipse.aether.repository.MirrorSelector;
 import org.eclipse.aether.repository.Proxy;
 import org.eclipse.aether.repository.ProxySelector;
 import org.eclipse.aether.repository.RemoteRepository;
+import org.eclipse.aether.repository.RepositoryKeyFunction;
 import org.eclipse.aether.repository.RepositoryPolicy;
 import org.eclipse.aether.spi.connector.checksum.ChecksumPolicyProvider;
+import org.eclipse.aether.spi.remoterepo.RepositoryKeyFunctionFactory;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -82,11 +84,17 @@ public class DefaultRemoteRepositoryManager implements 
RemoteRepositoryManager {
 
     private final ChecksumPolicyProvider checksumPolicyProvider;
 
+    private final RepositoryKeyFunctionFactory repositoryKeyFunctionFactory;
+
     @Inject
     public DefaultRemoteRepositoryManager(
-            UpdatePolicyAnalyzer updatePolicyAnalyzer, ChecksumPolicyProvider 
checksumPolicyProvider) {
+            UpdatePolicyAnalyzer updatePolicyAnalyzer,
+            ChecksumPolicyProvider checksumPolicyProvider,
+            RepositoryKeyFunctionFactory repositoryKeyFunctionFactory) {
         this.updatePolicyAnalyzer = requireNonNull(updatePolicyAnalyzer, 
"update policy analyzer cannot be null");
         this.checksumPolicyProvider = requireNonNull(checksumPolicyProvider, 
"checksum policy provider cannot be null");
+        this.repositoryKeyFunctionFactory =
+                requireNonNull(repositoryKeyFunctionFactory, "repository key 
function factory cannot be null");
     }
 
     @Override
@@ -102,6 +110,7 @@ public class DefaultRemoteRepositoryManager implements 
RemoteRepositoryManager {
             return dominantRepositories;
         }
 
+        RepositoryKeyFunction repositoryKeyFunction = 
repositoryKeyFunctionFactory.systemRepositoryKeyFunction(session);
         MirrorSelector mirrorSelector = session.getMirrorSelector();
         AuthenticationSelector authSelector = 
session.getAuthenticationSelector();
         ProxySelector proxySelector = session.getProxySelector();
@@ -121,15 +130,16 @@ public class DefaultRemoteRepositoryManager implements 
RemoteRepositoryManager {
                 }
             }
 
-            String key = getKey(repository);
+            String key = repositoryKeyFunction.apply(repository, null);
 
             for (ListIterator<RemoteRepository> it = result.listIterator(); 
it.hasNext(); ) {
                 RemoteRepository dominantRepository = it.next();
 
-                if (key.equals(getKey(dominantRepository))) {
+                if (key.equals(repositoryKeyFunction.apply(dominantRepository, 
null))) {
                     if (!dominantRepository.getMirroredRepositories().isEmpty()
                             && 
!repository.getMirroredRepositories().isEmpty()) {
-                        RemoteRepository mergedRepository = 
mergeMirrors(session, dominantRepository, repository);
+                        RemoteRepository mergedRepository =
+                                mergeMirrors(session, repositoryKeyFunction, 
dominantRepository, repository);
                         if (mergedRepository != dominantRepository) {
                             it.set(mergedRepository);
                         }
@@ -188,21 +198,20 @@ public class DefaultRemoteRepositoryManager implements 
RemoteRepositoryManager {
                 original.getUrl());
     }
 
-    private String getKey(RemoteRepository repository) {
-        return repository.getId();
-    }
-
     private RemoteRepository mergeMirrors(
-            RepositorySystemSession session, RemoteRepository dominant, 
RemoteRepository recessive) {
+            RepositorySystemSession session,
+            RepositoryKeyFunction repositoryKeyFunction,
+            RemoteRepository dominant,
+            RemoteRepository recessive) {
         RemoteRepository.Builder merged = null;
         RepositoryPolicy releases = null, snapshots = null;
 
         next:
         for (RemoteRepository rec : recessive.getMirroredRepositories()) {
-            String recKey = getKey(rec);
+            String recKey = repositoryKeyFunction.apply(rec, null);
 
             for (RemoteRepository dom : dominant.getMirroredRepositories()) {
-                if (recKey.equals(getKey(dom))) {
+                if (recKey.equals(repositoryKeyFunction.apply(dom, null))) {
                     continue next;
                 }
             }
diff --git 
a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/DefaultRepositoryKeyFunctionFactory.java
 
b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/DefaultRepositoryKeyFunctionFactory.java
new file mode 100644
index 000000000..4f7a70621
--- /dev/null
+++ 
b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/DefaultRepositoryKeyFunctionFactory.java
@@ -0,0 +1,88 @@
+/*
+ * 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;
+
+import javax.inject.Named;
+import javax.inject.Singleton;
+
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
+
+import org.eclipse.aether.ConfigurationProperties;
+import org.eclipse.aether.RepositorySystemSession;
+import org.eclipse.aether.repository.RemoteRepository;
+import org.eclipse.aether.repository.RepositoryKeyFunction;
+import org.eclipse.aether.spi.remoterepo.RepositoryKeyFunctionFactory;
+import org.eclipse.aether.util.ConfigUtils;
+import org.eclipse.aether.util.repository.RepositoryIdHelper;
+
+import static java.util.Objects.requireNonNull;
+
+@Singleton
+@Named
+public class DefaultRepositoryKeyFunctionFactory implements 
RepositoryKeyFunctionFactory {
+    /**
+     * Returns system-wide repository key function.
+     *
+     * @since 2.0.14
+     * @see #repositoryKeyFunction(Class, RepositorySystemSession, String, 
String)
+     */
+    @Override
+    public RepositoryKeyFunction 
systemRepositoryKeyFunction(RepositorySystemSession session) {
+        return repositoryKeyFunction(
+                DefaultRepositoryKeyFunctionFactory.class,
+                session,
+                
ConfigurationProperties.DEFAULT_REPOSITORY_SYSTEM_REPOSITORY_KEY_FUNCTION,
+                
ConfigurationProperties.REPOSITORY_SYSTEM_REPOSITORY_KEY_FUNCTION);
+    }
+
+    /**
+     * Method that based on configuration returns the "repository key 
function". The returned function will be session
+     * cached if session is equipped with cache, otherwise it will be non 
cached. Method never returns {@code null}.
+     * Only the {@code configurationKey} parameter may be {@code null} in 
which case no configuration lookup happens
+     * but the {@code defaultValue} is directly used instead.
+     *
+     * @since 2.0.14
+     */
+    @SuppressWarnings("unchecked")
+    @Override
+    public RepositoryKeyFunction repositoryKeyFunction(
+            Class<?> owner, RepositorySystemSession session, String 
defaultValue, String configurationKey) {
+        requireNonNull(session);
+        requireNonNull(defaultValue);
+        final RepositoryKeyFunction repositoryKeyFunction = 
RepositoryIdHelper.getRepositoryKeyFunction(
+                configurationKey != null
+                        ? ConfigUtils.getString(session, defaultValue, 
configurationKey)
+                        : defaultValue);
+        if (session.getCache() != null) {
+            // both are expensive methods; cache it in session (repo -> 
context -> ID)
+            return (repository, context) -> ((ConcurrentMap<RemoteRepository, 
ConcurrentMap<String, String>>)
+                            session.getCache()
+                                    .computeIfAbsent(
+                                            session,
+                                            owner.getName() + 
".repositoryKeyFunction",
+                                            ConcurrentHashMap::new))
+                    .computeIfAbsent(repository, k1 -> new 
ConcurrentHashMap<>())
+                    .computeIfAbsent(
+                            context == null ? "" : context, k2 -> 
repositoryKeyFunction.apply(repository, context));
+        } else {
+            return repositoryKeyFunction;
+        }
+    }
+}
diff --git 
a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/EnhancedLocalRepositoryManager.java
 
b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/EnhancedLocalRepositoryManager.java
index e55150f3f..dd314fed9 100644
--- 
a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/EnhancedLocalRepositoryManager.java
+++ 
b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/EnhancedLocalRepositoryManager.java
@@ -27,16 +27,15 @@ import java.util.HashSet;
 import java.util.Map;
 import java.util.Objects;
 import java.util.Properties;
-import java.util.function.Function;
 
 import org.eclipse.aether.RepositorySystemSession;
 import org.eclipse.aether.artifact.Artifact;
 import org.eclipse.aether.metadata.Metadata;
-import org.eclipse.aether.repository.ArtifactRepository;
 import org.eclipse.aether.repository.LocalArtifactRegistration;
 import org.eclipse.aether.repository.LocalArtifactRequest;
 import org.eclipse.aether.repository.LocalArtifactResult;
 import org.eclipse.aether.repository.RemoteRepository;
+import org.eclipse.aether.repository.RepositoryKeyFunction;
 
 import static java.util.Objects.requireNonNull;
 
@@ -72,11 +71,11 @@ class EnhancedLocalRepositoryManager extends 
SimpleLocalRepositoryManager {
     EnhancedLocalRepositoryManager(
             Path basedir,
             LocalPathComposer localPathComposer,
-            Function<ArtifactRepository, String> idToPathSegmentFunction,
+            RepositoryKeyFunction repositoryKeyFunction,
             String trackingFilename,
             TrackingFileManager trackingFileManager,
             LocalPathPrefixComposer localPathPrefixComposer) {
-        super(basedir, "enhanced", localPathComposer, idToPathSegmentFunction);
+        super(basedir, "enhanced", localPathComposer, repositoryKeyFunction);
         this.trackingFilename = requireNonNull(trackingFilename);
         this.trackingFileManager = requireNonNull(trackingFileManager);
         this.localPathPrefixComposer = requireNonNull(localPathPrefixComposer);
diff --git 
a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/EnhancedLocalRepositoryManagerFactory.java
 
b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/EnhancedLocalRepositoryManagerFactory.java
index b00f1dff9..e31a493c9 100644
--- 
a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/EnhancedLocalRepositoryManagerFactory.java
+++ 
b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/EnhancedLocalRepositoryManagerFactory.java
@@ -28,8 +28,8 @@ import org.eclipse.aether.repository.LocalRepository;
 import org.eclipse.aether.repository.LocalRepositoryManager;
 import org.eclipse.aether.repository.NoLocalRepositoryManagerException;
 import org.eclipse.aether.spi.localrepo.LocalRepositoryManagerFactory;
+import org.eclipse.aether.spi.remoterepo.RepositoryKeyFunctionFactory;
 import org.eclipse.aether.util.ConfigUtils;
-import org.eclipse.aether.util.repository.RepositoryIdHelper;
 
 import static java.util.Objects.requireNonNull;
 
@@ -66,14 +66,18 @@ public class EnhancedLocalRepositoryManagerFactory 
implements LocalRepositoryMan
 
     private final LocalPathPrefixComposerFactory 
localPathPrefixComposerFactory;
 
+    private final RepositoryKeyFunctionFactory repositoryKeyFunctionFactory;
+
     @Inject
     public EnhancedLocalRepositoryManagerFactory(
             final LocalPathComposer localPathComposer,
             final TrackingFileManager trackingFileManager,
-            final LocalPathPrefixComposerFactory 
localPathPrefixComposerFactory) {
+            final LocalPathPrefixComposerFactory 
localPathPrefixComposerFactory,
+            final RepositoryKeyFunctionFactory repositoryKeyFunctionFactory) {
         this.localPathComposer = requireNonNull(localPathComposer);
         this.trackingFileManager = requireNonNull(trackingFileManager);
         this.localPathPrefixComposerFactory = 
requireNonNull(localPathPrefixComposerFactory);
+        this.repositoryKeyFunctionFactory = 
requireNonNull(repositoryKeyFunctionFactory);
     }
 
     @Override
@@ -94,7 +98,7 @@ public class EnhancedLocalRepositoryManagerFactory implements 
LocalRepositoryMan
             return new EnhancedLocalRepositoryManager(
                     repository.getBasePath(),
                     localPathComposer,
-                    RepositoryIdHelper.cachedIdToPathSegment(session),
+                    
repositoryKeyFunctionFactory.systemRepositoryKeyFunction(session),
                     trackingFilename,
                     trackingFileManager,
                     localPathPrefixComposerFactory.createComposer(session));
diff --git 
a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/LocalPathPrefixComposerFactorySupport.java
 
b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/LocalPathPrefixComposerFactorySupport.java
index b86223b53..a01b377c8 100644
--- 
a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/LocalPathPrefixComposerFactorySupport.java
+++ 
b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/LocalPathPrefixComposerFactorySupport.java
@@ -18,13 +18,11 @@
  */
 package org.eclipse.aether.internal.impl;
 
-import java.util.function.Function;
-
 import org.eclipse.aether.RepositorySystemSession;
 import org.eclipse.aether.artifact.Artifact;
 import org.eclipse.aether.metadata.Metadata;
-import org.eclipse.aether.repository.ArtifactRepository;
 import org.eclipse.aether.repository.RemoteRepository;
+import org.eclipse.aether.repository.RepositoryKeyFunction;
 import org.eclipse.aether.util.ConfigUtils;
 
 /**
@@ -244,7 +242,7 @@ public abstract class LocalPathPrefixComposerFactorySupport 
implements LocalPath
 
         protected final String snapshotsPrefix;
 
-        protected final Function<ArtifactRepository, String> 
idToPathSegmentFunction;
+        protected final RepositoryKeyFunction repositoryKeyFunction;
 
         protected LocalPathPrefixComposerSupport(
                 boolean split,
@@ -256,7 +254,7 @@ public abstract class LocalPathPrefixComposerFactorySupport 
implements LocalPath
                 boolean splitRemoteRepositoryLast,
                 String releasesPrefix,
                 String snapshotsPrefix,
-                Function<ArtifactRepository, String> idToPathSegmentFunction) {
+                RepositoryKeyFunction repositoryKeyFunction) {
             this.split = split;
             this.localPrefix = localPrefix;
             this.splitLocal = splitLocal;
@@ -266,7 +264,7 @@ public abstract class LocalPathPrefixComposerFactorySupport 
implements LocalPath
             this.splitRemoteRepositoryLast = splitRemoteRepositoryLast;
             this.releasesPrefix = releasesPrefix;
             this.snapshotsPrefix = snapshotsPrefix;
-            this.idToPathSegmentFunction = idToPathSegmentFunction;
+            this.repositoryKeyFunction = repositoryKeyFunction;
         }
 
         @Override
@@ -288,13 +286,13 @@ public abstract class 
LocalPathPrefixComposerFactorySupport implements LocalPath
             }
             String result = remotePrefix;
             if (!splitRemoteRepositoryLast && splitRemoteRepository) {
-                result += "/" + idToPathSegmentFunction.apply(repository);
+                result += "/" + repositoryKeyFunction.apply(repository, null);
             }
             if (splitRemote) {
                 result += "/" + (artifact.isSnapshot() ? snapshotsPrefix : 
releasesPrefix);
             }
             if (splitRemoteRepositoryLast && splitRemoteRepository) {
-                result += "/" + idToPathSegmentFunction.apply(repository);
+                result += "/" + repositoryKeyFunction.apply(repository, null);
             }
             return result;
         }
@@ -318,13 +316,13 @@ public abstract class 
LocalPathPrefixComposerFactorySupport implements LocalPath
             }
             String result = remotePrefix;
             if (!splitRemoteRepositoryLast && splitRemoteRepository) {
-                result += "/" + idToPathSegmentFunction.apply(repository);
+                result += "/" + repositoryKeyFunction.apply(repository, null);
             }
             if (splitRemote) {
                 result += "/" + (isSnapshot(metadata) ? snapshotsPrefix : 
releasesPrefix);
             }
             if (splitRemoteRepositoryLast && splitRemoteRepository) {
-                result += "/" + idToPathSegmentFunction.apply(repository);
+                result += "/" + repositoryKeyFunction.apply(repository, null);
             }
             return result;
         }
diff --git 
a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/SimpleLocalRepositoryManager.java
 
b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/SimpleLocalRepositoryManager.java
index ea751d41a..0acf1b374 100644
--- 
a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/SimpleLocalRepositoryManager.java
+++ 
b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/SimpleLocalRepositoryManager.java
@@ -21,14 +21,10 @@ package org.eclipse.aether.internal.impl;
 import java.nio.file.Files;
 import java.nio.file.Path;
 import java.util.Objects;
-import java.util.SortedSet;
-import java.util.TreeSet;
-import java.util.function.Function;
 
 import org.eclipse.aether.RepositorySystemSession;
 import org.eclipse.aether.artifact.Artifact;
 import org.eclipse.aether.metadata.Metadata;
-import org.eclipse.aether.repository.ArtifactRepository;
 import org.eclipse.aether.repository.LocalArtifactRegistration;
 import org.eclipse.aether.repository.LocalArtifactRequest;
 import org.eclipse.aether.repository.LocalArtifactResult;
@@ -38,7 +34,7 @@ import org.eclipse.aether.repository.LocalMetadataResult;
 import org.eclipse.aether.repository.LocalRepository;
 import org.eclipse.aether.repository.LocalRepositoryManager;
 import org.eclipse.aether.repository.RemoteRepository;
-import org.eclipse.aether.util.StringDigestUtil;
+import org.eclipse.aether.repository.RepositoryKeyFunction;
 
 import static java.util.Objects.requireNonNull;
 
@@ -51,17 +47,17 @@ class SimpleLocalRepositoryManager implements 
LocalRepositoryManager {
 
     private final LocalPathComposer localPathComposer;
 
-    private final Function<ArtifactRepository, String> idToPathSegmentFunction;
+    private final RepositoryKeyFunction repositoryKeyFunction;
 
     SimpleLocalRepositoryManager(
             Path basePath,
             String type,
             LocalPathComposer localPathComposer,
-            Function<ArtifactRepository, String> idToPathSegmentFunction) {
+            RepositoryKeyFunction repositoryKeyFunction) {
         requireNonNull(basePath, "base directory cannot be null");
         repository = new LocalRepository(basePath.toAbsolutePath(), type);
         this.localPathComposer = requireNonNull(localPathComposer);
-        this.idToPathSegmentFunction = requireNonNull(idToPathSegmentFunction);
+        this.repositoryKeyFunction = requireNonNull(repositoryKeyFunction);
     }
 
     @Override
@@ -85,7 +81,7 @@ class SimpleLocalRepositoryManager implements 
LocalRepositoryManager {
     @Override
     public String getPathForLocalMetadata(Metadata metadata) {
         requireNonNull(metadata, "metadata cannot be null");
-        return localPathComposer.getPathForMetadata(metadata, "local");
+        return localPathComposer.getPathForMetadata(metadata, 
LocalRepository.ID);
     }
 
     @Override
@@ -101,35 +97,7 @@ class SimpleLocalRepositoryManager implements 
LocalRepositoryManager {
      * of the remote repository (as it may change).
      */
     protected String getRepositoryKey(RemoteRepository repository, String 
context) {
-        String key;
-
-        if (repository.isRepositoryManager()) {
-            // repository serves dynamic contents, take request parameters 
into account for key
-
-            StringBuilder buffer = new StringBuilder(128);
-
-            buffer.append(idToPathSegmentFunction.apply(repository));
-
-            buffer.append('-');
-
-            SortedSet<String> subKeys = new TreeSet<>();
-            for (RemoteRepository mirroredRepo : 
repository.getMirroredRepositories()) {
-                subKeys.add(mirroredRepo.getId());
-            }
-
-            StringDigestUtil sha1 = StringDigestUtil.sha1();
-            sha1.update(context);
-            for (String subKey : subKeys) {
-                sha1.update(subKey);
-            }
-            buffer.append(sha1.digest());
-
-            key = buffer.toString();
-        } else {
-            key = idToPathSegmentFunction.apply(repository);
-        }
-
-        return key;
+        return repositoryKeyFunction.apply(repository, context);
     }
 
     @Override
diff --git 
a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/SimpleLocalRepositoryManagerFactory.java
 
b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/SimpleLocalRepositoryManagerFactory.java
index c1e4ccd21..f45460852 100644
--- 
a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/SimpleLocalRepositoryManagerFactory.java
+++ 
b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/SimpleLocalRepositoryManagerFactory.java
@@ -27,6 +27,7 @@ import org.eclipse.aether.repository.LocalRepository;
 import org.eclipse.aether.repository.LocalRepositoryManager;
 import org.eclipse.aether.repository.NoLocalRepositoryManagerException;
 import org.eclipse.aether.spi.localrepo.LocalRepositoryManagerFactory;
+import org.eclipse.aether.spi.remoterepo.RepositoryKeyFunctionFactory;
 import org.eclipse.aether.util.repository.RepositoryIdHelper;
 
 import static java.util.Objects.requireNonNull;
@@ -41,17 +42,22 @@ public class SimpleLocalRepositoryManagerFactory implements 
LocalRepositoryManag
     private float priority;
 
     private final LocalPathComposer localPathComposer;
+    private final RepositoryKeyFunctionFactory repositoryKeyFunctionFactory;
 
     /**
      * No-arg constructor, as "simple" local repository is meant mainly for 
use in tests.
      */
     public SimpleLocalRepositoryManagerFactory() {
         this.localPathComposer = new DefaultLocalPathComposer();
+        this.repositoryKeyFunctionFactory = new 
DefaultRepositoryKeyFunctionFactory();
     }
 
     @Inject
-    public SimpleLocalRepositoryManagerFactory(final LocalPathComposer 
localPathComposer) {
+    public SimpleLocalRepositoryManagerFactory(
+            final LocalPathComposer localPathComposer,
+            final RepositoryKeyFunctionFactory repositoryKeyFunctionFactory) {
         this.localPathComposer = requireNonNull(localPathComposer);
+        this.repositoryKeyFunctionFactory = 
requireNonNull(repositoryKeyFunctionFactory);
     }
 
     @Override
@@ -65,7 +71,11 @@ public class SimpleLocalRepositoryManagerFactory implements 
LocalRepositoryManag
                     repository.getBasePath(),
                     "simple",
                     localPathComposer,
-                    RepositoryIdHelper.cachedIdToPathSegment(session));
+                    repositoryKeyFunctionFactory.repositoryKeyFunction(
+                            SimpleLocalRepositoryManagerFactory.class,
+                            session,
+                            RepositoryIdHelper.RepositoryKeyType.SIMPLE.name(),
+                            null));
         } else {
             throw new NoLocalRepositoryManagerException(repository);
         }
diff --git 
a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/checksum/FileTrustedChecksumsSourceSupport.java
 
b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/checksum/FileTrustedChecksumsSourceSupport.java
index fc417884c..8f052b92b 100644
--- 
a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/checksum/FileTrustedChecksumsSourceSupport.java
+++ 
b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/checksum/FileTrustedChecksumsSourceSupport.java
@@ -28,8 +28,10 @@ import org.eclipse.aether.ConfigurationProperties;
 import org.eclipse.aether.RepositorySystemSession;
 import org.eclipse.aether.artifact.Artifact;
 import org.eclipse.aether.repository.ArtifactRepository;
+import org.eclipse.aether.repository.RemoteRepository;
 import org.eclipse.aether.spi.checksums.TrustedChecksumsSource;
 import org.eclipse.aether.spi.connector.checksum.ChecksumAlgorithmFactory;
+import org.eclipse.aether.spi.remoterepo.RepositoryKeyFunctionFactory;
 import org.eclipse.aether.util.DirectoryUtils;
 
 import static java.util.Objects.requireNonNull;
@@ -53,10 +55,31 @@ import static java.util.Objects.requireNonNull;
  *
  * @since 1.9.0
  */
-abstract class FileTrustedChecksumsSourceSupport implements 
TrustedChecksumsSource {
+public abstract class FileTrustedChecksumsSourceSupport implements 
TrustedChecksumsSource {
     protected static final String CONFIG_PROPS_PREFIX =
             ConfigurationProperties.PREFIX_AETHER + "trustedChecksumsSource.";
 
+    /**
+     * <b>Experimental:</b> Configuration for "repository key" function.
+     * Note: repository key functions other than "nid" produce repository keys 
will be <em>way different
+     * that those produced with previous versions or without this option 
enabled</em>. Checksum source uses this key
+     * function to lay down and look up files to use in sources.
+     *
+     * @since 2.0.14
+     * @configurationSource {@link 
RepositorySystemSession#getConfigProperties()}
+     * @configurationType {@link java.lang.String}
+     * @configurationDefaultValue {@link #DEFAULT_REPOSITORY_KEY_FUNCTION}
+     */
+    public static final String CONFIG_PROP_REPOSITORY_KEY_FUNCTION = 
CONFIG_PROPS_PREFIX + "repositoryKeyFunction";
+
+    public static final String DEFAULT_REPOSITORY_KEY_FUNCTION = "nid";
+
+    private final RepositoryKeyFunctionFactory repositoryKeyFunctionFactory;
+
+    protected FileTrustedChecksumsSourceSupport(RepositoryKeyFunctionFactory 
repositoryKeyFunctionFactory) {
+        this.repositoryKeyFunctionFactory = 
requireNonNull(repositoryKeyFunctionFactory);
+    }
+
     /**
      * This implementation will call into underlying code only if enabled, and 
will enforce non-{@code null} return
      * value. In worst case, empty map should be returned, meaning "no trusted 
checksums available".
@@ -131,4 +154,23 @@ abstract class FileTrustedChecksumsSourceSupport 
implements TrustedChecksumsSour
             throw new UncheckedIOException(e);
         }
     }
+
+    /**
+     * Returns repository key to be used on file system layout.
+     *
+     * @since 2.0.14
+     */
+    protected String repositoryKey(RepositorySystemSession session, 
ArtifactRepository artifactRepository) {
+        if (artifactRepository instanceof RemoteRepository) {
+            return repositoryKeyFunctionFactory
+                    .repositoryKeyFunction(
+                            FileTrustedChecksumsSourceSupport.class,
+                            session,
+                            DEFAULT_REPOSITORY_KEY_FUNCTION,
+                            CONFIG_PROP_REPOSITORY_KEY_FUNCTION)
+                    .apply((RemoteRepository) artifactRepository, null);
+        } else {
+            return artifactRepository.getId();
+        }
+    }
 }
diff --git 
a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/checksum/SparseDirectoryTrustedChecksumsSource.java
 
b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/checksum/SparseDirectoryTrustedChecksumsSource.java
index 1d6af287b..0456b3f17 100644
--- 
a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/checksum/SparseDirectoryTrustedChecksumsSource.java
+++ 
b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/checksum/SparseDirectoryTrustedChecksumsSource.java
@@ -37,8 +37,8 @@ import org.eclipse.aether.internal.impl.LocalPathComposer;
 import org.eclipse.aether.repository.ArtifactRepository;
 import org.eclipse.aether.spi.connector.checksum.ChecksumAlgorithmFactory;
 import org.eclipse.aether.spi.io.ChecksumProcessor;
+import org.eclipse.aether.spi.remoterepo.RepositoryKeyFunctionFactory;
 import org.eclipse.aether.util.ConfigUtils;
-import org.eclipse.aether.util.repository.RepositoryIdHelper;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -106,7 +106,10 @@ public final class SparseDirectoryTrustedChecksumsSource 
extends FileTrustedChec
 
     @Inject
     public SparseDirectoryTrustedChecksumsSource(
-            ChecksumProcessor checksumProcessor, LocalPathComposer 
localPathComposer) {
+            RepositoryKeyFunctionFactory repositoryKeyFunctionFactory,
+            ChecksumProcessor checksumProcessor,
+            LocalPathComposer localPathComposer) {
+        super(repositoryKeyFunctionFactory);
         this.checksumProcessor = requireNonNull(checksumProcessor);
         this.localPathComposer = requireNonNull(localPathComposer);
     }
@@ -132,10 +135,7 @@ public final class SparseDirectoryTrustedChecksumsSource 
extends FileTrustedChec
         if (Files.isDirectory(basedir)) {
             for (ChecksumAlgorithmFactory checksumAlgorithmFactory : 
checksumAlgorithmFactories) {
                 Path checksumPath = basedir.resolve(calculateArtifactPath(
-                        originAware,
-                        artifact,
-                        
RepositoryIdHelper.cachedIdToPathSegment(session).apply(artifactRepository),
-                        checksumAlgorithmFactory));
+                        originAware, artifact, repositoryKey(session, 
artifactRepository), checksumAlgorithmFactory));
 
                 if (!Files.isRegularFile(checksumPath)) {
                     LOGGER.debug(
@@ -167,7 +167,7 @@ public final class SparseDirectoryTrustedChecksumsSource 
extends FileTrustedChec
         return new SparseDirectoryWriter(
                 getBasedir(session, LOCAL_REPO_PREFIX_DIR, 
CONFIG_PROP_BASEDIR, true),
                 isOriginAware(session),
-                RepositoryIdHelper.cachedIdToPathSegment(session));
+                r -> repositoryKey(session, r));
     }
 
     private String calculateArtifactPath(
diff --git 
a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/checksum/SummaryFileTrustedChecksumsSource.java
 
b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/checksum/SummaryFileTrustedChecksumsSource.java
index 85c9be19f..f7f451546 100644
--- 
a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/checksum/SummaryFileTrustedChecksumsSource.java
+++ 
b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/checksum/SummaryFileTrustedChecksumsSource.java
@@ -46,8 +46,8 @@ import org.eclipse.aether.internal.impl.LocalPathComposer;
 import org.eclipse.aether.repository.ArtifactRepository;
 import org.eclipse.aether.spi.connector.checksum.ChecksumAlgorithmFactory;
 import org.eclipse.aether.spi.io.PathProcessor;
+import org.eclipse.aether.spi.remoterepo.RepositoryKeyFunctionFactory;
 import org.eclipse.aether.util.ConfigUtils;
-import org.eclipse.aether.util.repository.RepositoryIdHelper;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -142,9 +142,11 @@ public final class SummaryFileTrustedChecksumsSource 
extends FileTrustedChecksum
 
     @Inject
     public SummaryFileTrustedChecksumsSource(
+            RepositoryKeyFunctionFactory repoKeyFunctionFactory,
             LocalPathComposer localPathComposer,
             RepositorySystemLifecycle repositorySystemLifecycle,
             PathProcessor pathProcessor) {
+        super(repoKeyFunctionFactory);
         this.localPathComposer = requireNonNull(localPathComposer);
         this.repositorySystemLifecycle = 
requireNonNull(repositorySystemLifecycle);
         this.pathProcessor = requireNonNull(pathProcessor);
@@ -177,7 +179,7 @@ public final class SummaryFileTrustedChecksumsSource 
extends FileTrustedChecksum
                 Path summaryFile = summaryFile(
                         basedir,
                         originAware,
-                        
RepositoryIdHelper.cachedIdToPathSegment(session).apply(artifactRepository),
+                        repositoryKey(session, artifactRepository),
                         checksumAlgorithmFactory.getFileExtension());
                 ConcurrentHashMap<String, String> algorithmChecksums =
                         checksums.computeIfAbsent(summaryFile, f -> 
loadProvidedChecksums(summaryFile));
@@ -199,7 +201,7 @@ public final class SummaryFileTrustedChecksumsSource 
extends FileTrustedChecksum
                 checksums,
                 getBasedir(session, LOCAL_REPO_PREFIX_DIR, 
CONFIG_PROP_BASEDIR, true),
                 isOriginAware(session),
-                RepositoryIdHelper.cachedIdToPathSegment(session));
+                r -> repositoryKey(session, r));
     }
 
     /**
@@ -263,17 +265,17 @@ public final class SummaryFileTrustedChecksumsSource 
extends FileTrustedChecksum
 
         private final boolean originAware;
 
-        private final Function<ArtifactRepository, String> 
idToPathSegmentFunction;
+        private final Function<ArtifactRepository, String> 
repositoryKeyFunction;
 
         private SummaryFileWriter(
                 ConcurrentHashMap<Path, ConcurrentHashMap<String, String>> 
cache,
                 Path basedir,
                 boolean originAware,
-                Function<ArtifactRepository, String> idToPathSegmentFunction) {
+                Function<ArtifactRepository, String> repositoryKeyFunction) {
             this.cache = cache;
             this.basedir = basedir;
             this.originAware = originAware;
-            this.idToPathSegmentFunction = idToPathSegmentFunction;
+            this.repositoryKeyFunction = repositoryKeyFunction;
         }
 
         @Override
@@ -287,7 +289,7 @@ public final class SummaryFileTrustedChecksumsSource 
extends FileTrustedChecksum
                 Path summaryFile = summaryFile(
                         basedir,
                         originAware,
-                        idToPathSegmentFunction.apply(artifactRepository),
+                        repositoryKeyFunction.apply(artifactRepository),
                         checksumAlgorithmFactory.getFileExtension());
                 String checksum = 
requireNonNull(trustedArtifactChecksums.get(checksumAlgorithmFactory.getName()));
 
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 3d6643876..32be14c79 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
@@ -48,9 +48,9 @@ import org.eclipse.aether.repository.RemoteRepository;
 import org.eclipse.aether.resolution.ArtifactResult;
 import org.eclipse.aether.spi.connector.filter.RemoteRepositoryFilter;
 import org.eclipse.aether.spi.io.PathProcessor;
+import org.eclipse.aether.spi.remoterepo.RepositoryKeyFunctionFactory;
 import org.eclipse.aether.spi.resolution.ArtifactResolverPostProcessor;
 import org.eclipse.aether.util.ConfigUtils;
-import org.eclipse.aether.util.repository.RepositoryIdHelper;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -157,7 +157,10 @@ public final class GroupIdRemoteRepositoryFilterSource 
extends RemoteRepositoryF
 
     @Inject
     public GroupIdRemoteRepositoryFilterSource(
-            RepositorySystemLifecycle repositorySystemLifecycle, PathProcessor 
pathProcessor) {
+            RepositoryKeyFunctionFactory repositoryKeyFunctionFactory,
+            RepositorySystemLifecycle repositorySystemLifecycle,
+            PathProcessor pathProcessor) {
+        super(repositoryKeyFunctionFactory);
         this.repositorySystemLifecycle = 
requireNonNull(repositorySystemLifecycle);
         this.pathProcessor = requireNonNull(pathProcessor);
     }
@@ -248,9 +251,7 @@ public final class GroupIdRemoteRepositoryFilterSource 
extends RemoteRepositoryF
     private Path ruleFile(RepositorySystemSession session, RemoteRepository 
remoteRepository) {
         return 
ruleFiles(session).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)
-                        + GROUP_ID_FILE_SUFFIX));
+                .resolve(GROUP_ID_FILE_PREFIX + repositoryKey(session, 
remoteRepository) + GROUP_ID_FILE_SUFFIX));
     }
 
     private GroupTree cacheRules(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 9a4b2c301..8e77aab21 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
@@ -47,9 +47,9 @@ import 
org.eclipse.aether.spi.connector.checksum.ChecksumAlgorithmFactory;
 import org.eclipse.aether.spi.connector.filter.RemoteRepositoryFilter;
 import org.eclipse.aether.spi.connector.layout.RepositoryLayout;
 import org.eclipse.aether.spi.connector.layout.RepositoryLayoutProvider;
+import org.eclipse.aether.spi.remoterepo.RepositoryKeyFunctionFactory;
 import org.eclipse.aether.transfer.NoRepositoryLayoutException;
 import org.eclipse.aether.util.ConfigUtils;
-import org.eclipse.aether.util.repository.RepositoryIdHelper;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -197,9 +197,11 @@ public final class PrefixesRemoteRepositoryFilterSource 
extends RemoteRepository
 
     @Inject
     public PrefixesRemoteRepositoryFilterSource(
+            RepositoryKeyFunctionFactory repositoryKeyFunctionFactory,
             Supplier<MetadataResolver> metadataResolver,
             Supplier<RemoteRepositoryManager> remoteRepositoryManager,
             RepositoryLayoutProvider repositoryLayoutProvider) {
+        super(repositoryKeyFunctionFactory);
         this.metadataResolver = requireNonNull(metadataResolver);
         this.remoteRepositoryManager = requireNonNull(remoteRepositoryManager);
         this.repositoryLayoutProvider = 
requireNonNull(repositoryLayoutProvider);
@@ -318,9 +320,8 @@ public final class PrefixesRemoteRepositoryFilterSource 
extends RemoteRepository
 
     private Path resolvePrefixesFromLocalConfiguration(
             RepositorySystemSession session, Path baseDir, RemoteRepository 
remoteRepository) {
-        Path filePath = baseDir.resolve(PREFIXES_FILE_PREFIX
-                + 
RepositoryIdHelper.cachedIdToPathSegment(session).apply(remoteRepository)
-                + PREFIXES_FILE_SUFFIX);
+        Path filePath =
+                baseDir.resolve(PREFIXES_FILE_PREFIX + repositoryKey(session, 
remoteRepository) + PREFIXES_FILE_SUFFIX);
         if (Files.isReadable(filePath)) {
             return filePath;
         } else {
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 438c5eaa8..e97900d11 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
@@ -24,9 +24,11 @@ import java.nio.file.Path;
 
 import org.eclipse.aether.ConfigurationProperties;
 import org.eclipse.aether.RepositorySystemSession;
+import 
org.eclipse.aether.internal.impl.checksum.FileTrustedChecksumsSourceSupport;
 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.spi.remoterepo.RepositoryKeyFunctionFactory;
 import org.eclipse.aether.util.DirectoryUtils;
 
 import static java.util.Objects.requireNonNull;
@@ -52,6 +54,27 @@ public abstract class RemoteRepositoryFilterSourceSupport 
implements RemoteRepos
     protected static final String CONFIG_PROPS_PREFIX =
             ConfigurationProperties.PREFIX_AETHER + "remoteRepositoryFilter.";
 
+    /**
+     * <b>Experimental:</b> Configuration for "repository key" function.
+     * Note: repository key functions other than "nid" produce repository keys 
will be <em>way different
+     * that those produced with previous versions or without this option 
enabled</em>. Filter uses this key function to
+     * lay down and look up files to use in filtering.
+     *
+     * @since 2.0.14
+     * @configurationSource {@link 
RepositorySystemSession#getConfigProperties()}
+     * @configurationType {@link java.lang.String}
+     * @configurationDefaultValue {@link #DEFAULT_REPOSITORY_KEY_FUNCTION}
+     */
+    public static final String CONFIG_PROP_REPOSITORY_KEY_FUNCTION = 
CONFIG_PROPS_PREFIX + "repositoryKeyFunction";
+
+    public static final String DEFAULT_REPOSITORY_KEY_FUNCTION = "nid";
+
+    private final RepositoryKeyFunctionFactory repositoryKeyFunctionFactory;
+
+    protected RemoteRepositoryFilterSourceSupport(RepositoryKeyFunctionFactory 
repositoryKeyFunctionFactory) {
+        this.repositoryKeyFunctionFactory = 
requireNonNull(repositoryKeyFunctionFactory);
+    }
+
     /**
      * Returns {@code true} if session configuration contains this name set to 
{@code true}.
      * <p>
@@ -88,6 +111,21 @@ public abstract class RemoteRepositoryFilterSourceSupport 
implements RemoteRepos
         return remoteRepository.toBareRemoteRepository();
     }
 
+    /**
+     * Returns repository key to be used on file system layout.
+     *
+     * @since 2.0.14
+     */
+    protected String repositoryKey(RepositorySystemSession session, 
RemoteRepository repository) {
+        return repositoryKeyFunctionFactory
+                .repositoryKeyFunction(
+                        FileTrustedChecksumsSourceSupport.class,
+                        session,
+                        DEFAULT_REPOSITORY_KEY_FUNCTION,
+                        CONFIG_PROP_REPOSITORY_KEY_FUNCTION)
+                .apply(repository, null);
+    }
+
     /**
      * Simple {@link RemoteRepositoryFilter.Result} immutable implementation.
      */
diff --git 
a/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/DefaultLocalPathPrefixComposerFactoryTest.java
 
b/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/DefaultLocalPathPrefixComposerFactoryTest.java
index 0a47ac7ae..8f6c53452 100644
--- 
a/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/DefaultLocalPathPrefixComposerFactoryTest.java
+++ 
b/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/DefaultLocalPathPrefixComposerFactoryTest.java
@@ -56,7 +56,8 @@ public class DefaultLocalPathPrefixComposerFactoryTest {
     void defaultConfigNoSplitAllNulls() {
         DefaultRepositorySystemSession session = TestUtils.newSession();
 
-        LocalPathPrefixComposerFactory factory = new 
DefaultLocalPathPrefixComposerFactory();
+        LocalPathPrefixComposerFactory factory =
+                new DefaultLocalPathPrefixComposerFactory(new 
DefaultRepositoryKeyFunctionFactory());
         LocalPathPrefixComposer composer = factory.createComposer(session);
         assertNotNull(composer);
 
@@ -79,7 +80,8 @@ public class DefaultLocalPathPrefixComposerFactoryTest {
         DefaultRepositorySystemSession session = TestUtils.newSession();
         
session.setConfigProperty(DefaultLocalPathPrefixComposerFactory.CONFIG_PROP_SPLIT,
 Boolean.TRUE.toString());
 
-        LocalPathPrefixComposerFactory factory = new 
DefaultLocalPathPrefixComposerFactory();
+        LocalPathPrefixComposerFactory factory =
+                new DefaultLocalPathPrefixComposerFactory(new 
DefaultRepositoryKeyFunctionFactory());
         LocalPathPrefixComposer composer = factory.createComposer(session);
         assertNotNull(composer);
 
@@ -110,7 +112,8 @@ public class DefaultLocalPathPrefixComposerFactoryTest {
         session.setConfigProperty(
                 
DefaultLocalPathPrefixComposerFactory.CONFIG_PROP_SPLIT_REMOTE_REPOSITORY, 
Boolean.TRUE.toString());
 
-        LocalPathPrefixComposerFactory factory = new 
DefaultLocalPathPrefixComposerFactory();
+        LocalPathPrefixComposerFactory factory =
+                new DefaultLocalPathPrefixComposerFactory(new 
DefaultRepositoryKeyFunctionFactory());
         LocalPathPrefixComposer composer = factory.createComposer(session);
         assertNotNull(composer);
 
@@ -175,7 +178,8 @@ public class DefaultLocalPathPrefixComposerFactoryTest {
         session.setConfigProperty(
                 
DefaultLocalPathPrefixComposerFactory.CONFIG_PROP_SPLIT_REMOTE_REPOSITORY, 
Boolean.TRUE.toString());
 
-        LocalPathPrefixComposerFactory factory = new 
DefaultLocalPathPrefixComposerFactory();
+        LocalPathPrefixComposerFactory factory =
+                new DefaultLocalPathPrefixComposerFactory(new 
DefaultRepositoryKeyFunctionFactory());
         LocalPathPrefixComposer composer = factory.createComposer(session);
         assertNotNull(composer);
 
diff --git 
a/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/DefaultRemoteRepositoryManagerTest.java
 
b/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/DefaultRemoteRepositoryManagerTest.java
index 20ec8462e..9b51a2db5 100644
--- 
a/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/DefaultRemoteRepositoryManagerTest.java
+++ 
b/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/DefaultRemoteRepositoryManagerTest.java
@@ -53,8 +53,10 @@ public class DefaultRemoteRepositoryManagerTest {
         session = TestUtils.newSession();
         session.setChecksumPolicy(null);
         session.setUpdatePolicy(null);
-        manager =
-                new DefaultRemoteRepositoryManager(new 
StubUpdatePolicyAnalyzer(), new DefaultChecksumPolicyProvider());
+        manager = new DefaultRemoteRepositoryManager(
+                new StubUpdatePolicyAnalyzer(),
+                new DefaultChecksumPolicyProvider(),
+                new DefaultRepositoryKeyFunctionFactory());
     }
 
     @AfterEach
diff --git 
a/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/DefaultRepositorySystemTest.java
 
b/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/DefaultRepositorySystemTest.java
index 08b75df3f..4fa1953d5 100644
--- 
a/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/DefaultRepositorySystemTest.java
+++ 
b/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/DefaultRepositorySystemTest.java
@@ -64,7 +64,9 @@ public class DefaultRepositorySystemTest {
                 mock(LocalRepositoryProvider.class),
                 new StubSyncContextFactory(),
                 new DefaultRemoteRepositoryManager(
-                        new DefaultUpdatePolicyAnalyzer(), new 
DefaultChecksumPolicyProvider()),
+                        new DefaultUpdatePolicyAnalyzer(),
+                        new DefaultChecksumPolicyProvider(),
+                        new DefaultRepositoryKeyFunctionFactory()),
                 new DefaultRepositorySystemLifecycle(),
                 Collections.emptyMap(),
                 new DefaultRepositorySystemValidator(Collections.emptyList()));
diff --git 
a/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/EnhancedLocalRepositoryManagerTest.java
 
b/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/EnhancedLocalRepositoryManagerTest.java
index 7d8a296cc..2e3468dc6 100644
--- 
a/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/EnhancedLocalRepositoryManagerTest.java
+++ 
b/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/EnhancedLocalRepositoryManagerTest.java
@@ -33,13 +33,13 @@ import org.eclipse.aether.internal.test.util.TestUtils;
 import org.eclipse.aether.metadata.DefaultMetadata;
 import org.eclipse.aether.metadata.Metadata;
 import org.eclipse.aether.metadata.Metadata.Nature;
-import org.eclipse.aether.repository.ArtifactRepository;
 import org.eclipse.aether.repository.LocalArtifactRegistration;
 import org.eclipse.aether.repository.LocalArtifactRequest;
 import org.eclipse.aether.repository.LocalArtifactResult;
 import org.eclipse.aether.repository.LocalMetadataRequest;
 import org.eclipse.aether.repository.LocalMetadataResult;
 import org.eclipse.aether.repository.RemoteRepository;
+import org.eclipse.aether.util.repository.RepositoryIdHelper;
 import org.junit.jupiter.api.AfterEach;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
@@ -108,10 +108,11 @@ public class EnhancedLocalRepositoryManagerTest {
         return new EnhancedLocalRepositoryManager(
                 basedir.toPath(),
                 new DefaultLocalPathComposer(),
-                ArtifactRepository::getId,
+                RepositoryIdHelper::simpleRepositoryKey,
                 "_remote.repositories",
                 trackingFileManager,
-                new 
DefaultLocalPathPrefixComposerFactory().createComposer(session));
+                new DefaultLocalPathPrefixComposerFactory(new 
DefaultRepositoryKeyFunctionFactory())
+                        .createComposer(session));
     }
 
     @AfterEach
diff --git 
a/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/EnhancedSplitLocalRepositoryManagerTest.java
 
b/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/EnhancedSplitLocalRepositoryManagerTest.java
index 4a78ea302..538c7afe0 100644
--- 
a/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/EnhancedSplitLocalRepositoryManagerTest.java
+++ 
b/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/EnhancedSplitLocalRepositoryManagerTest.java
@@ -20,8 +20,8 @@ package org.eclipse.aether.internal.impl;
 
 import org.eclipse.aether.artifact.Artifact;
 import org.eclipse.aether.artifact.DefaultArtifact;
-import org.eclipse.aether.repository.ArtifactRepository;
 import org.eclipse.aether.repository.RemoteRepository;
+import org.eclipse.aether.util.repository.RepositoryIdHelper;
 import org.junit.jupiter.api.Test;
 
 import static org.junit.jupiter.api.Assertions.*;
@@ -34,10 +34,11 @@ public class EnhancedSplitLocalRepositoryManagerTest 
extends EnhancedLocalReposi
         return new EnhancedLocalRepositoryManager(
                 basedir.toPath(),
                 new DefaultLocalPathComposer(),
-                ArtifactRepository::getId,
+                RepositoryIdHelper::simpleRepositoryKey,
                 "_remote.repositories",
                 trackingFileManager,
-                new 
DefaultLocalPathPrefixComposerFactory().createComposer(session));
+                new DefaultLocalPathPrefixComposerFactory(new 
DefaultRepositoryKeyFunctionFactory())
+                        .createComposer(session));
     }
 
     @Test
diff --git 
a/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/SimpleLocalRepositoryManagerTest.java
 
b/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/SimpleLocalRepositoryManagerTest.java
index fbe240992..5bd674136 100644
--- 
a/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/SimpleLocalRepositoryManagerTest.java
+++ 
b/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/SimpleLocalRepositoryManagerTest.java
@@ -26,10 +26,10 @@ import org.eclipse.aether.artifact.Artifact;
 import org.eclipse.aether.artifact.DefaultArtifact;
 import org.eclipse.aether.internal.test.util.TestFileUtils;
 import org.eclipse.aether.internal.test.util.TestUtils;
-import org.eclipse.aether.repository.ArtifactRepository;
 import org.eclipse.aether.repository.LocalArtifactRequest;
 import org.eclipse.aether.repository.LocalArtifactResult;
 import org.eclipse.aether.repository.RemoteRepository;
+import org.eclipse.aether.util.repository.RepositoryIdHelper;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.io.TempDir;
@@ -50,7 +50,7 @@ public class SimpleLocalRepositoryManagerTest {
     @BeforeEach
     void setup() throws IOException {
         manager = new SimpleLocalRepositoryManager(
-                basedir.toPath(), "simple", new DefaultLocalPathComposer(), 
ArtifactRepository::getId);
+                basedir.toPath(), "simple", new DefaultLocalPathComposer(), 
RepositoryIdHelper::simpleRepositoryKey);
         session = TestUtils.newSession();
     }
 
diff --git 
a/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/checksum/SparseDirectoryTrustedChecksumsSourceTest.java
 
b/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/checksum/SparseDirectoryTrustedChecksumsSourceTest.java
index 83e812a57..2053ee489 100644
--- 
a/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/checksum/SparseDirectoryTrustedChecksumsSourceTest.java
+++ 
b/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/checksum/SparseDirectoryTrustedChecksumsSourceTest.java
@@ -23,12 +23,15 @@ import org.eclipse.aether.impl.RepositorySystemLifecycle;
 import org.eclipse.aether.internal.impl.DefaultChecksumProcessor;
 import org.eclipse.aether.internal.impl.DefaultLocalPathComposer;
 import org.eclipse.aether.internal.impl.DefaultPathProcessor;
+import org.eclipse.aether.internal.impl.DefaultRepositoryKeyFunctionFactory;
 
 public class SparseDirectoryTrustedChecksumsSourceTest extends 
FileTrustedChecksumsSourceTestSupport {
     @Override
     protected FileTrustedChecksumsSourceSupport 
prepareSubject(RepositorySystemLifecycle lifecycle) {
         return new SparseDirectoryTrustedChecksumsSource(
-                new DefaultChecksumProcessor(new DefaultPathProcessor()), new 
DefaultLocalPathComposer());
+                new DefaultRepositoryKeyFunctionFactory(),
+                new DefaultChecksumProcessor(new DefaultPathProcessor()),
+                new DefaultLocalPathComposer());
     }
 
     @Override
diff --git 
a/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/checksum/SummaryFileTrustedChecksumsSourceTest.java
 
b/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/checksum/SummaryFileTrustedChecksumsSourceTest.java
index 2646d783c..6d31e5da8 100644
--- 
a/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/checksum/SummaryFileTrustedChecksumsSourceTest.java
+++ 
b/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/checksum/SummaryFileTrustedChecksumsSourceTest.java
@@ -26,6 +26,7 @@ import org.eclipse.aether.DefaultRepositorySystemSession;
 import org.eclipse.aether.artifact.DefaultArtifact;
 import org.eclipse.aether.impl.RepositorySystemLifecycle;
 import org.eclipse.aether.internal.impl.DefaultLocalPathComposer;
+import org.eclipse.aether.internal.impl.DefaultRepositoryKeyFunctionFactory;
 import org.eclipse.aether.internal.impl.DefaultRepositorySystemLifecycle;
 import org.eclipse.aether.internal.impl.SimpleLocalRepositoryManagerFactory;
 import org.eclipse.aether.repository.LocalRepository;
@@ -44,7 +45,10 @@ public class SummaryFileTrustedChecksumsSourceTest extends 
FileTrustedChecksumsS
     @Override
     protected FileTrustedChecksumsSourceSupport 
prepareSubject(RepositorySystemLifecycle lifecycle) {
         return new SummaryFileTrustedChecksumsSource(
-                new DefaultLocalPathComposer(), lifecycle, new 
PathProcessorSupport());
+                new DefaultRepositoryKeyFunctionFactory(),
+                new DefaultLocalPathComposer(),
+                lifecycle,
+                new PathProcessorSupport());
     }
 
     @Override
diff --git 
a/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/filter/GroupIdRemoteRepositoryFilterSourceTest.java
 
b/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/filter/GroupIdRemoteRepositoryFilterSourceTest.java
index 4a60622ad..6bcbdaaf9 100644
--- 
a/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/filter/GroupIdRemoteRepositoryFilterSourceTest.java
+++ 
b/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/filter/GroupIdRemoteRepositoryFilterSourceTest.java
@@ -26,6 +26,7 @@ import java.util.List;
 
 import org.eclipse.aether.DefaultRepositorySystemSession;
 import org.eclipse.aether.artifact.Artifact;
+import org.eclipse.aether.internal.impl.DefaultRepositoryKeyFunctionFactory;
 import org.eclipse.aether.internal.impl.DefaultRepositorySystemLifecycle;
 import org.eclipse.aether.repository.RemoteRepository;
 import org.eclipse.aether.resolution.ArtifactRequest;
@@ -42,7 +43,9 @@ public class GroupIdRemoteRepositoryFilterSourceTest extends 
RemoteRepositoryFil
     protected GroupIdRemoteRepositoryFilterSource 
getRemoteRepositoryFilterSource(
             DefaultRepositorySystemSession session, RemoteRepository 
remoteRepository) {
         return groupIdRemoteRepositoryFilterSource = new 
GroupIdRemoteRepositoryFilterSource(
-                new DefaultRepositorySystemLifecycle(), new 
PathProcessorSupport());
+                new DefaultRepositoryKeyFunctionFactory(),
+                new DefaultRepositorySystemLifecycle(),
+                new PathProcessorSupport());
     }
 
     @Override
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 7d7c21782..5db48616f 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
@@ -33,6 +33,7 @@ import org.eclipse.aether.artifact.Artifact;
 import org.eclipse.aether.impl.MetadataResolver;
 import org.eclipse.aether.impl.RemoteRepositoryManager;
 import org.eclipse.aether.internal.impl.DefaultArtifactPredicateFactory;
+import org.eclipse.aether.internal.impl.DefaultRepositoryKeyFunctionFactory;
 import org.eclipse.aether.internal.impl.DefaultRepositoryLayoutProvider;
 import org.eclipse.aether.internal.impl.Maven2RepositoryLayoutFactory;
 import org.eclipse.aether.repository.RemoteRepository;
@@ -84,7 +85,10 @@ public class PrefixesRemoteRepositoryFilterSourceTest 
extends RemoteRepositoryFi
                 new Maven2RepositoryLayoutFactory(
                         checksumsSelector(), new 
DefaultArtifactPredicateFactory(checksumsSelector()))));
         return new PrefixesRemoteRepositoryFilterSource(
-                () -> metadataResolver, () -> remoteRepositoryManager, 
layoutProvider);
+                new DefaultRepositoryKeyFunctionFactory(),
+                () -> metadataResolver,
+                () -> remoteRepositoryManager,
+                layoutProvider);
     }
 
     @Override
diff --git 
a/maven-resolver-spi/src/main/java/org/eclipse/aether/spi/remoterepo/RepositoryKeyFunctionFactory.java
 
b/maven-resolver-spi/src/main/java/org/eclipse/aether/spi/remoterepo/RepositoryKeyFunctionFactory.java
new file mode 100644
index 000000000..d537a10f6
--- /dev/null
+++ 
b/maven-resolver-spi/src/main/java/org/eclipse/aether/spi/remoterepo/RepositoryKeyFunctionFactory.java
@@ -0,0 +1,56 @@
+/*
+ * 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.spi.remoterepo;
+
+import org.eclipse.aether.RepositorySystemSession;
+import org.eclipse.aether.repository.RepositoryKeyFunction;
+
+/**
+ * A factory to create {@link RepositoryKeyFunction} instances.
+ *
+ * @since 2.0.14
+ */
+public interface RepositoryKeyFunctionFactory {
+    /**
+     * Returns system-wide repository key function.
+     *
+     * @param session The repository session, must not be {@code null}.
+     * @return The repository key function.
+     * @see #repositoryKeyFunction(Class, RepositorySystemSession, String, 
String)
+     * @see 
org.eclipse.aether.ConfigurationProperties#REPOSITORY_SYSTEM_REPOSITORY_KEY_FUNCTION
+     */
+    RepositoryKeyFunction systemRepositoryKeyFunction(RepositorySystemSession 
session);
+
+    /**
+     * Method that based on configuration returns the "repository key 
function". The returned function will be session
+     * cached if session is equipped with cache, otherwise it will be non 
cached. Method never returns {@code null}.
+     * Only the {@code configurationKey} parameter may be {@code null} in 
which case no configuration lookup happens
+     * but the {@code defaultValue} is directly used instead.
+     *
+     * @param owner The "owner" of key function (used to create cache-key), 
must not be {@code null}.
+     * @param session The repository session, must not be {@code null}.
+     * @param defaultValue The default value of repository key configuration, 
must not be {@code null}.
+     * @param configurationKey The configuration key to lookup configuration 
from, may be {@code null}, in which case
+     *                         no configuration lookup happens but the {@code 
defaultValue} is used to create the
+     *                         repository key function.
+     * @return The repository key function.
+     */
+    RepositoryKeyFunction repositoryKeyFunction(
+            Class<?> owner, RepositorySystemSession session, String 
defaultValue, String configurationKey);
+}
diff --git 
a/maven-resolver-spi/src/main/java/org/eclipse/aether/spi/remoterepo/package-info.java
 
b/maven-resolver-spi/src/main/java/org/eclipse/aether/spi/remoterepo/package-info.java
new file mode 100644
index 000000000..0a53f8235
--- /dev/null
+++ 
b/maven-resolver-spi/src/main/java/org/eclipse/aether/spi/remoterepo/package-info.java
@@ -0,0 +1,23 @@
+// CHECKSTYLE_OFF: RegexpHeader
+/*
+ * 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.
+ */
+/**
+ * The contract for remote repository customizations.
+ */
+package org.eclipse.aether.spi.remoterepo;
diff --git 
a/maven-resolver-supplier-mvn3/src/main/java/org/eclipse/aether/supplier/RepositorySystemSupplier.java
 
b/maven-resolver-supplier-mvn3/src/main/java/org/eclipse/aether/supplier/RepositorySystemSupplier.java
index c104ebc45..26abebd3e 100644
--- 
a/maven-resolver-supplier-mvn3/src/main/java/org/eclipse/aether/supplier/RepositorySystemSupplier.java
+++ 
b/maven-resolver-supplier-mvn3/src/main/java/org/eclipse/aether/supplier/RepositorySystemSupplier.java
@@ -72,6 +72,7 @@ import org.eclipse.aether.internal.impl.DefaultPathProcessor;
 import org.eclipse.aether.internal.impl.DefaultRemoteRepositoryManager;
 import org.eclipse.aether.internal.impl.DefaultRepositoryConnectorProvider;
 import org.eclipse.aether.internal.impl.DefaultRepositoryEventDispatcher;
+import org.eclipse.aether.internal.impl.DefaultRepositoryKeyFunctionFactory;
 import org.eclipse.aether.internal.impl.DefaultRepositoryLayoutProvider;
 import org.eclipse.aether.internal.impl.DefaultRepositorySystem;
 import org.eclipse.aether.internal.impl.DefaultRepositorySystemLifecycle;
@@ -138,6 +139,7 @@ import 
org.eclipse.aether.spi.connector.transport.http.ChecksumExtractorStrategy
 import org.eclipse.aether.spi.io.ChecksumProcessor;
 import org.eclipse.aether.spi.io.PathProcessor;
 import org.eclipse.aether.spi.localrepo.LocalRepositoryManagerFactory;
+import org.eclipse.aether.spi.remoterepo.RepositoryKeyFunctionFactory;
 import org.eclipse.aether.spi.resolution.ArtifactResolverPostProcessor;
 import org.eclipse.aether.spi.synccontext.SyncContextFactory;
 import org.eclipse.aether.spi.validator.ValidatorFactory;
@@ -247,7 +249,7 @@ public class RepositorySystemSupplier implements 
Supplier<RepositorySystem> {
     }
 
     protected LocalPathPrefixComposerFactory 
createLocalPathPrefixComposerFactory() {
-        return new DefaultLocalPathPrefixComposerFactory();
+        return new 
DefaultLocalPathPrefixComposerFactory(getRepositoryKeyFunctionFactory());
     }
 
     private RepositorySystemLifecycle repositorySystemLifecycle;
@@ -321,6 +323,20 @@ public class RepositorySystemSupplier implements 
Supplier<RepositorySystem> {
         return new DefaultUpdateCheckManager(getTrackingFileManager(), 
getUpdatePolicyAnalyzer(), getPathProcessor());
     }
 
+    private RepositoryKeyFunctionFactory repositoriesKeyFunctionFactory;
+
+    public final RepositoryKeyFunctionFactory 
getRepositoryKeyFunctionFactory() {
+        checkClosed();
+        if (repositoriesKeyFunctionFactory == null) {
+            repositoriesKeyFunctionFactory = 
createRepositoryKeyFunctionFactory();
+        }
+        return repositoriesKeyFunctionFactory;
+    }
+
+    protected RepositoryKeyFunctionFactory 
createRepositoryKeyFunctionFactory() {
+        return new DefaultRepositoryKeyFunctionFactory();
+    }
+
     private Map<String, NamedLockFactory> namedLockFactories;
 
     public final Map<String, NamedLockFactory> getNamedLockFactories() {
@@ -484,13 +500,18 @@ public class RepositorySystemSupplier implements 
Supplier<RepositorySystem> {
 
     protected LocalRepositoryProvider createLocalRepositoryProvider() {
         LocalPathComposer localPathComposer = getLocalPathComposer();
+        RepositoryKeyFunctionFactory repositoryKeyFunctionFactory = 
getRepositoryKeyFunctionFactory();
         HashMap<String, LocalRepositoryManagerFactory> 
localRepositoryProviders = new HashMap<>(2);
         localRepositoryProviders.put(
-                SimpleLocalRepositoryManagerFactory.NAME, new 
SimpleLocalRepositoryManagerFactory(localPathComposer));
+                SimpleLocalRepositoryManagerFactory.NAME,
+                new SimpleLocalRepositoryManagerFactory(localPathComposer, 
repositoryKeyFunctionFactory));
         localRepositoryProviders.put(
                 EnhancedLocalRepositoryManagerFactory.NAME,
                 new EnhancedLocalRepositoryManagerFactory(
-                        localPathComposer, getTrackingFileManager(), 
getLocalPathPrefixComposerFactory()));
+                        localPathComposer,
+                        getTrackingFileManager(),
+                        getLocalPathPrefixComposerFactory(),
+                        repositoryKeyFunctionFactory));
         return new DefaultLocalRepositoryProvider(localRepositoryProviders);
     }
 
@@ -505,7 +526,8 @@ public class RepositorySystemSupplier implements 
Supplier<RepositorySystem> {
     }
 
     protected RemoteRepositoryManager createRemoteRepositoryManager() {
-        return new DefaultRemoteRepositoryManager(getUpdatePolicyAnalyzer(), 
getChecksumPolicyProvider());
+        return new DefaultRemoteRepositoryManager(
+                getUpdatePolicyAnalyzer(), getChecksumPolicyProvider(), 
getRepositoryKeyFunctionFactory());
     }
 
     private Map<String, RemoteRepositoryFilterSource> 
remoteRepositoryFilterSources;
@@ -522,11 +544,15 @@ public class RepositorySystemSupplier implements 
Supplier<RepositorySystem> {
         HashMap<String, RemoteRepositoryFilterSource> result = new HashMap<>();
         result.put(
                 GroupIdRemoteRepositoryFilterSource.NAME,
-                new 
GroupIdRemoteRepositoryFilterSource(getRepositorySystemLifecycle(), 
getPathProcessor()));
+                new GroupIdRemoteRepositoryFilterSource(
+                        getRepositoryKeyFunctionFactory(), 
getRepositorySystemLifecycle(), getPathProcessor()));
         result.put(
                 PrefixesRemoteRepositoryFilterSource.NAME,
                 new PrefixesRemoteRepositoryFilterSource(
-                        this::getMetadataResolver, 
this::getRemoteRepositoryManager, getRepositoryLayoutProvider()));
+                        getRepositoryKeyFunctionFactory(),
+                        this::getMetadataResolver,
+                        this::getRemoteRepositoryManager,
+                        getRepositoryLayoutProvider()));
         return result;
     }
 
@@ -586,11 +612,15 @@ public class RepositorySystemSupplier implements 
Supplier<RepositorySystem> {
         HashMap<String, TrustedChecksumsSource> result = new HashMap<>();
         result.put(
                 SparseDirectoryTrustedChecksumsSource.NAME,
-                new 
SparseDirectoryTrustedChecksumsSource(getChecksumProcessor(), 
getLocalPathComposer()));
+                new SparseDirectoryTrustedChecksumsSource(
+                        getRepositoryKeyFunctionFactory(), 
getChecksumProcessor(), getLocalPathComposer()));
         result.put(
                 SummaryFileTrustedChecksumsSource.NAME,
                 new SummaryFileTrustedChecksumsSource(
-                        getLocalPathComposer(), 
getRepositorySystemLifecycle(), getPathProcessor()));
+                        getRepositoryKeyFunctionFactory(),
+                        getLocalPathComposer(),
+                        getRepositorySystemLifecycle(),
+                        getPathProcessor()));
         return result;
     }
 
diff --git 
a/maven-resolver-supplier-mvn4/src/main/java/org/eclipse/aether/supplier/RepositorySystemSupplier.java
 
b/maven-resolver-supplier-mvn4/src/main/java/org/eclipse/aether/supplier/RepositorySystemSupplier.java
index 0fc19c0e9..870b6b1c8 100644
--- 
a/maven-resolver-supplier-mvn4/src/main/java/org/eclipse/aether/supplier/RepositorySystemSupplier.java
+++ 
b/maven-resolver-supplier-mvn4/src/main/java/org/eclipse/aether/supplier/RepositorySystemSupplier.java
@@ -76,6 +76,7 @@ import org.eclipse.aether.internal.impl.DefaultPathProcessor;
 import org.eclipse.aether.internal.impl.DefaultRemoteRepositoryManager;
 import org.eclipse.aether.internal.impl.DefaultRepositoryConnectorProvider;
 import org.eclipse.aether.internal.impl.DefaultRepositoryEventDispatcher;
+import org.eclipse.aether.internal.impl.DefaultRepositoryKeyFunctionFactory;
 import org.eclipse.aether.internal.impl.DefaultRepositoryLayoutProvider;
 import org.eclipse.aether.internal.impl.DefaultRepositorySystem;
 import org.eclipse.aether.internal.impl.DefaultRepositorySystemLifecycle;
@@ -142,6 +143,7 @@ import 
org.eclipse.aether.spi.connector.transport.http.ChecksumExtractorStrategy
 import org.eclipse.aether.spi.io.ChecksumProcessor;
 import org.eclipse.aether.spi.io.PathProcessor;
 import org.eclipse.aether.spi.localrepo.LocalRepositoryManagerFactory;
+import org.eclipse.aether.spi.remoterepo.RepositoryKeyFunctionFactory;
 import org.eclipse.aether.spi.resolution.ArtifactResolverPostProcessor;
 import org.eclipse.aether.spi.synccontext.SyncContextFactory;
 import org.eclipse.aether.spi.validator.ValidatorFactory;
@@ -251,7 +253,7 @@ public class RepositorySystemSupplier implements 
Supplier<RepositorySystem> {
     }
 
     protected LocalPathPrefixComposerFactory 
createLocalPathPrefixComposerFactory() {
-        return new DefaultLocalPathPrefixComposerFactory();
+        return new 
DefaultLocalPathPrefixComposerFactory(getRepositoryKeyFunctionFactory());
     }
 
     private RepositorySystemLifecycle repositorySystemLifecycle;
@@ -325,6 +327,20 @@ public class RepositorySystemSupplier implements 
Supplier<RepositorySystem> {
         return new DefaultUpdateCheckManager(getTrackingFileManager(), 
getUpdatePolicyAnalyzer(), getPathProcessor());
     }
 
+    private RepositoryKeyFunctionFactory repositoriesKeyFunctionFactory;
+
+    public final RepositoryKeyFunctionFactory 
getRepositoryKeyFunctionFactory() {
+        checkClosed();
+        if (repositoriesKeyFunctionFactory == null) {
+            repositoriesKeyFunctionFactory = 
createRepositoryKeyFunctionFactory();
+        }
+        return repositoriesKeyFunctionFactory;
+    }
+
+    protected RepositoryKeyFunctionFactory 
createRepositoryKeyFunctionFactory() {
+        return new DefaultRepositoryKeyFunctionFactory();
+    }
+
     private Map<String, NamedLockFactory> namedLockFactories;
 
     public final Map<String, NamedLockFactory> getNamedLockFactories() {
@@ -488,13 +504,18 @@ public class RepositorySystemSupplier implements 
Supplier<RepositorySystem> {
 
     protected LocalRepositoryProvider createLocalRepositoryProvider() {
         LocalPathComposer localPathComposer = getLocalPathComposer();
+        RepositoryKeyFunctionFactory repositoryKeyFunctionFactory = 
getRepositoryKeyFunctionFactory();
         HashMap<String, LocalRepositoryManagerFactory> 
localRepositoryProviders = new HashMap<>(2);
         localRepositoryProviders.put(
-                SimpleLocalRepositoryManagerFactory.NAME, new 
SimpleLocalRepositoryManagerFactory(localPathComposer));
+                SimpleLocalRepositoryManagerFactory.NAME,
+                new SimpleLocalRepositoryManagerFactory(localPathComposer, 
repositoryKeyFunctionFactory));
         localRepositoryProviders.put(
                 EnhancedLocalRepositoryManagerFactory.NAME,
                 new EnhancedLocalRepositoryManagerFactory(
-                        localPathComposer, getTrackingFileManager(), 
getLocalPathPrefixComposerFactory()));
+                        localPathComposer,
+                        getTrackingFileManager(),
+                        getLocalPathPrefixComposerFactory(),
+                        repositoryKeyFunctionFactory));
         return new DefaultLocalRepositoryProvider(localRepositoryProviders);
     }
 
@@ -509,7 +530,8 @@ public class RepositorySystemSupplier implements 
Supplier<RepositorySystem> {
     }
 
     protected RemoteRepositoryManager createRemoteRepositoryManager() {
-        return new DefaultRemoteRepositoryManager(getUpdatePolicyAnalyzer(), 
getChecksumPolicyProvider());
+        return new DefaultRemoteRepositoryManager(
+                getUpdatePolicyAnalyzer(), getChecksumPolicyProvider(), 
getRepositoryKeyFunctionFactory());
     }
 
     private Map<String, RemoteRepositoryFilterSource> 
remoteRepositoryFilterSources;
@@ -526,11 +548,15 @@ public class RepositorySystemSupplier implements 
Supplier<RepositorySystem> {
         HashMap<String, RemoteRepositoryFilterSource> result = new HashMap<>();
         result.put(
                 GroupIdRemoteRepositoryFilterSource.NAME,
-                new 
GroupIdRemoteRepositoryFilterSource(getRepositorySystemLifecycle(), 
getPathProcessor()));
+                new GroupIdRemoteRepositoryFilterSource(
+                        getRepositoryKeyFunctionFactory(), 
getRepositorySystemLifecycle(), getPathProcessor()));
         result.put(
                 PrefixesRemoteRepositoryFilterSource.NAME,
                 new PrefixesRemoteRepositoryFilterSource(
-                        this::getMetadataResolver, 
this::getRemoteRepositoryManager, getRepositoryLayoutProvider()));
+                        getRepositoryKeyFunctionFactory(),
+                        this::getMetadataResolver,
+                        this::getRemoteRepositoryManager,
+                        getRepositoryLayoutProvider()));
         return result;
     }
 
@@ -590,11 +616,15 @@ public class RepositorySystemSupplier implements 
Supplier<RepositorySystem> {
         HashMap<String, TrustedChecksumsSource> result = new HashMap<>();
         result.put(
                 SparseDirectoryTrustedChecksumsSource.NAME,
-                new 
SparseDirectoryTrustedChecksumsSource(getChecksumProcessor(), 
getLocalPathComposer()));
+                new SparseDirectoryTrustedChecksumsSource(
+                        getRepositoryKeyFunctionFactory(), 
getChecksumProcessor(), getLocalPathComposer()));
         result.put(
                 SummaryFileTrustedChecksumsSource.NAME,
                 new SummaryFileTrustedChecksumsSource(
-                        getLocalPathComposer(), 
getRepositorySystemLifecycle(), getPathProcessor()));
+                        getRepositoryKeyFunctionFactory(),
+                        getLocalPathComposer(),
+                        getRepositorySystemLifecycle(),
+                        getPathProcessor()));
         return result;
     }
 
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 5c8bbbbcd..6cacbb707 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
@@ -18,27 +18,26 @@
  */
 package org.eclipse.aether.util.repository;
 
-import java.util.Arrays;
-import java.util.Collection;
-import java.util.Collections;
+import java.util.ArrayList;
+import java.util.Comparator;
 import java.util.Locale;
-import java.util.concurrent.ConcurrentHashMap;
-import java.util.function.Function;
-import java.util.function.Predicate;
+import java.util.SortedSet;
+import java.util.TreeSet;
 
-import org.eclipse.aether.RepositorySystemSession;
 import org.eclipse.aether.repository.ArtifactRepository;
 import org.eclipse.aether.repository.RemoteRepository;
+import org.eclipse.aether.repository.RepositoryKeyFunction;
 import org.eclipse.aether.util.PathUtils;
 import org.eclipse.aether.util.StringDigestUtil;
 
-import static java.util.Objects.requireNonNull;
-
 /**
- * Helper class for {@link ArtifactRepository#getId()} handling. This class 
provides  helper function (cached or uncached)
- * to get id of repository as it was originally envisioned: as path safe. 
While POMs are validated by Maven, there are
- * POMs out there that somehow define repositories with unsafe characters in 
their id. The problem affects mostly
+ * Helper class for {@link ArtifactRepository#getId()} handling. This class 
provides  helper methods
+ * to get id of repository as it was originally envisioned: as path safe, 
unique, etc. While POMs are validated by Maven,
+ * there are POMs out there that somehow define repositories with unsafe 
characters in their id. The problem affects mostly
  * {@link RemoteRepository} instances, as all other implementations have fixed 
ids that are path safe.
+ * <p>
+ * <em>Important:</em> multiple of these provided methods are not trivial 
processing-wise, and some sort of
+ * caching is warmly recommended.
  *
  * @see PathUtils
  * @since 2.0.11
@@ -46,103 +45,167 @@ import static java.util.Objects.requireNonNull;
 public final class RepositoryIdHelper {
     private RepositoryIdHelper() {}
 
-    private static final String CENTRAL_REPOSITORY_ID = "central";
-    private static final Collection<String> CENTRAL_URLS = 
Collections.unmodifiableList(Arrays.asList(
-            "https://repo.maven.apache.org/maven2";,
-            "https://repo1.maven.org/maven2";,
-            "https://maven-central.storage-download.googleapis.com/maven2";));
-    private static final Predicate<RemoteRepository> CENTRAL_DIRECT_ONLY =
-            remoteRepository -> 
CENTRAL_REPOSITORY_ID.equals(remoteRepository.getId())
-                    && 
"https".equals(remoteRepository.getProtocol().toLowerCase(Locale.ENGLISH))
-                    && CENTRAL_URLS.stream().anyMatch(remoteUrl -> {
-                        String rurl = 
remoteRepository.getUrl().toLowerCase(Locale.ENGLISH);
-                        if (rurl.endsWith("/")) {
-                            rurl = rurl.substring(0, rurl.length() - 1);
-                        }
-                        return rurl.equals(remoteUrl);
-                    })
-                    && remoteRepository.getPolicy(false).isEnabled()
-                    && !remoteRepository.getPolicy(true).isEnabled()
-                    && remoteRepository.getMirroredRepositories().isEmpty()
-                    && !remoteRepository.isRepositoryManager()
-                    && !remoteRepository.isBlocked();
+    /**
+     * Supported {@code repositoryKey} types.
+     *
+     * @since 2.0.14
+     */
+    public enum RepositoryKeyType {
+        /**
+         * The "simple" repository key, was default in Maven 3.
+         */
+        SIMPLE,
+        /**
+         * Crafts repository key using normalized {@link 
RemoteRepository#getId()}.
+         */
+        NID,
+        /**
+         * Crafts repository key using hashed {@link 
RemoteRepository#getUrl()}.
+         */
+        HURL,
+        /**
+         * Crafts unique repository key using normalized {@link 
RemoteRepository#getId()} and hashed {@link RemoteRepository#getUrl()}.
+         */
+        NID_HURL,
+        /**
+         * Crafts normalized unique repository key using {@link 
RemoteRepository#getId()} and all the remaining properties of
+         * {@link RemoteRepository} ignoring actual list of mirrors, if any 
(but mirrors are split).
+         */
+        NGURK,
+        /**
+         * Crafts unique repository key using {@link RemoteRepository#getId()} 
and all the remaining properties of
+         * {@link RemoteRepository}.
+         */
+        GURK
+    }
 
     /**
-     * Creates unique repository id for given {@link RemoteRepository}. For 
Maven Central this method will return
-     * string "central", while for any other remote repository it will return 
string created as
-     * {@code $(repository.id)-sha1(repository-aspects)}. The key material 
contains all relevant aspects
-     * of remote repository, so repository with same ID even if just policy 
changes (enabled/disabled), will map to
-     * different string id. The checksum and update policies are not 
participating in key creation.
-     * <p>
-     * This method is costly, so should be invoked sparingly, or cache results 
if needed.
-     * <p>
-     * <em>Important:</em>Do not use this method, or at least <em>do consider 
when do you want to use it</em>, as it
-     * totally disconnects repositories used in session. This method may be 
used under some special circumstances
-     * (ie reporting), but <em>must not be used within Resolver (and Maven) 
session for "usual" resolution and
-     * deployment use cases</em>.
+     * Selector method for {@link RepositoryKeyFunction} based on string 
representation of {@link RepositoryKeyType}
+     * enum.
      */
-    public static String remoteRepositoryUniqueId(RemoteRepository repository) 
{
-        if (CENTRAL_DIRECT_ONLY.test(repository)) {
-            return CENTRAL_REPOSITORY_ID;
-        } else {
-            StringBuilder buffer = new StringBuilder(256);
-            buffer.append(repository.getId());
-            buffer.append(" (").append(repository.getUrl());
-            buffer.append(", ").append(repository.getContentType());
-            boolean r = repository.getPolicy(false).isEnabled(),
-                    s = repository.getPolicy(true).isEnabled();
-            if (r && s) {
-                buffer.append(", releases+snapshots");
-            } else if (r) {
-                buffer.append(", releases");
-            } else if (s) {
-                buffer.append(", snapshots");
-            } else {
-                buffer.append(", disabled");
-            }
-            if (repository.isRepositoryManager()) {
-                buffer.append(", managed(");
-                for (RemoteRepository mirroredRepo : 
repository.getMirroredRepositories()) {
-                    buffer.append(remoteRepositoryUniqueId(mirroredRepo));
-                }
-                buffer.append(")");
+    public static RepositoryKeyFunction getRepositoryKeyFunction(String 
keyTypeString) {
+        RepositoryKeyType keyType = 
RepositoryKeyType.valueOf(keyTypeString.toUpperCase(Locale.ENGLISH));
+        switch (keyType) {
+            case SIMPLE:
+                return RepositoryIdHelper::simpleRepositoryKey;
+            case NID:
+                return RepositoryIdHelper::nidRepositoryKey;
+            case HURL:
+                return RepositoryIdHelper::hurlRepositoryKey;
+            case NID_HURL:
+                return RepositoryIdHelper::nidAndHurlRepositoryKey;
+            case NGURK:
+                return 
RepositoryIdHelper::normalizedGloballyUniqueRepositoryKey;
+            case GURK:
+                return RepositoryIdHelper::globallyUniqueRepositoryKey;
+            default:
+                throw new IllegalArgumentException("Unknown repository key 
type: " + keyType.name());
+        }
+    }
+
+    /**
+     * Simple {@code repositoryKey} function (classic). Returns {@link 
RemoteRepository#getId()}, unless
+     * {@link RemoteRepository#isRepositoryManager()} returns {@code true}, in 
which case this method creates
+     * unique identifier based on ID and current configuration of the remote 
repository and context.
+     * <p>
+     * This was the default {@code repositoryKey} method in Maven 3. Is 
exposed (others key methods are private) as
+     * it is directly used by "simple" LRM.
+     *
+     * @since 2.0.14
+     **/
+    public static String simpleRepositoryKey(RemoteRepository repository, 
String context) {
+        if (repository.isRepositoryManager()) {
+            StringBuilder buffer = new StringBuilder(128);
+            buffer.append(idToPathSegment(repository));
+            buffer.append('-');
+            SortedSet<String> subKeys = new TreeSet<>();
+            for (RemoteRepository mirroredRepo : 
repository.getMirroredRepositories()) {
+                subKeys.add(mirroredRepo.getId());
             }
-            if (repository.isBlocked()) {
-                buffer.append(", blocked");
+            StringDigestUtil sha1 = StringDigestUtil.sha1();
+            sha1.update(context);
+            for (String subKey : subKeys) {
+                sha1.update(subKey);
             }
-            buffer.append(")");
-            return idToPathSegment(repository) + "-" + 
StringDigestUtil.sha1(buffer.toString());
+            buffer.append(sha1.digest());
+            return buffer.toString();
+        } else {
+            return idToPathSegment(repository);
         }
     }
 
     /**
-     * Returns same instance of (session cached) function for session.
-     */
-    @SuppressWarnings("unchecked")
-    public static Function<ArtifactRepository, String> 
cachedIdToPathSegment(RepositorySystemSession session) {
-        requireNonNull(session, "session");
-        return (Function<ArtifactRepository, String>) session.getData()
-                .computeIfAbsent(
-                        RepositoryIdHelper.class.getSimpleName() + 
"-idToPathSegmentFunction",
-                        () -> cachedIdToPathSegmentFunction(session));
+     * The ID {@code repositoryKey} function that uses only the {@link 
RemoteRepository#getId()} value to derive a key.
+     *
+     * @since 2.0.14
+     **/
+    private static String nidRepositoryKey(RemoteRepository repository, String 
context) {
+        String seed = null;
+        if (repository.isRepositoryManager() && context != null && 
!context.isEmpty()) {
+            seed += context;
+        }
+        return idToPathSegment(repository) + (seed == null ? "" : "-" + 
StringDigestUtil.sha1(seed));
     }
 
     /**
-     * Returns new instance of function backed by cached or uncached (if 
session has no cache set)
-     * {@link #idToPathSegment(ArtifactRepository)} method call.
-     */
-    @SuppressWarnings("unchecked")
-    private static Function<ArtifactRepository, String> 
cachedIdToPathSegmentFunction(RepositorySystemSession session) {
-        if (session.getCache() != null) {
-            return repository -> ((ConcurrentHashMap<String, String>) 
session.getCache()
-                            .computeIfAbsent(
-                                    session,
-                                    RepositoryIdHelper.class.getSimpleName() + 
"-idToPathSegmentCache",
-                                    ConcurrentHashMap::new))
-                    .computeIfAbsent(repository.getId(), id -> 
idToPathSegment(repository));
-        } else {
-            return RepositoryIdHelper::idToPathSegment; // uncached
+     * The URL {@code repositoryKey} function that uses only the {@link 
RemoteRepository#getUrl()} hash to derive a key.
+     *
+     * @since 2.0.14
+     **/
+    private static String hurlRepositoryKey(RemoteRepository repository, 
String context) {
+        String seed = null;
+        if (repository.isRepositoryManager() && context != null && 
!context.isEmpty()) {
+            seed += context;
+        }
+        return StringDigestUtil.sha1(repository.getUrl()) + (seed == null ? "" 
: "-" + StringDigestUtil.sha1(seed));
+    }
+
+    /**
+     * The ID and URL {@code repositoryKey} function. This method creates 
unique identifier based on ID and URL
+     * of the remote repository.
+     *
+     * @since 2.0.14
+     **/
+    private static String nidAndHurlRepositoryKey(RemoteRepository repository, 
String context) {
+        String seed = repository.getUrl();
+        if (repository.isRepositoryManager() && context != null && 
!context.isEmpty()) {
+            seed += context;
+        }
+        return idToPathSegment(repository) + "-" + StringDigestUtil.sha1(seed);
+    }
+
+    /**
+     * Normalized globally unique {@code repositoryKey} function. This method 
creates unique identifier based on ID and current
+     * configuration of the remote repository ignoring mirrors (it records the 
fact repository is a mirror, but ignores
+     * mirrored repositories). If {@link 
RemoteRepository#isRepositoryManager()} returns {@code true}, the passed in
+     * {@code context} string is factored in as well.
+     *
+     * @since 2.0.14
+     **/
+    private static String 
normalizedGloballyUniqueRepositoryKey(RemoteRepository repository, String 
context) {
+        String seed = remoteRepositoryDescription(repository, false);
+        if (repository.isRepositoryManager() && context != null && 
!context.isEmpty()) {
+            seed += context;
         }
+        return idToPathSegment(repository) + "-" + StringDigestUtil.sha1(seed);
+    }
+
+    /**
+     * Globally unique {@code repositoryKey} function. This method creates 
unique identifier based on ID and current
+     * configuration of the remote repository. If {@link 
RemoteRepository#isRepositoryManager()} returns {@code true},
+     * the passed in {@code context} string is factored in as well.
+     * <p>
+     * <em>Important:</em> this repository key can be considered "stable" for 
normal remote repositories (where only
+     * ID and URL matters). But, for mirror repositories, the key will change 
if mirror members change.
+     *
+     * @since 2.0.14
+     **/
+    private static String globallyUniqueRepositoryKey(RemoteRepository 
repository, String context) {
+        String seed = remoteRepositoryDescription(repository, true);
+        if (repository.isRepositoryManager() && context != null && 
!context.isEmpty()) {
+            seed += context;
+        }
+        return idToPathSegment(repository) + "-" + StringDigestUtil.sha1(seed);
     }
 
     /**
@@ -150,11 +213,6 @@ public final class RepositoryIdHelper {
      * returned repository ID is "path segment" safe. Ideally, this method 
should never modify repository ID, as
      * Maven validation prevents use of illegal FS characters in them, but we 
found in Maven Central several POMs that
      * define remote repositories with illegal FS characters in their ID.
-     * <p>
-     * This method is simplistic on purpose, and if frequently used, best if 
results are cached (per session),
-     * see {@link #cachedIdToPathSegment(RepositorySystemSession)} method.
-     *
-     * @see #cachedIdToPathSegment(RepositorySystemSession)
      */
     private static String idToPathSegment(ArtifactRepository repository) {
         if (repository instanceof RemoteRepository) {
@@ -163,4 +221,53 @@ public final class RepositoryIdHelper {
             return repository.getId();
         }
     }
+
+    /**
+     * Creates unique string for given {@link RemoteRepository}. Ignores 
following properties:
+     * <ul>
+     *     <li>{@link RemoteRepository#getAuthentication()}</li>
+     *     <li>{@link RemoteRepository#getProxy()}</li>
+     *     <li>{@link RemoteRepository#getIntent()}</li>
+     * </ul>
+     */
+    private static String remoteRepositoryDescription(RemoteRepository 
repository, boolean mirrorDetails) {
+        StringBuilder buffer = new StringBuilder(256);
+        buffer.append(repository.getId());
+        buffer.append(" (").append(repository.getUrl());
+        buffer.append(", ").append(repository.getContentType());
+        boolean r = repository.getPolicy(false).isEnabled(),
+                s = repository.getPolicy(true).isEnabled();
+        if (r && s) {
+            buffer.append(", releases+snapshots");
+        } else if (r) {
+            buffer.append(", releases");
+        } else if (s) {
+            buffer.append(", snapshots");
+        } else {
+            buffer.append(", disabled");
+        }
+        if (repository.isRepositoryManager()) {
+            buffer.append(", managed");
+        }
+        if (!repository.getMirroredRepositories().isEmpty()) {
+            if (mirrorDetails) {
+                // sort them to make it stable ordering
+                ArrayList<RemoteRepository> mirroredRepositories =
+                        new ArrayList<>(repository.getMirroredRepositories());
+                
mirroredRepositories.sort(Comparator.comparing(RemoteRepository::getId));
+                buffer.append(", mirrorOf(");
+                for (RemoteRepository mirroredRepo : mirroredRepositories) {
+                    buffer.append(remoteRepositoryDescription(mirroredRepo, 
true));
+                }
+                buffer.append(")");
+            } else {
+                buffer.append(", isMirror");
+            }
+        }
+        if (repository.isBlocked()) {
+            buffer.append(", blocked");
+        }
+        buffer.append(")");
+        return buffer.toString();
+    }
 }
diff --git 
a/maven-resolver-util/src/test/java/org/eclipse/aether/util/repository/RepositoryIdHelperTest.java
 
b/maven-resolver-util/src/test/java/org/eclipse/aether/util/repository/RepositoryIdHelperTest.java
index bdbc15c1e..74f5b73b5 100644
--- 
a/maven-resolver-util/src/test/java/org/eclipse/aether/util/repository/RepositoryIdHelperTest.java
+++ 
b/maven-resolver-util/src/test/java/org/eclipse/aether/util/repository/RepositoryIdHelperTest.java
@@ -18,59 +18,87 @@
  */
 package org.eclipse.aether.util.repository;
 
-import java.util.function.Function;
+import java.util.Collections;
 
-import org.eclipse.aether.DefaultRepositoryCache;
-import org.eclipse.aether.DefaultRepositorySystemSession;
-import org.eclipse.aether.repository.ArtifactRepository;
 import org.eclipse.aether.repository.RemoteRepository;
+import org.eclipse.aether.repository.RepositoryKeyFunction;
+import org.eclipse.aether.repository.RepositoryPolicy;
 import org.junit.jupiter.api.Test;
 
 import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.assertNotEquals;
-import static org.junit.jupiter.api.Assertions.assertNotSame;
-import static org.junit.jupiter.api.Assertions.assertSame;
 
 public class RepositoryIdHelperTest {
-    @Test
-    void caching() {
-        DefaultRepositorySystemSession session = new 
DefaultRepositorySystemSession(s -> false);
-        session.setCache(new DefaultRepositoryCache()); // session has cache 
set
-        Function<ArtifactRepository, String> safeId = 
RepositoryIdHelper.cachedIdToPathSegment(session);
-
-        RemoteRepository good = new RemoteRepository.Builder("good", 
"default", "https://somewhere.com";).build();
-        RemoteRepository bad = new RemoteRepository.Builder("bad/id", 
"default", "https://somewhere.com";).build();
-
-        String goodId = good.getId();
-        String goodFixedId = safeId.apply(good);
-        assertEquals(goodId, goodFixedId);
-        assertSame(goodFixedId, safeId.apply(good));
+    private final RemoteRepository central = new RemoteRepository.Builder(
+                    "central", "default", 
"https://repo.maven.apache.org/maven2/";)
+            .setSnapshotPolicy(new RepositoryPolicy(false, null, null))
+            .build();
+    private final RemoteRepository central_legacy = new 
RemoteRepository.Builder(
+                    "central", "default", "https://repo1.maven.org/maven2/";)
+            .setSnapshotPolicy(new RepositoryPolicy(false, null, null))
+            .build();
+    private final RemoteRepository central_trivial =
+            new RemoteRepository.Builder("central", "default", 
"https://repo1.maven.org/maven2/";).build();
+    private final RemoteRepository central_mirror = new 
RemoteRepository.Builder(
+                    "my-mirror", "default", "https://mymrm.com/maven/";)
+            .setSnapshotPolicy(new RepositoryPolicy(false, null, null))
+            .setMirroredRepositories(Collections.singletonList(central))
+            .build();
+    private final RemoteRepository asf_snapshots = new 
RemoteRepository.Builder(
+                    "apache-snapshots", "default", 
"https://repository.apache.org/content/repositories/snapshots/";)
+            .setReleasePolicy(new RepositoryPolicy(false, null, null))
+            .build();
+    private final RemoteRepository file_unfriendly = new 
RemoteRepository.Builder(
+                    "apache/snapshots", "default", 
"https://repository.apache.org/content/repositories/snapshots/";)
+            .setReleasePolicy(new RepositoryPolicy(false, null, null))
+            .build();
 
-        String badId = bad.getId();
-        String badFixedId = safeId.apply(bad);
-        assertNotEquals(badId, badFixedId);
-        assertEquals("bad-SLASH-id", badFixedId);
-        assertSame(badFixedId, safeId.apply(bad));
+    @Test
+    void simple() {
+        RepositoryKeyFunction func =
+                
RepositoryIdHelper.getRepositoryKeyFunction(RepositoryIdHelper.RepositoryKeyType.SIMPLE.name());
+        assertEquals("central", func.apply(central, null));
+        assertEquals("central", func.apply(central_legacy, null));
+        assertEquals("central", func.apply(central_trivial, null));
+        assertEquals("my-mirror", func.apply(central_mirror, null));
+        assertEquals("apache-snapshots", func.apply(asf_snapshots, null));
+        assertEquals("apache-SLASH-snapshots", func.apply(file_unfriendly, 
null));
     }
 
     @Test
-    void nonCaching() {
-        DefaultRepositorySystemSession session = new 
DefaultRepositorySystemSession(s -> false);
-        session.setCache(null); // session has no cache set
-        Function<ArtifactRepository, String> safeId = 
RepositoryIdHelper.cachedIdToPathSegment(session);
-
-        RemoteRepository good = new RemoteRepository.Builder("good", 
"default", "https://somewhere.com";).build();
-        RemoteRepository bad = new RemoteRepository.Builder("bad/id", 
"default", "https://somewhere.com";).build();
+    void nid() {
+        RepositoryKeyFunction func =
+                
RepositoryIdHelper.getRepositoryKeyFunction(RepositoryIdHelper.RepositoryKeyType.NID.name());
+        assertEquals("central", func.apply(central, null));
+        assertEquals("central", func.apply(central_legacy, null));
+        assertEquals("central", func.apply(central_trivial, null));
+        assertEquals("my-mirror", func.apply(central_mirror, null));
+        assertEquals("apache-snapshots", func.apply(asf_snapshots, null));
+        assertEquals("apache-SLASH-snapshots", func.apply(file_unfriendly, 
null));
+    }
 
-        String goodId = good.getId();
-        String goodFixedId = safeId.apply(good);
-        assertEquals(goodId, goodFixedId);
-        assertNotSame(goodFixedId, safeId.apply(good));
+    @Test
+    void nidHurl() {
+        RepositoryKeyFunction func =
+                
RepositoryIdHelper.getRepositoryKeyFunction(RepositoryIdHelper.RepositoryKeyType.NID_HURL.name());
+        assertEquals("central-0aeeb43004cebeccad6fdf0fec27084167d5880a", 
func.apply(central, null));
+        assertEquals("central-a27bb55260d64d6035671716555d10644054c89d", 
func.apply(central_legacy, null));
+        assertEquals("central-a27bb55260d64d6035671716555d10644054c89d", 
func.apply(central_trivial, null));
+        assertEquals("my-mirror-eb106d0adc4a56b55067f069a2fed5526fd6cb18", 
func.apply(central_mirror, null));
+        
assertEquals("apache-snapshots-5c4f89479e3c71fb3c2fbc6213fb00f6371fbb96", 
func.apply(asf_snapshots, null));
+        assertEquals(
+                
"apache-SLASH-snapshots-5c4f89479e3c71fb3c2fbc6213fb00f6371fbb96", 
func.apply(file_unfriendly, null));
+    }
 
-        String badId = bad.getId();
-        String badFixedId = safeId.apply(bad);
-        assertNotEquals(badId, badFixedId);
-        assertEquals("bad-SLASH-id", badFixedId);
-        assertNotSame(badFixedId, safeId.apply(bad));
+    @Test
+    void ngurk() {
+        RepositoryKeyFunction func =
+                
RepositoryIdHelper.getRepositoryKeyFunction(RepositoryIdHelper.RepositoryKeyType.NGURK.name());
+        assertEquals("central-ff5deec948d038ceb880e13e9f61455903b0d0a6", 
func.apply(central, null));
+        assertEquals("central-ffb5c2a34e47c429571fc29752730e9ce6e44d79", 
func.apply(central_legacy, null));
+        assertEquals("central-acc6c84ca8674036eda6708502b5f02fb09a9731", 
func.apply(central_trivial, null));
+        assertEquals("my-mirror-256631324003f5718aca1e80db8377c7f9ecd852", 
func.apply(central_mirror, null));
+        
assertEquals("apache-snapshots-62375dea6c3c8bebdbae5cca79a4f5ad2eaebf34", 
func.apply(asf_snapshots, null));
+        assertEquals(
+                
"apache-SLASH-snapshots-2e126ec79795c077a3c42dc536fa28c13c3bdb0d", 
func.apply(file_unfriendly, null));
     }
 }
diff --git a/src/site/markdown/configuration.md 
b/src/site/markdown/configuration.md
index 4735a16ed..295203c09 100644
--- a/src/site/markdown/configuration.md
+++ b/src/site/markdown/configuration.md
@@ -107,6 +107,7 @@ To modify this file, edit the template and regenerate.
 | `"aether.remoteRepositoryFilter.prefixes.skipped"` | `Boolean` | 
Configuration to skip the Prefixes filter for given request. This configuration 
is evaluated and if <code>true</code> 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 
<code>#CONFIG_PROP_ENABLED</code> still show the "real st [...]
 | `"aether.remoteRepositoryFilter.prefixes.useMirroredRepositories"` | 
`Boolean` | Configuration to allow Prefixes filter to auto-discover prefixes 
from mirrored repositories as well. For this to work <em>Maven should be 
aware</em> 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 <code>#CONFIG_PROP_ENABLED</code> with 
repository ID suffix. |  `false`  | [...]
 | `"aether.remoteRepositoryFilter.prefixes.useRepositoryManagers"` | `Boolean` 
| Configuration to allow Prefixes filter to auto-discover prefixes from 
repository managers as well. For this to work <em>Maven should be aware</em> 
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 <code>#CONFIG_PROP_ENABLED</code> with repository ID 
suffix. <em>Note: as of today, n [...]
+| `"aether.remoteRepositoryFilter.repositoryKeyFunction"` | `String` | 
<b>Experimental:</b> Configuration for "repository key" function. Note: 
repository key functions other than "nid" produce repository keys will be 
<em>way different that those produced with previous versions or without this 
option enabled</em>. Filter uses this key function to lay down and look up 
files to use in filtering. |  `"nid"`  | 2.0.14 |  No  | Session Configuration |
 | `"aether.snapshotFilter"` | `Boolean` | The key in the repository session's 
<code>RepositorySystemSession#getConfigProperties() 
configurationproperties</code> used to store a <code>Boolean</code> 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 |
@@ -121,6 +122,7 @@ To modify this file, edit the template and regenerate.
 | `"aether.syncContext.named.time"` | `Long` | The maximum of time amount to 
be blocked to obtain lock. |  `900l`  | 1.7.0 |  No  | Session Configuration |
 | `"aether.syncContext.named.time.unit"` | `String` | The unit of maximum time 
amount to be blocked to obtain lock. Use TimeUnit enum names. |  `"SECONDS"`  | 
1.7.0 |  No  | Session Configuration |
 | `"aether.system.dependencyVisitor"` | `String` | A flag indicating which 
visitor should be used to "flatten" the dependency graph into list. In Maven 4 
the default is new "levelOrder", while Maven 3 used "preOrder". This property 
accepts values "preOrder", "postOrder" and "levelOrder". |  `"levelOrder"`  | 
2.0.0 |  No  | Session Configuration |
+| `"aether.system.repositoryKeyFunction"` | `String` | <b>Experimental:</b> 
Configuration for system-wide "repository key" function. Accepted and 
recommended values: "nid" (default), "nid_hurl" and "ngurk", while "simple" is 
Maven 3 legacy, technically equivalent to "nid". For complete description see 
enum 
<code>org.eclipse.aether.util.repository.RepositoryIdHelper.RepositoryKeyType</code>
 in utils. <em>Warning:</em> repository key function affects Resolver 
fundamentally and may have une [...]
 | `"aether.transport.apache.followRedirects"` | `Boolean` | If enabled, Apache 
HttpClient will follow HTTP redirects. |  `true`  | 2.0.2 |  Yes  | Session 
Configuration |
 | `"aether.transport.apache.https.cipherSuites"` | `String` | Comma-separated 
list of <a 
href="https://docs.oracle.com/javase/7/docs/technotes/guides/security/StandardNames.html#ciphersuites";>Cipher
 Suites</a> which are enabled for HTTPS connections. |  -  | 2.0.0 |  No  | 
Session Configuration |
 | `"aether.transport.apache.https.protocols"` | `String` | Comma-separated 
list of <a 
href="https://docs.oracle.com/javase/7/docs/technotes/guides/security/StandardNames.html#jssenames";>Protocols
 </a> which are enabled for HTTPS connections. |  -  | 2.0.0 |  No  | Session 
Configuration |
@@ -157,6 +159,7 @@ To modify this file, edit the template and regenerate.
 | `"aether.transport.wagon.perms.dirMode"` | `String` | Octal numerical 
notation of permissions to set for newly created directories. Only considered 
by certain Wagon providers. |  -  |  |  Yes  | Session Configuration |
 | `"aether.transport.wagon.perms.fileMode"` | `String` | Octal numerical 
notation of permissions to set for newly created files. Only considered by 
certain Wagon providers. |  -  |  |  Yes  | Session Configuration |
 | `"aether.transport.wagon.perms.group"` | `String` | Group which should own 
newly created directories/files. Only considered by certain Wagon providers. |  
-  |  |  Yes  | Session Configuration |
+| `"aether.trustedChecksumsSource.repositoryKeyFunction"` | `String` | 
<b>Experimental:</b> Configuration for "repository key" function. Note: 
repository key functions other than "nid" produce repository keys will be 
<em>way different that those produced with previous versions or without this 
option enabled</em>. Checksum source uses this key function to lay down and 
look up files to use in sources. |  `"nid"`  | 2.0.14 |  No  | Session 
Configuration |
 | `"aether.trustedChecksumsSource.sparseDirectory"` | `Boolean` | Is checksum 
source enabled? |  `false`  | 1.9.0 |  No  | Session Configuration |
 | `"aether.trustedChecksumsSource.sparseDirectory.basedir"` | `String` | The 
basedir where checksums are. If relative, is resolved from local repository 
root. |  `".checksums"`  | 1.9.0 |  No  | Session Configuration |
 | `"aether.trustedChecksumsSource.sparseDirectory.originAware"` | `Boolean` | 
Is source origin aware? |  `true`  | 1.9.0 |  No  | Session Configuration |
diff --git a/src/site/markdown/repository-key-function.md 
b/src/site/markdown/repository-key-function.md
new file mode 100644
index 000000000..250df5e28
--- /dev/null
+++ b/src/site/markdown/repository-key-function.md
@@ -0,0 +1,123 @@
+# Repository Key Function
+<!---
+ 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.
+-->
+
+One long outstanding issue in Maven (across all versions) was how to identify
+remote repositories (this problem mostly tackles them, as local, workspace
+and other repositories are usually "singletons" and have fixed IDs).
+
+Existing Maven versions mostly limited themselves to `RemoteRepository#getId()`
+method to "key" repositories, but this strategy many times proves suboptimal.
+
+Known issues that Maven users cannot fight against:
+* different IDs for same URLs, examples (from Central) are `apache-snapshots` 
(plural), `apache-snapshot` (singular) 
+  or `apache.snapshot` (dot vs dash) defined repositories, that all point to 
same ASF snapshot repository.
+* same IDs for different URLs (two totally disconnected project may define 
repository `project-releases` in their POM, 
+  while in fact those two repositories are not related at all)
+* repository IDs that are [not 
file-friendly](https://github.com/apache/maven-resolver/issues/1564). Usually 
this should 
+  be impossible, as Maven validates and forbids these characters in ID field, 
but in some cases 
+  (ancient or generated POMs) this may happen.
+
+Remote repositories that user cannot "fix", usually enter the build via those 
POMs that are not authored by user
+themselves, so project POM and parent POMs can be safely excluded. In turn, 
these may come from POMs that are
+being pulled in as third-party plugin or dependency POMs.
+
+While we don't find the first issue deal-breaker (and we did not provide yet a 
function for fixing it), the latter two
+may produce various problems with local repository, split local repository and 
so on, causing a total mix-up of expected
+layout, or even wrongly grouped artifacts.
+
+For those eager to fully control used repositories, Maven 3.9.x line added the 
`-itr`/`--ignore-transitive-repositories`
+CLI option, but while this solves the problem, it does it by fully delegating 
the work onto the user itself, to define
+all the needed remote repositories (for dependencies but also for plugins) in 
project POM that build requires. 
+In certain cases this option is the recommended way, but many times it proves 
too burdensome.
+
+Hence, Maven Resolver 2.x introduces notion of "repository key function", 
which is a function that creates 
+Remote Repository "key", with following properties:
+* is derived from and can be used to identify `RemoteRepository`
+* produced keys are "file system friendly" as well
+* is configurable (see below)
+
+Latest Resolver uses repository key at these places (and these must be 
aligned; must use same function):
+* `EnhancedLocalRepositoryManager`, the default LRM, where artifact 
availability is being calculated
+* `LocalPathPrefixComposer`, in case of "split local repository" to calculate 
prefix/suffix elements based on artifact originating repository (if enabled)
+* `RemoteRepositoryManager` that consolidates existing and newly discovered 
repositories (by eliminating them or merging mirrors, as needed)
+
+In these cases, the repository key function affects how Resolver (and hence, 
Maven) works _fundamentally_, what 
+`RemoteRepository` it considers "same" or "different". Which artifacts are 
considered as coming from "same origin"
+or "different origin" (i.e. split local repository).
+
+Furthermore, repository key function (possibly different one) is used in two 
components to map remote repository configuration to file paths:
+* Trusted Checksums Source
+* Remote Repository Filter
+
+In these cases, the repository key function only role is to provide "file 
system friendly" path segments based on
+`RemoteRepository` instances.
+
+**Important implication:** When Resolver/Maven is reconfigured to use 
alternative repository key function, it is
+worthwhile to start with new, empty local repository (as keys are used in LRM 
maintained metadata).
+
+## Implemented Repository Key Functions
+
+The function is configurable, while the default function remains Maven 3.x 
compatible. The existing functions are:
+* `simple` (Maven 3 default; technically equivalent to `nid`)
+* `nid` (default)
+* `nid_hurl`
+* `ngurk`
+
+These below are recommended only for some special cases:
+* `hurl`
+* `gurk`
+
+## Recommended New Repository Key Functions
+
+### `nid`
+
+This key still relies solely on `RemoteRepository#getId()` but applies 
transformation to returned value to make it
+"file system path segment friendly". Is usable in the simplest use cases, and 
behaves as Maven 3 did.
+Technically is equivalent to legacy `simple` repository key function.
+
+### `nid_hurl`
+
+This key relies on `RemoteRepository#getId()` and `RemoteRepository#getUrl()`, 
and forms a key based on these two.
+This means if you have same-ID repository pointing to two different URLs, they 
will be considered different. Still,
+on disk the produced key string is user-friendly, as ID remains readable.
+
+### `ngurk`
+
+This key relies on **all properties** (details below) of `RemoteRepository`, 
but is "normalized" in a way that only the 
+fact that a `RemoteRepository` is a mirror (or not) is recorded, while the 
list of the mirrored repositories does not 
+affect key production. This also means that if you have two "similar" 
`RemoteRepository`, with same ID, same URL, but
+one has snapshots enabled, the other snapshots disabled, they will be 
considered different.
+
+This function leaves out following `RemoteRepository` properties: 
`Authorization`, `Proxy`, `Intent`, `Mirrors` 
+(but checks is list empty or not) and update policies for releases and 
snapshots.
+
+## Special Repository Key Functions
+
+These functions are **not recommended for everyday use**, but may prove useful 
in some cases.
+
+### `hurl`
+
+This key relies solely on `RemoteRepository#getUrl()`.
+This means that repository URL becomes what repository ID was for equality 
check. Note: this function does not perform
+any kind of URL "normalization", URL is used as-is. The problem with this 
function is that it will produce
+"human unfriendly" repository key that is fully disconnected and hard to trace 
back to origin repository.
+
+### `gurk`
+
+Similar to `ngurk` but does not normalize mirrors. As a consequence, and due 
dynamism of mirrors, the key of same
+remote repository (for example `<mirrorOf>external:*</mirrorOf>`) **may change 
during the build**.
diff --git a/src/site/site.xml b/src/site/site.xml
index 91e5e983a..677ec4ff1 100644
--- a/src/site/site.xml
+++ b/src/site/site.xml
@@ -32,6 +32,7 @@ under the License.
       <item name="Local Repository" href="local-repository.html"/>
       <item name="Remote Repository Filtering" 
href="remote-repository-filtering.html"/>
       <item name="Third-party Integrations" 
href="third-party-integrations.html"/>
+      <item name="Repository Key Function" 
href="repository-key-function.html"/>
       <item name="Download" href="download.html"/>
     </menu>
     <menu name="Guides">


Reply via email to