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/09/27 20:02:59 UTC

[maven-resolver] branch master updated: [MRESOLVER-273] More compact filesystem friendly mapper (#194)

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 5566bd5b [MRESOLVER-273] More compact filesystem friendly mapper (#194)
5566bd5b is described below

commit 5566bd5b3b0e59d124d820e6274da8e2da7804b0
Author: Tamas Cservenak <ta...@cservenak.net>
AuthorDate: Tue Sep 27 22:02:54 2022 +0200

    [MRESOLVER-273] More compact filesystem friendly mapper (#194)
    
    Create more compact FS NameMapper. Also, clean up existing ones and reduce clutter and mess.
    
    High level changes:
    * Introduce new HashingNameMapper (implements the "more compact" name mapper)
    * Introduce StringDigestUtil for String hashing (cleanup the mess of digest/checksum mixup, drop SimpleDigest as it is not part of API and is package private/internal stuff)
    * Cleanup and simplify existing NameMapper implementations (they are not components anymore)
    * Introduce providers for "user facing" configuration names, as those are usually combination of existing NameMappers (like one wrapping other, etc). Hence, to keep things simple, no NameMapper is component anymore but dedicated providers are constructing them. No user facing change happens by this, as mapper names remains same.
---
 .github/workflows/maven-verify.yml                 |   3 +
 .../src/main/java/TestNioLock.java                 |   2 +-
 .../eclipse/aether/impl/guice/AetherModule.java    |  45 ++++---
 .../eclipse/aether/internal/impl/SimpleDigest.java |  88 ------------
 .../impl/SimpleLocalRepositoryManager.java         |   9 +-
 .../synccontext/DefaultSyncContextFactory.java     |  20 +--
 ...taticNameMapper.java => BasedirNameMapper.java} |  64 ++++-----
 .../named/DiscriminatingNameMapper.java            |  43 +++---
 .../impl/synccontext/named/FileGAVNameMapper.java  | 131 ------------------
 .../impl/synccontext/named/GAVNameMapper.java      | 100 ++++++++++----
 .../impl/synccontext/named/HashingNameMapper.java  |  91 +++++++++++++
 .../impl/synccontext/named/NameMapper.java         |  20 ++-
 .../synccontext/named/NamedLockFactoryAdapter.java |   8 +-
 .../impl/synccontext/named/StaticNameMapper.java   |  51 +++----
 .../DiscriminatingNameMapperProvider.java          |  53 ++++++++
 .../named/providers/FileGAVNameMapperProvider.java |  53 ++++++++
 .../FileHashingGAVNameMapperProvider.java          |  54 ++++++++
 .../named/providers/GAVNameMapperProvider.java}    |  38 ++++--
 .../named/providers/StaticNameMapperProvider.java} |  38 ++++--
 .../synccontext/named/providers/package-info.java  |  22 +--
 .../src/site/markdown/synccontextfactory.md.vm     |  10 +-
 .../impl/synccontext/FileLockAdapterTest.java      |   5 +-
 .../NamedLockFactoryAdapterTestSupport.java        |   2 +-
 .../synccontext/named/BasedirNameMapperTest.java   | 148 +++++++++++++++++++++
 .../impl/synccontext/named/GAVNameMapperTest.java  |  95 +++++++++++++
 .../synccontext/named/HashingNameMapperTest.java   | 146 ++++++++++++++++++++
 .../synccontext/named/NameMapperTestSupport.java   |  55 ++++++++
 .../NamedLockFactoryAdapterTestSupport.java        |   2 +-
 .../named/providers/FileLockNamedLockFactory.java  |   2 -
 .../org/eclipse/aether/util/DirectoryUtils.java    | 120 +++++++++++++++++
 .../org/eclipse/aether/util/StringDigestUtil.java  |  93 +++++++++++++
 .../eclipse/aether/util/DirectoryUtilsTest.java    | 128 ++++++++++++++++++
 .../eclipse/aether/util/StringDigestUtilTest.java  | 101 ++++++++++++++
 src/site/markdown/local-repository.md              |   2 +-
 34 files changed, 1416 insertions(+), 426 deletions(-)

diff --git a/.github/workflows/maven-verify.yml b/.github/workflows/maven-verify.yml
index 62709ed2..0d507e8c 100644
--- a/.github/workflows/maven-verify.yml
+++ b/.github/workflows/maven-verify.yml
@@ -25,4 +25,7 @@ jobs:
   build:
     name: Verify
     uses: apache/maven-gh-actions-shared/.github/workflows/maven-verify.yml@v2
+    with:
+      ff-site-run: false
+
 
diff --git a/maven-resolver-demos/maven-resolver-demo-snippets/src/main/java/TestNioLock.java b/maven-resolver-demos/maven-resolver-demo-snippets/src/main/java/TestNioLock.java
index 5d906553..8ec810a2 100644
--- a/maven-resolver-demos/maven-resolver-demo-snippets/src/main/java/TestNioLock.java
+++ b/maven-resolver-demos/maven-resolver-demo-snippets/src/main/java/TestNioLock.java
@@ -32,7 +32,7 @@ import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.atomic.AtomicInteger;
 
 /**
- * A simple tool to check file locking on your OS/FS/Java combo. To use this tool, just copy it to baseDir on
+ * A simple tool to check file locking on your OS/FS/Java combo. To use this tool, just copy it to basedir on
  * the volume you plan to use as local repository and compile and run it:
  * <ul>
  *   <li><pre>javac TestNioLock.java</pre></li>
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 ce7ccc28..d003928e 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
@@ -56,11 +56,12 @@ import org.eclipse.aether.internal.impl.collect.DependencyCollectorDelegate;
 import org.eclipse.aether.internal.impl.collect.bf.BfDependencyCollector;
 import org.eclipse.aether.internal.impl.collect.df.DfDependencyCollector;
 import org.eclipse.aether.internal.impl.synccontext.DefaultSyncContextFactory;
-import org.eclipse.aether.internal.impl.synccontext.named.GAVNameMapper;
-import org.eclipse.aether.internal.impl.synccontext.named.DiscriminatingNameMapper;
 import org.eclipse.aether.internal.impl.synccontext.named.NameMapper;
-import org.eclipse.aether.internal.impl.synccontext.named.StaticNameMapper;
-import org.eclipse.aether.internal.impl.synccontext.named.FileGAVNameMapper;
+import org.eclipse.aether.internal.impl.synccontext.named.providers.DiscriminatingNameMapperProvider;
+import org.eclipse.aether.internal.impl.synccontext.named.providers.FileGAVNameMapperProvider;
+import org.eclipse.aether.internal.impl.synccontext.named.providers.FileHashingGAVNameMapperProvider;
+import org.eclipse.aether.internal.impl.synccontext.named.providers.GAVNameMapperProvider;
+import org.eclipse.aether.internal.impl.synccontext.named.providers.StaticNameMapperProvider;
 import org.eclipse.aether.named.NamedLockFactory;
 import org.eclipse.aether.named.providers.FileLockNamedLockFactory;
 import org.eclipse.aether.named.providers.LocalReadWriteLockNamedLockFactory;
@@ -206,14 +207,16 @@ public class AetherModule
                 .to( org.eclipse.aether.internal.impl.synccontext.legacy.DefaultSyncContextFactory.class )
                 .in( Singleton.class );
 
-        bind( NameMapper.class ).annotatedWith( Names.named( StaticNameMapper.NAME ) )
-                .to( StaticNameMapper.class ).in( Singleton.class );
-        bind( NameMapper.class ).annotatedWith( Names.named( GAVNameMapper.NAME ) )
-                .to( GAVNameMapper.class ).in( Singleton.class );
-        bind( NameMapper.class ).annotatedWith( Names.named( DiscriminatingNameMapper.NAME ) )
-                .to( DiscriminatingNameMapper.class ).in( Singleton.class );
-        bind( NameMapper.class ).annotatedWith( Names.named( FileGAVNameMapper.NAME ) )
-                .to( FileGAVNameMapper.class ).in( Singleton.class );
+        bind( NameMapper.class ).annotatedWith( Names.named( StaticNameMapperProvider.NAME ) )
+                .toProvider( StaticNameMapperProvider.class ).in( Singleton.class );
+        bind( NameMapper.class ).annotatedWith( Names.named( GAVNameMapperProvider.NAME ) )
+                .toProvider( GAVNameMapperProvider.class ).in( Singleton.class );
+        bind( NameMapper.class ).annotatedWith( Names.named( DiscriminatingNameMapperProvider.NAME ) )
+                .toProvider( DiscriminatingNameMapperProvider.class ).in( Singleton.class );
+        bind( NameMapper.class ).annotatedWith( Names.named( FileGAVNameMapperProvider.NAME ) )
+                .toProvider( FileGAVNameMapperProvider.class ).in( Singleton.class );
+        bind( NameMapper.class ).annotatedWith( Names.named( FileHashingGAVNameMapperProvider.NAME ) )
+                .toProvider( FileHashingGAVNameMapperProvider.class ).in( Singleton.class );
 
         bind( NamedLockFactory.class ).annotatedWith( Names.named( NoopNamedLockFactory.NAME ) )
                 .to( NoopNamedLockFactory.class ).in( Singleton.class );
@@ -271,16 +274,18 @@ public class AetherModule
     @Provides
     @Singleton
     Map<String, NameMapper> provideNameMappers(
-            @Named( StaticNameMapper.NAME ) NameMapper staticNameMapper,
-            @Named( GAVNameMapper.NAME ) NameMapper gavNameMapper,
-            @Named( DiscriminatingNameMapper.NAME ) NameMapper discriminatingNameMapper,
-            @Named( FileGAVNameMapper.NAME ) NameMapper fileGavNameMapper )
+            @Named( StaticNameMapperProvider.NAME ) NameMapper staticNameMapper,
+            @Named( GAVNameMapperProvider.NAME ) NameMapper gavNameMapper,
+            @Named( DiscriminatingNameMapperProvider.NAME ) NameMapper discriminatingNameMapper,
+            @Named( FileGAVNameMapperProvider.NAME ) NameMapper fileGavNameMapper,
+            @Named( FileHashingGAVNameMapperProvider.NAME ) NameMapper fileHashingGavNameMapper )
     {
         Map<String, NameMapper> nameMappers = new HashMap<>();
-        nameMappers.put( StaticNameMapper.NAME, staticNameMapper );
-        nameMappers.put( GAVNameMapper.NAME, gavNameMapper );
-        nameMappers.put( DiscriminatingNameMapper.NAME, discriminatingNameMapper );
-        nameMappers.put( FileGAVNameMapper.NAME, fileGavNameMapper );
+        nameMappers.put( StaticNameMapperProvider.NAME, staticNameMapper );
+        nameMappers.put( GAVNameMapperProvider.NAME, gavNameMapper );
+        nameMappers.put( DiscriminatingNameMapperProvider.NAME, discriminatingNameMapper );
+        nameMappers.put( FileGAVNameMapperProvider.NAME, fileGavNameMapper );
+        nameMappers.put( FileHashingGAVNameMapperProvider.NAME, fileHashingGavNameMapper );
         return Collections.unmodifiableMap( nameMappers );
     }
 
diff --git a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/SimpleDigest.java b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/SimpleDigest.java
deleted file mode 100644
index f6ea12dd..00000000
--- a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/SimpleDigest.java
+++ /dev/null
@@ -1,88 +0,0 @@
-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 java.nio.charset.StandardCharsets;
-import java.security.MessageDigest;
-import java.security.NoSuchAlgorithmException;
-import java.util.Arrays;
-
-/**
- * A simple digester for strings. It will traverse through a list of digest algorithms and pick the
- * strongest one available.
- */
-class SimpleDigest
-{
-
-    private static final String[] HASH_ALGOS = new String[] { "SHA-1", "MD5" };
-
-    private final MessageDigest digest;
-
-    SimpleDigest()
-    {
-        MessageDigest md = null;
-        for ( String hashAlgo : HASH_ALGOS )
-        {
-            try
-            {
-                md = MessageDigest.getInstance( hashAlgo );
-                break;
-            }
-            catch ( NoSuchAlgorithmException ne )
-            {
-            }
-        }
-        if ( md == null )
-        {
-            throw new IllegalStateException( "Not supported digests: " + Arrays.toString( HASH_ALGOS ) );
-        }
-        this.digest = md;
-    }
-
-    public void update( String data )
-    {
-        if ( data == null || data.isEmpty() )
-        {
-            return;
-        }
-        digest.update( data.getBytes( StandardCharsets.UTF_8 ) );
-    }
-
-    @SuppressWarnings( "checkstyle:magicnumber" )
-    public String digest()
-    {
-        StringBuilder buffer = new StringBuilder( 64 );
-
-        byte[] bytes = digest.digest();
-        for ( byte aByte : bytes )
-        {
-            int b = aByte & 0xFF;
-
-            if ( b < 0x10 )
-            {
-                buffer.append( '0' );
-            }
-
-            buffer.append( Integer.toHexString( b ) );
-        }
-
-        return buffer.toString();
-    }
-}
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 a16ede57..6758ca00 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
@@ -39,6 +39,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;
 
 /**
  * A local repository manager that realizes the classical Maven 2.0 local repository.
@@ -119,13 +120,13 @@ class SimpleLocalRepositoryManager
                 subKeys.add( mirroredRepo.getId() );
             }
 
-            SimpleDigest digest = new SimpleDigest();
-            digest.update( context );
+            StringDigestUtil sha1 = StringDigestUtil.sha1();
+            sha1.update( context );
             for ( String subKey : subKeys )
             {
-                digest.update( subKey );
+                sha1.update( subKey );
             }
-            buffer.append( digest.digest() );
+            buffer.append( sha1.digest() );
 
             key = buffer.toString();
         }
diff --git a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/synccontext/DefaultSyncContextFactory.java b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/synccontext/DefaultSyncContextFactory.java
index 96996698..65ba3d73 100644
--- a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/synccontext/DefaultSyncContextFactory.java
+++ b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/synccontext/DefaultSyncContextFactory.java
@@ -30,12 +30,13 @@ import java.util.concurrent.CopyOnWriteArrayList;
 
 import org.eclipse.aether.RepositorySystemSession;
 import org.eclipse.aether.SyncContext;
-import org.eclipse.aether.internal.impl.synccontext.named.DiscriminatingNameMapper;
-import org.eclipse.aether.internal.impl.synccontext.named.FileGAVNameMapper;
-import org.eclipse.aether.internal.impl.synccontext.named.GAVNameMapper;
 import org.eclipse.aether.internal.impl.synccontext.named.NameMapper;
 import org.eclipse.aether.internal.impl.synccontext.named.NamedLockFactoryAdapter;
-import org.eclipse.aether.internal.impl.synccontext.named.StaticNameMapper;
+import org.eclipse.aether.internal.impl.synccontext.named.providers.DiscriminatingNameMapperProvider;
+import org.eclipse.aether.internal.impl.synccontext.named.providers.FileGAVNameMapperProvider;
+import org.eclipse.aether.internal.impl.synccontext.named.providers.FileHashingGAVNameMapperProvider;
+import org.eclipse.aether.internal.impl.synccontext.named.providers.GAVNameMapperProvider;
+import org.eclipse.aether.internal.impl.synccontext.named.providers.StaticNameMapperProvider;
 import org.eclipse.aether.named.NamedLockFactory;
 import org.eclipse.aether.named.providers.FileLockNamedLockFactory;
 import org.eclipse.aether.named.providers.LocalReadWriteLockNamedLockFactory;
@@ -64,7 +65,7 @@ public final class DefaultSyncContextFactory
 
     private static final String NAME_MAPPER_KEY = "aether.syncContext.named.nameMapper";
 
-    private static final String DEFAULT_NAME_MAPPER_NAME = GAVNameMapper.NAME;
+    private static final String DEFAULT_NAME_MAPPER_NAME = GAVNameMapperProvider.NAME;
 
     private static final String FACTORY_KEY = "aether.syncContext.named.factory";
 
@@ -102,10 +103,11 @@ public final class DefaultSyncContextFactory
     public void initService( final ServiceLocator locator )
     {
         HashMap<String, NameMapper> mappers = new HashMap<>();
-        mappers.put( StaticNameMapper.NAME, new StaticNameMapper() );
-        mappers.put( GAVNameMapper.NAME, new GAVNameMapper() );
-        mappers.put( DiscriminatingNameMapper.NAME, new DiscriminatingNameMapper( new GAVNameMapper() ) );
-        mappers.put( FileGAVNameMapper.NAME, new FileGAVNameMapper() );
+        mappers.put( StaticNameMapperProvider.NAME, new StaticNameMapperProvider().get() );
+        mappers.put( GAVNameMapperProvider.NAME, new GAVNameMapperProvider().get() );
+        mappers.put( DiscriminatingNameMapperProvider.NAME, new DiscriminatingNameMapperProvider().get() );
+        mappers.put( FileGAVNameMapperProvider.NAME, new FileGAVNameMapperProvider().get() );
+        mappers.put( FileHashingGAVNameMapperProvider.NAME, new FileHashingGAVNameMapperProvider().get() );
         this.nameMappers = mappers;
 
         HashMap<String, NamedLockFactory> factories = new HashMap<>();
diff --git a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/synccontext/named/StaticNameMapper.java b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/synccontext/named/BasedirNameMapper.java
similarity index 50%
copy from maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/synccontext/named/StaticNameMapper.java
copy to maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/synccontext/named/BasedirNameMapper.java
index 14fc7e79..f9d5bd6e 100644
--- a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/synccontext/named/StaticNameMapper.java
+++ b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/synccontext/named/BasedirNameMapper.java
@@ -19,49 +19,41 @@ package org.eclipse.aether.internal.impl.synccontext.named;
  * under the License.
  */
 
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.nio.file.Path;
+import java.util.Collection;
+import java.util.stream.Collectors;
+
 import org.eclipse.aether.RepositorySystemSession;
 import org.eclipse.aether.artifact.Artifact;
 import org.eclipse.aether.metadata.Metadata;
-import org.eclipse.aether.util.ConfigUtils;
+import org.eclipse.aether.util.DirectoryUtils;
 
-import javax.inject.Inject;
-import javax.inject.Named;
-import javax.inject.Singleton;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.Objects;
+import static java.util.Objects.requireNonNull;
 
 /**
- * Static {@link NameMapper}, always assigns one same name, effectively becoming equivalent to "static" sync context.
+ * Wrapping {@link NameMapper} class that is file system friendly: it wraps another
+ * {@link NameMapper} and resolves the resulting "file system friendly" names against local
+ * repository basedir.
+ *
+ * @since TBD
  */
-@Singleton
-@Named( StaticNameMapper.NAME )
-public class StaticNameMapper implements NameMapper
+public class BasedirNameMapper implements NameMapper
 {
-    public static final String NAME = "static";
+    private static final String CONFIG_PROP_LOCKS_DIR = "aether.syncContext.named.basedir.locksDir";
 
-    /**
-     * Configuration property to pass in static name
-     */
-    private static final String CONFIG_PROP_NAME = "aether.syncContext.named.static.name";
+    private final NameMapper delegate;
 
-    private final String name;
-
-    /**
-     * Uses string {@code "static"} for the static name
-     */
-    @Inject
-    public StaticNameMapper()
+    public BasedirNameMapper( final NameMapper delegate )
     {
-        this( NAME );
+        this.delegate = requireNonNull( delegate );
     }
 
-    /**
-     * Uses passed in non-{@code null} string for the static name
-     */
-    public StaticNameMapper( final String name )
+    @Override
+    public boolean isFileSystemFriendly()
     {
-        this.name = Objects.requireNonNull( name );
+        return delegate.isFileSystemFriendly();
     }
 
     @Override
@@ -69,6 +61,18 @@ public class StaticNameMapper implements NameMapper
                                          final Collection<? extends Artifact> artifacts,
                                          final Collection<? extends Metadata> metadatas )
     {
-        return Collections.singletonList( ConfigUtils.getString( session, name, CONFIG_PROP_NAME ) );
+        try
+        {
+            final Path basedir = DirectoryUtils.resolveDirectory(
+                    session, ".locks", CONFIG_PROP_LOCKS_DIR, false );
+
+            return delegate.nameLocks( session, artifacts, metadatas ).stream()
+                    .map( name -> basedir.resolve( name ).toAbsolutePath().toString() )
+                    .collect( Collectors.toList() );
+        }
+        catch ( IOException e )
+        {
+            throw new UncheckedIOException( e );
+        }
     }
 }
diff --git a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/synccontext/named/DiscriminatingNameMapper.java b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/synccontext/named/DiscriminatingNameMapper.java
index 862403e1..0aa74aee 100644
--- a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/synccontext/named/DiscriminatingNameMapper.java
+++ b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/synccontext/named/DiscriminatingNameMapper.java
@@ -22,27 +22,21 @@ package org.eclipse.aether.internal.impl.synccontext.named;
 import org.eclipse.aether.RepositorySystemSession;
 import org.eclipse.aether.artifact.Artifact;
 import org.eclipse.aether.metadata.Metadata;
-import org.eclipse.aether.util.ChecksumUtils;
 import org.eclipse.aether.util.ConfigUtils;
+import org.eclipse.aether.util.StringDigestUtil;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
-import javax.inject.Inject;
-import javax.inject.Named;
-import javax.inject.Singleton;
 import java.io.File;
 import java.net.InetAddress;
 import java.net.UnknownHostException;
-import java.nio.charset.StandardCharsets;
 import java.util.Collection;
-import java.util.Collections;
-import java.util.Map;
 import java.util.Objects;
 
 import static java.util.stream.Collectors.toList;
 
 /**
- * Discriminating {@link NameMapper}, that wraps another {@link NameMapper} and adds a "discriminator" as prefix, that
+ * Wrapping {@link NameMapper}, that wraps another {@link NameMapper} and adds a "discriminator" as prefix, that
  * makes lock names unique including the hostname and local repository (by default). The discriminator may be passed
  * in via {@link RepositorySystemSession} or is automatically calculated based on the local hostname and repository
  * path. The implementation retains order of collection elements as it got it from
@@ -50,12 +44,8 @@ import static java.util.stream.Collectors.toList;
  * <p>
  * The default setup wraps {@link GAVNameMapper}, but manually may be created any instance needed.
  */
-@Singleton
-@Named( DiscriminatingNameMapper.NAME )
 public class DiscriminatingNameMapper implements NameMapper
 {
-    public static final String NAME = "discriminating";
-
     /**
      * Configuration property to pass in discriminator
      */
@@ -72,25 +62,31 @@ public class DiscriminatingNameMapper implements NameMapper
 
     private static final Logger LOGGER = LoggerFactory.getLogger( DiscriminatingNameMapper.class );
 
-    private final NameMapper nameMapper;
+    private final NameMapper delegate;
 
     private final String hostname;
 
-    @Inject
-    public DiscriminatingNameMapper( @Named( GAVNameMapper.NAME ) final NameMapper nameMapper )
+    public DiscriminatingNameMapper( final NameMapper delegate )
     {
-        this.nameMapper = Objects.requireNonNull( nameMapper );
+        this.delegate = Objects.requireNonNull( delegate );
         this.hostname = getHostname();
     }
 
+    @Override
+    public boolean isFileSystemFriendly()
+    {
+        return false; // uses ":" in produced lock names
+    }
+
     @Override
     public Collection<String> nameLocks( final RepositorySystemSession session,
                                          final Collection<? extends Artifact> artifacts,
                                          final Collection<? extends Metadata> metadatas )
     {
         String discriminator = createDiscriminator( session );
-        return nameMapper.nameLocks( session, artifacts, metadatas ).stream().map( s -> discriminator + ":" + s )
-                         .collect( toList() );
+        return delegate.nameLocks( session, artifacts, metadatas ).stream()
+                .map( s -> discriminator + ":" + s )
+                .collect( toList() );
     }
 
     private String getHostname()
@@ -117,16 +113,7 @@ public class DiscriminatingNameMapper implements NameMapper
             discriminator = hostname + ":" + basedir;
             try
             {
-                Map<String, Object> checksums = ChecksumUtils
-                        .calc( discriminator.getBytes( StandardCharsets.UTF_8 ), Collections.singletonList( "SHA-1" ) );
-                Object checksum = checksums.get( "SHA-1" );
-
-                if ( checksum instanceof Exception )
-                {
-                    throw (Exception) checksum;
-                }
-
-                return String.valueOf( checksum );
+                return StringDigestUtil.sha1( discriminator );
             }
             catch ( Exception e )
             {
diff --git a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/synccontext/named/FileGAVNameMapper.java b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/synccontext/named/FileGAVNameMapper.java
deleted file mode 100644
index 500b3306..00000000
--- a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/synccontext/named/FileGAVNameMapper.java
+++ /dev/null
@@ -1,131 +0,0 @@
-package org.eclipse.aether.internal.impl.synccontext.named;
-
-/*
- * 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.named.support.FileSystemFriendly;
-
-import javax.inject.Named;
-import javax.inject.Singleton;
-
-import java.io.File;
-import java.io.IOException;
-import java.io.UncheckedIOException;
-import java.nio.file.Path;
-import java.util.Collection;
-import java.util.TreeSet;
-import java.util.concurrent.ConcurrentHashMap;
-import java.util.concurrent.ConcurrentMap;
-
-/**
- * A {@link NameMapper} that creates same name mapping as Takari Local Repository does, with
- * {@code baseDir} (local repo). Part of code blatantly copies parts of the Takari
- * {@code LockingSyncContext}.
- *
- * @see <a href="https://github.com/takari/takari-local-repository/blob/24133e50a0478dccb5620ac2f2255187608f165b/src/main/java/io/takari/aether/concurrency/LockingSyncContext.java">Takari
- * LockingSyncContext.java</a>
- */
-@Singleton
-@Named( FileGAVNameMapper.NAME )
-public class FileGAVNameMapper
-    implements NameMapper, FileSystemFriendly
-{
-    public static final String NAME = "file-gav";
-
-    private static final String LOCK_SUFFIX = ".resolverlock";
-
-    private static final char SEPARATOR = '~';
-
-    private final ConcurrentMap<String, Path> baseDirs;
-
-    public FileGAVNameMapper()
-    {
-        this.baseDirs = new ConcurrentHashMap<>();
-    }
-
-    @Override
-    public TreeSet<String> nameLocks( final RepositorySystemSession session,
-                                      final Collection<? extends Artifact> artifacts,
-                                      final Collection<? extends Metadata> metadatas )
-    {
-        File localRepositoryBasedir = session.getLocalRepository().getBasedir();
-        // here we abuse concurrent hash map to make sure costly getCanonicalFile is invoked only once
-        Path baseDir = baseDirs.computeIfAbsent(
-            localRepositoryBasedir.getPath(), k ->
-            {
-                try
-                {
-                    return new File( localRepositoryBasedir, ".locks" ).getCanonicalFile().toPath();
-                }
-                catch ( IOException e )
-                {
-                    throw new UncheckedIOException( e );
-                }
-            }
-        );
-
-        TreeSet<String> paths = new TreeSet<>();
-        if ( artifacts != null )
-        {
-            for ( Artifact artifact : artifacts )
-            {
-                paths.add( getPath( baseDir, artifact ) + LOCK_SUFFIX );
-            }
-        }
-        if ( metadatas != null )
-        {
-            for ( Metadata metadata : metadatas )
-            {
-                paths.add( getPath( baseDir, metadata ) + LOCK_SUFFIX );
-            }
-        }
-        return paths;
-    }
-
-    private String getPath( final Path baseDir, final Artifact artifact )
-    {
-        // NOTE: Don't use LRM.getPath*() as those paths could be different across processes, e.g. due to staging LRMs.
-        String path = artifact.getGroupId()
-            + SEPARATOR + artifact.getArtifactId()
-            + SEPARATOR + artifact.getBaseVersion();
-        return baseDir.resolve( path ).toAbsolutePath().toString();
-    }
-
-    private String getPath( final Path baseDir, final Metadata metadata )
-    {
-        // NOTE: Don't use LRM.getPath*() as those paths could be different across processes, e.g. due to staging.
-        String path = "";
-        if ( metadata.getGroupId().length() > 0 )
-        {
-            path += metadata.getGroupId();
-            if ( metadata.getArtifactId().length() > 0 )
-            {
-                path += SEPARATOR + metadata.getArtifactId();
-                if ( metadata.getVersion().length() > 0 )
-                {
-                    path += SEPARATOR + metadata.getVersion();
-                }
-            }
-        }
-        return baseDir.resolve( path ).toAbsolutePath().toString();
-    }
-}
diff --git a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/synccontext/named/GAVNameMapper.java b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/synccontext/named/GAVNameMapper.java
index ea0149c4..1492aa3d 100644
--- a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/synccontext/named/GAVNameMapper.java
+++ b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/synccontext/named/GAVNameMapper.java
@@ -19,24 +19,52 @@ package org.eclipse.aether.internal.impl.synccontext.named;
  * under the License.
  */
 
+import java.util.Collection;
+import java.util.TreeSet;
+
 import org.eclipse.aether.RepositorySystemSession;
 import org.eclipse.aether.artifact.Artifact;
 import org.eclipse.aether.metadata.Metadata;
 
-import javax.inject.Named;
-import javax.inject.Singleton;
-import java.util.Collection;
-import java.util.TreeSet;
+import static java.util.Objects.requireNonNull;
 
 /**
  * Artifact GAV {@link NameMapper}, uses artifact and metadata coordinates to name their corresponding locks. Is not
- * considering local repository, only the artifact coordinates.
+ * considering local repository, only the artifact coordinates. May use custom prefixes and sufixes and separators,
+ * hence this instance may or may not be filesystem friendly (depends on strings used).
  */
-@Singleton
-@Named( GAVNameMapper.NAME )
 public class GAVNameMapper implements NameMapper
 {
-    public static final String NAME = "gav";
+    private final boolean fileSystemFriendly;
+
+    private final String artifactPrefix;
+
+    private final String artifactSuffix;
+
+    private final String metadataPrefix;
+
+    private final String metadataSuffix;
+
+    private final String fieldSeparator;
+
+    public GAVNameMapper( boolean fileSystemFriendly,
+                          String artifactPrefix, String artifactSuffix,
+                          String metadataPrefix, String metadataSuffix,
+                          String fieldSeparator )
+    {
+        this.fileSystemFriendly = fileSystemFriendly;
+        this.artifactPrefix = requireNonNull( artifactPrefix );
+        this.artifactSuffix = requireNonNull( artifactSuffix );
+        this.metadataPrefix = requireNonNull( metadataPrefix );
+        this.metadataSuffix = requireNonNull( metadataSuffix );
+        this.fieldSeparator = requireNonNull( fieldSeparator );
+    }
+
+    @Override
+    public boolean isFileSystemFriendly()
+    {
+        return fileSystemFriendly;
+    }
 
     @Override
     public Collection<String> nameLocks( final RepositorySystemSession session,
@@ -45,15 +73,12 @@ public class GAVNameMapper implements NameMapper
     {
         // Deadlock prevention: https://stackoverflow.com/a/16780988/696632
         // We must acquire multiple locks always in the same order!
-        Collection<String> keys = new TreeSet<>();
+        TreeSet<String> keys = new TreeSet<>();
         if ( artifacts != null )
         {
             for ( Artifact artifact : artifacts )
             {
-                String key = "artifact:" + artifact.getGroupId()
-                             + ":" + artifact.getArtifactId()
-                             + ":" + artifact.getBaseVersion();
-                keys.add( key );
+                keys.add( getArtifactName( artifact ) );
             }
         }
 
@@ -61,22 +86,45 @@ public class GAVNameMapper implements NameMapper
         {
             for ( Metadata metadata : metadatas )
             {
-                StringBuilder key = new StringBuilder( "metadata:" );
-                if ( !metadata.getGroupId().isEmpty() )
+                keys.add( getMetadataName( metadata ) );
+            }
+        }
+        return keys;
+    }
+
+    private String getArtifactName( Artifact artifact )
+    {
+        return artifactPrefix + artifact.getGroupId()
+                + fieldSeparator + artifact.getArtifactId()
+                + fieldSeparator + artifact.getBaseVersion()
+                + artifactSuffix;
+    }
+
+    private String getMetadataName( Metadata metadata )
+    {
+        String name = metadataPrefix;
+        if ( !metadata.getGroupId().isEmpty() )
+        {
+            name += metadata.getGroupId();
+            if ( !metadata.getArtifactId().isEmpty() )
+            {
+                name += fieldSeparator + metadata.getArtifactId();
+                if ( !metadata.getVersion().isEmpty() )
                 {
-                    key.append( metadata.getGroupId() );
-                    if ( !metadata.getArtifactId().isEmpty() )
-                    {
-                        key.append( ':' ).append( metadata.getArtifactId() );
-                        if ( !metadata.getVersion().isEmpty() )
-                        {
-                            key.append( ':' ).append( metadata.getVersion() );
-                        }
-                    }
+                    name += fieldSeparator + metadata.getVersion();
                 }
-                keys.add( key.toString() );
             }
         }
-        return keys;
+        return name + metadataSuffix;
+    }
+
+    public static NameMapper gav()
+    {
+        return new GAVNameMapper( false, "artifact:", "", "metadata:", "", ":" );
+    }
+
+    public static NameMapper fileGav()
+    {
+        return new GAVNameMapper( true, "", ".lock", "", ".lock", "~" );
     }
 }
diff --git a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/synccontext/named/HashingNameMapper.java b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/synccontext/named/HashingNameMapper.java
new file mode 100644
index 00000000..feadd2cb
--- /dev/null
+++ b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/synccontext/named/HashingNameMapper.java
@@ -0,0 +1,91 @@
+package org.eclipse.aether.internal.impl.synccontext.named;
+
+/*
+ * 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 java.util.Collection;
+import java.util.stream.Collectors;
+
+import org.eclipse.aether.RepositorySystemSession;
+import org.eclipse.aether.artifact.Artifact;
+import org.eclipse.aether.metadata.Metadata;
+import org.eclipse.aether.util.ConfigUtils;
+import org.eclipse.aether.util.StringDigestUtil;
+
+import static java.util.Objects.requireNonNull;
+
+/**
+ * Wrapping {@link NameMapper}, that wraps another {@link NameMapper} and hashes resulting strings. It makes use of
+ * fact that (proper) Hash will create unique fixed length string for each different input string (so injection still
+ * stands). This mapper produces file system friendly names. Supports different "depths" (0-4 inclusive) where the
+ * name will contain 0 to 4 level deep directories.
+ * <p>
+ * This mapper is usable in any scenario, but intent was to produce more "compact" name mapper for file locking.
+ *
+ * @since TBD
+ */
+public class HashingNameMapper implements NameMapper
+{
+    private static final String CONFIG_PROP_DEPTH = "aether.syncContext.named.hashing.depth";
+
+    private final NameMapper delegate;
+
+    public HashingNameMapper( final NameMapper delegate )
+    {
+        this.delegate = requireNonNull( delegate );
+    }
+
+    @Override
+    public boolean isFileSystemFriendly()
+    {
+        return true; // hashes delegated strings, so whatever it wrapped, it does not come through
+    }
+
+    @Override
+    public Collection<String> nameLocks( RepositorySystemSession session,
+                                         Collection<? extends Artifact> artifacts,
+                                         Collection<? extends Metadata> metadatas )
+    {
+        final int depth = ConfigUtils.getInteger( session, 2, CONFIG_PROP_DEPTH );
+        if ( depth < 0 || depth > 4 )
+        {
+            throw new IllegalArgumentException( "allowed depth value is between 0 and 4 (inclusive)" );
+        }
+        return delegate.nameLocks( session, artifacts, metadatas ).stream()
+                .map( n -> hashName( n, depth ) )
+                .collect( Collectors.toList() );
+    }
+
+    private String hashName( final String name, final int depth )
+    {
+        String hashedName = StringDigestUtil.sha1( name );
+        if ( depth == 0 )
+        {
+            return hashedName;
+        }
+        StringBuilder prefix = new StringBuilder( "" );
+        int i = 0;
+        while ( i < hashedName.length() && i / 2 < depth )
+        {
+            prefix.append( hashedName, i, i + 2 ).append( "/" );
+            i += 2;
+        }
+        return prefix.append( hashedName ).toString();
+    }
+}
diff --git a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/synccontext/named/NameMapper.java b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/synccontext/named/NameMapper.java
index b5fd2f0e..fa8e8f76 100644
--- a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/synccontext/named/NameMapper.java
+++ b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/synccontext/named/NameMapper.java
@@ -19,24 +19,38 @@ package org.eclipse.aether.internal.impl.synccontext.named;
  * under the License.
  */
 
+import java.util.Collection;
+
 import org.eclipse.aether.RepositorySystemSession;
 import org.eclipse.aether.artifact.Artifact;
 import org.eclipse.aether.metadata.Metadata;
 
-import java.util.Collection;
-
 /**
  * Component mapping lock names to passed in artifacts and metadata as required.
  */
 public interface NameMapper
 {
+    /**
+     * Returns {@code true} if lock names returned by this lock name mapper are file system friendly, can be used
+     * as file names and paths.
+     *
+     * @since TBD
+     */
+    boolean isFileSystemFriendly();
+
     /**
      * Creates (opaque) names for passed in artifacts and metadata. Returned collection has max size of sum of the
      * passed in artifacts and metadata collections, or less. If an empty collection is returned, there will be no
      * locking happening. Never returns {@code null}. The resulting collection MUST BE "stable" (always sorted by
      * same criteria) to avoid deadlocks by acquiring locks in same order, essentially disregarding the order of
      * the input collections.
+     * <p>
+     * There is no requirement of any kind of "parity" between input element count (sum of two collections, that is)
+     * and output collection size, just the returned upper size limit is defined (sum of the passed in two collections
+     * size). If returned collection is empty, no locking will happen, if single element, one lock will be used, if two
+     * then two named locks will be used etc.
      */
-    Collection<String> nameLocks( RepositorySystemSession session, Collection<? extends Artifact> artifacts,
+    Collection<String> nameLocks( RepositorySystemSession session,
+                                  Collection<? extends Artifact> artifacts,
                                   Collection<? extends Metadata> metadatas );
 }
diff --git a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/synccontext/named/NamedLockFactoryAdapter.java b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/synccontext/named/NamedLockFactoryAdapter.java
index bc1a7420..07ee8e6d 100644
--- a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/synccontext/named/NamedLockFactoryAdapter.java
+++ b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/synccontext/named/NamedLockFactoryAdapter.java
@@ -25,7 +25,7 @@ import org.eclipse.aether.artifact.Artifact;
 import org.eclipse.aether.metadata.Metadata;
 import org.eclipse.aether.named.NamedLock;
 import org.eclipse.aether.named.NamedLockFactory;
-import org.eclipse.aether.named.support.FileSystemFriendly;
+import org.eclipse.aether.named.providers.FileLockNamedLockFactory;
 import org.eclipse.aether.util.ConfigUtils;
 
 import org.slf4j.Logger;
@@ -59,11 +59,11 @@ public final class NamedLockFactoryAdapter
         this.nameMapper = Objects.requireNonNull( nameMapper );
         this.namedLockFactory = Objects.requireNonNull( namedLockFactory );
         // TODO: this is ad-hoc "validation", experimental and likely to change
-        if ( this.namedLockFactory instanceof FileSystemFriendly
-                && !( this.nameMapper instanceof FileSystemFriendly ) )
+        if ( this.namedLockFactory instanceof FileLockNamedLockFactory
+                && !this.nameMapper.isFileSystemFriendly() )
         {
             throw new IllegalArgumentException(
-                    "Misconfiguration: FS friendly lock factory requires FS friendly name mapper"
+                    "Misconfiguration: FileLockNamedLockFactory lock factory requires FS friendly NameMapper"
             );
         }
     }
diff --git a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/synccontext/named/StaticNameMapper.java b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/synccontext/named/StaticNameMapper.java
index 14fc7e79..4dcfaac4 100644
--- a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/synccontext/named/StaticNameMapper.java
+++ b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/synccontext/named/StaticNameMapper.java
@@ -19,49 +19,23 @@ package org.eclipse.aether.internal.impl.synccontext.named;
  * under the License.
  */
 
+import java.util.Collection;
+import java.util.Collections;
+
 import org.eclipse.aether.RepositorySystemSession;
 import org.eclipse.aether.artifact.Artifact;
 import org.eclipse.aether.metadata.Metadata;
-import org.eclipse.aether.util.ConfigUtils;
-
-import javax.inject.Inject;
-import javax.inject.Named;
-import javax.inject.Singleton;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.Objects;
 
 /**
- * Static {@link NameMapper}, always assigns one same name, effectively becoming equivalent to "static" sync context.
+ * Static {@link NameMapper}, always assigns one same name, effectively becoming equivalent to "static" sync context:
+ * always maps ANY input to same name.
  */
-@Singleton
-@Named( StaticNameMapper.NAME )
 public class StaticNameMapper implements NameMapper
 {
-    public static final String NAME = "static";
-
-    /**
-     * Configuration property to pass in static name
-     */
-    private static final String CONFIG_PROP_NAME = "aether.syncContext.named.static.name";
-
-    private final String name;
-
-    /**
-     * Uses string {@code "static"} for the static name
-     */
-    @Inject
-    public StaticNameMapper()
-    {
-        this( NAME );
-    }
-
-    /**
-     * Uses passed in non-{@code null} string for the static name
-     */
-    public StaticNameMapper( final String name )
+    @Override
+    public boolean isFileSystemFriendly()
     {
-        this.name = Objects.requireNonNull( name );
+        return true;
     }
 
     @Override
@@ -69,6 +43,13 @@ public class StaticNameMapper implements NameMapper
                                          final Collection<? extends Artifact> artifacts,
                                          final Collection<? extends Metadata> metadatas )
     {
-        return Collections.singletonList( ConfigUtils.getString( session, name, CONFIG_PROP_NAME ) );
+        if ( ( artifacts != null && !artifacts.isEmpty() ) || ( metadatas != null && !metadatas.isEmpty() ) )
+        {
+            return Collections.singletonList( "static" );
+        }
+        else
+        {
+            return Collections.emptyList();
+        }
     }
 }
diff --git a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/synccontext/named/providers/DiscriminatingNameMapperProvider.java b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/synccontext/named/providers/DiscriminatingNameMapperProvider.java
new file mode 100644
index 00000000..3a58772f
--- /dev/null
+++ b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/synccontext/named/providers/DiscriminatingNameMapperProvider.java
@@ -0,0 +1,53 @@
+package org.eclipse.aether.internal.impl.synccontext.named.providers;
+
+/*
+ * 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.Provider;
+import javax.inject.Singleton;
+
+import org.eclipse.aether.internal.impl.synccontext.named.DiscriminatingNameMapper;
+import org.eclipse.aether.internal.impl.synccontext.named.GAVNameMapper;
+import org.eclipse.aether.internal.impl.synccontext.named.NameMapper;
+
+/**
+ * The "discriminating" name mapper provider.
+ *
+ * @since TBD
+ */
+@Singleton
+@Named( DiscriminatingNameMapperProvider.NAME )
+public class DiscriminatingNameMapperProvider implements Provider<NameMapper>
+{
+    public static final String NAME = "discriminating";
+
+    private final NameMapper mapper;
+
+    public DiscriminatingNameMapperProvider()
+    {
+        this.mapper = new DiscriminatingNameMapper( GAVNameMapper.gav() );
+    }
+
+    @Override
+    public NameMapper get()
+    {
+        return mapper;
+    }
+}
diff --git a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/synccontext/named/providers/FileGAVNameMapperProvider.java b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/synccontext/named/providers/FileGAVNameMapperProvider.java
new file mode 100644
index 00000000..041d458b
--- /dev/null
+++ b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/synccontext/named/providers/FileGAVNameMapperProvider.java
@@ -0,0 +1,53 @@
+package org.eclipse.aether.internal.impl.synccontext.named.providers;
+
+/*
+ * 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.Provider;
+import javax.inject.Singleton;
+
+import org.eclipse.aether.internal.impl.synccontext.named.BasedirNameMapper;
+import org.eclipse.aether.internal.impl.synccontext.named.GAVNameMapper;
+import org.eclipse.aether.internal.impl.synccontext.named.NameMapper;
+
+/**
+ * The "file-gav" name mapper provider.
+ *
+ * @since TBD
+ */
+@Singleton
+@Named( FileGAVNameMapperProvider.NAME )
+public class FileGAVNameMapperProvider implements Provider<NameMapper>
+{
+    public static final String NAME = "file-gav";
+
+    private final NameMapper mapper;
+
+    public FileGAVNameMapperProvider()
+    {
+        this.mapper = new BasedirNameMapper( GAVNameMapper.fileGav() );
+    }
+
+    @Override
+    public NameMapper get()
+    {
+        return mapper;
+    }
+}
diff --git a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/synccontext/named/providers/FileHashingGAVNameMapperProvider.java b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/synccontext/named/providers/FileHashingGAVNameMapperProvider.java
new file mode 100644
index 00000000..a81580c8
--- /dev/null
+++ b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/synccontext/named/providers/FileHashingGAVNameMapperProvider.java
@@ -0,0 +1,54 @@
+package org.eclipse.aether.internal.impl.synccontext.named.providers;
+
+/*
+ * 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.Provider;
+import javax.inject.Singleton;
+
+import org.eclipse.aether.internal.impl.synccontext.named.BasedirNameMapper;
+import org.eclipse.aether.internal.impl.synccontext.named.GAVNameMapper;
+import org.eclipse.aether.internal.impl.synccontext.named.HashingNameMapper;
+import org.eclipse.aether.internal.impl.synccontext.named.NameMapper;
+
+/**
+ * The "file-hgav" name mapper provider.
+ *
+ * @since TBD
+ */
+@Singleton
+@Named( FileHashingGAVNameMapperProvider.NAME )
+public class FileHashingGAVNameMapperProvider implements Provider<NameMapper>
+{
+    public static final String NAME = "file-hgav";
+
+    private final NameMapper mapper;
+
+    public FileHashingGAVNameMapperProvider()
+    {
+        this.mapper = new BasedirNameMapper( new HashingNameMapper( GAVNameMapper.gav() ) );
+    }
+
+    @Override
+    public NameMapper get()
+    {
+        return mapper;
+    }
+}
diff --git a/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/synccontext/FileLockAdapterTest.java b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/synccontext/named/providers/GAVNameMapperProvider.java
similarity index 53%
copy from maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/synccontext/FileLockAdapterTest.java
copy to maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/synccontext/named/providers/GAVNameMapperProvider.java
index bebd3674..e8b93ddb 100644
--- a/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/synccontext/FileLockAdapterTest.java
+++ b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/synccontext/named/providers/GAVNameMapperProvider.java
@@ -1,4 +1,4 @@
-package org.eclipse.aether.internal.impl.synccontext;
+package org.eclipse.aether.internal.impl.synccontext.named.providers;
 
 /*
  * Licensed to the Apache Software Foundation (ASF) under one
@@ -19,18 +19,34 @@ package org.eclipse.aether.internal.impl.synccontext;
  * under the License.
  */
 
-import org.eclipse.aether.internal.impl.synccontext.named.FileGAVNameMapper;
-import org.eclipse.aether.named.providers.FileLockNamedLockFactory;
-import org.junit.BeforeClass;
+import javax.inject.Named;
+import javax.inject.Provider;
+import javax.inject.Singleton;
 
-public class FileLockAdapterTest
-    extends NamedLockFactoryAdapterTestSupport
+import org.eclipse.aether.internal.impl.synccontext.named.GAVNameMapper;
+import org.eclipse.aether.internal.impl.synccontext.named.NameMapper;
+
+/**
+ * The "gav" name mapper provider.
+ *
+ * @since TBD
+ */
+@Singleton
+@Named( GAVNameMapperProvider.NAME )
+public class GAVNameMapperProvider implements Provider<NameMapper>
 {
-    @BeforeClass
-    public static void createNamedLockFactory()
+    public static final String NAME = "gav";
+
+    private final NameMapper mapper;
+
+    public GAVNameMapperProvider()
+    {
+        this.mapper = GAVNameMapper.gav();
+    }
+
+    @Override
+    public NameMapper get()
     {
-        nameMapper = new FileGAVNameMapper();
-        namedLockFactory = new FileLockNamedLockFactory();
-        createAdapter();
+        return mapper;
     }
 }
diff --git a/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/synccontext/FileLockAdapterTest.java b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/synccontext/named/providers/StaticNameMapperProvider.java
similarity index 52%
copy from maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/synccontext/FileLockAdapterTest.java
copy to maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/synccontext/named/providers/StaticNameMapperProvider.java
index bebd3674..58edc12b 100644
--- a/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/synccontext/FileLockAdapterTest.java
+++ b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/synccontext/named/providers/StaticNameMapperProvider.java
@@ -1,4 +1,4 @@
-package org.eclipse.aether.internal.impl.synccontext;
+package org.eclipse.aether.internal.impl.synccontext.named.providers;
 
 /*
  * Licensed to the Apache Software Foundation (ASF) under one
@@ -19,18 +19,34 @@ package org.eclipse.aether.internal.impl.synccontext;
  * under the License.
  */
 
-import org.eclipse.aether.internal.impl.synccontext.named.FileGAVNameMapper;
-import org.eclipse.aether.named.providers.FileLockNamedLockFactory;
-import org.junit.BeforeClass;
+import javax.inject.Named;
+import javax.inject.Provider;
+import javax.inject.Singleton;
 
-public class FileLockAdapterTest
-    extends NamedLockFactoryAdapterTestSupport
+import org.eclipse.aether.internal.impl.synccontext.named.NameMapper;
+import org.eclipse.aether.internal.impl.synccontext.named.StaticNameMapper;
+
+/**
+ * The "static" name mapper provider.
+ *
+ * @since TBD
+ */
+@Singleton
+@Named( StaticNameMapperProvider.NAME )
+public class StaticNameMapperProvider implements Provider<NameMapper>
 {
-    @BeforeClass
-    public static void createNamedLockFactory()
+    public static final String NAME = "static";
+
+    private final NameMapper mapper;
+
+    public StaticNameMapperProvider()
+    {
+        this.mapper = new StaticNameMapper();
+    }
+
+    @Override
+    public NameMapper get()
     {
-        nameMapper = new FileGAVNameMapper();
-        namedLockFactory = new FileLockNamedLockFactory();
-        createAdapter();
+        return mapper;
     }
 }
diff --git a/maven-resolver-named-locks/src/main/java/org/eclipse/aether/named/support/FileSystemFriendly.java b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/synccontext/named/providers/package-info.java
similarity index 55%
rename from maven-resolver-named-locks/src/main/java/org/eclipse/aether/named/support/FileSystemFriendly.java
rename to maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/synccontext/named/providers/package-info.java
index d029e4e9..fe86e2d3 100644
--- a/maven-resolver-named-locks/src/main/java/org/eclipse/aether/named/support/FileSystemFriendly.java
+++ b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/synccontext/named/providers/package-info.java
@@ -1,5 +1,4 @@
-package org.eclipse.aether.named.support;
-
+// CHECKSTYLE_OFF: RegexpHeader
 /*
  * Licensed to the Apache Software Foundation (ASF) under one
  * or more contributor license agreements.  See the NOTICE file
@@ -8,9 +7,9 @@ package org.eclipse.aether.named.support;
  * 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
@@ -18,16 +17,9 @@ package org.eclipse.aether.named.support;
  * specific language governing permissions and limitations
  * under the License.
  */
-
 /**
- * A marker interface that mark component "file system friendly". In case of lock factory, it
- * would mean that passed in lock names MUST ADHERE to file path naming convention (and not use
- * some special, non FS friendly characters in it). Essentially, component marked with this
- * interface expects (or uses) that "name" is an absolute and valid file path.
- *
- * <strong>Important note:</strong> Experimental interface, is not meant to be used outside of
- * Maven Resolver codebase. May change or be removed completely without any further notice.
+ * As end-user "mappers" are actually configurations, constructed from several NameMapper implementations, this
+ * package is holding providers that are constructing them, as no NameMapper is a component anymore.
  */
-public interface FileSystemFriendly
-{
-}
+package org.eclipse.aether.internal.impl.synccontext.named.providers;
+
diff --git a/maven-resolver-impl/src/site/markdown/synccontextfactory.md.vm b/maven-resolver-impl/src/site/markdown/synccontextfactory.md.vm
index 9c1f492e..afe611fb 100644
--- a/maven-resolver-impl/src/site/markdown/synccontextfactory.md.vm
+++ b/maven-resolver-impl/src/site/markdown/synccontextfactory.md.vm
@@ -55,7 +55,7 @@ For the `aether.syncContext.named.factory` property following values are allowed
 
 - `rwlock-local` (default), uses JVM `ReentrantReadWriteLock` per lock name, usable for MT builds.
 - `semaphore-local`, uses JVM `Semaphore` per lock name, usable for MT builds.
-- `file-lock`, uses advisory file locking, usable for MP builds (must be used with `file-gav` name mapping).
+- `file-lock`, uses advisory file locking, usable for MP builds (must be used with any of `file-` prefixed name mapping).
 - `noop`, implement no-op locking (no locking). For experimenting only. Has same functionality as old "nolock"
   SyncContextFactory implementation.
 
@@ -64,6 +64,7 @@ For the `aether.syncContext.named.nameMapper` property following values are allo
 - `discriminating` (default), uses hostname + local repo + GAV to create unique lock names for artifacts.
 - `gav` uses GAV to create unique lock names for artifacts and metadata. Is not unique if multiple local repositories are involved.
 - `file-gav` uses GAV and session to create absolute file paths (to be used with `file-lock` factory)
+- `file-hgav` uses more compact layout than `file-gav` by SHA-1 digest, similar to git (to be used with `file-lock` factory)
 - `static` uses static (same) string as lock name for any input. Effectively providing functionality same as old
   "global" locking SyncContextFactory.
 
@@ -76,10 +77,13 @@ Extra values for factory (these need extra setup and will work with Sisu DI only
 
 Other configuration keys:
 
-- `aether.syncContext.named.static.name`, the value to use as static lock name, if `static` name mapper is used.
-  If not set, defaults to "static".
+- `aether.syncContext.named.basedir.locksDir`, the relative or absolute directory path to to store the lock files.
+  If relative, is resolved against local repository, if absolute, is used as is. If not set, defaults to ".locks".
 - `aether.syncContext.named.discriminating.discriminator`, when `discriminating` name mapper is used, sets the a
   discriminator uniquely identifying a host and local repository pair. If not set, discriminator is calculated by
   applying `sha1(hostname + ":" + localRepoPath)` + GAV name mapper.
 - `aether.syncContext.named.discriminating.hostname`, the hostname to be used to calculate discriminator value,
   if above value not set. If not set, the hostname is detected using Java API.
+- `aether.syncContext.named.hashing.depth`, the depth of sub-directories (0=xxxx..., 1=xx/xxxx..., 2=xx/xx/xxxx..., etc)
+  to be created by HashingNameMapper (used with `file-hgav` mapper). Only integer values from 0 to 4 are accepted (inclusive).
+  If not set, defaults to 2.
diff --git a/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/synccontext/FileLockAdapterTest.java b/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/synccontext/FileLockAdapterTest.java
index bebd3674..218af4c1 100644
--- a/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/synccontext/FileLockAdapterTest.java
+++ b/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/synccontext/FileLockAdapterTest.java
@@ -19,7 +19,8 @@ package org.eclipse.aether.internal.impl.synccontext;
  * under the License.
  */
 
-import org.eclipse.aether.internal.impl.synccontext.named.FileGAVNameMapper;
+import org.eclipse.aether.internal.impl.synccontext.named.BasedirNameMapper;
+import org.eclipse.aether.internal.impl.synccontext.named.GAVNameMapper;
 import org.eclipse.aether.named.providers.FileLockNamedLockFactory;
 import org.junit.BeforeClass;
 
@@ -29,7 +30,7 @@ public class FileLockAdapterTest
     @BeforeClass
     public static void createNamedLockFactory()
     {
-        nameMapper = new FileGAVNameMapper();
+        nameMapper = new BasedirNameMapper( GAVNameMapper.fileGav() );
         namedLockFactory = new FileLockNamedLockFactory();
         createAdapter();
     }
diff --git a/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/synccontext/NamedLockFactoryAdapterTestSupport.java b/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/synccontext/NamedLockFactoryAdapterTestSupport.java
index ce176a76..9deadba8 100644
--- a/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/synccontext/NamedLockFactoryAdapterTestSupport.java
+++ b/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/synccontext/NamedLockFactoryAdapterTestSupport.java
@@ -57,7 +57,7 @@ public abstract class NamedLockFactoryAdapterTestSupport
     /**
      * Subclass MAY populate this field but subclass must take care of proper cleanup as well, if needed!
      */
-    protected static NameMapper nameMapper = new DiscriminatingNameMapper(new GAVNameMapper());
+    protected static NameMapper nameMapper = new DiscriminatingNameMapper( GAVNameMapper.gav() );
 
     /**
      * Subclass MUST populate this field but subclass must take care of proper cleanup as well, if needed! Once set,
diff --git a/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/synccontext/named/BasedirNameMapperTest.java b/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/synccontext/named/BasedirNameMapperTest.java
new file mode 100644
index 00000000..3f8dfc5d
--- /dev/null
+++ b/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/synccontext/named/BasedirNameMapperTest.java
@@ -0,0 +1,148 @@
+package org.eclipse.aether.internal.impl.synccontext.named;
+
+/*
+ * 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 java.io.File;
+import java.io.IOException;
+import java.util.Collection;
+import java.util.Iterator;
+
+import org.eclipse.aether.artifact.DefaultArtifact;
+import org.eclipse.aether.metadata.DefaultMetadata;
+import org.eclipse.aether.metadata.Metadata;
+import org.hamcrest.Matchers;
+import org.junit.Test;
+
+import static java.util.Collections.emptyList;
+import static java.util.Collections.singletonList;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.equalTo;
+import static org.hamcrest.Matchers.hasSize;
+
+public class BasedirNameMapperTest extends NameMapperTestSupport
+{
+    private final String PS = File.separator;
+
+    BasedirNameMapper mapper = new BasedirNameMapper( new HashingNameMapper( GAVNameMapper.gav() ) );
+
+    @Test
+    public void nullsAndEmptyInputs()
+    {
+        Collection<String> names;
+
+        names = mapper.nameLocks( session, null, null );
+        assertThat( names, Matchers.empty() );
+
+        names = mapper.nameLocks( session, null, emptyList() );
+        assertThat( names, Matchers.empty() );
+
+        names = mapper.nameLocks( session, emptyList(), null );
+        assertThat( names, Matchers.empty() );
+
+        names = mapper.nameLocks( session, emptyList(), emptyList() );
+        assertThat( names, Matchers.empty() );
+    }
+
+    @Test
+    public void defaultLocksDir()
+    {
+        configProperties.put( "aether.syncContext.named.hashing.depth", "0" );
+        configProperties.put( "aether.syncContext.named.basedir.locksDir", null );
+        DefaultArtifact artifact = new DefaultArtifact( "group:artifact:1.0" );
+        Collection<String> names = mapper.nameLocks( session, singletonList( artifact ), null );
+        assertThat( names, hasSize( 1 ) );
+        assertThat( names.iterator().next(),
+                equalTo( basedir + PS + ".locks" + PS + "46e98183d232f1e16f863025080c7f2b9797fd10" ) );
+    }
+
+    @Test
+    public void relativeLocksDir()
+    {
+        configProperties.put( "aether.syncContext.named.hashing.depth", "0" );
+        configProperties.put( "aether.syncContext.named.basedir.locksDir", "my/locks" );
+        DefaultArtifact artifact = new DefaultArtifact( "group:artifact:1.0" );
+        Collection<String> names = mapper.nameLocks( session, singletonList( artifact ), null );
+        assertThat( names, hasSize( 1 ) );
+        assertThat( names.iterator().next(),
+                equalTo( basedir + PS + "my" + PS + "locks" + PS + "46e98183d232f1e16f863025080c7f2b9797fd10" ) );
+    }
+
+    @Test
+    public void absoluteLocksDir() throws IOException
+    {
+        String absoluteLocksDir = "/my/locks";
+        String customBaseDir = new File( absoluteLocksDir ).getCanonicalPath();
+
+        configProperties.put( "aether.syncContext.named.hashing.depth", "0" );
+        configProperties.put( "aether.syncContext.named.basedir.locksDir", absoluteLocksDir );
+        DefaultArtifact artifact = new DefaultArtifact( "group:artifact:1.0" );
+        Collection<String> names = mapper.nameLocks( session, singletonList( artifact ), null );
+        assertThat( names, hasSize( 1 ) );
+        assertThat( names.iterator().next(), // ends with as we do not test drive letter on non-Win plaf
+                equalTo( customBaseDir + PS + "46e98183d232f1e16f863025080c7f2b9797fd10" ) );
+    }
+
+    @Test
+    public void singleArtifact()
+    {
+        configProperties.put( "aether.syncContext.named.hashing.depth", "0" );
+
+        DefaultArtifact artifact = new DefaultArtifact( "group:artifact:1.0" );
+        Collection<String> names = mapper.nameLocks( session, singletonList( artifact ), null );
+
+        assertThat( names, hasSize( 1 ) );
+        assertThat( names.iterator().next(),
+                equalTo( basedir + PS + ".locks" + PS + "46e98183d232f1e16f863025080c7f2b9797fd10" ) );
+    }
+
+    @Test
+    public void singleMetadata()
+    {
+        configProperties.put( "aether.syncContext.named.hashing.depth", "0" );
+
+        DefaultMetadata metadata =
+                new DefaultMetadata( "group", "artifact", "maven-metadata.xml", Metadata.Nature.RELEASE_OR_SNAPSHOT );
+        Collection<String> names = mapper.nameLocks( session, null, singletonList( metadata ) );
+
+        assertThat( names, hasSize( 1 ) );
+        assertThat( names.iterator().next(),
+                equalTo( basedir + PS + ".locks" + PS + "293b3990971f4b4b02b220620d2538eaac5f221b" ) );
+    }
+
+    @Test
+    public void oneAndOne()
+    {
+        configProperties.put( "aether.syncContext.named.hashing.depth", "0" );
+
+        DefaultArtifact artifact = new DefaultArtifact( "agroup:artifact:1.0" );
+        DefaultMetadata metadata =
+                new DefaultMetadata( "bgroup", "artifact", "maven-metadata.xml", Metadata.Nature.RELEASE_OR_SNAPSHOT );
+        Collection<String> names = mapper.nameLocks( session, singletonList( artifact ), singletonList( metadata ) );
+
+        assertThat( names, hasSize( 2 ) );
+        Iterator<String> namesIterator = names.iterator();
+
+        // they are sorted as well
+        assertThat( namesIterator.next(),
+                equalTo( basedir + PS + ".locks" + PS + "d36504431d00d1c6e4d1c34258f2bf0a004de085" ) );
+        assertThat( namesIterator.next(),
+                equalTo( basedir + PS + ".locks" + PS + "fbcebba60d7eb931eca634f6ca494a8a1701b638" ) );
+    }
+}
diff --git a/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/synccontext/named/GAVNameMapperTest.java b/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/synccontext/named/GAVNameMapperTest.java
new file mode 100644
index 00000000..a943bca6
--- /dev/null
+++ b/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/synccontext/named/GAVNameMapperTest.java
@@ -0,0 +1,95 @@
+package org.eclipse.aether.internal.impl.synccontext.named;
+
+/*
+ * 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 java.util.Collection;
+import java.util.Iterator;
+
+import org.eclipse.aether.artifact.DefaultArtifact;
+import org.eclipse.aether.metadata.DefaultMetadata;
+import org.eclipse.aether.metadata.Metadata;
+import org.hamcrest.Matchers;
+import org.junit.Test;
+
+import static java.util.Collections.emptyList;
+import static java.util.Collections.singletonList;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.equalTo;
+import static org.hamcrest.Matchers.hasSize;
+
+public class GAVNameMapperTest extends NameMapperTestSupport
+{
+    NameMapper mapper = GAVNameMapper.fileGav();
+
+    @Test
+    public void nullsAndEmptyInputs()
+    {
+        Collection<String> names;
+
+        names = mapper.nameLocks( session, null, null );
+        assertThat( names, Matchers.empty() );
+
+        names = mapper.nameLocks( session, null, emptyList() );
+        assertThat( names, Matchers.empty() );
+
+        names = mapper.nameLocks( session, emptyList(), null );
+        assertThat( names, Matchers.empty() );
+
+        names = mapper.nameLocks( session, emptyList(), emptyList() );
+        assertThat( names, Matchers.empty() );
+    }
+
+    @Test
+    public void singleArtifact()
+    {
+        DefaultArtifact artifact = new DefaultArtifact( "group:artifact:1.0" );
+        Collection<String> names = mapper.nameLocks( session, singletonList( artifact ), null );
+
+        assertThat( names, hasSize( 1 ) );
+        assertThat( names.iterator().next(), equalTo( "group~artifact~1.0.lock" ) );
+    }
+
+    @Test
+    public void singleMetadata()
+    {
+        DefaultMetadata metadata =
+                new DefaultMetadata( "group", "artifact", "maven-metadata.xml", Metadata.Nature.RELEASE_OR_SNAPSHOT );
+        Collection<String> names = mapper.nameLocks( session, null, singletonList( metadata ) );
+
+        assertThat( names, hasSize( 1 ) );
+        assertThat( names.iterator().next(), equalTo( "group~artifact.lock" ) );
+    }
+
+    @Test
+    public void oneAndOne()
+    {
+        DefaultArtifact artifact = new DefaultArtifact( "agroup:artifact:1.0" );
+        DefaultMetadata metadata =
+                new DefaultMetadata( "bgroup", "artifact", "maven-metadata.xml", Metadata.Nature.RELEASE_OR_SNAPSHOT );
+        Collection<String> names = mapper.nameLocks( session, singletonList( artifact ), singletonList( metadata ) );
+
+        assertThat( names, hasSize( 2 ) );
+        Iterator<String> namesIterator = names.iterator();
+
+        // they are sorted as well
+        assertThat( namesIterator.next(), equalTo( "agroup~artifact~1.0.lock" ) );
+        assertThat( namesIterator.next(), equalTo( "bgroup~artifact.lock" ) );
+    }
+}
diff --git a/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/synccontext/named/HashingNameMapperTest.java b/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/synccontext/named/HashingNameMapperTest.java
new file mode 100644
index 00000000..12356ec5
--- /dev/null
+++ b/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/synccontext/named/HashingNameMapperTest.java
@@ -0,0 +1,146 @@
+package org.eclipse.aether.internal.impl.synccontext.named;
+
+/*
+ * 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 java.util.Collection;
+import java.util.Iterator;
+
+import org.eclipse.aether.artifact.DefaultArtifact;
+import org.eclipse.aether.metadata.DefaultMetadata;
+import org.eclipse.aether.metadata.Metadata;
+import org.hamcrest.Matchers;
+import org.junit.Test;
+
+import static java.util.Collections.emptyList;
+import static java.util.Collections.singletonList;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.equalTo;
+import static org.hamcrest.Matchers.hasSize;
+
+public class HashingNameMapperTest extends NameMapperTestSupport
+{
+    HashingNameMapper mapper = new HashingNameMapper( GAVNameMapper.gav() );
+
+    @Test
+    public void nullsAndEmptyInputs()
+    {
+        Collection<String> names;
+
+        names = mapper.nameLocks( session, null, null );
+        assertThat( names, Matchers.empty() );
+
+        names = mapper.nameLocks( session, null, emptyList() );
+        assertThat( names, Matchers.empty() );
+
+        names = mapper.nameLocks( session, emptyList(), null );
+        assertThat( names, Matchers.empty() );
+
+        names = mapper.nameLocks( session, emptyList(), emptyList() );
+        assertThat( names, Matchers.empty() );
+    }
+
+    @Test
+    public void singleArtifact_depth0()
+    {
+        configProperties.put( "aether.syncContext.named.hashing.depth", "0" );
+        DefaultArtifact artifact = new DefaultArtifact( "group:artifact:1.0" );
+        Collection<String> names = mapper.nameLocks( session, singletonList( artifact ), null );
+
+        assertThat( names, hasSize( 1 ) );
+        assertThat( names.iterator().next(),
+                equalTo( "46e98183d232f1e16f863025080c7f2b9797fd10" ) );
+    }
+
+    @Test
+    public void singleMetadata_depth0()
+    {
+        configProperties.put( "aether.syncContext.named.hashing.depth", "0" );
+        DefaultMetadata metadata =
+                new DefaultMetadata( "group", "artifact", "maven-metadata.xml", Metadata.Nature.RELEASE_OR_SNAPSHOT );
+        Collection<String> names = mapper.nameLocks( session, null, singletonList( metadata ) );
+
+        assertThat( names, hasSize( 1 ) );
+        assertThat( names.iterator().next(),
+                equalTo( "293b3990971f4b4b02b220620d2538eaac5f221b" ) );
+    }
+
+    @Test
+    public void oneAndOne_depth0()
+    {
+        configProperties.put( "aether.syncContext.named.hashing.depth", "0" );
+        DefaultArtifact artifact = new DefaultArtifact( "agroup:artifact:1.0" );
+        DefaultMetadata metadata =
+                new DefaultMetadata( "bgroup", "artifact", "maven-metadata.xml", Metadata.Nature.RELEASE_OR_SNAPSHOT );
+        Collection<String> names = mapper.nameLocks( session, singletonList( artifact ), singletonList( metadata ) );
+
+        assertThat( names, hasSize( 2 ) );
+        Iterator<String> namesIterator = names.iterator();
+
+        // they are sorted as well
+        assertThat( namesIterator.next(),
+                equalTo( "d36504431d00d1c6e4d1c34258f2bf0a004de085" ) );
+        assertThat( namesIterator.next(),
+                equalTo( "fbcebba60d7eb931eca634f6ca494a8a1701b638" ) );
+    }
+
+    @Test
+    public void singleArtifact_depth2()
+    {
+        configProperties.put( "aether.syncContext.named.hashing.depth", "2" );
+        DefaultArtifact artifact = new DefaultArtifact( "group:artifact:1.0" );
+        Collection<String> names = mapper.nameLocks( session, singletonList( artifact ), null );
+
+        assertThat( names, hasSize( 1 ) );
+        assertThat( names.iterator().next(),
+                equalTo( "46/e9/46e98183d232f1e16f863025080c7f2b9797fd10" ) );
+    }
+
+    @Test
+    public void singleMetadata_depth2()
+    {
+        configProperties.put( "aether.syncContext.named.hashing.depth", "2" );
+        DefaultMetadata metadata =
+                new DefaultMetadata( "group", "artifact", "maven-metadata.xml", Metadata.Nature.RELEASE_OR_SNAPSHOT );
+        Collection<String> names = mapper.nameLocks( session, null, singletonList( metadata ) );
+
+        assertThat( names, hasSize( 1 ) );
+        assertThat( names.iterator().next(),
+                equalTo( "29/3b/293b3990971f4b4b02b220620d2538eaac5f221b" ) );
+    }
+
+    @Test
+    public void oneAndOne_depth2()
+    {
+        configProperties.put( "aether.syncContext.named.hashing.depth", "2" );
+        DefaultArtifact artifact = new DefaultArtifact( "agroup:artifact:1.0" );
+        DefaultMetadata metadata =
+                new DefaultMetadata( "bgroup", "artifact", "maven-metadata.xml", Metadata.Nature.RELEASE_OR_SNAPSHOT );
+        Collection<String> names = mapper.nameLocks( session, singletonList( artifact ), singletonList( metadata ) );
+
+        assertThat( names, hasSize( 2 ) );
+        Iterator<String> namesIterator = names.iterator();
+
+        // they are sorted as well
+        assertThat( namesIterator.next(),
+                equalTo( "d3/65/d36504431d00d1c6e4d1c34258f2bf0a004de085" ) );
+        assertThat( namesIterator.next(),
+                equalTo( "fb/ce/fbcebba60d7eb931eca634f6ca494a8a1701b638" ) );
+    }
+}
diff --git a/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/synccontext/named/NameMapperTestSupport.java b/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/synccontext/named/NameMapperTestSupport.java
new file mode 100644
index 00000000..4a3a60e5
--- /dev/null
+++ b/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/synccontext/named/NameMapperTestSupport.java
@@ -0,0 +1,55 @@
+package org.eclipse.aether.internal.impl.synccontext.named;
+
+/*
+ * 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 java.io.File;
+import java.io.IOException;
+import java.util.HashMap;
+
+import org.eclipse.aether.RepositorySystemSession;
+import org.eclipse.aether.repository.LocalRepository;
+import org.junit.Before;
+
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+/**
+ * Simple support class for {@link NameMapper} implementation UTs.
+ */
+public abstract class NameMapperTestSupport
+{
+    protected String basedir;
+
+    protected HashMap<String, Object> configProperties;
+
+    protected RepositorySystemSession session;
+
+    @Before
+    public void before() throws IOException
+    {
+        basedir = new File("/home/maven/.m2/repository").getCanonicalPath();
+        configProperties = new HashMap<>();
+
+        LocalRepository localRepository = new LocalRepository( new File( basedir ) );
+        session = mock( RepositorySystemSession.class );
+        when( session.getConfigProperties() ).thenReturn( configProperties );
+        when( session.getLocalRepository() ).thenReturn( localRepository );
+    }
+}
diff --git a/maven-resolver-named-locks-hazelcast/src/test/java/org/eclipse/aether/named/hazelcast/NamedLockFactoryAdapterTestSupport.java b/maven-resolver-named-locks-hazelcast/src/test/java/org/eclipse/aether/named/hazelcast/NamedLockFactoryAdapterTestSupport.java
index db5beb2c..86a55ae9 100644
--- a/maven-resolver-named-locks-hazelcast/src/test/java/org/eclipse/aether/named/hazelcast/NamedLockFactoryAdapterTestSupport.java
+++ b/maven-resolver-named-locks-hazelcast/src/test/java/org/eclipse/aether/named/hazelcast/NamedLockFactoryAdapterTestSupport.java
@@ -66,7 +66,7 @@ public abstract class NamedLockFactoryAdapterTestSupport
     protected static void setNamedLockFactory( final NamedLockFactory namedLockFactory )
     {
         adapter = new NamedLockFactoryAdapter(
-                new DiscriminatingNameMapper( new GAVNameMapper() ), namedLockFactory
+                new DiscriminatingNameMapper( GAVNameMapper.gav() ), namedLockFactory
         );
     }
 
diff --git a/maven-resolver-named-locks/src/main/java/org/eclipse/aether/named/providers/FileLockNamedLockFactory.java b/maven-resolver-named-locks/src/main/java/org/eclipse/aether/named/providers/FileLockNamedLockFactory.java
index d15b1be5..cacabe8e 100644
--- a/maven-resolver-named-locks/src/main/java/org/eclipse/aether/named/providers/FileLockNamedLockFactory.java
+++ b/maven-resolver-named-locks/src/main/java/org/eclipse/aether/named/providers/FileLockNamedLockFactory.java
@@ -33,7 +33,6 @@ import javax.inject.Named;
 import javax.inject.Singleton;
 
 import org.eclipse.aether.named.support.FileLockNamedLock;
-import org.eclipse.aether.named.support.FileSystemFriendly;
 import org.eclipse.aether.named.support.NamedLockFactorySupport;
 import org.eclipse.aether.named.support.NamedLockSupport;
 
@@ -47,7 +46,6 @@ import org.eclipse.aether.named.support.NamedLockSupport;
 @Named( FileLockNamedLockFactory.NAME )
 public class FileLockNamedLockFactory
     extends NamedLockFactorySupport
-    implements FileSystemFriendly
 {
     public static final String NAME = "file-lock";
 
diff --git a/maven-resolver-util/src/main/java/org/eclipse/aether/util/DirectoryUtils.java b/maven-resolver-util/src/main/java/org/eclipse/aether/util/DirectoryUtils.java
new file mode 100644
index 00000000..ee1739a5
--- /dev/null
+++ b/maven-resolver-util/src/main/java/org/eclipse/aether/util/DirectoryUtils.java
@@ -0,0 +1,120 @@
+package org.eclipse.aether.util;
+
+/*
+ * 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 java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+
+import org.eclipse.aether.RepositorySystemSession;
+
+import static java.util.Objects.requireNonNull;
+
+/**
+ * A utility class to calculate (and create if needed) paths backed by directories using configuration properties from
+ * repository system session and others.
+ *
+ * @see RepositorySystemSession#getConfigProperties()
+ * @see RepositorySystemSession#getLocalRepository()
+ * @since TBD
+ */
+public final class DirectoryUtils
+{
+    private DirectoryUtils()
+    {
+        // hide constructor
+    }
+
+    /**
+     * Creates {@link Path} instance out of passed in {@code name} parameter. May create a directory on resulting path,
+     * if not exists. Following outcomes may happen:
+     * <ul>
+     *     <li>{@code name} is absolute path - results in {@link Path} instance created directly from name.</li>
+     *     <li>{@code name} is relative path - results in {@link Path} instance resolved with {@code base} parameter.
+     *     </li>
+     * </ul>
+     * Resulting path is being checked is a directory, and if not, it will be created. If resulting path exists but
+     * is not a directory, this method will fail.
+     *
+     * @param name      The name to create directory with, cannot be {@code null}.
+     * @param base      The base {@link Path} to resolve name, if it is relative path, cannot be {@code null}.
+     * @param mayCreate If resulting path does not exist, should it create?
+     * @return The {@link Path} instance that is resolved and backed by existing directory.
+     * @throws IOException If some IO related errors happens.
+     */
+    public static Path resolveDirectory( String name, Path base, boolean mayCreate ) throws IOException
+    {
+        requireNonNull( name, "name is null" );
+        requireNonNull( base, "base is null" );
+        final Path namePath = Paths.get( name );
+        final Path result;
+        if ( namePath.isAbsolute() )
+        {
+            result = namePath.normalize();
+        }
+        else
+        {
+            result = base.resolve( namePath ).normalize();
+        }
+
+        if ( !Files.exists( result ) )
+        {
+            if ( mayCreate )
+            {
+                Files.createDirectories( result );
+            }
+        }
+        else if ( !Files.isDirectory( result ) )
+        {
+            throw new IOException( "Path exists, but is not a directory: " + result );
+        }
+        return result;
+    }
+
+    /**
+     * Creates {@link Path} instance out of session configuration, and (if relative) resolve it against local
+     * repository
+     * basedir. Pre-populates values and invokes {@link #resolveDirectory(String, Path, boolean)}.
+     * <p>
+     * For this method to work, {@link org.eclipse.aether.repository.LocalRepository#getBasedir()} must return
+     * non-{@code null} value, otherwise {@link NullPointerException} is thrown.
+     *
+     * @param session     The session, may not be {@code null}.
+     * @param defaultName The default value if not present in session configuration, may not be {@code null}.
+     * @param nameKey     The key to look up for in session configuration to obtain user set value.
+     * @param mayCreate   If resulting path does not exist, should it create?
+     * @return The {@link Path} instance that is resolved and backed by existing directory.
+     * @throws IOException If some IO related errors happens.
+     */
+    public static Path resolveDirectory( RepositorySystemSession session,
+                                         String defaultName,
+                                         String nameKey,
+                                         boolean mayCreate )
+            throws IOException
+    {
+        requireNonNull( session, "session is null" );
+        requireNonNull( defaultName, "defaultName is null" );
+        requireNonNull( nameKey, "nameKey is null" );
+        requireNonNull( session.getLocalRepository().getBasedir(), "session.localRepository.basedir is null" );
+        return resolveDirectory( ConfigUtils.getString( session, defaultName, nameKey ),
+                session.getLocalRepository().getBasedir().toPath(), mayCreate );
+    }
+}
diff --git a/maven-resolver-util/src/main/java/org/eclipse/aether/util/StringDigestUtil.java b/maven-resolver-util/src/main/java/org/eclipse/aether/util/StringDigestUtil.java
new file mode 100644
index 00000000..0f3fa8c2
--- /dev/null
+++ b/maven-resolver-util/src/main/java/org/eclipse/aether/util/StringDigestUtil.java
@@ -0,0 +1,93 @@
+package org.eclipse.aether.util;
+
+/*
+ * 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 java.nio.charset.StandardCharsets;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+
+/**
+ * A simple digester utility for Strings. Uses {@link MessageDigest} for requested algorithm. Supports one-pass or
+ * several rounds of updates, and as result emits hex encoded String.
+ *
+ * @since TBD
+ */
+public final class StringDigestUtil
+{
+    private final MessageDigest digest;
+
+    /**
+     * Constructs instance with given algorithm.
+     *
+     * @see #sha1()
+     * @see #sha1(String)
+     */
+    public StringDigestUtil( final String alg )
+    {
+        try
+        {
+            this.digest = MessageDigest.getInstance( alg );
+        }
+        catch ( NoSuchAlgorithmException e )
+        {
+            throw new IllegalStateException( "Not supported digest algorithm: " + alg );
+        }
+    }
+
+    /**
+     * Updates instance with passed in string.
+     */
+    public StringDigestUtil update( String data )
+    {
+        if ( data != null && !data.isEmpty() )
+        {
+            digest.update( data.getBytes( StandardCharsets.UTF_8 ) );
+        }
+        return this;
+    }
+
+    /**
+     * Returns the digest of all strings passed via {@link #update(String)} as hex string. There is no state preserved
+     * and due implementation of {@link MessageDigest#digest()}, same applies here: this instance "resets" itself.
+     * Hence, the digest hex encoded string is returned only once.
+     *
+     * @see MessageDigest#digest()
+     */
+    public String digest()
+    {
+        return ChecksumUtils.toHexString( digest.digest() );
+    }
+
+    /**
+     * Helper method to create {@link StringDigestUtil} using SHA-1 digest algorithm.
+     */
+    public static StringDigestUtil sha1()
+    {
+        return new StringDigestUtil( "SHA-1" );
+    }
+
+    /**
+     * Helper method to calculate SHA-1 digest and hex encode it.
+     */
+    public static String sha1( final String string )
+    {
+        return sha1().update( string ).digest();
+    }
+}
diff --git a/maven-resolver-util/src/test/java/org/eclipse/aether/util/DirectoryUtilsTest.java b/maven-resolver-util/src/test/java/org/eclipse/aether/util/DirectoryUtilsTest.java
new file mode 100644
index 00000000..f47c69a7
--- /dev/null
+++ b/maven-resolver-util/src/test/java/org/eclipse/aether/util/DirectoryUtilsTest.java
@@ -0,0 +1,128 @@
+package org.eclipse.aether.util;
+
+/*
+ * 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 java.io.IOException;
+import java.nio.file.FileSystems;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TestName;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.equalTo;
+import static org.hamcrest.Matchers.startsWith;
+import static org.junit.Assume.assumeTrue;
+
+public class DirectoryUtilsTest
+{
+    @Rule
+    public TestName testName = new TestName();
+
+    @Test
+    public void expectedCasesRelative() throws IOException
+    {
+        // hack for surefire: sets the property but directory may not exist
+        Files.createDirectories( Paths.get ( System.getProperty( "java.io.tmpdir" ) ) );
+
+        Path tmpDir = Files.createTempDirectory( testName.getMethodName() );
+        Path result;
+
+        result = DirectoryUtils.resolveDirectory( "foo", tmpDir, false );
+        assertThat( result, equalTo( tmpDir.resolve( "foo" ) ) );
+
+        result = DirectoryUtils.resolveDirectory( "foo/bar", tmpDir, false );
+        assertThat( result, equalTo( tmpDir.resolve( "foo/bar" ) ) );
+
+        result = DirectoryUtils.resolveDirectory( "foo/./bar/..", tmpDir, false );
+        assertThat( result, equalTo( tmpDir.resolve( "foo" ) ) );
+    }
+
+    @Test
+    public void expectedCasesAbsolute() throws IOException
+    {
+        // TODO: this test is skipped on Windows, as it is not clear which drive letter will `new File("/foo")`
+        // path get. According to Windows (and  assuming Java Path does separator change OK), "\foo" file should
+        // get resolved to CWD drive + "\foo" path, but seems Java 17 is different from 11 and 8 in this respect.
+        // This below WORKS on win + Java8 abd win + Java11 but FAILS on win + Java17
+        assumeTrue( !"WindowsFileSystem".equals( FileSystems.getDefault().getClass().getSimpleName() ) );
+
+        // hack for surefire: sets the property but directory may not exist
+        Files.createDirectories( Paths.get ( System.getProperty( "java.io.tmpdir" ) ) );
+
+        Path tmpDir = Files.createTempDirectory( testName.getMethodName() );
+        Path result;
+
+        result = DirectoryUtils.resolveDirectory( "/foo", tmpDir, false );
+        assertThat( result, equalTo( FileSystems.getDefault().getPath( "/foo" ).toAbsolutePath() ) );
+
+        result = DirectoryUtils.resolveDirectory( "/foo/bar", tmpDir, false );
+        assertThat( result, equalTo( FileSystems.getDefault().getPath( "/foo/bar" ).toAbsolutePath() ) );
+
+        result = DirectoryUtils.resolveDirectory( "/foo/./bar/..", tmpDir, false );
+        assertThat( result, equalTo( FileSystems.getDefault().getPath( "/foo" ).toAbsolutePath() ) );
+    }
+
+    @Test
+    public void existsButIsADirectory() throws IOException
+    {
+        // hack for surefire: sets the property but directory may not exist
+        Files.createDirectories( Paths.get ( System.getProperty( "java.io.tmpdir" ) ) );
+
+        Path tmpDir = Files.createTempDirectory( testName.getMethodName() );
+        Files.createDirectories( tmpDir.resolve( "foo" ) );
+        Path result = DirectoryUtils.resolveDirectory( "foo", tmpDir, false );
+        assertThat( result, equalTo( tmpDir.resolve( "foo" ) ) );
+    }
+
+    @Test
+    public void existsButNotADirectory() throws IOException
+    {
+        // hack for surefire: sets the property but directory may not exist
+        Files.createDirectories( Paths.get ( System.getProperty( "java.io.tmpdir" ) ) );
+
+        Path tmpDir = Files.createTempDirectory( testName.getMethodName() );
+        Files.createFile( tmpDir.resolve( "foo" ) );
+        try
+        {
+            DirectoryUtils.resolveDirectory( "foo", tmpDir, false );
+        }
+        catch ( IOException e )
+        {
+            assertThat( e.getMessage(), startsWith( "Path exists, but is not a directory:" ) );
+        }
+    }
+
+    @Test
+    public void notExistsAndIsCreated() throws IOException
+    {
+        // hack for surefire: sets the property but directory may not exist
+        Files.createDirectories( Paths.get ( System.getProperty( "java.io.tmpdir" ) ) );
+
+        Path tmpDir = Files.createTempDirectory( testName.getMethodName() );
+        Files.createDirectories( tmpDir.resolve( "foo" ) );
+        Path result = DirectoryUtils.resolveDirectory( "foo", tmpDir, true );
+        assertThat( result, equalTo( tmpDir.resolve( "foo" ) ) );
+        assertThat( Files.isDirectory( tmpDir.resolve( "foo" ) ), equalTo( true ) );
+    }
+}
diff --git a/maven-resolver-util/src/test/java/org/eclipse/aether/util/StringDigestUtilTest.java b/maven-resolver-util/src/test/java/org/eclipse/aether/util/StringDigestUtilTest.java
new file mode 100644
index 00000000..23d9839f
--- /dev/null
+++ b/maven-resolver-util/src/test/java/org/eclipse/aether/util/StringDigestUtilTest.java
@@ -0,0 +1,101 @@
+package org.eclipse.aether.util;
+
+/*
+ * 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.junit.Test;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.is;
+import static org.junit.Assert.fail;
+
+public class StringDigestUtilTest
+{
+    @Test
+    public void sha1Simple()
+    {
+        assertThat( StringDigestUtil.sha1( null ),
+                is( "da39a3ee5e6b4b0d3255bfef95601890afd80709" ) );
+        assertThat( StringDigestUtil.sha1( "" ),
+                is( "da39a3ee5e6b4b0d3255bfef95601890afd80709" ) );
+        assertThat( StringDigestUtil.sha1( "something" ),
+                is( "1af17e73721dbe0c40011b82ed4bb1a7dbe3ce29" ) );
+        assertThat( StringDigestUtil.sha1().update( null ).digest(),
+                is( "da39a3ee5e6b4b0d3255bfef95601890afd80709" ) );
+        assertThat( StringDigestUtil.sha1().update( "" ).digest(),
+                is( "da39a3ee5e6b4b0d3255bfef95601890afd80709" ) );
+        assertThat( StringDigestUtil.sha1().update( "something" ).digest(),
+                is( "1af17e73721dbe0c40011b82ed4bb1a7dbe3ce29" ) );
+        assertThat( StringDigestUtil.sha1().update( "some" ).update( "thing" ).digest(),
+                is( "1af17e73721dbe0c40011b82ed4bb1a7dbe3ce29" ) );
+    }
+
+    @Test
+    public void sha1Manual()
+    {
+        assertThat( new StringDigestUtil( "SHA-1" ).digest(),
+                is( "da39a3ee5e6b4b0d3255bfef95601890afd80709" ) );
+        assertThat( new StringDigestUtil( "SHA-1" ).update( "" ).digest(),
+                is( "da39a3ee5e6b4b0d3255bfef95601890afd80709" ) );
+        assertThat( new StringDigestUtil( "SHA-1" ).update( "something" ).digest(),
+                is( "1af17e73721dbe0c40011b82ed4bb1a7dbe3ce29" ) );
+        assertThat( new StringDigestUtil( "SHA-1" ).update( null ).digest(),
+                is( "da39a3ee5e6b4b0d3255bfef95601890afd80709" ) );
+        assertThat( new StringDigestUtil( "SHA-1" ).update( "" ).digest(),
+                is( "da39a3ee5e6b4b0d3255bfef95601890afd80709" ) );
+        assertThat( new StringDigestUtil( "SHA-1" ).update( "something" ).digest(),
+                is( "1af17e73721dbe0c40011b82ed4bb1a7dbe3ce29" ) );
+        assertThat( new StringDigestUtil( "SHA-1" ).update( "some" ).update( "thing" ).digest(),
+                is( "1af17e73721dbe0c40011b82ed4bb1a7dbe3ce29" ) );
+    }
+
+    @Test
+    public void md5Manual()
+    {
+        assertThat( new StringDigestUtil( "MD5" ).digest(),
+                is( "d41d8cd98f00b204e9800998ecf8427e" ) );
+        assertThat( new StringDigestUtil( "MD5" ).update( "" ).digest(),
+                is( "d41d8cd98f00b204e9800998ecf8427e" ) );
+        assertThat( new StringDigestUtil( "MD5" ).update( "something" ).digest(),
+                is( "437b930db84b8079c2dd804a71936b5f" ) );
+        assertThat( new StringDigestUtil( "MD5" ).update( null ).digest(),
+                is( "d41d8cd98f00b204e9800998ecf8427e" ) );
+        assertThat( new StringDigestUtil( "MD5" ).update( "" ).digest(),
+                is( "d41d8cd98f00b204e9800998ecf8427e" ) );
+        assertThat( new StringDigestUtil( "MD5" ).update( "something" ).digest(),
+                is( "437b930db84b8079c2dd804a71936b5f" ) );
+        assertThat( new StringDigestUtil( "MD5" ).update( "some" ).update( "thing" ).digest(),
+                is( "437b930db84b8079c2dd804a71936b5f" ) );
+    }
+
+    @Test
+    public void unsupportedAlg()
+    {
+        try
+        {
+            new StringDigestUtil( "FOO-BAR" );
+            fail( "StringDigestUtil should throw" );
+        }
+        catch ( IllegalStateException e )
+        {
+            // good
+        }
+    }
+}
+
diff --git a/src/site/markdown/local-repository.md b/src/site/markdown/local-repository.md
index 3554c396..44440660 100644
--- a/src/site/markdown/local-repository.md
+++ b/src/site/markdown/local-repository.md
@@ -156,7 +156,7 @@ To manually instantiate a simple LRM, one needs to invoke following code:
 
 ```java
 LocalRepositoryManager simple = new SimpleLocalRepositoryManagerFactory()
-        .newInstance( session, new LocalRepository( baseDir ) );
+        .newInstance( session, new LocalRepository( basedir ) );
 ```
 
 Note: This code snippet above instantiates a component, that is not