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 e327728d [MRESOLVER-685] Connector pipelining (#679)
e327728d is described below

commit e327728d54ce85662af95ea1afea762c94fcb1f6
Author: Tamas Cservenak <ta...@cservenak.net>
AuthorDate: Tue Apr 8 14:36:08 2025 +0200

    [MRESOLVER-685] Connector pipelining (#679)
    
    Ability to "pipe" connectors one onto another in controlled and configured 
fashion.
    
    The ide is following:
    - first (actually connecting to remote) connector is chosen as today (based 
on priority)
    - next, new type of factories PipelinedRepositoryConnectorFactory is 
ordered (by priority) and they can (but does not have to) wrap the delegate in 
configured order.
    
    Factored out existing RRF connector into new factory, and introduced 
another OfflinePRCF, as so far, "resolver offline" was in fact managed at 
different spots, and user was still able to circumvent offline setting. This 
now connector now wraps connector and refuses going remote if session if 
offline.
    
    Vanilla resolver does this:
    `offline( rrf( basic( repo ) ) )`
    
    Ultimate goal, envision this:
    `offline( mimir( sigcheck( rrf( basic( repo ) ) ) ) )`
    
    ---
    
    https://issues.apache.org/jira/browse/MRESOLVER-685
---
 .../connector/basic/BasicRepositoryConnector.java  |   2 +-
 .../internal/impl/DefaultOfflineController.java    |  32 +++++-
 .../impl/DefaultRepositoryConnectorProvider.java   |  34 ++++---
 ...ilteringPipelineRepositoryConnectorFactory.java |  76 ++++++++++++++
 .../impl/filter/FilteringRepositoryConnector.java  |   2 +-
 .../OfflinePipelineRepositoryConnectorFactory.java |  70 +++++++++++++
 .../impl/offline/OfflineRepositoryConnector.java   | 109 +++++++++++++++++++++
 .../PipelineRepositoryConnectorFactory.java        |  54 ++++++++++
 .../aether/supplier/RepositorySystemSupplier.java  |  26 ++++-
 .../aether/supplier/RepositorySystemSupplier.java  |  26 ++++-
 src/site/markdown/configuration.md                 |   5 +-
 11 files changed, 416 insertions(+), 20 deletions(-)

diff --git 
a/maven-resolver-connector-basic/src/main/java/org/eclipse/aether/connector/basic/BasicRepositoryConnector.java
 
b/maven-resolver-connector-basic/src/main/java/org/eclipse/aether/connector/basic/BasicRepositoryConnector.java
index bcc880ad..425e887e 100644
--- 
a/maven-resolver-connector-basic/src/main/java/org/eclipse/aether/connector/basic/BasicRepositoryConnector.java
+++ 
b/maven-resolver-connector-basic/src/main/java/org/eclipse/aether/connector/basic/BasicRepositoryConnector.java
@@ -434,7 +434,7 @@ final class BasicRepositoryConnector implements 
RepositoryConnector {
 
     @Override
     public String toString() {
-        return String.valueOf(repository);
+        return BasicRepositoryConnectorFactory.NAME + "( " + repository + " )";
     }
 
     abstract class TaskRunner implements Runnable {
diff --git 
a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/DefaultOfflineController.java
 
b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/DefaultOfflineController.java
index 11d342fe..b639e91e 100644
--- 
a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/DefaultOfflineController.java
+++ 
b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/DefaultOfflineController.java
@@ -41,7 +41,7 @@ public class DefaultOfflineController implements 
OfflineController {
     private static final String CONFIG_PROPS_PREFIX = 
ConfigurationProperties.PREFIX_AETHER + "offline.";
 
     /**
-     * Comma-separated list of protocols which are supposed to be resolved 
offline.
+     * Comma-separated list of protocols which are supposed to be resolved 
when session is offline.
      *
      * @configurationSource {@link 
RepositorySystemSession#getConfigProperties()}
      * @configurationType {@link java.lang.String}
@@ -49,13 +49,22 @@ public class DefaultOfflineController implements 
OfflineController {
     public static final String CONFIG_PROP_OFFLINE_PROTOCOLS = 
CONFIG_PROPS_PREFIX + "protocols";
 
     /**
-     * Comma-separated list of hosts which are supposed to be resolved offline.
+     * Comma-separated list of hosts which are supposed to be resolved when 
session is offline.
      *
      * @configurationSource {@link 
RepositorySystemSession#getConfigProperties()}
      * @configurationType {@link java.lang.String}
      */
     public static final String CONFIG_PROP_OFFLINE_HOSTS = CONFIG_PROPS_PREFIX 
+ "hosts";
 
+    /**
+     * Comma-separated list of repository IDs which are supposed to be 
resolved when session is offline.
+     *
+     * @configurationSource {@link 
RepositorySystemSession#getConfigProperties()}
+     * @configurationType {@link java.lang.String}
+     * @since TBD
+     */
+    public static final String CONFIG_PROP_OFFLINE_REPOSITORIES = 
CONFIG_PROPS_PREFIX + "repositories";
+
     private static final Pattern SEP = Pattern.compile("\\s*,\\s*");
 
     @Override
@@ -63,7 +72,9 @@ public class DefaultOfflineController implements 
OfflineController {
             throws RepositoryOfflineException {
         requireNonNull(session, "session cannot be null");
         requireNonNull(repository, "repository cannot be null");
-        if (isOfflineProtocol(session, repository) || isOfflineHost(session, 
repository)) {
+        if (isOfflineProtocol(session, repository)
+                || isOfflineHost(session, repository)
+                || isOfflineRepository(session, repository)) {
             return;
         }
 
@@ -100,6 +111,21 @@ public class DefaultOfflineController implements 
OfflineController {
         return false;
     }
 
+    private boolean isOfflineRepository(RepositorySystemSession session, 
RemoteRepository repository) {
+        String[] repositories = getConfig(session, 
CONFIG_PROP_OFFLINE_REPOSITORIES);
+        if (repositories != null) {
+            String repositoryId = repository.getId();
+            if (!repositoryId.isEmpty()) {
+                for (String r : repositories) {
+                    if (r.equals(repositoryId)) {
+                        return true;
+                    }
+                }
+            }
+        }
+        return false;
+    }
+
     private String[] getConfig(RepositorySystemSession session, String key) {
         String value = ConfigUtils.getString(session, "", key).trim();
         if (value.isEmpty()) {
diff --git 
a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/DefaultRepositoryConnectorProvider.java
 
b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/DefaultRepositoryConnectorProvider.java
index b2bc0ace..c655bc50 100644
--- 
a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/DefaultRepositoryConnectorProvider.java
+++ 
b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/DefaultRepositoryConnectorProvider.java
@@ -28,13 +28,11 @@ import java.util.List;
 import java.util.Map;
 
 import org.eclipse.aether.RepositorySystemSession;
-import org.eclipse.aether.impl.RemoteRepositoryFilterManager;
 import org.eclipse.aether.impl.RepositoryConnectorProvider;
-import org.eclipse.aether.internal.impl.filter.FilteringRepositoryConnector;
 import org.eclipse.aether.repository.RemoteRepository;
+import org.eclipse.aether.spi.connector.PipelineRepositoryConnectorFactory;
 import org.eclipse.aether.spi.connector.RepositoryConnector;
 import org.eclipse.aether.spi.connector.RepositoryConnectorFactory;
-import org.eclipse.aether.spi.connector.filter.RemoteRepositoryFilter;
 import org.eclipse.aether.transfer.NoRepositoryConnectorException;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -51,14 +49,14 @@ public class DefaultRepositoryConnectorProvider implements 
RepositoryConnectorPr
 
     private final Map<String, RepositoryConnectorFactory> connectorFactories;
 
-    private final RemoteRepositoryFilterManager remoteRepositoryFilterManager;
+    private final Map<String, PipelineRepositoryConnectorFactory> 
pipelineConnectorFactories;
 
     @Inject
     public DefaultRepositoryConnectorProvider(
             Map<String, RepositoryConnectorFactory> connectorFactories,
-            RemoteRepositoryFilterManager remoteRepositoryFilterManager) {
+            Map<String, PipelineRepositoryConnectorFactory> 
pipelineConnectorFactories) {
         this.connectorFactories = 
Collections.unmodifiableMap(connectorFactories);
-        this.remoteRepositoryFilterManager = 
requireNonNull(remoteRepositoryFilterManager);
+        this.pipelineConnectorFactories = 
Collections.unmodifiableMap(pipelineConnectorFactories);
     }
 
     @Override
@@ -78,7 +76,6 @@ public class DefaultRepositoryConnectorProvider implements 
RepositoryConnectorPr
         PrioritizedComponents<RepositoryConnectorFactory> factories = 
PrioritizedComponents.reuseOrCreate(
                 session, RepositoryConnectorFactory.class, connectorFactories, 
RepositoryConnectorFactory::getPriority);
 
-        RemoteRepositoryFilter filter = 
remoteRepositoryFilterManager.getRemoteRepositoryFilter(session);
         List<NoRepositoryConnectorException> errors = new ArrayList<>();
         for (PrioritizedComponent<RepositoryConnectorFactory> factory : 
factories.getEnabled()) {
             try {
@@ -94,11 +91,13 @@ public class DefaultRepositoryConnectorProvider implements 
RepositoryConnectorPr
                     LOGGER.debug(buffer.toString());
                 }
 
-                if (filter != null) {
-                    return new FilteringRepositoryConnector(repository, 
connector, filter);
-                } else {
-                    return connector;
+                connector = pipelineConnector(session, repository, connector);
+
+                if (LOGGER.isDebugEnabled()) {
+                    LOGGER.debug("Final pipeline: {}", connector);
                 }
+
+                return connector;
             } catch (NoRepositoryConnectorException e) {
                 // continue and try next factory
                 LOGGER.debug("Could not obtain connector factory for {}", 
repository, e);
@@ -125,4 +124,17 @@ public class DefaultRepositoryConnectorProvider implements 
RepositoryConnectorPr
         }
         throw ex;
     }
+
+    protected RepositoryConnector pipelineConnector(
+            RepositorySystemSession session, RemoteRepository repository, 
RepositoryConnector delegate) {
+        PrioritizedComponents<PipelineRepositoryConnectorFactory> factories = 
PrioritizedComponents.reuseOrCreate(
+                session,
+                PipelineRepositoryConnectorFactory.class,
+                pipelineConnectorFactories,
+                PipelineRepositoryConnectorFactory::getPriority);
+        for (PrioritizedComponent<PipelineRepositoryConnectorFactory> factory 
: factories.getEnabled()) {
+            delegate = factory.getComponent().newInstance(session, repository, 
delegate);
+        }
+        return delegate;
+    }
 }
diff --git 
a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/filter/FilteringPipelineRepositoryConnectorFactory.java
 
b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/filter/FilteringPipelineRepositoryConnectorFactory.java
new file mode 100644
index 00000000..106f7068
--- /dev/null
+++ 
b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/filter/FilteringPipelineRepositoryConnectorFactory.java
@@ -0,0 +1,76 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.eclipse.aether.internal.impl.filter;
+
+import javax.inject.Inject;
+import javax.inject.Named;
+import javax.inject.Singleton;
+
+import org.eclipse.aether.RepositorySystemSession;
+import org.eclipse.aether.impl.RemoteRepositoryFilterManager;
+import org.eclipse.aether.repository.RemoteRepository;
+import org.eclipse.aether.spi.connector.PipelineRepositoryConnectorFactory;
+import org.eclipse.aether.spi.connector.RepositoryConnector;
+import org.eclipse.aether.spi.connector.filter.RemoteRepositoryFilter;
+
+import static java.util.Objects.requireNonNull;
+
+/**
+ * A filtering connector factory.
+ *
+ * @since TBD
+ */
+@Singleton
+@Named(FilteringPipelineRepositoryConnectorFactory.NAME)
+public final class FilteringPipelineRepositoryConnectorFactory implements 
PipelineRepositoryConnectorFactory {
+    public static final String NAME = "rrf";
+
+    private final RemoteRepositoryFilterManager remoteRepositoryFilterManager;
+
+    /**
+     * This connector should be usually the right-most in pipeline.
+     */
+    private float priority = 10000;
+
+    @Inject
+    public 
FilteringPipelineRepositoryConnectorFactory(RemoteRepositoryFilterManager 
remoteRepositoryFilterManager) {
+        this.remoteRepositoryFilterManager = 
requireNonNull(remoteRepositoryFilterManager);
+    }
+
+    @Override
+    public RepositoryConnector newInstance(
+            RepositorySystemSession session, RemoteRepository repository, 
RepositoryConnector delegate) {
+        RemoteRepositoryFilter filter = 
remoteRepositoryFilterManager.getRemoteRepositoryFilter(session);
+        if (filter != null) {
+            return new FilteringRepositoryConnector(repository, delegate, 
filter);
+        } else {
+            return delegate;
+        }
+    }
+
+    @Override
+    public float getPriority() {
+        return priority;
+    }
+
+    public FilteringPipelineRepositoryConnectorFactory setPriority(float 
priority) {
+        this.priority = priority;
+        return this;
+    }
+}
diff --git 
a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/filter/FilteringRepositoryConnector.java
 
b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/filter/FilteringRepositoryConnector.java
index 0ffe4ce9..2102f905 100644
--- 
a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/filter/FilteringRepositoryConnector.java
+++ 
b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/filter/FilteringRepositoryConnector.java
@@ -104,6 +104,6 @@ public final class FilteringRepositoryConnector implements 
RepositoryConnector {
 
     @Override
     public String toString() {
-        return "filtered(" + delegate.toString() + ")";
+        return FilteringPipelineRepositoryConnectorFactory.NAME + "( " + 
delegate.toString() + " )";
     }
 }
diff --git 
a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/offline/OfflinePipelineRepositoryConnectorFactory.java
 
b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/offline/OfflinePipelineRepositoryConnectorFactory.java
new file mode 100644
index 00000000..a5ef87e0
--- /dev/null
+++ 
b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/offline/OfflinePipelineRepositoryConnectorFactory.java
@@ -0,0 +1,70 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.eclipse.aether.internal.impl.offline;
+
+import javax.inject.Inject;
+import javax.inject.Named;
+import javax.inject.Singleton;
+
+import org.eclipse.aether.RepositorySystemSession;
+import org.eclipse.aether.impl.OfflineController;
+import org.eclipse.aether.repository.RemoteRepository;
+import org.eclipse.aether.spi.connector.PipelineRepositoryConnectorFactory;
+import org.eclipse.aether.spi.connector.RepositoryConnector;
+
+import static java.util.Objects.requireNonNull;
+
+/**
+ * Offline connector factory.
+ *
+ * @since TBD
+ */
+@Singleton
+@Named(OfflinePipelineRepositoryConnectorFactory.NAME)
+public final class OfflinePipelineRepositoryConnectorFactory implements 
PipelineRepositoryConnectorFactory {
+    public static final String NAME = "offline";
+
+    private final OfflineController offlineController;
+
+    /**
+     * This connector should be usually the left-most in pipeline.
+     */
+    private float priority = 0;
+
+    @Inject
+    public OfflinePipelineRepositoryConnectorFactory(OfflineController 
offlineController) {
+        this.offlineController = requireNonNull(offlineController);
+    }
+
+    @Override
+    public RepositoryConnector newInstance(
+            RepositorySystemSession session, RemoteRepository repository, 
RepositoryConnector delegate) {
+        return new OfflineRepositoryConnector(session, repository, 
offlineController, delegate);
+    }
+
+    @Override
+    public float getPriority() {
+        return priority;
+    }
+
+    public OfflinePipelineRepositoryConnectorFactory setPriority(float 
priority) {
+        this.priority = priority;
+        return this;
+    }
+}
diff --git 
a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/offline/OfflineRepositoryConnector.java
 
b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/offline/OfflineRepositoryConnector.java
new file mode 100644
index 00000000..6c280306
--- /dev/null
+++ 
b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/offline/OfflineRepositoryConnector.java
@@ -0,0 +1,109 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.eclipse.aether.internal.impl.offline;
+
+import java.util.Collection;
+
+import org.eclipse.aether.RepositorySystemSession;
+import org.eclipse.aether.impl.OfflineController;
+import org.eclipse.aether.internal.impl.Utils;
+import org.eclipse.aether.repository.RemoteRepository;
+import org.eclipse.aether.spi.connector.ArtifactDownload;
+import org.eclipse.aether.spi.connector.ArtifactUpload;
+import org.eclipse.aether.spi.connector.MetadataDownload;
+import org.eclipse.aether.spi.connector.MetadataUpload;
+import org.eclipse.aether.spi.connector.RepositoryConnector;
+import org.eclipse.aether.transfer.ArtifactTransferException;
+import org.eclipse.aether.transfer.MetadataTransferException;
+import org.eclipse.aether.transfer.RepositoryOfflineException;
+
+import static java.util.Objects.requireNonNull;
+
+/**
+ * Offline connector, that prevents ANY remote access in case session is 
offline.
+ *
+ * @since TBD
+ */
+public final class OfflineRepositoryConnector implements RepositoryConnector {
+    private final RepositorySystemSession session;
+    private final RemoteRepository remoteRepository;
+    private final OfflineController offlineController;
+    private final RepositoryConnector delegate;
+
+    public OfflineRepositoryConnector(
+            RepositorySystemSession session,
+            RemoteRepository remoteRepository,
+            OfflineController offlineController,
+            RepositoryConnector delegate) {
+        this.session = requireNonNull(session);
+        this.remoteRepository = requireNonNull(remoteRepository);
+        this.offlineController = requireNonNull(offlineController);
+        this.delegate = requireNonNull(delegate);
+    }
+
+    @Override
+    public void close() {
+        delegate.close();
+    }
+
+    @Override
+    public void get(
+            Collection<? extends ArtifactDownload> artifactDownloads,
+            Collection<? extends MetadataDownload> metadataDownloads) {
+        try {
+            Utils.checkOffline(session, offlineController, remoteRepository);
+        } catch (RepositoryOfflineException e) {
+            if (artifactDownloads != null && !artifactDownloads.isEmpty()) {
+                artifactDownloads.forEach(
+                        d -> d.setException(new 
ArtifactTransferException(d.getArtifact(), remoteRepository, e)));
+            }
+            if (metadataDownloads != null && !metadataDownloads.isEmpty()) {
+                metadataDownloads.forEach(
+                        d -> d.setException(new 
MetadataTransferException(d.getMetadata(), remoteRepository, e)));
+            }
+            return;
+        }
+        delegate.get(artifactDownloads, metadataDownloads);
+    }
+
+    @Override
+    public void put(
+            Collection<? extends ArtifactUpload> artifactUploads,
+            Collection<? extends MetadataUpload> metadataUploads) {
+        try {
+            Utils.checkOffline(session, offlineController, remoteRepository);
+        } catch (RepositoryOfflineException e) {
+            if (artifactUploads != null && !artifactUploads.isEmpty()) {
+                artifactUploads.forEach(
+                        d -> d.setException(new 
ArtifactTransferException(d.getArtifact(), remoteRepository, e)));
+            }
+            if (metadataUploads != null && !metadataUploads.isEmpty()) {
+                metadataUploads.forEach(
+                        d -> d.setException(new 
MetadataTransferException(d.getMetadata(), remoteRepository, e)));
+            }
+            return;
+        }
+        delegate.put(artifactUploads, metadataUploads);
+    }
+
+    @Override
+    public String toString() {
+        return OfflinePipelineRepositoryConnectorFactory.NAME + "( " + 
delegate.toString() + " )";
+    }
+}
diff --git 
a/maven-resolver-spi/src/main/java/org/eclipse/aether/spi/connector/PipelineRepositoryConnectorFactory.java
 
b/maven-resolver-spi/src/main/java/org/eclipse/aether/spi/connector/PipelineRepositoryConnectorFactory.java
new file mode 100644
index 00000000..3fcd71b4
--- /dev/null
+++ 
b/maven-resolver-spi/src/main/java/org/eclipse/aether/spi/connector/PipelineRepositoryConnectorFactory.java
@@ -0,0 +1,54 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.eclipse.aether.spi.connector;
+
+import org.eclipse.aether.RepositorySystemSession;
+import org.eclipse.aether.repository.RemoteRepository;
+
+/**
+ * A pipeline factory to create piped repository connectors.
+ *
+ * @since TBD
+ */
+public interface PipelineRepositoryConnectorFactory {
+
+    /**
+     * Create a piped repository connector for the specified remote 
repository. Typically, a factory will inspect
+     * {@link RemoteRepository#getProtocol()} and {@link 
RemoteRepository#getContentType()} to determine whether it can
+     * handle a repository. This method never throws or returns {@code null}, 
least can do is to return the passed in
+     * delegate connector instance.
+     *
+     * @param session The repository system session from which to configure 
the connector, must not be {@code null}. In
+     *            particular, a connector must notify any {@link 
RepositorySystemSession#getTransferListener()} set for
+     *            the session and should obey the timeouts configured for the 
session.
+     * @param repository The remote repository to create a connector for, must 
not be {@code null}.
+     * @param delegate The delegate connector, never {@code null}. The 
delegate is "right hand" connector in connector
+     *                 pipeline.
+     * @return The connector for the given repository, never {@code null}. If 
pipeline wants to step aside, it must
+     * return the passed in delegate connector instance.
+     */
+    RepositoryConnector newInstance(
+            RepositorySystemSession session, RemoteRepository repository, 
RepositoryConnector delegate);
+
+    /**
+     * The priority of this pipeline factory. Higher priority makes connector 
closer to right end (tail) of pipeline
+     * (closest to delegate), while lower priority makes it closer to left 
hand (head) of the pipeline.
+     */
+    float getPriority();
+}
diff --git 
a/maven-resolver-supplier-mvn3/src/main/java/org/eclipse/aether/supplier/RepositorySystemSupplier.java
 
b/maven-resolver-supplier-mvn3/src/main/java/org/eclipse/aether/supplier/RepositorySystemSupplier.java
index 851d3f8a..4b9442e4 100644
--- 
a/maven-resolver-supplier-mvn3/src/main/java/org/eclipse/aether/supplier/RepositorySystemSupplier.java
+++ 
b/maven-resolver-supplier-mvn3/src/main/java/org/eclipse/aether/supplier/RepositorySystemSupplier.java
@@ -99,8 +99,10 @@ 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.filter.DefaultRemoteRepositoryFilterManager;
+import 
org.eclipse.aether.internal.impl.filter.FilteringPipelineRepositoryConnectorFactory;
 import 
org.eclipse.aether.internal.impl.filter.GroupIdRemoteRepositoryFilterSource;
 import 
org.eclipse.aether.internal.impl.filter.PrefixesRemoteRepositoryFilterSource;
+import 
org.eclipse.aether.internal.impl.offline.OfflinePipelineRepositoryConnectorFactory;
 import 
org.eclipse.aether.internal.impl.resolution.TrustedChecksumsArtifactResolverPostProcessor;
 import org.eclipse.aether.internal.impl.synccontext.DefaultSyncContextFactory;
 import org.eclipse.aether.internal.impl.synccontext.named.NameMapper;
@@ -121,6 +123,7 @@ import 
org.eclipse.aether.spi.artifact.generator.ArtifactGeneratorFactory;
 import org.eclipse.aether.spi.artifact.transformer.ArtifactTransformer;
 import org.eclipse.aether.spi.checksums.ProvidedChecksumsSource;
 import org.eclipse.aether.spi.checksums.TrustedChecksumsSource;
+import org.eclipse.aether.spi.connector.PipelineRepositoryConnectorFactory;
 import org.eclipse.aether.spi.connector.RepositoryConnectorFactory;
 import org.eclipse.aether.spi.connector.checksum.ChecksumAlgorithmFactory;
 import 
org.eclipse.aether.spi.connector.checksum.ChecksumAlgorithmFactorySelector;
@@ -703,6 +706,27 @@ public class RepositorySystemSupplier implements 
Supplier<RepositorySystem> {
         return result;
     }
 
+    private Map<String, PipelineRepositoryConnectorFactory> 
pipelineRepositoryConnectorFactories;
+
+    public final Map<String, PipelineRepositoryConnectorFactory> 
getPipelineRepositoryConnectorFactories() {
+        checkClosed();
+        if (pipelineRepositoryConnectorFactories == null) {
+            pipelineRepositoryConnectorFactories = 
createPipelineRepositoryConnectorFactories();
+        }
+        return pipelineRepositoryConnectorFactories;
+    }
+
+    protected Map<String, PipelineRepositoryConnectorFactory> 
createPipelineRepositoryConnectorFactories() {
+        HashMap<String, PipelineRepositoryConnectorFactory> result = new 
HashMap<>();
+        result.put(
+                FilteringPipelineRepositoryConnectorFactory.NAME,
+                new 
FilteringPipelineRepositoryConnectorFactory(getRemoteRepositoryFilterManager()));
+        result.put(
+                OfflinePipelineRepositoryConnectorFactory.NAME,
+                new 
OfflinePipelineRepositoryConnectorFactory(getOfflineController()));
+        return result;
+    }
+
     private RepositoryConnectorProvider repositoryConnectorProvider;
 
     public final RepositoryConnectorProvider getRepositoryConnectorProvider() {
@@ -715,7 +739,7 @@ public class RepositorySystemSupplier implements 
Supplier<RepositorySystem> {
 
     protected RepositoryConnectorProvider createRepositoryConnectorProvider() {
         return new DefaultRepositoryConnectorProvider(
-                getRepositoryConnectorFactories(), 
getRemoteRepositoryFilterManager());
+                getRepositoryConnectorFactories(), 
getPipelineRepositoryConnectorFactories());
     }
 
     private Installer installer;
diff --git 
a/maven-resolver-supplier-mvn4/src/main/java/org/eclipse/aether/supplier/RepositorySystemSupplier.java
 
b/maven-resolver-supplier-mvn4/src/main/java/org/eclipse/aether/supplier/RepositorySystemSupplier.java
index 220ca806..ce194ebe 100644
--- 
a/maven-resolver-supplier-mvn4/src/main/java/org/eclipse/aether/supplier/RepositorySystemSupplier.java
+++ 
b/maven-resolver-supplier-mvn4/src/main/java/org/eclipse/aether/supplier/RepositorySystemSupplier.java
@@ -103,8 +103,10 @@ 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.filter.DefaultRemoteRepositoryFilterManager;
+import 
org.eclipse.aether.internal.impl.filter.FilteringPipelineRepositoryConnectorFactory;
 import 
org.eclipse.aether.internal.impl.filter.GroupIdRemoteRepositoryFilterSource;
 import 
org.eclipse.aether.internal.impl.filter.PrefixesRemoteRepositoryFilterSource;
+import 
org.eclipse.aether.internal.impl.offline.OfflinePipelineRepositoryConnectorFactory;
 import 
org.eclipse.aether.internal.impl.resolution.TrustedChecksumsArtifactResolverPostProcessor;
 import org.eclipse.aether.internal.impl.synccontext.DefaultSyncContextFactory;
 import org.eclipse.aether.internal.impl.synccontext.named.NameMapper;
@@ -125,6 +127,7 @@ import 
org.eclipse.aether.spi.artifact.generator.ArtifactGeneratorFactory;
 import org.eclipse.aether.spi.artifact.transformer.ArtifactTransformer;
 import org.eclipse.aether.spi.checksums.ProvidedChecksumsSource;
 import org.eclipse.aether.spi.checksums.TrustedChecksumsSource;
+import org.eclipse.aether.spi.connector.PipelineRepositoryConnectorFactory;
 import org.eclipse.aether.spi.connector.RepositoryConnectorFactory;
 import org.eclipse.aether.spi.connector.checksum.ChecksumAlgorithmFactory;
 import 
org.eclipse.aether.spi.connector.checksum.ChecksumAlgorithmFactorySelector;
@@ -707,6 +710,27 @@ public class RepositorySystemSupplier implements 
Supplier<RepositorySystem> {
         return result;
     }
 
+    private Map<String, PipelineRepositoryConnectorFactory> 
pipelineRepositoryConnectorFactories;
+
+    public final Map<String, PipelineRepositoryConnectorFactory> 
getPipelineRepositoryConnectorFactories() {
+        checkClosed();
+        if (pipelineRepositoryConnectorFactories == null) {
+            pipelineRepositoryConnectorFactories = 
createPipelineRepositoryConnectorFactories();
+        }
+        return pipelineRepositoryConnectorFactories;
+    }
+
+    protected Map<String, PipelineRepositoryConnectorFactory> 
createPipelineRepositoryConnectorFactories() {
+        HashMap<String, PipelineRepositoryConnectorFactory> result = new 
HashMap<>();
+        result.put(
+                FilteringPipelineRepositoryConnectorFactory.NAME,
+                new 
FilteringPipelineRepositoryConnectorFactory(getRemoteRepositoryFilterManager()));
+        result.put(
+                OfflinePipelineRepositoryConnectorFactory.NAME,
+                new 
OfflinePipelineRepositoryConnectorFactory(getOfflineController()));
+        return result;
+    }
+
     private RepositoryConnectorProvider repositoryConnectorProvider;
 
     public final RepositoryConnectorProvider getRepositoryConnectorProvider() {
@@ -719,7 +743,7 @@ public class RepositorySystemSupplier implements 
Supplier<RepositorySystem> {
 
     protected RepositoryConnectorProvider createRepositoryConnectorProvider() {
         return new DefaultRepositoryConnectorProvider(
-                getRepositoryConnectorFactories(), 
getRemoteRepositoryFilterManager());
+                getRepositoryConnectorFactories(), 
getPipelineRepositoryConnectorFactories());
     }
 
     private Installer installer;
diff --git a/src/site/markdown/configuration.md 
b/src/site/markdown/configuration.md
index be696288..e015543b 100644
--- a/src/site/markdown/configuration.md
+++ b/src/site/markdown/configuration.md
@@ -82,8 +82,9 @@ under the License.
 | `"aether.named.ipc.nativeName"` | `String` | The name if the IPC server 
native executable (without file extension like ".exe") |  `"ipc-sync"`  | 2.0.1 
|  No  | Java System Properties |
 | `"aether.named.ipc.nofork"` | `Boolean` | Should the IPC server not fork? 
(i.e. for testing purposes) |  `false`  | 2.0.1 |  No  | Java System Properties 
|
 | `"aether.named.ipc.nonative"` | `Boolean` | Should the IPC server not use 
native executable? |  `true`  | 2.0.1 |  No  | Java System Properties |
-| `"aether.offline.hosts"` | `String` | Comma-separated list of hosts which 
are supposed to be resolved offline. |  -  |  |  No  | Session Configuration |
-| `"aether.offline.protocols"` | `String` | Comma-separated list of protocols 
which are supposed to be resolved offline. |  -  |  |  No  | Session 
Configuration |
+| `"aether.offline.hosts"` | `String` | Comma-separated list of hosts which 
are supposed to be resolved when session is offline. |  -  |  |  No  | Session 
Configuration |
+| `"aether.offline.protocols"` | `String` | Comma-separated list of protocols 
which are supposed to be accessed when session is offline. |  -  |  |  No  | 
Session Configuration |
+| `"aether.offline.repositories"` | `String` | Comma-separated list of 
repository IDs which are supposed to be resolved when session is offline. |  -  
| TBD |  No  | Session Configuration |
 | `"aether.priority.<class>"` | `Float` | The priority to use for a certain 
extension class. <code>&lt;class&gt;</code> can either be the fully qualified 
name or the simple name of a class. If the class name ends with Factory that 
suffix could optionally be left out. This configuration is used by 
<code>org.eclipse.aether.internal.impl.PrioritizedComponents</code> internal 
utility to sort classes by priority. This is reusable utility (so an extension 
can make use of it), but by default in [...]
 | `"aether.priority.cached"` | `Boolean` | A flag indicating whether the 
created ordered components should be cached in session. |  `true`  | 2.0.0 |  
No  | Session Configuration |
 | `"aether.priority.implicit"` | `Boolean` | A flag indicating whether the 
priorities of pluggable extensions are implicitly given by their iteration 
order such that the first extension has the highest priority. If set, an 
extension's built-in priority as well as any corresponding 
<code>aether.priority.*</code> configuration properties are ignored when 
searching for a suitable implementation among the available extensions. This 
priority mode is meant for cases where the application will  [...]


Reply via email to