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

[maven-resolver] 01/01: [EXPERIMENT] Dynamic LRM

This is an automated email from the ASF dual-hosted git repository.

cstamas pushed a commit to branch dynamic-lrm
in repository https://gitbox.apache.org/repos/asf/maven-resolver.git

commit 322b98048aecd6565090b63d8fb673c46ee3cd32
Author: Tamas Cservenak <ta...@cservenak.net>
AuthorDate: Thu Apr 14 14:07:20 2022 +0200

    [EXPERIMENT] Dynamic LRM
---
 .../eclipse/aether/impl/DefaultServiceLocator.java |   3 +
 .../eclipse/aether/impl/guice/AetherModule.java    |   7 +-
 .../impl/DefaultDynamicPrefixComposerFactory.java  | 120 +++++++
 .../impl/DynamicLocalRepositoryManager.java        | 152 +++++++++
 ...a => DynamicLocalRepositoryManagerFactory.java} |  49 ++-
 .../internal/impl/DynamicPrefixComposer.java       |  78 +++++
 .../impl/DynamicPrefixComposerFactory.java         |  38 +++
 .../impl/EnhancedLocalRepositoryManager.java       |  70 +++-
 .../impl/SimpleLocalRepositoryManager.java         |  34 +-
 .../impl/SimpleLocalRepositoryManagerFactory.java  |   2 +-
 .../impl/DynamicLocalRepositoryManagerTest.java    | 355 +++++++++++++++++++++
 11 files changed, 869 insertions(+), 39 deletions(-)

diff --git a/maven-resolver-impl/src/main/java/org/eclipse/aether/impl/DefaultServiceLocator.java b/maven-resolver-impl/src/main/java/org/eclipse/aether/impl/DefaultServiceLocator.java
index e3bf55a2..e021760f 100644
--- a/maven-resolver-impl/src/main/java/org/eclipse/aether/impl/DefaultServiceLocator.java
+++ b/maven-resolver-impl/src/main/java/org/eclipse/aether/impl/DefaultServiceLocator.java
@@ -35,7 +35,9 @@ import org.eclipse.aether.internal.impl.ArtifactPathComposer;
 import org.eclipse.aether.internal.impl.DefaultArtifactPathComposer;
 import org.eclipse.aether.internal.impl.DefaultArtifactResolver;
 import org.eclipse.aether.internal.impl.DefaultChecksumPolicyProvider;
+import org.eclipse.aether.internal.impl.DefaultDynamicPrefixComposerFactory;
 import org.eclipse.aether.internal.impl.DefaultTrackingFileManager;
+import org.eclipse.aether.internal.impl.DynamicPrefixComposerFactory;
 import org.eclipse.aether.internal.impl.TrackingFileManager;
 import org.eclipse.aether.internal.impl.checksum.DefaultChecksumAlgorithmFactorySelector;
 import org.eclipse.aether.internal.impl.collect.DefaultDependencyCollector;
@@ -230,6 +232,7 @@ public final class DefaultServiceLocator
         addService( NamedLockFactorySelector.class, SimpleNamedLockFactorySelector.class );
         addService( ChecksumAlgorithmFactorySelector.class, DefaultChecksumAlgorithmFactorySelector.class );
         addService( ArtifactPathComposer.class, DefaultArtifactPathComposer.class );
+        addService( DynamicPrefixComposerFactory.class, DefaultDynamicPrefixComposerFactory.class );
     }
 
     private <T> Entry<T> getEntry( Class<T> type, boolean create )
diff --git a/maven-resolver-impl/src/main/java/org/eclipse/aether/impl/guice/AetherModule.java b/maven-resolver-impl/src/main/java/org/eclipse/aether/impl/guice/AetherModule.java
index 49e29adc..eeebaa29 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
@@ -42,7 +42,9 @@ import org.eclipse.aether.impl.RepositoryConnectorProvider;
 import org.eclipse.aether.impl.RepositoryEventDispatcher;
 import org.eclipse.aether.internal.impl.ArtifactPathComposer;
 import org.eclipse.aether.internal.impl.DefaultArtifactPathComposer;
+import org.eclipse.aether.internal.impl.DefaultDynamicPrefixComposerFactory;
 import org.eclipse.aether.internal.impl.DefaultTrackingFileManager;
+import org.eclipse.aether.internal.impl.DynamicPrefixComposerFactory;
 import org.eclipse.aether.internal.impl.FileProvidedChecksumsSource;
 import org.eclipse.aether.internal.impl.TrackingFileManager;
 import org.eclipse.aether.internal.impl.checksum.Md5ChecksumAlgorithmFactory;
@@ -174,7 +176,10 @@ public class AetherModule
         bind( OfflineController.class ) //
                 .to( DefaultOfflineController.class ).in( Singleton.class );
 
-        bind( ArtifactPathComposer.class ).to( DefaultArtifactPathComposer.class ).in( Singleton.class );
+        bind( ArtifactPathComposer.class ) //
+                .to( DefaultArtifactPathComposer.class ).in( Singleton.class );
+        bind( DynamicPrefixComposerFactory.class ) //
+                .to( DefaultDynamicPrefixComposerFactory.class ).in( Singleton.class );
         bind( LocalRepositoryProvider.class ) //
                 .to( DefaultLocalRepositoryProvider.class ).in( Singleton.class );
         bind( LocalRepositoryManagerFactory.class ).annotatedWith( Names.named( "simple" ) ) //
diff --git a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/DefaultDynamicPrefixComposerFactory.java b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/DefaultDynamicPrefixComposerFactory.java
new file mode 100644
index 00000000..e03a935c
--- /dev/null
+++ b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/DefaultDynamicPrefixComposerFactory.java
@@ -0,0 +1,120 @@
+package org.eclipse.aether.internal.impl;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *  http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import javax.inject.Named;
+import javax.inject.Singleton;
+
+import org.eclipse.aether.RepositorySystemSession;
+import org.eclipse.aether.artifact.Artifact;
+import org.eclipse.aether.metadata.Metadata;
+import org.eclipse.aether.repository.RemoteRepository;
+
+/**
+ * Default implementation of {@link DynamicPrefixComposerFactory}.
+ *
+ * @since TBD
+ */
+@Singleton
+@Named
+public final class DefaultDynamicPrefixComposerFactory implements DynamicPrefixComposerFactory
+{
+    @Override
+    public DynamicPrefixComposer createComposer( RepositorySystemSession session )
+    {
+        return new Blow();
+    }
+
+    private static final class Blow implements DynamicPrefixComposer
+    {
+        @Override
+        public boolean isRemoteSplitByOrigin()
+        {
+            return true;
+        }
+
+        @Override
+        public String getPrefixForLocalArtifact( Artifact artifact )
+        {
+            return "local-" + ( artifact.isSnapshot() ? "snapshot" : "release" );
+        }
+
+        @Override
+        public String getPrefixForRemoteArtifact( Artifact artifact, RemoteRepository repository, String context )
+        {
+            return "remote-"
+                    + ( artifact.isSnapshot() ? "snapshot" : "release" ) + '/'
+                    + ( repository == null ? "norepository" : repository.getId() ) + '/'
+                    + ( context == null || context.isEmpty() ? "noctx" : context );
+        }
+
+        @Override
+        public String getPrefixForLocalMetadata( Metadata metadata )
+        {
+            return "local-" + ( isSnapshot( metadata ) ? "snapshot" : "release" );
+        }
+
+        @Override
+        public String getPrefixForRemoteMetadata( Metadata metadata, RemoteRepository repository, String context )
+        {
+            return "remote-"
+                    + ( isSnapshot( metadata ) ? "snapshot" : "release" ) + '/'
+                    + ( repository == null ? "norepository" : repository.getId() ) + '/'
+                    + ( context == null || context.isEmpty() ? "noctx" : context );
+        }
+
+        private boolean isSnapshot( Metadata metadata )
+        {
+            return metadata.getVersion() != null && metadata.getVersion().endsWith( "SNAPSHOT" );
+        }
+    }
+    private static final class NoopDynamicPrefixComposer implements DynamicPrefixComposer
+    {
+        @Override
+        public boolean isRemoteSplitByOrigin()
+        {
+            return false;
+        }
+
+        @Override
+        public String getPrefixForLocalArtifact( Artifact artifact )
+        {
+            return null;
+        }
+
+        @Override
+        public String getPrefixForRemoteArtifact( Artifact artifact, RemoteRepository repository, String context )
+        {
+            return null;
+        }
+
+        @Override
+        public String getPrefixForLocalMetadata( Metadata metadata )
+        {
+            return null;
+        }
+
+        @Override
+        public String getPrefixForRemoteMetadata( Metadata metadata, RemoteRepository repository, String context )
+        {
+            return null;
+        }
+    }
+}
diff --git a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/DynamicLocalRepositoryManager.java b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/DynamicLocalRepositoryManager.java
new file mode 100644
index 00000000..b75b1d5e
--- /dev/null
+++ b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/DynamicLocalRepositoryManager.java
@@ -0,0 +1,152 @@
+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.io.File;
+import java.util.Objects;
+
+import org.eclipse.aether.RepositorySystemSession;
+import org.eclipse.aether.artifact.Artifact;
+import org.eclipse.aether.metadata.Metadata;
+import org.eclipse.aether.repository.LocalArtifactRegistration;
+import org.eclipse.aether.repository.LocalArtifactRequest;
+import org.eclipse.aether.repository.LocalArtifactResult;
+import org.eclipse.aether.repository.RemoteRepository;
+
+import static java.util.Objects.requireNonNull;
+
+/**
+ * Implementation of dynamic local repository manager, subject to change without prior notice.
+ *
+ * @see DynamicLocalRepositoryManagerFactory
+ * @since TBD
+ */
+class DynamicLocalRepositoryManager
+        extends EnhancedLocalRepositoryManager
+{
+    private final DynamicPrefixComposer dynamicPrefixComposer;
+
+    DynamicLocalRepositoryManager( File basedir,
+                                   ArtifactPathComposer artifactPathComposer,
+                                   RepositorySystemSession session,
+                                   TrackingFileManager trackingFileManager,
+                                   DynamicPrefixComposer dynamicPrefixComposer )
+    {
+        super( basedir, artifactPathComposer, session, trackingFileManager );
+        this.dynamicPrefixComposer = requireNonNull( dynamicPrefixComposer );
+    }
+
+    private String concatPaths( String prefix, String artifactPath )
+    {
+        if ( prefix == null || prefix.isEmpty() )
+        {
+            return artifactPath;
+        }
+        return prefix + '/' + artifactPath;
+    }
+
+    @Override
+    public String getPathForLocalArtifact( Artifact artifact )
+    {
+        return concatPaths(
+                dynamicPrefixComposer.getPrefixForLocalArtifact( artifact ),
+                super.getPathForLocalArtifact( artifact )
+        );
+    }
+
+    @Override
+    public String getPathForRemoteArtifact( Artifact artifact, RemoteRepository repository, String context )
+    {
+        return concatPaths(
+                dynamicPrefixComposer.getPrefixForRemoteArtifact( artifact, repository, context ),
+                super.getPathForRemoteArtifact( artifact, repository, context )
+        );
+    }
+
+    @Override
+    public String getPathForLocalMetadata( Metadata metadata )
+    {
+        return concatPaths(
+                dynamicPrefixComposer.getPrefixForLocalMetadata( metadata ),
+                super.getPathForLocalMetadata( metadata )
+        );
+    }
+
+    @Override
+    public String getPathForRemoteMetadata( Metadata metadata, RemoteRepository repository, String context )
+    {
+        return concatPaths(
+                dynamicPrefixComposer.getPrefixForRemoteMetadata( metadata, repository, context ),
+                super.getPathForRemoteMetadata( metadata, repository, context )
+        );
+    }
+
+    @Override
+    public LocalArtifactResult find( RepositorySystemSession session, LocalArtifactRequest request )
+    {
+        if ( !dynamicPrefixComposer.isRemoteSplitByOrigin() )
+        {
+            return super.find( session, request );
+        }
+        else
+        {
+            // we have split storage, so not "tracking" needed (to emulate split)
+            // try local first, and then try remotes as in request
+            Artifact artifact = request.getArtifact();
+            LocalArtifactResult result = new LocalArtifactResult( request );
+            String path = getPathForLocalArtifact( artifact );
+            File file = new File( getRepository().getBasedir(), path );
+
+            if ( Objects.equals( artifact.getVersion(), artifact.getBaseVersion() ) && file.isFile() )
+            {
+                result.setFile( file );
+                result.setAvailable( true );
+            }
+
+            if ( !result.isAvailable() )
+            {
+                for ( RemoteRepository repository : request.getRepositories() )
+                {
+                    path = getPathForRemoteArtifact( artifact, repository, request.getContext() );
+                    file = new File( getRepository().getBasedir(), path );
+                    if ( file.isFile() )
+                    {
+                        result.setFile( file );
+                        result.setAvailable( true );
+                        result.setRepository( repository );
+                        break;
+                    }
+                }
+            }
+            return result;
+        }
+    }
+
+    @Override
+    public void add( RepositorySystemSession session, LocalArtifactRegistration request )
+    {
+        requireNonNull( session, "session cannot be null" );
+        requireNonNull( request, "request cannot be null" );
+        if ( !dynamicPrefixComposer.isRemoteSplitByOrigin() )
+        {
+            super.add( session, request );
+        }
+    }
+}
diff --git a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/SimpleLocalRepositoryManagerFactory.java b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/DynamicLocalRepositoryManagerFactory.java
similarity index 50%
copy from maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/SimpleLocalRepositoryManagerFactory.java
copy to maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/DynamicLocalRepositoryManagerFactory.java
index d62301f5..46e72999 100644
--- a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/SimpleLocalRepositoryManagerFactory.java
+++ b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/DynamicLocalRepositoryManagerFactory.java
@@ -23,8 +23,6 @@ import javax.inject.Inject;
 import javax.inject.Named;
 import javax.inject.Singleton;
 
-import java.util.Objects;
-
 import org.eclipse.aether.RepositorySystemSession;
 import org.eclipse.aether.repository.LocalRepository;
 import org.eclipse.aether.repository.LocalRepositoryManager;
@@ -36,32 +34,51 @@ import org.eclipse.aether.spi.locator.ServiceLocator;
 import static java.util.Objects.requireNonNull;
 
 /**
- * Creates local repository managers for repository type {@code "simple"}.
+ * Creates dynamic local repository managers for repository types {@code "default"} or {@code "" (automatic)}. This is
+ * a completely new local repository implementation with capabilities to split "local" (locally installed) and "remote"
+ * (cached from remote) artifacts, and also split release and snapshot artifacts. It is able to split even
+ * by origin repositories as well. Resolution of locally cached artifacts will be rejected in case the current
+ * resolution request does not match the known source repositories of an artifact. If cache split by origin enabled,
+ * it physically separates artifact caches per remote repository, while if split not enabled, similar technique is
+ * used as in case of enhanced local repository, will emulate physically separated artifact caches per remote
+ * repository.
+ *
+ * @since TBD
  */
 @Singleton
-@Named( "simple" )
-public class SimpleLocalRepositoryManagerFactory
+@Named( "dynamic" )
+public class DynamicLocalRepositoryManagerFactory
     implements LocalRepositoryManagerFactory, Service
 {
-    private float priority;
+    private float priority = 11.0f;
 
     private ArtifactPathComposer artifactPathComposer;
 
-    public SimpleLocalRepositoryManagerFactory()
+    private DynamicPrefixComposerFactory dynamicPrefixComposerFactory;
+
+    private TrackingFileManager trackingFileManager;
+
+    public DynamicLocalRepositoryManagerFactory()
     {
-        // enable no-arg constructor
+        // no arg ctor for ServiceLocator
     }
 
     @Inject
-    public SimpleLocalRepositoryManagerFactory( final ArtifactPathComposer artifactPathComposer )
+    public DynamicLocalRepositoryManagerFactory( final ArtifactPathComposer artifactPathComposer,
+                                                 final TrackingFileManager trackingFileManager,
+                                                 final DynamicPrefixComposerFactory dynamicPrefixComposerFactory )
     {
         this.artifactPathComposer = requireNonNull( artifactPathComposer );
+        this.trackingFileManager = requireNonNull( trackingFileManager );
+        this.dynamicPrefixComposerFactory = requireNonNull( dynamicPrefixComposerFactory );
     }
 
     @Override
     public void initService( final ServiceLocator locator )
     {
-        this.artifactPathComposer = Objects.requireNonNull( locator.getService( ArtifactPathComposer.class ) );
+        this.artifactPathComposer = requireNonNull( locator.getService( ArtifactPathComposer.class ) );
+        this.trackingFileManager = requireNonNull( locator.getService( TrackingFileManager.class ) );
+        this.dynamicPrefixComposerFactory = requireNonNull( locator.getService( DynamicPrefixComposerFactory.class ) );
     }
 
     @Override
@@ -71,9 +88,15 @@ public class SimpleLocalRepositoryManagerFactory
         requireNonNull( session, "session cannot be null" );
         requireNonNull( repository, "repository cannot be null" );
 
-        if ( "".equals( repository.getContentType() ) || "simple".equals( repository.getContentType() ) )
+        if ( "".equals( repository.getContentType() ) || "default".equals( repository.getContentType() ) )
         {
-            return new SimpleLocalRepositoryManager( repository.getBasedir(), "simple", artifactPathComposer );
+            return new DynamicLocalRepositoryManager(
+                    repository.getBasedir(),
+                    artifactPathComposer,
+                    session,
+                    trackingFileManager,
+                    dynamicPrefixComposerFactory.createComposer( session )
+            );
         }
         else
         {
@@ -93,7 +116,7 @@ public class SimpleLocalRepositoryManagerFactory
      * @param priority The priority.
      * @return This component for chaining, never {@code null}.
      */
-    public SimpleLocalRepositoryManagerFactory setPriority( float priority )
+    public DynamicLocalRepositoryManagerFactory setPriority( float priority )
     {
         this.priority = priority;
         return this;
diff --git a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/DynamicPrefixComposer.java b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/DynamicPrefixComposer.java
new file mode 100644
index 00000000..d923e231
--- /dev/null
+++ b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/DynamicPrefixComposer.java
@@ -0,0 +1,78 @@
+package org.eclipse.aether.internal.impl;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *  http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import org.eclipse.aether.artifact.Artifact;
+import org.eclipse.aether.metadata.Metadata;
+import org.eclipse.aether.repository.RemoteRepository;
+
+/**
+ * Composes prefixes for {@link DynamicLocalRepositoryManager}.
+ *
+ * @since TBD
+ */
+public interface DynamicPrefixComposer
+{
+    /**
+     * Returns {@code true} if methods
+     * {@link #getPrefixForRemoteArtifact(Artifact, RemoteRepository, String)} and
+     * {@link #getPrefixForRemoteMetadata(Metadata, RemoteRepository, String)} factor in into
+     * returned prefix the origin, thus physically separating the cache storage for origin.
+     *
+     * @return {@code true} if remote caches are physically separated by origin.
+     */
+    boolean isRemoteSplitByOrigin();
+
+    /**
+     * Gets the path prefix for a locally installed artifact.
+     *
+     * @param artifact The artifact for which to determine the prefix, must not be {@code null}.
+     * @return The prefix, may be {@code null} (note: {@code null}s and empty strings are treated equally).
+     */
+    String getPrefixForLocalArtifact( Artifact artifact );
+
+    /**
+     * Gets the path prefix for an artifact cached from a remote repository.
+     *
+     * @param artifact   The artifact for which to determine the prefix, must not be {@code null}.
+     * @param repository The source repository of the artifact, must not be {@code null}.
+     * @param context    The resolution context in which the artifact is being requested, may be {@code null}.
+     * @return The prefix, may be {@code null} (note: {@code null}s and empty strings are treated equally).
+     */
+    String getPrefixForRemoteArtifact( Artifact artifact, RemoteRepository repository, String context );
+
+    /**
+     * Gets the path prefix for locally installed metadata.
+     *
+     * @param metadata The metadata for which to determine the prefix, must not be {@code null}.
+     * @return The prefix, may be {@code null} (note: {@code null}s and empty strings are treated equally).
+     */
+    String getPrefixForLocalMetadata( Metadata metadata );
+
+    /**
+     * Gets the path prefix for metadata cached from a remote repository.
+     *
+     * @param metadata   The metadata for which to determine the prefix, must not be {@code null}.
+     * @param repository The source repository of the metadata, must not be {@code null}.
+     * @param context    The resolution context in which the metadata is being requested, may be {@code null}.
+     * @return The prefix, may be {@code null} (note: {@code null}s and empty strings are treated equally).
+     */
+    String getPrefixForRemoteMetadata( Metadata metadata, RemoteRepository repository, String context );
+}
diff --git a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/DynamicPrefixComposerFactory.java b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/DynamicPrefixComposerFactory.java
new file mode 100644
index 00000000..25373091
--- /dev/null
+++ b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/DynamicPrefixComposerFactory.java
@@ -0,0 +1,38 @@
+package org.eclipse.aether.internal.impl;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *  http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import org.eclipse.aether.RepositorySystemSession;
+
+/**
+ * Creates instances of {@link DynamicPrefixComposer}.
+ *
+ * @since TBD
+ */
+public interface DynamicPrefixComposerFactory
+{
+    /**
+     * Creates {@link DynamicPrefixComposer} instance out of whatever configuration it finds in passed in session.
+     *
+     * @param session The repository session.
+     * @return The created instance, never {@code null}.
+     */
+    DynamicPrefixComposer createComposer( RepositorySystemSession session );
+}
diff --git a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/EnhancedLocalRepositoryManager.java b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/EnhancedLocalRepositoryManager.java
index 13110db6..e09804fa 100644
--- a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/EnhancedLocalRepositoryManager.java
+++ b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/EnhancedLocalRepositoryManager.java
@@ -8,9 +8,9 @@ package org.eclipse.aether.internal.impl;
  * to you under the Apache License, Version 2.0 (the
  * "License"); you may not use this file except in compliance
  * with the License.  You may obtain a copy of the License at
- * 
+ *
  *  http://www.apache.org/licenses/LICENSE-2.0
- * 
+ *
  * Unless required by applicable law or agreed to in writing,
  * software distributed under the License is distributed on an
  * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
@@ -25,6 +25,7 @@ import java.util.Collections;
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.Map;
+
 import static java.util.Objects.requireNonNull;
 
 import java.util.Objects;
@@ -44,7 +45,7 @@ import org.eclipse.aether.util.ConfigUtils;
  * <code>_remote.repositories</code>, with content key as filename&gt;repo_id and value as empty string. If a file has
  * been installed in the repository, but not downloaded from a remote repository, it is tracked as empty repository id
  * and always resolved. For example:
- * 
+ *
  * <pre>
  * artifact-1.0.pom>=
  * artifact-1.0.jar>=
@@ -54,11 +55,11 @@ import org.eclipse.aether.util.ConfigUtils;
  * artifact-1.0-classifier.zip>central=
  * artifact-1.0.pom>my_repo_id=
  * </pre>
- * 
+ *
  * @see EnhancedLocalRepositoryManagerFactory
  */
 class EnhancedLocalRepositoryManager
-    extends SimpleLocalRepositoryManager
+        extends SimpleLocalRepositoryManager
 {
 
     private static final String LOCAL_REPO_ID = "";
@@ -75,22 +76,50 @@ class EnhancedLocalRepositoryManager
         super( basedir, "enhanced", artifactPathComposer );
         String filename = ConfigUtils.getString( session, "", "aether.enhancedLocalRepository.trackingFilename" );
         if ( filename.isEmpty() || filename.contains( "/" ) || filename.contains( "\\" )
-            || filename.contains( ".." ) )
+                || filename.contains( ".." ) )
         {
             filename = "_remote.repositories";
         }
         this.trackingFilename = filename;
-        this.trackingFileManager = Objects.requireNonNull( trackingFileManager );
+        this.trackingFileManager = requireNonNull( trackingFileManager );
     }
 
     @Override
     public LocalArtifactResult find( RepositorySystemSession session, LocalArtifactRequest request )
     {
-        String path = getPathForArtifact( request.getArtifact(), false );
+        Artifact artifact = request.getArtifact();
+        String path = getPathForLocalArtifact( artifact );
         File file = new File( getRepository().getBasedir(), path );
 
         LocalArtifactResult result = new LocalArtifactResult( request );
 
+        // request may ask for specific timestamped snapshot, while getPathForLocalArtifact turns it into -SNAPSHOT
+        if ( Objects.equals( artifact.getVersion(), artifact.getBaseVersion() ) )
+        {
+            checkFind( file, result );
+        }
+
+        if ( !result.isAvailable() )
+        {
+            for ( RemoteRepository repository : request.getRepositories() )
+            {
+                path = getPathForRemoteArtifact( artifact, repository, request.getContext() );
+                file = new File( getRepository().getBasedir(), path );
+
+                checkFind( file, result );
+
+                if ( result.isAvailable() )
+                {
+                    break;
+                }
+            }
+        }
+
+        return result;
+    }
+
+    private void checkFind( File file, LocalArtifactResult result )
+    {
         if ( file.isFile() )
         {
             result.setFile( file );
@@ -104,8 +133,8 @@ class EnhancedLocalRepositoryManager
             }
             else
             {
-                String context = request.getContext();
-                for ( RemoteRepository repository : request.getRepositories() )
+                String context = result.getRequest().getContext();
+                for ( RemoteRepository repository : result.getRequest().getRepositories() )
                 {
                     if ( props.get( getKey( file, getRepositoryKey( repository, context ) ) ) != null )
                     {
@@ -126,8 +155,6 @@ class EnhancedLocalRepositoryManager
                 }
             }
         }
-
-        return result;
     }
 
     @Override
@@ -142,7 +169,17 @@ class EnhancedLocalRepositoryManager
         {
             repositories = getRepositoryKeys( request.getRepository(), request.getContexts() );
         }
-        addArtifact( request.getArtifact(), repositories, request.getRepository() == null );
+        if ( request.getRepository() == null )
+        {
+            addArtifact( request.getArtifact(), repositories, null, null );
+        }
+        else
+        {
+            for ( String context : request.getContexts() )
+            {
+                addArtifact( request.getArtifact(), repositories, request.getRepository(), context );
+            }
+        }
     }
 
     private Collection<String> getRepositoryKeys( RemoteRepository repository, Collection<String> contexts )
@@ -160,9 +197,12 @@ class EnhancedLocalRepositoryManager
         return keys;
     }
 
-    private void addArtifact( Artifact artifact, Collection<String> repositories, boolean local )
+    private void addArtifact( Artifact artifact, Collection<String> repositories, RemoteRepository repository,
+                              String context )
     {
-        String path = getPathForArtifact( requireNonNull( artifact, "artifact cannot be null" ), local );
+        requireNonNull( artifact, "artifact cannot be null" );
+        String path = repository == null ? getPathForLocalArtifact( artifact )
+                : getPathForRemoteArtifact( artifact, repository, context );
         File file = new File( getRepository().getBasedir(), path );
         addRepo( file, repositories );
     }
diff --git a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/SimpleLocalRepositoryManager.java b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/SimpleLocalRepositoryManager.java
index c208e53c..b0147574 100644
--- a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/SimpleLocalRepositoryManager.java
+++ b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/SimpleLocalRepositoryManager.java
@@ -21,6 +21,8 @@ package org.eclipse.aether.internal.impl;
 
 import java.io.File;
 import static java.util.Objects.requireNonNull;
+
+import java.util.Objects;
 import java.util.SortedSet;
 import java.util.TreeSet;
 
@@ -61,16 +63,11 @@ class SimpleLocalRepositoryManager
         return repository;
     }
 
-    protected String getPathForArtifact( Artifact artifact, boolean local )
-    {
-        return artifactPathComposer.getPathForArtifact( artifact, local );
-    }
-
     @Override
     public String getPathForLocalArtifact( Artifact artifact )
     {
         requireNonNull( artifact, "artifact cannot be null" );
-        return getPathForArtifact( artifact, true );
+        return artifactPathComposer.getPathForArtifact( artifact, true );
     }
 
     @Override
@@ -78,7 +75,7 @@ class SimpleLocalRepositoryManager
     {
         requireNonNull( artifact, "artifact cannot be null" );
         requireNonNull( repository, "repository cannot be null" );
-        return getPathForArtifact( artifact, false );
+        return artifactPathComposer.getPathForArtifact( artifact, false );
     }
 
     @Override
@@ -141,16 +138,35 @@ class SimpleLocalRepositoryManager
     {
         requireNonNull( session, "session cannot be null" );
         requireNonNull( request, "request cannot be null" );
-        String path = getPathForArtifact( request.getArtifact(), false );
+        Artifact artifact = request.getArtifact();
+        String path = getPathForLocalArtifact( artifact );
         File file = new File( getRepository().getBasedir(), path );
 
         LocalArtifactResult result = new LocalArtifactResult( request );
-        if ( file.isFile() )
+
+        // request may ask for specific timestamped snapshot, while getPathForLocalArtifact turns it into -SNAPSHOT
+        if ( Objects.equals( artifact.getVersion(), artifact.getBaseVersion() ) && file.isFile() )
         {
             result.setFile( file );
             result.setAvailable( true );
         }
 
+        if ( !result.isAvailable() )
+        {
+            for ( RemoteRepository repository : request.getRepositories() )
+            {
+                path = getPathForRemoteArtifact( artifact, repository, request.getContext() );
+                file = new File( getRepository().getBasedir(), path );
+                if ( file.isFile() )
+                {
+                    result.setFile( file );
+                    result.setAvailable( true );
+                    break;
+                }
+            }
+
+        }
+
         return result;
     }
 
diff --git a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/SimpleLocalRepositoryManagerFactory.java b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/SimpleLocalRepositoryManagerFactory.java
index d62301f5..d19af00e 100644
--- a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/SimpleLocalRepositoryManagerFactory.java
+++ b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/SimpleLocalRepositoryManagerFactory.java
@@ -45,7 +45,7 @@ public class SimpleLocalRepositoryManagerFactory
 {
     private float priority;
 
-    private ArtifactPathComposer artifactPathComposer;
+    private ArtifactPathComposer artifactPathComposer = new DefaultArtifactPathComposer(); // to enable Maven tests
 
     public SimpleLocalRepositoryManagerFactory()
     {
diff --git a/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/DynamicLocalRepositoryManagerTest.java b/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/DynamicLocalRepositoryManagerTest.java
new file mode 100644
index 00000000..be22e8d2
--- /dev/null
+++ b/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/DynamicLocalRepositoryManagerTest.java
@@ -0,0 +1,355 @@
+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.io.File;
+import java.io.IOException;
+import java.net.URI;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+
+import org.eclipse.aether.RepositorySystemSession;
+import org.eclipse.aether.artifact.Artifact;
+import org.eclipse.aether.artifact.DefaultArtifact;
+import org.eclipse.aether.internal.test.util.TestFileUtils;
+import org.eclipse.aether.internal.test.util.TestUtils;
+import org.eclipse.aether.metadata.DefaultMetadata;
+import org.eclipse.aether.metadata.Metadata;
+import org.eclipse.aether.metadata.Metadata.Nature;
+import org.eclipse.aether.repository.LocalArtifactRegistration;
+import org.eclipse.aether.repository.LocalArtifactRequest;
+import org.eclipse.aether.repository.LocalArtifactResult;
+import org.eclipse.aether.repository.LocalMetadataRequest;
+import org.eclipse.aether.repository.LocalMetadataResult;
+import org.eclipse.aether.repository.RemoteRepository;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+public class DynamicLocalRepositoryManagerTest
+{
+
+    private Artifact artifact;
+
+    private Artifact snapshot;
+
+    private File basedir;
+
+    private DynamicLocalRepositoryManager manager;
+
+    private File artifactFile;
+
+    private RemoteRepository repository;
+
+    private String testContext = "project/compile";
+
+    private TrackingFileManager trackingFileManager;
+
+    private RepositorySystemSession session;
+
+    private Metadata metadata;
+
+    private Metadata noVerMetadata;
+
+    @Before
+    public void setup()
+        throws Exception
+    {
+        String url = TestFileUtils.createTempDir( "enhanced-remote-repo" ).toURI().toURL().toString();
+        repository =
+            new RemoteRepository.Builder( "enhanced-remote-repo", "default", url ).setRepositoryManager( true ).build();
+
+        artifact =
+            new DefaultArtifact( "gid", "aid", "", "jar", "1-test", Collections.<String, String> emptyMap(),
+                                 TestFileUtils.createTempFile( "artifact" ) );
+
+        snapshot =
+            new DefaultArtifact( "gid", "aid", "", "jar", "1.0-20120710.231549-9",
+                                 Collections.<String, String> emptyMap(), TestFileUtils.createTempFile( "artifact" ) );
+
+        metadata =
+            new DefaultMetadata( "gid", "aid", "1-test", "maven-metadata.xml", Nature.RELEASE,
+                                 TestFileUtils.createTempFile( "metadata" ) );
+
+        noVerMetadata =
+            new DefaultMetadata( "gid", "aid", null, "maven-metadata.xml", Nature.RELEASE,
+                                 TestFileUtils.createTempFile( "metadata" ) );
+
+        basedir = TestFileUtils.createTempDir( "enhanced-repo" );
+        session = TestUtils.newSession();
+        trackingFileManager = new DefaultTrackingFileManager();
+        manager = new DynamicLocalRepositoryManager(
+                basedir,
+                new DefaultArtifactPathComposer(),
+                session,
+                trackingFileManager,
+                new DefaultDynamicPrefixComposerFactory().createComposer( session )
+        );
+
+        artifactFile = new File( basedir, manager.getPathForLocalArtifact( artifact ) );
+    }
+
+    @After
+    public void tearDown()
+        throws Exception
+    {
+        TestFileUtils.deleteFile( basedir );
+        TestFileUtils.deleteFile( new File( new URI( repository.getUrl() ) ) );
+
+        session = null;
+        manager = null;
+        repository = null;
+        artifact = null;
+    }
+
+    private long addLocalArtifact( Artifact artifact )
+        throws IOException
+    {
+        manager.add( session, new LocalArtifactRegistration( artifact ) );
+        String path = manager.getPathForLocalArtifact( artifact );
+
+        return copy( artifact, path );
+    }
+
+    private long addRemoteArtifact( Artifact artifact )
+        throws IOException
+    {
+        Collection<String> contexts = Arrays.asList( testContext );
+        manager.add( session, new LocalArtifactRegistration( artifact, repository, contexts ) );
+        String path = manager.getPathForRemoteArtifact( artifact, repository, testContext );
+        return copy( artifact, path );
+    }
+
+    private long copy( Metadata metadata, String path )
+        throws IOException
+    {
+        if ( metadata.getFile() == null )
+        {
+            return -1L;
+        }
+        return TestFileUtils.copyFile( metadata.getFile(), new File( basedir, path ) );
+    }
+
+    private long copy( Artifact artifact, String path )
+        throws IOException
+    {
+        if ( artifact.getFile() == null )
+        {
+            return -1L;
+        }
+        File artifactFile = new File( basedir, path );
+        return TestFileUtils.copyFile( artifact.getFile(), artifactFile );
+    }
+
+    @Test
+    public void testGetPathForLocalArtifact()
+    {
+        Artifact artifact = new DefaultArtifact( "g.i.d:a.i.d:1.0-SNAPSHOT" );
+        assertEquals( "1.0-SNAPSHOT", artifact.getBaseVersion() );
+        assertEquals( "g/i/d/a.i.d/1.0-SNAPSHOT/a.i.d-1.0-SNAPSHOT.jar", manager.getPathForLocalArtifact( artifact ) );
+
+        artifact = new DefaultArtifact( "g.i.d:a.i.d:1.0-20110329.221805-4" );
+        assertEquals( "1.0-SNAPSHOT", artifact.getBaseVersion() );
+        assertEquals( "g/i/d/a.i.d/1.0-SNAPSHOT/a.i.d-1.0-SNAPSHOT.jar", manager.getPathForLocalArtifact( artifact ) );
+    }
+
+    @Test
+    public void testGetPathForRemoteArtifact()
+    {
+        RemoteRepository remoteRepo = new RemoteRepository.Builder( "repo", "default", "ram:/void" ).build();
+
+        Artifact artifact = new DefaultArtifact( "g.i.d:a.i.d:1.0-SNAPSHOT" );
+        assertEquals( "1.0-SNAPSHOT", artifact.getBaseVersion() );
+        assertEquals( "g/i/d/a.i.d/1.0-SNAPSHOT/a.i.d-1.0-SNAPSHOT.jar",
+                      manager.getPathForRemoteArtifact( artifact, remoteRepo, "" ) );
+
+        artifact = new DefaultArtifact( "g.i.d:a.i.d:1.0-20110329.221805-4" );
+        assertEquals( "1.0-SNAPSHOT", artifact.getBaseVersion() );
+        assertEquals( "g/i/d/a.i.d/1.0-SNAPSHOT/a.i.d-1.0-20110329.221805-4.jar",
+                      manager.getPathForRemoteArtifact( artifact, remoteRepo, "" ) );
+    }
+
+    @Test
+    public void testFindLocalArtifact()
+        throws Exception
+    {
+        addLocalArtifact( artifact );
+
+        LocalArtifactRequest request = new LocalArtifactRequest( artifact, null, null );
+        LocalArtifactResult result = manager.find( session, request );
+        assertTrue( result.isAvailable() );
+        assertNull( result.getRepository() );
+
+        snapshot = snapshot.setVersion( snapshot.getBaseVersion() );
+        addLocalArtifact( snapshot );
+
+        request = new LocalArtifactRequest( snapshot, null, null );
+        result = manager.find( session, request );
+        assertTrue( result.isAvailable() );
+        assertNull( result.getRepository() );
+    }
+
+    @Test
+    public void testFindRemoteArtifact()
+        throws Exception
+    {
+        addRemoteArtifact( artifact );
+
+        LocalArtifactRequest request = new LocalArtifactRequest( artifact, Arrays.asList( repository ), testContext );
+        LocalArtifactResult result = manager.find( session, request );
+        assertTrue( result.isAvailable() );
+        assertEquals( repository, result.getRepository() );
+
+        addRemoteArtifact( snapshot );
+
+        request = new LocalArtifactRequest( snapshot, Arrays.asList( repository ), testContext );
+        result = manager.find( session, request );
+        assertTrue( result.isAvailable() );
+        assertEquals( repository, result.getRepository() );
+    }
+
+    @Test
+    public void testDoNotFindDifferentContext()
+        throws Exception
+    {
+        addRemoteArtifact( artifact );
+
+        LocalArtifactRequest request = new LocalArtifactRequest( artifact, Arrays.asList( repository ), "different" );
+        LocalArtifactResult result = manager.find( session, request );
+        assertFalse( result.isAvailable() );
+    }
+
+    @Test
+    public void testDoNotFindNullFile()
+        throws Exception
+    {
+        artifact = artifact.setFile( null );
+        addLocalArtifact( artifact );
+
+        LocalArtifactRequest request = new LocalArtifactRequest( artifact, Arrays.asList( repository ), testContext );
+        LocalArtifactResult result = manager.find( session, request );
+        assertFalse( result.isAvailable() );
+    }
+
+    @Test
+    public void testDoNotFindDeletedFile()
+        throws Exception
+    {
+        addLocalArtifact( artifact );
+        assertTrue( "could not delete artifact file", artifactFile.delete() );
+
+        LocalArtifactRequest request = new LocalArtifactRequest( artifact, Arrays.asList( repository ), testContext );
+        LocalArtifactResult result = manager.find( session, request );
+        assertFalse( result.isAvailable() );
+    }
+
+    @Test
+    public void testFindUntrackedFile()
+        throws Exception
+    {
+        copy( artifact, manager.getPathForLocalArtifact( artifact ) );
+
+        LocalArtifactRequest request = new LocalArtifactRequest( artifact, Arrays.asList( repository ), testContext );
+        LocalArtifactResult result = manager.find( session, request );
+        assertTrue( result.isAvailable() );
+    }
+
+    private long addMetadata( Metadata metadata, RemoteRepository repo )
+        throws IOException
+    {
+        String path;
+        if ( repo == null )
+        {
+            path = manager.getPathForLocalMetadata( metadata );
+        }
+        else
+        {
+            path = manager.getPathForRemoteMetadata( metadata, repo, testContext );
+        }
+        System.err.println( path );
+
+        return copy( metadata, path );
+    }
+
+    @Test
+    public void testFindLocalMetadata()
+        throws Exception
+    {
+        addMetadata( metadata, null );
+
+        LocalMetadataRequest request = new LocalMetadataRequest( metadata, null, testContext );
+        LocalMetadataResult result = manager.find( session, request );
+
+        assertNotNull( result.getFile() );
+    }
+
+    @Test
+    public void testFindLocalMetadataNoVersion()
+        throws Exception
+    {
+        addMetadata( noVerMetadata, null );
+
+        LocalMetadataRequest request = new LocalMetadataRequest( noVerMetadata, null, testContext );
+        LocalMetadataResult result = manager.find( session, request );
+
+        assertNotNull( result.getFile() );
+    }
+
+    @Test
+    public void testDoNotFindRemoteMetadataDifferentContext()
+        throws Exception
+    {
+        addMetadata( noVerMetadata, repository );
+        addMetadata( metadata, repository );
+
+        LocalMetadataRequest request = new LocalMetadataRequest( noVerMetadata, repository, "different" );
+        LocalMetadataResult result = manager.find( session, request );
+        assertNull( result.getFile() );
+
+        request = new LocalMetadataRequest( metadata, repository, "different" );
+        result = manager.find( session, request );
+        assertNull( result.getFile() );
+    }
+
+    @Test
+    public void testFindArtifactUsesTimestampedVersion()
+        throws Exception
+    {
+        Artifact artifact = new DefaultArtifact( "g.i.d:a.i.d:1.0-SNAPSHOT" );
+        File file = new File( basedir, manager.getPathForLocalArtifact( artifact ) );
+        TestFileUtils.writeString( file, "test" );
+        addLocalArtifact( artifact );
+
+        artifact = artifact.setVersion( "1.0-20110329.221805-4" );
+        LocalArtifactRequest request = new LocalArtifactRequest();
+        request.setArtifact( artifact );
+        LocalArtifactResult result = manager.find( session, request );
+        assertNull( result.toString(), result.getFile() );
+        assertFalse( result.toString(), result.isAvailable() );
+    }
+
+}