You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@maven.apache.org by cs...@apache.org on 2022/04/25 12:41:09 UTC

[maven-resolver] branch master updated: [MRESOLVER-252] Make LRM path composition reusable (#167) [MRESOLVER-253] Split LRM (#168)

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 b1aa64fb [MRESOLVER-252] Make LRM path composition reusable (#167) [MRESOLVER-253] Split LRM (#168)
b1aa64fb is described below

commit b1aa64fbe6d1460d1b98d57039ec4a00de3d1a91
Author: Tamas Cservenak <ta...@cservenak.net>
AuthorDate: Mon Apr 25 14:41:04 2022 +0200

    [MRESOLVER-252] Make LRM path composition reusable (#167) [MRESOLVER-253] Split LRM (#168)
    
    [MRESOLVER-252] Make LRM path composition reusable (#167)
    
    LocalRepositoryManager path composition was enclosed
    into SimpleLocalRepositoryManager in not-quite reusable
    manner. Make it reusable, by making it into a component.
    
    Currently FileProvidedChecksumsSource was reusing local
    paths, is adjusted now.
    
    Also, make LRM implementations more encapsulated and
    clear up many ctors leaving only one for simplicity.
    
    [MRESOLVER-253] Split LRM (#168)
    
    Enhances existing "enhanced" LRM (local repository manager) with "prefix" from from composer. Default behaviour is not changed (will not use prefix, local repo will be as today).
    
    The point in change is that it introduces a "composer", that is composing LRM path prefix, and is able to apply different strategies, and split local repository into "installed" (locally built and installed) and "cached" (downloaded from remote), etc.
    
    There are several composers options out of the box.
---
 .../eclipse/aether/impl/DefaultServiceLocator.java |   3 +
 .../eclipse/aether/impl/guice/AetherModule.java    |  10 +
 .../internal/impl/DefaultLocalPathComposer.java    | 117 +++++++++
 .../DefaultLocalPathPrefixComposerFactory.java     |  61 +++++
 .../impl/EnhancedLocalRepositoryManager.java       | 136 +++++++++--
 .../EnhancedLocalRepositoryManagerFactory.java     |  46 +++-
 .../internal/impl/FileProvidedChecksumsSource.java |  10 +-
 .../aether/internal/impl/LocalPathComposer.java    |  53 +++++
 .../internal/impl/LocalPathPrefixComposer.java     |  66 ++++++
 .../impl/LocalPathPrefixComposerFactory.java       |  38 +++
 .../LocalPathPrefixComposerFactorySupport.java     | 261 +++++++++++++++++++++
 .../impl/SimpleLocalRepositoryManager.java         | 157 +++++--------
 .../impl/SimpleLocalRepositoryManagerFactory.java  |  30 ++-
 .../DefaultLocalPathPrefixComposerFactoryTest.java | 229 ++++++++++++++++++
 .../impl/EnhancedLocalRepositoryManagerTest.java   |  24 +-
 .../EnhancedSplitLocalRepositoryManagerTest.java   |  74 ++++++
 .../impl/FileProvidedChecksumsSourceTest.java      |   2 +-
 .../impl/SimpleLocalRepositoryManagerTest.java     |   3 +-
 src/site/markdown/configuration.md                 |   9 +
 src/site/markdown/local-repository.md              | 162 +++++++++++++
 src/site/site.xml                                  |   1 +
 21 files changed, 1341 insertions(+), 151 deletions(-)

diff --git a/maven-resolver-impl/src/main/java/org/eclipse/aether/impl/DefaultServiceLocator.java b/maven-resolver-impl/src/main/java/org/eclipse/aether/impl/DefaultServiceLocator.java
index c17e2378..815c07cd 100644
--- a/maven-resolver-impl/src/main/java/org/eclipse/aether/impl/DefaultServiceLocator.java
+++ b/maven-resolver-impl/src/main/java/org/eclipse/aether/impl/DefaultServiceLocator.java
@@ -31,6 +31,8 @@ import java.util.Map;
 import static java.util.Objects.requireNonNull;
 
 import org.eclipse.aether.RepositorySystem;
+import org.eclipse.aether.internal.impl.LocalPathComposer;
+import org.eclipse.aether.internal.impl.DefaultLocalPathComposer;
 import org.eclipse.aether.internal.impl.DefaultArtifactResolver;
 import org.eclipse.aether.internal.impl.DefaultChecksumPolicyProvider;
 import org.eclipse.aether.internal.impl.DefaultTrackingFileManager;
@@ -227,6 +229,7 @@ public final class DefaultServiceLocator
         addService( TrackingFileManager.class, DefaultTrackingFileManager.class );
         addService( NamedLockFactorySelector.class, SimpleNamedLockFactorySelector.class );
         addService( ChecksumAlgorithmFactorySelector.class, DefaultChecksumAlgorithmFactorySelector.class );
+        addService( LocalPathComposer.class, DefaultLocalPathComposer.class );
     }
 
     private <T> Entry<T> getEntry( Class<T> type, boolean create )
diff --git a/maven-resolver-impl/src/main/java/org/eclipse/aether/impl/guice/AetherModule.java b/maven-resolver-impl/src/main/java/org/eclipse/aether/impl/guice/AetherModule.java
index 93ef99ec..02bd7b08 100644
--- a/maven-resolver-impl/src/main/java/org/eclipse/aether/impl/guice/AetherModule.java
+++ b/maven-resolver-impl/src/main/java/org/eclipse/aether/impl/guice/AetherModule.java
@@ -40,7 +40,11 @@ import org.eclipse.aether.impl.OfflineController;
 import org.eclipse.aether.impl.RemoteRepositoryManager;
 import org.eclipse.aether.impl.RepositoryConnectorProvider;
 import org.eclipse.aether.impl.RepositoryEventDispatcher;
+import org.eclipse.aether.internal.impl.DefaultLocalPathPrefixComposerFactory;
+import org.eclipse.aether.internal.impl.LocalPathComposer;
+import org.eclipse.aether.internal.impl.DefaultLocalPathComposer;
 import org.eclipse.aether.internal.impl.DefaultTrackingFileManager;
+import org.eclipse.aether.internal.impl.LocalPathPrefixComposerFactory;
 import org.eclipse.aether.internal.impl.FileProvidedChecksumsSource;
 import org.eclipse.aether.internal.impl.TrackingFileManager;
 import org.eclipse.aether.internal.impl.checksum.Md5ChecksumAlgorithmFactory;
@@ -171,6 +175,12 @@ public class AetherModule
                 .to( DefaultRepositoryEventDispatcher.class ).in( Singleton.class );
         bind( OfflineController.class ) //
                 .to( DefaultOfflineController.class ).in( Singleton.class );
+
+        bind( LocalPathComposer.class )
+                .to( DefaultLocalPathComposer.class ).in( Singleton.class );
+        bind( LocalPathPrefixComposerFactory.class )
+                .to( DefaultLocalPathPrefixComposerFactory.class ).in( Singleton.class );
+
         bind( LocalRepositoryProvider.class ) //
                 .to( DefaultLocalRepositoryProvider.class ).in( Singleton.class );
         bind( LocalRepositoryManagerFactory.class ).annotatedWith( Names.named( "simple" ) ) //
diff --git a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/DefaultLocalPathComposer.java b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/DefaultLocalPathComposer.java
new file mode 100644
index 00000000..00b7a33e
--- /dev/null
+++ b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/DefaultLocalPathComposer.java
@@ -0,0 +1,117 @@
+package org.eclipse.aether.internal.impl;
+
+/*
+ * 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.
+ */
+
+import javax.inject.Named;
+import javax.inject.Singleton;
+
+import org.eclipse.aether.artifact.Artifact;
+import org.eclipse.aether.metadata.Metadata;
+
+import static java.util.Objects.requireNonNull;
+
+/**
+ * Default implementation of {@link LocalPathComposer}.
+ *
+ * @since TBD
+ */
+@Singleton
+@Named
+public final class DefaultLocalPathComposer implements LocalPathComposer
+{
+    @Override
+    public String getPathForArtifact( Artifact artifact, boolean local )
+    {
+        requireNonNull( artifact );
+
+        StringBuilder path = new StringBuilder( 128 );
+
+        path.append( artifact.getGroupId().replace( '.', '/' ) ).append( '/' );
+
+        path.append( artifact.getArtifactId() ).append( '/' );
+
+        path.append( artifact.getBaseVersion() ).append( '/' );
+
+        path.append( artifact.getArtifactId() ).append( '-' );
+        if ( local )
+        {
+            path.append( artifact.getBaseVersion() );
+        }
+        else
+        {
+            path.append( artifact.getVersion() );
+        }
+
+        if ( artifact.getClassifier().length() > 0 )
+        {
+            path.append( '-' ).append( artifact.getClassifier() );
+        }
+
+        if ( artifact.getExtension().length() > 0 )
+        {
+            path.append( '.' ).append( artifact.getExtension() );
+        }
+
+        return path.toString();
+    }
+
+    @Override
+    public String getPathForMetadata( Metadata metadata, String repositoryKey )
+    {
+        requireNonNull( metadata );
+        requireNonNull( repositoryKey );
+
+        StringBuilder path = new StringBuilder( 128 );
+
+        if ( metadata.getGroupId().length() > 0 )
+        {
+            path.append( metadata.getGroupId().replace( '.', '/' ) ).append( '/' );
+
+            if ( metadata.getArtifactId().length() > 0 )
+            {
+                path.append( metadata.getArtifactId() ).append( '/' );
+
+                if ( metadata.getVersion().length() > 0 )
+                {
+                    path.append( metadata.getVersion() ).append( '/' );
+                }
+            }
+        }
+
+        path.append( insertRepositoryKey( metadata.getType(), repositoryKey ) );
+
+        return path.toString();
+    }
+
+    private String insertRepositoryKey( String metadataType, String repositoryKey )
+    {
+        String result;
+        int idx = metadataType.indexOf( '.' );
+        if ( idx < 0 )
+        {
+            result = metadataType + '-' + repositoryKey;
+        }
+        else
+        {
+            result = metadataType.substring( 0, idx ) + '-' + repositoryKey + metadataType.substring( idx );
+        }
+        return result;
+    }
+}
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
new file mode 100644
index 00000000..e06be06c
--- /dev/null
+++ b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/DefaultLocalPathPrefixComposerFactory.java
@@ -0,0 +1,61 @@
+package org.eclipse.aether.internal.impl;
+
+/*
+ * 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.
+ */
+
+import javax.inject.Named;
+import javax.inject.Singleton;
+
+import org.eclipse.aether.RepositorySystemSession;
+
+/**
+ * Default local path prefix composer factory: it fully reuses {@link LocalPathPrefixComposerFactorySupport} class
+ * without changing anything from it.
+ *
+ * @since TBD
+ */
+@Singleton
+@Named
+public final class DefaultLocalPathPrefixComposerFactory extends LocalPathPrefixComposerFactorySupport
+{
+    @Override
+    public LocalPathPrefixComposer createComposer( RepositorySystemSession session )
+    {
+        return new DefaultLocalPathPrefixComposer( isSplit( session ), getLocalPrefix( session ),
+                isSplitLocal( session ), getRemotePrefix( session ), isSplitRemote( session ),
+                isSplitRemoteRepository( session ), isSplitRemoteRepositoryLast( session ),
+                getReleasesPrefix( session ), getSnapshotsPrefix( session ) );
+    }
+
+    /**
+     * {@link LocalPathPrefixComposer} implementation that fully reuses {@link LocalPathPrefixComposerSupport} class.
+     */
+    private static class DefaultLocalPathPrefixComposer extends LocalPathPrefixComposerSupport
+    {
+        @SuppressWarnings( "checkstyle:parameternumber" )
+        private DefaultLocalPathPrefixComposer( boolean split, String localPrefix, boolean splitLocal,
+                                               String remotePrefix, boolean splitRemote, boolean splitRemoteRepository,
+                                               boolean splitRemoteRepositoryLast,
+                                               String releasesPrefix, String snapshotsPrefix )
+        {
+            super( split, localPrefix, splitLocal, remotePrefix, splitRemote, splitRemoteRepository,
+                    splitRemoteRepositoryLast, releasesPrefix, snapshotsPrefix );
+        }
+    }
+}
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 65da5321..0b0e398a 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
@@ -8,9 +8,9 @@ package org.eclipse.aether.internal.impl;
  * 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
@@ -25,6 +25,7 @@ import java.util.Collections;
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.Map;
+
 import static java.util.Objects.requireNonNull;
 
 import java.util.Objects;
@@ -32,11 +33,11 @@ import java.util.Properties;
 
 import org.eclipse.aether.RepositorySystemSession;
 import org.eclipse.aether.artifact.Artifact;
+import org.eclipse.aether.metadata.Metadata;
 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.util.ConfigUtils;
 
 /**
  * These are implementation details for enhanced local repository manager, subject to change without prior notice.
@@ -44,7 +45,7 @@ import org.eclipse.aether.util.ConfigUtils;
  * <code>_remote.repositories</code>, with content key as filename&gt;repo_id and value as empty string. If a file has
  * been installed in the repository, but not downloaded from a remote repository, it is tracked as empty repository id
  * and always resolved. For example:
- * 
+ *
  * <pre>
  * artifact-1.0.pom>=
  * artifact-1.0.jar>=
@@ -54,11 +55,11 @@ import org.eclipse.aether.util.ConfigUtils;
  * artifact-1.0-classifier.zip>central=
  * artifact-1.0.pom>my_repo_id=
  * </pre>
- * 
+ *
  * @see EnhancedLocalRepositoryManagerFactory
  */
 class EnhancedLocalRepositoryManager
-    extends SimpleLocalRepositoryManager
+        extends SimpleLocalRepositoryManager
 {
 
     private static final String LOCAL_REPO_ID = "";
@@ -67,29 +68,103 @@ class EnhancedLocalRepositoryManager
 
     private final TrackingFileManager trackingFileManager;
 
+    private final LocalPathPrefixComposer localPathPrefixComposer;
+
     EnhancedLocalRepositoryManager( File basedir,
-                                    RepositorySystemSession session,
-                                    TrackingFileManager trackingFileManager )
+                                    LocalPathComposer localPathComposer,
+                                    String trackingFilename,
+                                    TrackingFileManager trackingFileManager,
+                                    LocalPathPrefixComposer localPathPrefixComposer )
     {
-        super( basedir, "enhanced" );
-        String filename = ConfigUtils.getString( session, "", "aether.enhancedLocalRepository.trackingFilename" );
-        if ( filename.isEmpty() || filename.contains( "/" ) || filename.contains( "\\" )
-            || filename.contains( ".." ) )
+        super( basedir, "enhanced", localPathComposer );
+        this.trackingFilename = requireNonNull( trackingFilename );
+        this.trackingFileManager = requireNonNull( trackingFileManager );
+        this.localPathPrefixComposer = requireNonNull( localPathPrefixComposer );
+    }
+
+    private String concatPaths( String prefix, String artifactPath )
+    {
+        if ( prefix == null || prefix.isEmpty() )
         {
-            filename = "_remote.repositories";
+            return artifactPath;
         }
-        this.trackingFilename = filename;
-        this.trackingFileManager = Objects.requireNonNull( trackingFileManager );
+        return prefix + '/' + artifactPath;
     }
 
     @Override
-    public LocalArtifactResult find( RepositorySystemSession session, LocalArtifactRequest request )
+    public String getPathForLocalArtifact( Artifact artifact )
     {
-        String path = getPathForArtifact( request.getArtifact(), false );
-        File file = new File( getRepository().getBasedir(), path );
+        return concatPaths(
+                localPathPrefixComposer.getPathPrefixForLocalArtifact( artifact ),
+                super.getPathForLocalArtifact( artifact )
+        );
+    }
+
+    @Override
+    public String getPathForRemoteArtifact( Artifact artifact, RemoteRepository repository, String context )
+    {
+        return concatPaths(
+                localPathPrefixComposer.getPathPrefixForRemoteArtifact( artifact, repository ),
+                super.getPathForRemoteArtifact( artifact, repository, context )
+        );
+    }
 
+    @Override
+    public String getPathForLocalMetadata( Metadata metadata )
+    {
+        return concatPaths(
+                localPathPrefixComposer.getPathPrefixForLocalMetadata( metadata ),
+                super.getPathForLocalMetadata( metadata )
+        );
+    }
+
+    @Override
+    public String getPathForRemoteMetadata( Metadata metadata, RemoteRepository repository, String context )
+    {
+        return concatPaths(
+                localPathPrefixComposer.getPathPrefixForRemoteMetadata( metadata, repository ),
+                super.getPathForRemoteMetadata( metadata, repository, context )
+        );
+    }
+
+    @Override
+    public LocalArtifactResult find( RepositorySystemSession session, LocalArtifactRequest request )
+    {
+        Artifact artifact = request.getArtifact();
         LocalArtifactResult result = new LocalArtifactResult( request );
 
+        String path;
+        File file;
+
+        // Local repository CANNOT have timestamped installed, they are created only during deploy
+        if ( Objects.equals( artifact.getVersion(), artifact.getBaseVersion() ) )
+        {
+            path = getPathForLocalArtifact( artifact );
+            file = new File( getRepository().getBasedir(), path );
+            checkFind( file, result );
+        }
+
+        if ( !result.isAvailable() )
+        {
+            for ( RemoteRepository repository : request.getRepositories() )
+            {
+                path = getPathForRemoteArtifact( artifact, repository, request.getContext() );
+                file = new File( getRepository().getBasedir(), path );
+
+                checkFind( file, result );
+
+                if ( result.isAvailable() )
+                {
+                    break;
+                }
+            }
+        }
+
+        return result;
+    }
+
+    private void checkFind( File file, LocalArtifactResult result )
+    {
         if ( file.isFile() )
         {
             result.setFile( file );
@@ -103,8 +178,8 @@ class EnhancedLocalRepositoryManager
             }
             else
             {
-                String context = request.getContext();
-                for ( RemoteRepository repository : request.getRepositories() )
+                String context = result.getRequest().getContext();
+                for ( RemoteRepository repository : result.getRequest().getRepositories() )
                 {
                     if ( props.get( getKey( file, getRepositoryKey( repository, context ) ) ) != null )
                     {
@@ -125,8 +200,6 @@ class EnhancedLocalRepositoryManager
                 }
             }
         }
-
-        return result;
     }
 
     @Override
@@ -141,7 +214,17 @@ class EnhancedLocalRepositoryManager
         {
             repositories = getRepositoryKeys( request.getRepository(), request.getContexts() );
         }
-        addArtifact( request.getArtifact(), repositories, request.getRepository() == null );
+        if ( request.getRepository() == null )
+        {
+            addArtifact( request.getArtifact(), repositories, null, null );
+        }
+        else
+        {
+            for ( String context : request.getContexts() )
+            {
+                addArtifact( request.getArtifact(), repositories, request.getRepository(), context );
+            }
+        }
     }
 
     private Collection<String> getRepositoryKeys( RemoteRepository repository, Collection<String> contexts )
@@ -159,9 +242,12 @@ class EnhancedLocalRepositoryManager
         return keys;
     }
 
-    private void addArtifact( Artifact artifact, Collection<String> repositories, boolean local )
+    private void addArtifact( Artifact artifact, Collection<String> repositories, RemoteRepository repository,
+                              String context )
     {
-        String path = getPathForArtifact( requireNonNull( artifact, "artifact cannot be null" ), local );
+        requireNonNull( artifact, "artifact cannot be null" );
+        String path = repository == null ? getPathForLocalArtifact( artifact )
+                : getPathForRemoteArtifact( artifact, repository, context );
         File file = new File( getRepository().getBasedir(), path );
         addRepo( file, repositories );
     }
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 d5d64b1c..98fcddb5 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
@@ -19,8 +19,6 @@ package org.eclipse.aether.internal.impl;
  * under the License.
  */
 
-import java.util.Objects;
-
 import javax.inject.Inject;
 import javax.inject.Named;
 import javax.inject.Singleton;
@@ -32,6 +30,9 @@ import org.eclipse.aether.repository.NoLocalRepositoryManagerException;
 import org.eclipse.aether.spi.localrepo.LocalRepositoryManagerFactory;
 import org.eclipse.aether.spi.locator.Service;
 import org.eclipse.aether.spi.locator.ServiceLocator;
+import org.eclipse.aether.util.ConfigUtils;
+
+import static java.util.Objects.requireNonNull;
 
 /**
  * Creates enhanced local repository managers for repository types {@code "default"} or {@code "" (automatic)}. Enhanced
@@ -45,36 +46,64 @@ import org.eclipse.aether.spi.locator.ServiceLocator;
 public class EnhancedLocalRepositoryManagerFactory
     implements LocalRepositoryManagerFactory, Service
 {
+    private static final String CONFIG_PROP_TRACKING_FILENAME = "aether.enhancedLocalRepository.trackingFilename";
+
+    private static final String DEFAULT_TRACKING_FILENAME = "_remote.repositories";
+
     private float priority = 10.0f;
 
+    private LocalPathComposer localPathComposer;
+
     private TrackingFileManager trackingFileManager;
 
+    private LocalPathPrefixComposerFactory localPathPrefixComposerFactory;
+
     public EnhancedLocalRepositoryManagerFactory()
     {
         // no arg ctor for ServiceLocator
     }
 
     @Inject
-    public EnhancedLocalRepositoryManagerFactory( final TrackingFileManager trackingFileManager )
+    public EnhancedLocalRepositoryManagerFactory( final LocalPathComposer localPathComposer,
+                   final TrackingFileManager trackingFileManager,
+                   final LocalPathPrefixComposerFactory localPathPrefixComposerFactory )
     {
-        this.trackingFileManager = Objects.requireNonNull( trackingFileManager );
+        this.localPathComposer = requireNonNull( localPathComposer );
+        this.trackingFileManager = requireNonNull( trackingFileManager );
+        this.localPathPrefixComposerFactory = requireNonNull( localPathPrefixComposerFactory );
     }
 
     @Override
     public void initService( final ServiceLocator locator )
     {
-        this.trackingFileManager = Objects.requireNonNull( locator.getService( TrackingFileManager.class ) );
+        this.localPathComposer = requireNonNull( locator.getService( LocalPathComposer.class ) );
+        this.trackingFileManager = requireNonNull( locator.getService( TrackingFileManager.class ) );
+        this.localPathPrefixComposerFactory = new DefaultLocalPathPrefixComposerFactory();
     }
 
+    @Override
     public LocalRepositoryManager newInstance( RepositorySystemSession session, LocalRepository repository )
         throws NoLocalRepositoryManagerException
     {
-        Objects.requireNonNull( session, "session cannot be null" );
-        Objects.requireNonNull( repository, "repository cannot be null" );
+        requireNonNull( session, "session cannot be null" );
+        requireNonNull( repository, "repository cannot be null" );
+
+        String trackingFilename = ConfigUtils.getString( session, "", CONFIG_PROP_TRACKING_FILENAME );
+        if ( trackingFilename.isEmpty() || trackingFilename.contains( "/" ) || trackingFilename.contains( "\\" )
+                || trackingFilename.contains( ".." ) )
+        {
+            trackingFilename = DEFAULT_TRACKING_FILENAME;
+        }
 
         if ( "".equals( repository.getContentType() ) || "default".equals( repository.getContentType() ) )
         {
-            return new EnhancedLocalRepositoryManager( repository.getBasedir(), session, trackingFileManager );
+            return new EnhancedLocalRepositoryManager(
+                    repository.getBasedir(),
+                    localPathComposer,
+                    trackingFilename,
+                    trackingFileManager,
+                    localPathPrefixComposerFactory.createComposer( session )
+            );
         }
         else
         {
@@ -82,6 +111,7 @@ public class EnhancedLocalRepositoryManagerFactory
         }
     }
 
+    @Override
     public float getPriority()
     {
         return priority;
diff --git a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/FileProvidedChecksumsSource.java b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/FileProvidedChecksumsSource.java
index 2289fa56..ff325025 100644
--- a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/FileProvidedChecksumsSource.java
+++ b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/FileProvidedChecksumsSource.java
@@ -32,7 +32,6 @@ import org.slf4j.LoggerFactory;
 import javax.inject.Inject;
 import javax.inject.Named;
 import javax.inject.Singleton;
-import java.io.File;
 import java.io.IOException;
 import java.net.URI;
 import java.nio.file.Files;
@@ -68,14 +67,13 @@ public final class FileProvidedChecksumsSource
 
     private final FileProcessor fileProcessor;
 
-    private final SimpleLocalRepositoryManager simpleLocalRepositoryManager;
+    private final LocalPathComposer localPathComposer;
 
     @Inject
-    public FileProvidedChecksumsSource( FileProcessor fileProcessor )
+    public FileProvidedChecksumsSource( FileProcessor fileProcessor, LocalPathComposer localPathComposer )
     {
         this.fileProcessor = requireNonNull( fileProcessor );
-        // we really needs just "local layout" from it (relative paths), so baseDir here is irrelevant
-        this.simpleLocalRepositoryManager = new SimpleLocalRepositoryManager( new File( "" ) );
+        this.localPathComposer = requireNonNull( localPathComposer );
     }
 
     @Override
@@ -92,7 +90,7 @@ public final class FileProvidedChecksumsSource
         for ( ChecksumAlgorithmFactory checksumAlgorithmFactory : checksumAlgorithmFactories )
         {
             checksumFilePaths.add( new ChecksumFilePath(
-                    simpleLocalRepositoryManager.getPathForArtifact( transfer.getArtifact(), false ) + '.'
+                    localPathComposer.getPathForArtifact( transfer.getArtifact(), false ) + '.'
                     + checksumAlgorithmFactory.getFileExtension(), checksumAlgorithmFactory ) );
         }
         return getProvidedChecksums( baseDir, checksumFilePaths, ArtifactIdUtils.toId( transfer.getArtifact() ) );
diff --git a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/LocalPathComposer.java b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/LocalPathComposer.java
new file mode 100644
index 00000000..cc7a5c38
--- /dev/null
+++ b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/LocalPathComposer.java
@@ -0,0 +1,53 @@
+package org.eclipse.aether.internal.impl;
+
+/*
+ * 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.
+ */
+
+import org.eclipse.aether.artifact.Artifact;
+import org.eclipse.aether.metadata.Metadata;
+
+/**
+ * Composes {@link Artifact} and {@link Metadata} relative paths to be used in
+ * {@link org.eclipse.aether.repository.LocalRepositoryManager}.
+ *
+ * @since TBD
+ */
+public interface LocalPathComposer
+{
+    /**
+     * Gets the relative path for a locally installed (local=true) or remotely cached (local=false) artifact.
+     *
+     * @param artifact The artifact for which to determine the path, must not be {@code null}.
+     * @param local    {@code true} if artifact is locally installed or {@code false} if artifact is remotely cached.
+     * @return A relative path representing artifact path.
+     */
+    String getPathForArtifact( Artifact artifact, boolean local );
+
+    /**
+     * Gets the relative path for locally installed (repositoryKey=local) or remotely cached metadata. The
+     * {@code repositoryKey} should be used at caller discretion, it merely denotes the origin of the metadata, while
+     * value "local" usually means local origin, but again, this is not a must or enforced, just how things happened
+     * so far.
+     *
+     * @param metadata      The metadata for which to determine the path, must not be {@code null}.
+     * @param repositoryKey The repository key, never {@code null}.
+     * @return A relative path representing metadata path.
+     */
+    String getPathForMetadata( Metadata metadata, String repositoryKey );
+}
diff --git a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/LocalPathPrefixComposer.java b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/LocalPathPrefixComposer.java
new file mode 100644
index 00000000..b6f54dce
--- /dev/null
+++ b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/LocalPathPrefixComposer.java
@@ -0,0 +1,66 @@
+package org.eclipse.aether.internal.impl;
+
+/*
+ * 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.
+ */
+
+import org.eclipse.aether.artifact.Artifact;
+import org.eclipse.aether.metadata.Metadata;
+import org.eclipse.aether.repository.RemoteRepository;
+
+/**
+ * Composes path prefixes for {@link EnhancedLocalRepositoryManager}.
+ *
+ * @since TBD
+ */
+public interface LocalPathPrefixComposer
+{
+    /**
+     * Gets the path prefix for a locally installed artifact.
+     *
+     * @param artifact The artifact for which to determine the prefix, must not be {@code null}.
+     * @return The prefix, may be {@code null} (note: {@code null}s and empty strings are treated equally).
+     */
+    String getPathPrefixForLocalArtifact( Artifact artifact );
+
+    /**
+     * Gets the path prefix for an artifact cached from a remote repository.
+     *
+     * @param artifact      The artifact for which to determine the prefix, must not be {@code null}.
+     * @param repository    The remote repository, never {@code null}.
+     * @return The prefix, may be {@code null} (note: {@code null}s and empty strings are treated equally).
+     */
+    String getPathPrefixForRemoteArtifact( Artifact artifact, RemoteRepository repository );
+
+    /**
+     * Gets the path prefix for locally installed metadata.
+     *
+     * @param metadata The metadata for which to determine the prefix, must not be {@code null}.
+     * @return The prefix, may be {@code null} (note: {@code null}s and empty strings are treated equally).
+     */
+    String getPathPrefixForLocalMetadata( Metadata metadata );
+
+    /**
+     * Gets the path prefix for metadata cached from a remote repository.
+     *
+     * @param metadata      The metadata for which to determine the prefix, must not be {@code null}.
+     * @param repository    The remote repository, never {@code null}.
+     * @return The prefix, may be {@code null} (note: {@code null}s and empty strings are treated equally).
+     */
+    String getPathPrefixForRemoteMetadata( Metadata metadata, RemoteRepository repository );
+}
diff --git a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/LocalPathPrefixComposerFactory.java b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/LocalPathPrefixComposerFactory.java
new file mode 100644
index 00000000..6404cc5e
--- /dev/null
+++ b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/LocalPathPrefixComposerFactory.java
@@ -0,0 +1,38 @@
+package org.eclipse.aether.internal.impl;
+
+/*
+ * 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.
+ */
+
+import org.eclipse.aether.RepositorySystemSession;
+
+/**
+ * Creates instances of {@link LocalPathPrefixComposer}.
+ *
+ * @since TBD
+ */
+public interface LocalPathPrefixComposerFactory
+{
+    /**
+     * Creates {@link LocalPathPrefixComposer} instance out of whatever configuration it finds in passed in session.
+     *
+     * @param session The repository session, never {@code null}.
+     * @return The created instance, never {@code null}.
+     */
+    LocalPathPrefixComposer createComposer( RepositorySystemSession 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
new file mode 100644
index 00000000..b28509f8
--- /dev/null
+++ b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/LocalPathPrefixComposerFactorySupport.java
@@ -0,0 +1,261 @@
+package org.eclipse.aether.internal.impl;
+
+/*
+ * 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.
+ */
+
+import org.eclipse.aether.RepositorySystemSession;
+import org.eclipse.aether.artifact.Artifact;
+import org.eclipse.aether.metadata.Metadata;
+import org.eclipse.aether.repository.RemoteRepository;
+import org.eclipse.aether.util.ConfigUtils;
+
+/**
+ * Support class for {@link LocalPathPrefixComposerFactory} implementations: it predefines and makes re-usable
+ * common configuration getters, and defines a support class for {@link LocalPathPrefixComposer} carrying same
+ * configuration and providing default implementation for all methods.
+ *
+ * Implementors should extend this class to implement custom split strategies. If one needs to alter default
+ * configuration, they should override any configuration getter from this class.
+ *
+ * @see DefaultLocalPathPrefixComposerFactory
+ * @since TBD
+ */
+public abstract class LocalPathPrefixComposerFactorySupport implements LocalPathPrefixComposerFactory
+{
+    protected static final String CONF_PROP_SPLIT = "aether.enhancedLocalRepository.split";
+
+    protected static final boolean DEFAULT_SPLIT = false;
+
+    protected static final String CONF_PROP_LOCAL_PREFIX = "aether.enhancedLocalRepository.localPrefix";
+
+    protected static final String DEFAULT_LOCAL_PREFIX = "installed";
+
+    protected static final String CONF_PROP_SPLIT_LOCAL = "aether.enhancedLocalRepository.splitLocal";
+
+    protected static final boolean DEFAULT_SPLIT_LOCAL = false;
+
+    protected static final String CONF_PROP_REMOTE_PREFIX = "aether.enhancedLocalRepository.remotePrefix";
+
+    protected static final String DEFAULT_REMOTE_PREFIX = "cached";
+
+    protected static final String CONF_PROP_SPLIT_REMOTE = "aether.enhancedLocalRepository.splitRemote";
+
+    protected static final boolean DEFAULT_SPLIT_REMOTE = false;
+
+    protected static final String CONF_PROP_SPLIT_REMOTE_REPOSITORY =
+            "aether.enhancedLocalRepository.splitRemoteRepository";
+
+    protected static final boolean DEFAULT_SPLIT_REMOTE_REPOSITORY = false;
+
+    protected static final String CONF_PROP_SPLIT_REMOTE_REPOSITORY_LAST =
+            "aether.enhancedLocalRepository.splitRemoteRepositoryLast";
+
+    protected static final boolean DEFAULT_SPLIT_REMOTE_REPOSITORY_LAST = false;
+
+    protected static final String CONF_PROP_RELEASES_PREFIX = "aether.enhancedLocalRepository.releasesPrefix";
+
+    protected static final String DEFAULT_RELEASES_PREFIX = "releases";
+
+    protected static final String CONF_PROP_SNAPSHOTS_PREFIX = "aether.enhancedLocalRepository.snapshotsPrefix";
+
+    protected static final String DEFAULT_SNAPSHOTS_PREFIX = "snapshots";
+
+    protected boolean isSplit( RepositorySystemSession session )
+    {
+        return ConfigUtils.getBoolean(
+                session, DEFAULT_SPLIT, CONF_PROP_SPLIT );
+    }
+
+    protected String getLocalPrefix( RepositorySystemSession session )
+    {
+        return ConfigUtils.getString(
+                session, DEFAULT_LOCAL_PREFIX, CONF_PROP_LOCAL_PREFIX );
+    }
+
+    protected boolean isSplitLocal( RepositorySystemSession session )
+    {
+        return ConfigUtils.getBoolean(
+                session, DEFAULT_SPLIT_LOCAL, CONF_PROP_SPLIT_LOCAL );
+    }
+
+    protected String getRemotePrefix( RepositorySystemSession session )
+    {
+        return ConfigUtils.getString(
+                session, DEFAULT_REMOTE_PREFIX, CONF_PROP_REMOTE_PREFIX );
+    }
+
+    protected boolean isSplitRemote( RepositorySystemSession session )
+    {
+        return ConfigUtils.getBoolean(
+                session, DEFAULT_SPLIT_REMOTE, CONF_PROP_SPLIT_REMOTE );
+    }
+
+    protected boolean isSplitRemoteRepository( RepositorySystemSession session )
+    {
+        return ConfigUtils.getBoolean(
+                session, DEFAULT_SPLIT_REMOTE_REPOSITORY, CONF_PROP_SPLIT_REMOTE_REPOSITORY );
+    }
+
+    protected boolean isSplitRemoteRepositoryLast( RepositorySystemSession session )
+    {
+        return ConfigUtils.getBoolean(
+                session, DEFAULT_SPLIT_REMOTE_REPOSITORY_LAST, CONF_PROP_SPLIT_REMOTE_REPOSITORY_LAST );
+    }
+
+    protected String getReleasesPrefix( RepositorySystemSession session )
+    {
+        return ConfigUtils.getString(
+                session, DEFAULT_RELEASES_PREFIX, CONF_PROP_RELEASES_PREFIX );
+    }
+
+    protected String getSnapshotsPrefix( RepositorySystemSession session )
+    {
+        return ConfigUtils.getString(
+                session, DEFAULT_SNAPSHOTS_PREFIX, CONF_PROP_SNAPSHOTS_PREFIX );
+    }
+
+    /**
+     * Support class for composers: it defines protected members for all the predefined configuration values and
+     * provides default implementation for methods. Implementors may change it's behaviour by overriding methods.
+     */
+    @SuppressWarnings( "checkstyle:parameternumber" )
+    protected abstract static class LocalPathPrefixComposerSupport implements LocalPathPrefixComposer
+    {
+        protected final boolean split;
+
+        protected final String localPrefix;
+
+        protected final boolean splitLocal;
+
+        protected final String remotePrefix;
+
+        protected final boolean splitRemote;
+
+        protected final boolean splitRemoteRepository;
+
+        protected final boolean splitRemoteRepositoryLast;
+
+        protected final String releasesPrefix;
+
+        protected final String snapshotsPrefix;
+
+        protected LocalPathPrefixComposerSupport( boolean split,
+                                                  String localPrefix,
+                                                  boolean splitLocal,
+                                                  String remotePrefix,
+                                                  boolean splitRemote,
+                                                  boolean splitRemoteRepository,
+                                                  boolean splitRemoteRepositoryLast,
+                                                  String releasesPrefix,
+                                                  String snapshotsPrefix )
+        {
+            this.split = split;
+            this.localPrefix = localPrefix;
+            this.splitLocal = splitLocal;
+            this.remotePrefix = remotePrefix;
+            this.splitRemote = splitRemote;
+            this.splitRemoteRepository = splitRemoteRepository;
+            this.splitRemoteRepositoryLast = splitRemoteRepositoryLast;
+            this.releasesPrefix = releasesPrefix;
+            this.snapshotsPrefix = snapshotsPrefix;
+        }
+
+        @Override
+        public String getPathPrefixForLocalArtifact( Artifact artifact )
+        {
+            if ( !split )
+            {
+                return null;
+            }
+            String result = localPrefix;
+            if ( splitLocal )
+            {
+                result += "/" + ( artifact.isSnapshot() ? snapshotsPrefix : releasesPrefix );
+            }
+            return result;
+        }
+
+        @Override
+        public String getPathPrefixForRemoteArtifact( Artifact artifact, RemoteRepository repository )
+        {
+            if ( !split )
+            {
+                return null;
+            }
+            String result = remotePrefix;
+            if ( !splitRemoteRepositoryLast && splitRemoteRepository )
+            {
+                result += "/" + repository.getId();
+            }
+            if ( splitRemote )
+            {
+                result += "/" + ( artifact.isSnapshot() ? snapshotsPrefix : releasesPrefix );
+            }
+            if ( splitRemoteRepositoryLast && splitRemoteRepository )
+            {
+                result += "/" + repository.getId();
+            }
+            return result;
+        }
+
+        @Override
+        public String getPathPrefixForLocalMetadata( Metadata metadata )
+        {
+            if ( !split )
+            {
+                return null;
+            }
+            String result = localPrefix;
+            if ( splitLocal )
+            {
+                result += "/" + ( isSnapshot( metadata ) ? snapshotsPrefix : releasesPrefix );
+            }
+            return result;
+        }
+
+        @Override
+        public String getPathPrefixForRemoteMetadata( Metadata metadata, RemoteRepository repository )
+        {
+            if ( !split )
+            {
+                return null;
+            }
+            String result = remotePrefix;
+            if ( !splitRemoteRepositoryLast && splitRemoteRepository )
+            {
+                result += "/" + repository.getId();
+            }
+            if ( splitRemote )
+            {
+                result += "/" + ( isSnapshot( metadata ) ? snapshotsPrefix : releasesPrefix );
+            }
+            if ( splitRemoteRepositoryLast && splitRemoteRepository )
+            {
+                result += "/" + repository.getId();
+            }
+            return result;
+        }
+
+        protected boolean isSnapshot( Metadata metadata )
+        {
+            return !metadata.getVersion().isEmpty()
+                    && metadata.getVersion().endsWith( "-SNAPSHOT" );
+        }
+    }
+}
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 9023d17a..a16ede57 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
@@ -8,9 +8,9 @@ package org.eclipse.aether.internal.impl;
  * 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
@@ -20,7 +20,10 @@ package org.eclipse.aether.internal.impl;
  */
 
 import java.io.File;
+
 import static java.util.Objects.requireNonNull;
+
+import java.util.Objects;
 import java.util.SortedSet;
 import java.util.TreeSet;
 
@@ -41,92 +44,62 @@ import org.eclipse.aether.repository.RemoteRepository;
  * A local repository manager that realizes the classical Maven 2.0 local repository.
  */
 class SimpleLocalRepositoryManager
-    implements LocalRepositoryManager
+        implements LocalRepositoryManager
 {
 
     private final LocalRepository repository;
 
-    SimpleLocalRepositoryManager( File basedir )
-    {
-        this( basedir, "simple" );
-    }
-
-    SimpleLocalRepositoryManager( String basedir )
-    {
-        this( ( basedir != null ) ? new File( basedir ) : null, "simple" );
-    }
+    private final LocalPathComposer localPathComposer;
 
-    SimpleLocalRepositoryManager( File basedir, String type )
+    SimpleLocalRepositoryManager( File basedir, String type, LocalPathComposer localPathComposer )
     {
         requireNonNull( basedir, "base directory cannot be null" );
         repository = new LocalRepository( basedir.getAbsoluteFile(), type );
+        this.localPathComposer = requireNonNull( localPathComposer );
     }
 
+    @Override
     public LocalRepository getRepository()
     {
         return repository;
     }
 
-    String getPathForArtifact( Artifact artifact, boolean local )
-    {
-        StringBuilder path = new StringBuilder( 128 );
-
-        path.append( artifact.getGroupId().replace( '.', '/' ) ).append( '/' );
-
-        path.append( artifact.getArtifactId() ).append( '/' );
-
-        path.append( artifact.getBaseVersion() ).append( '/' );
-
-        path.append( artifact.getArtifactId() ).append( '-' );
-        if ( local )
-        {
-            path.append( artifact.getBaseVersion() );
-        }
-        else
-        {
-            path.append( artifact.getVersion() );
-        }
-
-        if ( artifact.getClassifier().length() > 0 )
-        {
-            path.append( '-' ).append( artifact.getClassifier() );
-        }
-
-        if ( artifact.getExtension().length() > 0 )
-        {
-            path.append( '.' ).append( artifact.getExtension() );
-        }
-
-        return path.toString();
-    }
-
+    @Override
     public String getPathForLocalArtifact( Artifact artifact )
     {
         requireNonNull( artifact, "artifact cannot be null" );
-        return getPathForArtifact( artifact, true );
+        return localPathComposer.getPathForArtifact( artifact, true );
     }
 
+    @Override
     public String getPathForRemoteArtifact( Artifact artifact, RemoteRepository repository, String context )
     {
         requireNonNull( artifact, "artifact cannot be null" );
         requireNonNull( repository, "repository cannot be null" );
-        return getPathForArtifact( artifact, false );
+        return localPathComposer.getPathForArtifact( artifact, false );
     }
 
+    @Override
     public String getPathForLocalMetadata( Metadata metadata )
     {
         requireNonNull( metadata, "metadata cannot be null" );
-        return getPath( metadata, "local" );
+        return localPathComposer.getPathForMetadata( metadata, "local" );
     }
 
+    @Override
     public String getPathForRemoteMetadata( Metadata metadata, RemoteRepository repository, String context )
     {
         requireNonNull( metadata, "metadata cannot be null" );
         requireNonNull( repository, "repository cannot be null" );
-        return getPath( metadata, getRepositoryKey( repository, context ) );
+        return localPathComposer.getPathForMetadata( metadata, getRepositoryKey( repository, context ) );
     }
 
-    String getRepositoryKey( RemoteRepository repository, String context )
+    /**
+     * 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 (as it may change).
+     */
+    protected String getRepositoryKey( RemoteRepository repository, String context )
     {
         String key;
 
@@ -166,62 +139,49 @@ class SimpleLocalRepositoryManager
         return key;
     }
 
-    private String getPath( Metadata metadata, String repositoryKey )
+    @Override
+    public LocalArtifactResult find( RepositorySystemSession session, LocalArtifactRequest request )
     {
-        StringBuilder path = new StringBuilder( 128 );
+        requireNonNull( session, "session cannot be null" );
+        requireNonNull( request, "request cannot be null" );
+        Artifact artifact = request.getArtifact();
+        LocalArtifactResult result = new LocalArtifactResult( request );
 
-        if ( metadata.getGroupId().length() > 0 )
-        {
-            path.append( metadata.getGroupId().replace( '.', '/' ) ).append( '/' );
+        String path;
+        File file;
 
-            if ( metadata.getArtifactId().length() > 0 )
+        // Local repository CANNOT have timestamped installed, they are created only during deploy
+        if ( Objects.equals( artifact.getVersion(), artifact.getBaseVersion() ) )
+        {
+            path = getPathForLocalArtifact( artifact );
+            file = new File( getRepository().getBasedir(), path );
+            if ( file.isFile() )
             {
-                path.append( metadata.getArtifactId() ).append( '/' );
-
-                if ( metadata.getVersion().length() > 0 )
-                {
-                    path.append( metadata.getVersion() ).append( '/' );
-                }
+                result.setFile( file );
+                result.setAvailable( true );
             }
         }
 
-        path.append( insertRepositoryKey( metadata.getType(), repositoryKey ) );
-
-        return path.toString();
-    }
-
-    private String insertRepositoryKey( String filename, String repositoryKey )
-    {
-        String result;
-        int idx = filename.indexOf( '.' );
-        if ( idx < 0 )
-        {
-            result = filename + '-' + repositoryKey;
-        }
-        else
+        if ( !result.isAvailable() )
         {
-            result = filename.substring( 0, idx ) + '-' + repositoryKey + filename.substring( idx );
-        }
-        return result;
-    }
-
-    public LocalArtifactResult find( RepositorySystemSession session, LocalArtifactRequest request )
-    {
-        requireNonNull( session, "session cannot be null" );
-        requireNonNull( request, "request cannot be null" );
-        String path = getPathForArtifact( request.getArtifact(), false );
-        File file = new File( getRepository().getBasedir(), path );
+            for ( RemoteRepository repository : request.getRepositories() )
+            {
+                path = getPathForRemoteArtifact( artifact, repository, request.getContext() );
+                file = new File( getRepository().getBasedir(), path );
+                if ( file.isFile() )
+                {
+                    result.setFile( file );
+                    result.setAvailable( true );
+                    break;
+                }
+            }
 
-        LocalArtifactResult result = new LocalArtifactResult( request );
-        if ( file.isFile() )
-        {
-            result.setFile( file );
-            result.setAvailable( true );
         }
 
         return result;
     }
 
+    @Override
     public void add( RepositorySystemSession session, LocalArtifactRegistration request )
     {
         requireNonNull( session, "session cannot be null" );
@@ -230,11 +190,6 @@ class SimpleLocalRepositoryManager
     }
 
     @Override
-    public String toString()
-    {
-        return String.valueOf( getRepository() );
-    }
-
     public LocalMetadataResult find( RepositorySystemSession session, LocalMetadataRequest request )
     {
         requireNonNull( session, "session cannot be null" );
@@ -265,6 +220,7 @@ class SimpleLocalRepositoryManager
         return result;
     }
 
+    @Override
     public void add( RepositorySystemSession session, LocalMetadataRegistration request )
     {
         requireNonNull( session, "session cannot be null" );
@@ -272,4 +228,9 @@ class SimpleLocalRepositoryManager
         // noop
     }
 
+    @Override
+    public String toString()
+    {
+        return String.valueOf( getRepository() );
+    }
 }
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 6903e4ce..9f428a3d 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
@@ -19,6 +19,7 @@ package org.eclipse.aether.internal.impl;
  * under the License.
  */
 
+import javax.inject.Inject;
 import javax.inject.Named;
 import javax.inject.Singleton;
 
@@ -29,6 +30,10 @@ 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.locator.Service;
+import org.eclipse.aether.spi.locator.ServiceLocator;
+
+import static java.util.Objects.requireNonNull;
 
 /**
  * Creates local repository managers for repository type {@code "simple"}.
@@ -36,24 +41,40 @@ import org.eclipse.aether.spi.localrepo.LocalRepositoryManagerFactory;
 @Singleton
 @Named( "simple" )
 public class SimpleLocalRepositoryManagerFactory
-    implements LocalRepositoryManagerFactory
+    implements LocalRepositoryManagerFactory, Service
 {
     private float priority;
 
+    private LocalPathComposer localPathComposer;
+
     public SimpleLocalRepositoryManagerFactory()
     {
         // enable no-arg constructor
+        this.localPathComposer = new DefaultLocalPathComposer(); // maven UTs needs this
+    }
+
+    @Inject
+    public SimpleLocalRepositoryManagerFactory( final LocalPathComposer localPathComposer )
+    {
+        this.localPathComposer = requireNonNull( localPathComposer );
+    }
+
+    @Override
+    public void initService( final ServiceLocator locator )
+    {
+        this.localPathComposer = Objects.requireNonNull( locator.getService( LocalPathComposer.class ) );
     }
 
+    @Override
     public LocalRepositoryManager newInstance( RepositorySystemSession session, LocalRepository repository )
         throws NoLocalRepositoryManagerException
     {
-        Objects.requireNonNull( session, "session cannot be null" );
-        Objects.requireNonNull( repository, "repository cannot be null" );
+        requireNonNull( session, "session cannot be null" );
+        requireNonNull( repository, "repository cannot be null" );
 
         if ( "".equals( repository.getContentType() ) || "simple".equals( repository.getContentType() ) )
         {
-            return new SimpleLocalRepositoryManager( repository.getBasedir() );
+            return new SimpleLocalRepositoryManager( repository.getBasedir(), "simple", localPathComposer );
         }
         else
         {
@@ -61,6 +82,7 @@ public class SimpleLocalRepositoryManagerFactory
         }
     }
 
+    @Override
     public float getPriority()
     {
         return priority;
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
new file mode 100644
index 00000000..2728c5bb
--- /dev/null
+++ b/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/DefaultLocalPathPrefixComposerFactoryTest.java
@@ -0,0 +1,229 @@
+package org.eclipse.aether.internal.impl;
+
+/*
+ * 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.
+ */
+
+import org.eclipse.aether.DefaultRepositorySystemSession;
+import org.eclipse.aether.artifact.Artifact;
+import org.eclipse.aether.artifact.DefaultArtifact;
+import org.eclipse.aether.internal.test.util.TestUtils;
+import org.eclipse.aether.metadata.DefaultMetadata;
+import org.eclipse.aether.metadata.Metadata;
+import org.eclipse.aether.repository.RemoteRepository;
+import org.junit.Test;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+
+/**
+ * UT for {@link DefaultLocalPathPrefixComposerFactory}.
+ */
+public class DefaultLocalPathPrefixComposerFactoryTest
+{
+    private final Artifact releaseArtifact = new DefaultArtifact("org.group:artifact:1.0");
+
+    private final Artifact snapshotArtifact = new DefaultArtifact("org.group:artifact:1.0-20220228.180000-1");
+
+    private final Metadata releaseMetadata = new DefaultMetadata( "org.group", "artifact", "1.0", "maven-metadata.xml", Metadata.Nature.RELEASE_OR_SNAPSHOT );
+
+    private final Metadata snapshotMetadata = new DefaultMetadata( "org.group", "artifact", "1.0-SNAPSHOT", "maven-metadata.xml", Metadata.Nature.RELEASE_OR_SNAPSHOT );
+
+    private final Metadata gaMetadata = new DefaultMetadata( "org.group", "artifact", null, "maven-metadata.xml", Metadata.Nature.RELEASE_OR_SNAPSHOT );
+
+    private final Metadata gMetadata = new DefaultMetadata( "org.group", null, null, "maven-metadata.xml", Metadata.Nature.RELEASE_OR_SNAPSHOT );
+
+    private final RemoteRepository repository = new RemoteRepository.Builder( "my-repo", "default", "https://repo.maven.apache.org/maven2/" ).build();
+
+    @Test
+    public void defaultConfigNoSplitAllNulls()
+    {
+        DefaultRepositorySystemSession session = TestUtils.newSession();
+
+        LocalPathPrefixComposerFactory factory = new DefaultLocalPathPrefixComposerFactory();
+        LocalPathPrefixComposer composer = factory.createComposer( session );
+        assertNotNull( composer );
+
+        String prefix;
+        prefix = composer.getPathPrefixForLocalArtifact( releaseArtifact );
+        assertNull( prefix );
+
+        prefix = composer.getPathPrefixForLocalMetadata( releaseMetadata );
+        assertNull( prefix );
+
+        prefix = composer.getPathPrefixForRemoteArtifact( releaseArtifact, repository );
+        assertNull( prefix );
+
+        prefix = composer.getPathPrefixForRemoteMetadata( releaseMetadata, repository );
+        assertNull( prefix );
+    }
+
+    @Test
+    public void splitEnabled()
+    {
+        DefaultRepositorySystemSession session = TestUtils.newSession();
+        session.setConfigProperty( "aether.enhancedLocalRepository.split", Boolean.TRUE.toString() );
+
+        LocalPathPrefixComposerFactory factory = new DefaultLocalPathPrefixComposerFactory();
+        LocalPathPrefixComposer composer = factory.createComposer( session );
+        assertNotNull( composer );
+
+        String prefix;
+        prefix = composer.getPathPrefixForLocalArtifact( releaseArtifact );
+        assertNotNull( prefix );
+        assertEquals( "installed", prefix );
+
+        prefix = composer.getPathPrefixForLocalMetadata( releaseMetadata );
+        assertNotNull( prefix );
+        assertEquals( "installed", prefix );
+
+        prefix = composer.getPathPrefixForRemoteArtifact( releaseArtifact, repository );
+        assertNotNull( prefix );
+        assertEquals( "cached", prefix );
+
+        prefix = composer.getPathPrefixForRemoteMetadata( releaseMetadata, repository );
+        assertNotNull( prefix );
+        assertEquals( "cached", prefix );
+    }
+
+    @Test
+    public void saneConfig()
+    {
+        DefaultRepositorySystemSession session = TestUtils.newSession();
+        session.setConfigProperty( "aether.enhancedLocalRepository.split", Boolean.TRUE.toString() );
+        session.setConfigProperty( "aether.enhancedLocalRepository.splitLocal", Boolean.TRUE.toString() );
+        session.setConfigProperty( "aether.enhancedLocalRepository.splitRemoteRepository", Boolean.TRUE.toString() );
+
+        LocalPathPrefixComposerFactory factory = new DefaultLocalPathPrefixComposerFactory();
+        LocalPathPrefixComposer composer = factory.createComposer( session );
+        assertNotNull( composer );
+
+        String prefix;
+        prefix = composer.getPathPrefixForLocalArtifact( releaseArtifact );
+        assertNotNull( prefix );
+        assertEquals( "installed/releases", prefix );
+
+        prefix = composer.getPathPrefixForLocalArtifact( snapshotArtifact );
+        assertNotNull( prefix );
+        assertEquals( "installed/snapshots", prefix );
+
+        prefix = composer.getPathPrefixForLocalMetadata( releaseMetadata );
+        assertNotNull( prefix );
+        assertEquals( "installed/releases", prefix );
+
+        prefix = composer.getPathPrefixForLocalMetadata( snapshotMetadata );
+        assertNotNull( prefix );
+        assertEquals( "installed/snapshots", prefix );
+
+        prefix = composer.getPathPrefixForLocalMetadata( gaMetadata );
+        assertNotNull( prefix );
+        assertEquals( "installed/releases", prefix );
+
+        prefix = composer.getPathPrefixForLocalMetadata( gMetadata );
+        assertNotNull( prefix );
+        assertEquals( "installed/releases", prefix );
+
+        prefix = composer.getPathPrefixForRemoteArtifact( releaseArtifact, repository );
+        assertNotNull( prefix );
+        assertEquals( "cached/my-repo", prefix );
+
+        prefix = composer.getPathPrefixForRemoteArtifact( snapshotArtifact, repository );
+        assertNotNull( prefix );
+        assertEquals( "cached/my-repo", prefix );
+
+        prefix = composer.getPathPrefixForRemoteMetadata( releaseMetadata, repository );
+        assertNotNull( prefix );
+        assertEquals( "cached/my-repo", prefix );
+
+        prefix = composer.getPathPrefixForRemoteMetadata( snapshotMetadata, repository );
+        assertNotNull( prefix );
+        assertEquals( "cached/my-repo", prefix );
+
+        prefix = composer.getPathPrefixForRemoteMetadata( gaMetadata, repository );
+        assertNotNull( prefix );
+        assertEquals( "cached/my-repo", prefix );
+
+        prefix = composer.getPathPrefixForRemoteMetadata( gMetadata, repository );
+        assertNotNull( prefix );
+        assertEquals( "cached/my-repo", prefix );
+    }
+
+    @Test
+    public void fullConfig()
+    {
+        DefaultRepositorySystemSession session = TestUtils.newSession();
+        session.setConfigProperty( "aether.enhancedLocalRepository.split", Boolean.TRUE.toString() );
+        session.setConfigProperty( "aether.enhancedLocalRepository.splitLocal", Boolean.TRUE.toString() );
+        session.setConfigProperty( "aether.enhancedLocalRepository.splitRemote", Boolean.TRUE.toString() );
+        session.setConfigProperty( "aether.enhancedLocalRepository.splitRemoteRepository", Boolean.TRUE.toString() );
+
+        LocalPathPrefixComposerFactory factory = new DefaultLocalPathPrefixComposerFactory();
+        LocalPathPrefixComposer composer = factory.createComposer( session );
+        assertNotNull( composer );
+
+        String prefix;
+        prefix = composer.getPathPrefixForLocalArtifact( releaseArtifact );
+        assertNotNull( prefix );
+        assertEquals( "installed/releases", prefix );
+
+        prefix = composer.getPathPrefixForLocalArtifact( snapshotArtifact );
+        assertNotNull( prefix );
+        assertEquals( "installed/snapshots", prefix );
+
+        prefix = composer.getPathPrefixForLocalMetadata( releaseMetadata );
+        assertNotNull( prefix );
+        assertEquals( "installed/releases", prefix );
+
+        prefix = composer.getPathPrefixForLocalMetadata( snapshotMetadata );
+        assertNotNull( prefix );
+        assertEquals( "installed/snapshots", prefix );
+
+        prefix = composer.getPathPrefixForLocalMetadata( gaMetadata );
+        assertNotNull( prefix );
+        assertEquals( "installed/releases", prefix );
+
+        prefix = composer.getPathPrefixForLocalMetadata( gMetadata );
+        assertNotNull( prefix );
+        assertEquals( "installed/releases", prefix );
+
+        prefix = composer.getPathPrefixForRemoteArtifact( releaseArtifact, repository );
+        assertNotNull( prefix );
+        assertEquals( "cached/my-repo/releases", prefix );
+
+        prefix = composer.getPathPrefixForRemoteArtifact( snapshotArtifact, repository );
+        assertNotNull( prefix );
+        assertEquals( "cached/my-repo/snapshots", prefix );
+
+        prefix = composer.getPathPrefixForRemoteMetadata( releaseMetadata, repository );
+        assertNotNull( prefix );
+        assertEquals( "cached/my-repo/releases", prefix );
+
+        prefix = composer.getPathPrefixForRemoteMetadata( snapshotMetadata, repository );
+        assertNotNull( prefix );
+        assertEquals( "cached/my-repo/snapshots", prefix );
+
+        prefix = composer.getPathPrefixForRemoteMetadata( gaMetadata, repository );
+        assertNotNull( prefix );
+        assertEquals( "cached/my-repo/releases", prefix );
+
+        prefix = composer.getPathPrefixForRemoteMetadata( gMetadata, repository );
+        assertNotNull( prefix );
+        assertEquals( "cached/my-repo/releases", prefix );
+    }
+}
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 729b0123..f69d8bd7 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
@@ -28,10 +28,9 @@ import java.util.Arrays;
 import java.util.Collection;
 import java.util.Collections;
 
-import org.eclipse.aether.RepositorySystemSession;
+import org.eclipse.aether.DefaultRepositorySystemSession;
 import org.eclipse.aether.artifact.Artifact;
 import org.eclipse.aether.artifact.DefaultArtifact;
-import org.eclipse.aether.internal.impl.EnhancedLocalRepositoryManager;
 import org.eclipse.aether.internal.test.util.TestFileUtils;
 import org.eclipse.aether.internal.test.util.TestUtils;
 import org.eclipse.aether.metadata.DefaultMetadata;
@@ -54,9 +53,9 @@ public class EnhancedLocalRepositoryManagerTest
 
     private Artifact snapshot;
 
-    private File basedir;
+    protected File basedir;
 
-    private EnhancedLocalRepositoryManager manager;
+    protected EnhancedLocalRepositoryManager manager;
 
     private File artifactFile;
 
@@ -64,9 +63,9 @@ public class EnhancedLocalRepositoryManagerTest
 
     private String testContext = "project/compile";
 
-    private TrackingFileManager trackingFileManager;
+    protected TrackingFileManager trackingFileManager;
 
-    private RepositorySystemSession session;
+    protected DefaultRepositorySystemSession session;
 
     private Metadata metadata;
 
@@ -99,11 +98,22 @@ public class EnhancedLocalRepositoryManagerTest
         basedir = TestFileUtils.createTempDir( "enhanced-repo" );
         session = TestUtils.newSession();
         trackingFileManager = new DefaultTrackingFileManager();
-        manager = new EnhancedLocalRepositoryManager( basedir, session, trackingFileManager );
+        manager = getManager();
 
         artifactFile = new File( basedir, manager.getPathForLocalArtifact( artifact ) );
     }
 
+    protected EnhancedLocalRepositoryManager getManager()
+    {
+        return new EnhancedLocalRepositoryManager(
+                basedir,
+                new DefaultLocalPathComposer(),
+                "_remote.repositories",
+                trackingFileManager,
+                new DefaultLocalPathPrefixComposerFactory().createComposer( session )
+        );
+    }
+
     @After
     public void tearDown()
         throws Exception
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
new file mode 100644
index 00000000..54036460
--- /dev/null
+++ b/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/EnhancedSplitLocalRepositoryManagerTest.java
@@ -0,0 +1,74 @@
+package org.eclipse.aether.internal.impl;
+
+/*
+ * 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.
+ */
+
+import org.eclipse.aether.artifact.Artifact;
+import org.eclipse.aether.artifact.DefaultArtifact;
+import org.eclipse.aether.repository.RemoteRepository;
+import org.junit.Test;
+
+import static org.junit.Assert.assertEquals;
+
+public class EnhancedSplitLocalRepositoryManagerTest extends EnhancedLocalRepositoryManagerTest
+{
+
+    @Override
+    protected EnhancedLocalRepositoryManager getManager()
+    {
+        session.setConfigProperty( "aether.enhancedLocalRepository.split", Boolean.TRUE.toString() );
+        return new EnhancedLocalRepositoryManager(
+                basedir,
+                new DefaultLocalPathComposer(),
+                "_remote.repositories",
+                trackingFileManager,
+                new DefaultLocalPathPrefixComposerFactory().createComposer( session )
+        );
+    }
+
+    @Test
+    @Override
+    public void testGetPathForLocalArtifact()
+    {
+        Artifact artifact = new DefaultArtifact( "g.i.d:a.i.d:1.0-SNAPSHOT" );
+        assertEquals( "1.0-SNAPSHOT", artifact.getBaseVersion() );
+        assertEquals( "installed/g/i/d/a.i.d/1.0-SNAPSHOT/a.i.d-1.0-SNAPSHOT.jar", manager.getPathForLocalArtifact( artifact ) );
+
+        artifact = new DefaultArtifact( "g.i.d:a.i.d:1.0-20110329.221805-4" );
+        assertEquals( "1.0-SNAPSHOT", artifact.getBaseVersion() );
+        assertEquals( "installed/g/i/d/a.i.d/1.0-SNAPSHOT/a.i.d-1.0-SNAPSHOT.jar", manager.getPathForLocalArtifact( artifact ) );
+    }
+
+    @Test
+    @Override
+    public void testGetPathForRemoteArtifact()
+    {
+        RemoteRepository remoteRepo = new RemoteRepository.Builder( "repo", "default", "ram:/void" ).build();
+
+        Artifact artifact = new DefaultArtifact( "g.i.d:a.i.d:1.0-SNAPSHOT" );
+        assertEquals( "1.0-SNAPSHOT", artifact.getBaseVersion() );
+        assertEquals( "cached/g/i/d/a.i.d/1.0-SNAPSHOT/a.i.d-1.0-SNAPSHOT.jar",
+                      manager.getPathForRemoteArtifact( artifact, remoteRepo, "" ) );
+
+        artifact = new DefaultArtifact( "g.i.d:a.i.d:1.0-20110329.221805-4" );
+        assertEquals( "1.0-SNAPSHOT", artifact.getBaseVersion() );
+        assertEquals( "cached/g/i/d/a.i.d/1.0-SNAPSHOT/a.i.d-1.0-20110329.221805-4.jar",
+                      manager.getPathForRemoteArtifact( artifact, remoteRepo, "" ) );
+    }
+}
diff --git a/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/FileProvidedChecksumsSourceTest.java b/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/FileProvidedChecksumsSourceTest.java
index e062a32b..680889fd 100644
--- a/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/FileProvidedChecksumsSourceTest.java
+++ b/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/FileProvidedChecksumsSourceTest.java
@@ -57,7 +57,7 @@ public class FileProvidedChecksumsSourceTest
     RemoteRepository repository = new RemoteRepository.Builder("test", "default", "https://irrelevant.com").build();
     session = TestUtils.newSession();
     repositoryLayout = new Maven2RepositoryLayoutFactory().newInstance(session, repository);
-    subject = new FileProvidedChecksumsSource(new TestFileProcessor() );
+    subject = new FileProvidedChecksumsSource(new TestFileProcessor(), new DefaultLocalPathComposer() );
 
     // populate local repository
     Path baseDir = session.getLocalRepository().getBasedir().toPath().resolve( FileProvidedChecksumsSource.LOCAL_REPO_PREFIX);
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 9a985261..a46721ef 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
@@ -27,7 +27,6 @@ import java.io.IOException;
 import org.eclipse.aether.RepositorySystemSession;
 import org.eclipse.aether.artifact.Artifact;
 import org.eclipse.aether.artifact.DefaultArtifact;
-import org.eclipse.aether.internal.impl.SimpleLocalRepositoryManager;
 import org.eclipse.aether.internal.test.util.TestFileUtils;
 import org.eclipse.aether.internal.test.util.TestUtils;
 import org.eclipse.aether.repository.LocalArtifactRequest;
@@ -53,7 +52,7 @@ public class SimpleLocalRepositoryManagerTest
         throws IOException
     {
         basedir = TestFileUtils.createTempDir( "simple-repo" );
-        manager = new SimpleLocalRepositoryManager( basedir );
+        manager = new SimpleLocalRepositoryManager( basedir, "simple", new DefaultLocalPathComposer() );
         session = TestUtils.newSession();
     }
 
diff --git a/src/site/markdown/configuration.md b/src/site/markdown/configuration.md
index 1524530a..9115e0fd 100644
--- a/src/site/markdown/configuration.md
+++ b/src/site/markdown/configuration.md
@@ -47,6 +47,15 @@ Option | Type | Description | Default Value | Supports Repo ID Suffix
 `aether.dependencyCollector.impl` | String | The name of the dependency collector implementation to use: depth-first (original) named `df`, and breadth-first (new in 1.8.0) named `bf`. Both collectors produce equivalent results, but they may differ performance wise, depending on project being applied to. Our experience shows that existing `df` is well suited for smaller to medium size projects, while `bf` may perform better on huge projects with many dependencies. Experiment (and come ba [...]
 `aether.dependencyCollector.bf.skipper` | boolean | Flag controlling whether to skip resolving duplicate/conflicting nodes during the breadth-first (`bf`) dependency collection process. | `true` | no
 `aether.dependencyManager.verbose` | boolean | Flag controlling the verbose mode for dependency management. If enabled, the original attributes of a dependency before its update due to dependency managemnent will be recorded in the node's `DependencyNode#getData()` when building a dependency graph. | `false` | no
+`aether.enhancedLocalRepository.localPrefix` | String | The prefix to use for locally installed artifacts. | `"installed"` | no
+`aether.enhancedLocalRepository.snapshotsPrefix` | String | The prefix to use for snapshot artifacts. | `"snapshots"` | no
+`aether.enhancedLocalRepository.split` | boolean | Whether LRM should split local and remote artifacts. | `false` | no
+`aether.enhancedLocalRepository.splitLocal` | boolean | Whether locally installed artifacts should be split by version (release/snapshot). | `false` | no
+`aether.enhancedLocalRepository.splitRemote` | boolean | Whether cached artifacts should be split by version (release/snapshot). | `false` | no
+`aether.enhancedLocalRepository.splitRemoteRepository` | boolean | Whether cached artifacts should be split by origin repository (repository ID). | `false` | no
+`aether.enhancedLocalRepository.splitRemoteRepositoryLast` | boolean | For cached artifacts, if both `splitRemote` and `splitRemoteRepository` are set to `true` sets the splitting order: by default it is repositoryId/version (false) or version/repositoryId (true) | `false` | no
+`aether.enhancedLocalRepository.remotePrefix` | String | The prefix to use for downloaded and cached artifacts. | `"cached"` | no
+`aether.enhancedLocalRepository.releasesPrefix` | String | The prefix to use for release artifacts. | `"releases"` | no
 `aether.enhancedLocalRepository.trackingFilename` | String | Filename of the file in which to track the remote repositories. | `"_remote.repositories"` | no
 `aether.interactive` | boolean | A flag indicating whether interaction with the user is allowed. | `false` | no
 `aether.metadataResolver.threads` | int | Number of threads to use in parallel for resolving metadata. | `4` | no
diff --git a/src/site/markdown/local-repository.md b/src/site/markdown/local-repository.md
new file mode 100644
index 00000000..9ee50e2d
--- /dev/null
+++ b/src/site/markdown/local-repository.md
@@ -0,0 +1,162 @@
+# Local Repository
+<!--
+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.
+-->
+
+Maven Resolver implements a "local repository" (that is used by Maven itself
+as well), that since the beginning of time was a "mixed bag of beans", 
+it served twofold purposes: to cache the artifacts downloaded from 
+remote, but also to store the artifacts locally installed (locally built and 
+installed, to be more precise). Both of these artifacts were stored in bulk 
+in the local repository.
+
+Local repository implementations implement the `LocalRepositoryManager` (LRM) 
+interface, and Resolver out of the box provides two implementations for it: 
+"simple" and "enhanced". 
+
+## Simple LRM
+
+Simple is a fully functional LRM implementation, but is used 
+mainly in tests, it is not recommended in production environments. 
+
+To manually instantiate a simple LRM, one needs to invoke following code:
+
+```java
+LocalRepositoryManager simple = new SimpleLocalRepositoryManagerFactory()
+        .newInstance( session, new LocalRepository( baseDir ) );
+```
+
+Note: This code snippet above instantiates a component, that is not 
+recommended way to use it, as it should be rather injected whenever possible. 
+This example above is merely a showcase how to obtain LRM implementation 
+in unit tests.
+
+## Enhanced LRM
+
+Enhanced LRM on the other hand is enhanced with several extra 
+features, one most notable is scoping cached content by its origin and context: 
+if you downloaded an artifact A1 from repository R1 
+and later initiate build that requires same artifact A1, but repository R1 
+is not defined in build, the A1 artifact cached from R1 remote repository should be handled 
+as not present, and needs to be re-downloaded, despite same coordinates. 
+Those two, originating from two different repositories may not be the same thing. 
+This is meant to protect users from "bad practice" (artifact coordinates are 
+unique in ideal world).
+
+### Split Local Repository
+
+Latest addition to the enhanced LRM is *split* feature. By default, split 
+feature is **not enabled**, enhanced LRM behaves as it behaved in all 
+previous versions of Resolver.
+
+Enhanced LRM is able to split the content of local repository by 
+several conditions:
+
+* differentiate between *cached* and locally *installed* artifacts
+* differentiate *cached* artifacts based on their origin (remote repository)
+* differentiate between *release* and *snapshot* versioned artifacts
+
+The split feature is implemented by the `LocalPathPrefixComposer` interface, 
+that adds different "prefixes" for the locally stored artifacts, based on 
+their context.
+
+#### Note About Release And Snapshot Differentiation
+
+The prefix composer is able to differentiate between release and snapshot 
+versioned artifacts, and this is clear-cut: Maven Artifacts are either 
+this or that.
+
+On the other hand, in case of Maven Metadata, things are not so clear. 
+Resolver is able to differentiate *only* based on `metadata/version` 
+field, but that field is not always present. 
+
+Maven Metadata exists in 3 variants:
+
+* G level metadata does not carry version.
+* GA level metadata does not carry version.
+* GAV level metadata carries version.
+
+In short, G and GA level metadata *always* end up in releases, despite 
+GA level metadata body may contain mixed release and snapshot versions enlisted, 
+as this metadata *does not contain* the `metadata/version` field.
+
+The GAV level metadata gets differentiated based on version it carries, so 
+they may end up in releases or snapshots, depending on their value of 
+`metadata/version` field.
+
+#### Use Cases
+
+Most direct use case is simpler local repository eviction. One can delete all 
+locally built artifacts without deleting the cached ones, hence, no 
+need to re-download the "whole universe". Similarly, deletion of cached ones 
+can happen based even on origin repository (if split by remote repository 
+was enabled beforehand).
+
+Example configuration with split by remote repository:
+```java
+$ mvn ... -Daether.enhancedLocalRepository.split \
+          -Daether.enhancedLocalRepository.splitRemoteRepository
+```
+
+Another use case is interesting for "branched development". Before split feature,
+a developer simultaneously working on several branches of same project was forced
+to rebuild all (better: install all), as same built artifacts from different
+branches would still land in the local repository on same coordinates, hence, they
+would constantly overwrite each other. It was easy to get into false error
+state where partially overlapping content were present in the local repository from
+different branches. Now one can set unique local prefix for each
+branch it is working on (or even by project, like 
+`-Daether.enhancedLocalRepository.localPrefix=$PROJECT/$BRANCH`, but use
+actual values, these expressions are merely an example, there is no interpolation
+happening!) and the
+local repository becomes usable even simultaneously, even concurrently from
+different terminals, as different projects and their branches can simply 
+coexist in local repository. They will land in different places, due different
+prefixes.
+
+Example configuration for branches:
+```java
+$ mvn ... -Daether.enhancedLocalRepository.split \
+          -Daether.enhancedLocalRepository.localPrefix=maven-resolver/mresolver-253
+          -Daether.enhancedLocalRepository.splitRemoteRepository
+```
+
+For complete reference of enhanced LRM configuration possibilities, refer to 
+[configuration page](configuration.html).
+
+#### Split Repository Considerations
+
+**Word of warning**: on every change of "split" parameters, user must be aware
+of the consequences. For example, if one change all aspects of split
+configuration (all the prefixes), it may be considered logically equivalent 
+of defining a new local repository, despite local repository root (`-Dmaven.repo.local`) 
+is unchanged! Simply put, as all prefixes will be "new", the composed paths will
+point to potentially non-existing locations, hence, resolver will consider
+it as a "new" local repository in every aspect.
+
+#### Implementing Custom Split Strategy
+
+To implement custom split strategy, one needs to create a component of
+type `LocalPathPrefixComposerFactory` and override the default component
+offered by Resolver (for example by using Eclipse Sisu priorities for 
+components). This should be done by extending `LocalPathPrefixComposerFactorySupport` 
+class that provides all the defaults.
+
+The factory should create a stateless instance of a composer
+configured from passed in session, that will be used with the enhanced LRM
+throughout the session.
diff --git a/src/site/site.xml b/src/site/site.xml
index 0ad3356b..64a26dd2 100644
--- a/src/site/site.xml
+++ b/src/site/site.xml
@@ -28,6 +28,7 @@ under the License.
       <item name="Introduction" href="index.html"/>
       <item name="Configuration" href="configuration.html"/>
       <item name="About Checksums" href="about-checksums.html"/>
+      <item name="About Local Repository" href="local-repository.html"/>
       <item name="Included Checksum Strategies" href="included-checksum-strategies.html"/>
       <item name="Maven 3.8.x" href="maven-3.8.x.html"/>
       <item name="JavaDocs" href="apidocs/index.html"/>