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 <[email protected]>
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

Reply via email to