Allow AdminApiFactory to be replaced dynamically

Since change Ie760bf3e1 the GerritSshApi class is no longer a singleton,
and is instead instantiated on demand by a new AdminApiFactory.

This breaks implementations that consume the replication plugin as a
library and extend the GerritSshApi, since the extended class no longer
gets instantiated in place of GerritSshApi.

Refactor it so that AdminApiFactory is an interface with a default
implementation that gets bound as a dynamic item, which can be replaced
by derived implementations.

Change-Id: Ia150d6802e11015fa00ee9144b3dfbfa696c7a0d
Signed-off-by: David Pursehouse <dpursehouse@collab.net>
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/AdminApiFactory.java b/src/main/java/com/googlesource/gerrit/plugins/replication/AdminApiFactory.java
index 528aff2..de6e91e 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/replication/AdminApiFactory.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/replication/AdminApiFactory.java
@@ -19,33 +19,45 @@
 import java.util.Optional;
 import org.eclipse.jgit.transport.URIish;
 
-@Singleton
-public class AdminApiFactory {
+/** Factory for creating an {@link AdminApi} instance for a remote URI. */
+public interface AdminApiFactory {
+  /**
+   * Create an {@link AdminApi} for the given remote URI.
+   *
+   * @param uri the remote URI.
+   * @return An API for the given remote URI, or {@code Optional.empty} if there is no appropriate
+   *     API for the URI.
+   */
+  Optional<AdminApi> create(URIish uri);
 
-  private final SshHelper sshHelper;
+  @Singleton
+  static class DefaultAdminApiFactory implements AdminApiFactory {
+    protected final SshHelper sshHelper;
 
-  @Inject
-  AdminApiFactory(SshHelper sshHelper) {
-    this.sshHelper = sshHelper;
-  }
-
-  public Optional<AdminApi> create(URIish uri) {
-    if (isGerrit(uri)) {
-      return Optional.of(new GerritSshApi(sshHelper, uri));
-    } else if (!uri.isRemote()) {
-      return Optional.of(new LocalFS(uri));
-    } else if (isSSH(uri)) {
-      return Optional.of(new RemoteSsh(sshHelper, uri));
+    @Inject
+    public DefaultAdminApiFactory(SshHelper sshHelper) {
+      this.sshHelper = sshHelper;
     }
-    return Optional.empty();
+
+    @Override
+    public Optional<AdminApi> create(URIish uri) {
+      if (isGerrit(uri)) {
+        return Optional.of(new GerritSshApi(sshHelper, uri));
+      } else if (!uri.isRemote()) {
+        return Optional.of(new LocalFS(uri));
+      } else if (isSSH(uri)) {
+        return Optional.of(new RemoteSsh(sshHelper, uri));
+      }
+      return Optional.empty();
+    }
   }
 
-  public static boolean isGerrit(URIish uri) {
+  static boolean isGerrit(URIish uri) {
     String scheme = uri.getScheme();
     return scheme != null && scheme.toLowerCase().equals("gerrit+ssh");
   }
 
-  public static boolean isSSH(URIish uri) {
+  static boolean isSSH(URIish uri) {
     if (!uri.isRemote()) {
       return false;
     }
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/ReplicationModule.java b/src/main/java/com/googlesource/gerrit/plugins/replication/ReplicationModule.java
index 1898a4f..4162973 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/replication/ReplicationModule.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/replication/ReplicationModule.java
@@ -22,6 +22,7 @@
 import com.google.gerrit.extensions.events.HeadUpdatedListener;
 import com.google.gerrit.extensions.events.LifecycleListener;
 import com.google.gerrit.extensions.events.ProjectDeletedListener;
+import com.google.gerrit.extensions.registration.DynamicItem;
 import com.google.gerrit.extensions.registration.DynamicSet;
 import com.google.gerrit.server.events.EventTypes;
 import com.google.inject.AbstractModule;
@@ -67,7 +68,9 @@
     EventTypes.register(ReplicationScheduledEvent.TYPE, ReplicationScheduledEvent.class);
     bind(SshSessionFactory.class).toProvider(ReplicationSshSessionFactoryProvider.class);
 
-    bind(AdminApiFactory.class);
+    DynamicItem.itemOf(binder(), AdminApiFactory.class);
+    DynamicItem.bind(binder(), AdminApiFactory.class)
+        .to(AdminApiFactory.DefaultAdminApiFactory.class);
 
     bind(TransportFactory.class).to(TransportFactoryImpl.class).in(Scopes.SINGLETON);
   }
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/ReplicationQueue.java b/src/main/java/com/googlesource/gerrit/plugins/replication/ReplicationQueue.java
index 203bc2f..60604db 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/replication/ReplicationQueue.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/replication/ReplicationQueue.java
@@ -65,7 +65,7 @@
   private final WorkQueue workQueue;
   private final DynamicItem<EventDispatcher> dispatcher;
   private final ReplicationConfig config;
-  private final AdminApiFactory adminApiFactory;
+  private final DynamicItem<AdminApiFactory> adminApiFactory;
   private final ReplicationState.Factory replicationStateFactory;
   private final EventsStorage eventsStorage;
   private volatile boolean running;
@@ -74,7 +74,7 @@
   @Inject
   ReplicationQueue(
       WorkQueue wq,
-      AdminApiFactory aaf,
+      DynamicItem<AdminApiFactory> aaf,
       ReplicationConfig rc,
       DynamicItem<EventDispatcher> dis,
       ReplicationStateListeners sl,
@@ -260,7 +260,7 @@
   }
 
   private boolean createProject(URIish replicateURI, Project.NameKey projectName, String head) {
-    Optional<AdminApi> adminApi = adminApiFactory.create(replicateURI);
+    Optional<AdminApi> adminApi = adminApiFactory.get().create(replicateURI);
     if (adminApi.isPresent() && adminApi.get().createProject(projectName, head)) {
       return true;
     }
@@ -270,7 +270,7 @@
   }
 
   private void deleteProject(URIish replicateURI, Project.NameKey projectName) {
-    Optional<AdminApi> adminApi = adminApiFactory.create(replicateURI);
+    Optional<AdminApi> adminApi = adminApiFactory.get().create(replicateURI);
     if (adminApi.isPresent()) {
       adminApi.get().deleteProject(projectName);
       return;
@@ -280,7 +280,7 @@
   }
 
   private void updateHead(URIish replicateURI, Project.NameKey projectName, String newHead) {
-    Optional<AdminApi> adminApi = adminApiFactory.create(replicateURI);
+    Optional<AdminApi> adminApi = adminApiFactory.get().create(replicateURI);
     if (adminApi.isPresent()) {
       adminApi.get().updateHead(projectName, newHead);
       return;
diff --git a/src/main/resources/Documentation/extension-point.md b/src/main/resources/Documentation/extension-point.md
index 345fd8f..076aded 100644
--- a/src/main/resources/Documentation/extension-point.md
+++ b/src/main/resources/Documentation/extension-point.md
@@ -1,7 +1,7 @@
 @PLUGIN@ extension points
 ==============
 
-The replication plugin exposes an extension point to allow influencing the behaviour of the replication events from another plugin or a script.
+The replication plugin exposes an extension point to allow influencing its behaviour from another plugin or a script.
 Extension points can be defined from the replication plugin only when it is loaded as [libModule](/config-gerrit.html#gerrit.installModule) and
 implemented by another plugin by declaring a `provided` dependency from the replication plugin.
 
@@ -35,5 +35,19 @@
   Example:
 
   ```
-  DynamicItem.bind(binder(),ReplicationPushFilter.class).to(ReplicationPushFilterImpl.class);
+  DynamicItem.bind(binder(), ReplicationPushFilter.class).to(ReplicationPushFilterImpl.class);
+  ```
+
+* `com.googlesource.gerrit.plugins.replication.AdminApiFactory`
+
+  Create an instance of `AdminApi` for a given remote URL. The default implementation
+  provides API instances for local FS, remote SSH, and remote Gerrit.
+
+  Only one factory at a time is supported. The implementation needs to be bound as a
+  `DynamicItem`.
+
+  Example:
+
+  ```
+  DynamicItem.bind(binder(), AdminApiFactory.class).to(AdminApiFactoryImpl.class);
   ```