Request by remote name instead of remote URL

The replication-status currently accepts a remote-url as a URL segment.
This can be tedious from a client point of view because the fully
qualified replication-url might not be known plus user cannot ask for
multiple urls which are logically grouped. To mitigate this situation
use remote name instead of remote url.

Bug: Issue 14803
Change-Id: Ibd6c2a943204cacc035b74fa34e34220417274c3
diff --git a/README.md b/README.md
index a5c70e0..c3966f0 100644
--- a/README.md
+++ b/README.md
@@ -9,19 +9,25 @@
 The cache information is then exposed via a project's resource REST endpoint:
 
 ```bash
-curl -v --user <user> '<gerrit-server>/a/projects/<project-name>/remotes/<remote-url>/replication-status'
+curl -v --user <user> '<gerrit-server>/a/projects/<project-name>/remotes/<remote-name>/replication-status'
 ```
 
 * <project-name>: an (url-encoded) project repository
-* <remote-url>: an (url-encoded) remote URL for the replication
+* <remote-name>: an (url-encoded) remote name for the replication
 
 For instance, to assess the replication status of the project `some/project` to
 the
-`https://github.com/some/project.git` URL, the following endpoint should be
-called:
+`https://github.com/some/project.git` URL with the replication configuration:
+
+```
+[remote "github-replication"]
+    url = https://github.com/${name}.git
+```
+
+the following endpoint should be called:
 
 ```bash
-curl -v --user <user> '<gerrit-server>/a/projects/some%2Fproject/remotes/https%3A%2F%2Fgithub.com%2Fsome%2Fproject.git/replication-status'
+curl -v --user <user> '<gerrit-server>/a/projects/some%2Fproject/remotes/github-replication/replication-status'
 ```
 
 A payload, similar to this may be returned:
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replicationstatus/ConfigParser.java b/src/main/java/com/googlesource/gerrit/plugins/replicationstatus/ConfigParser.java
new file mode 100644
index 0000000..c9d7ab8
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/replicationstatus/ConfigParser.java
@@ -0,0 +1,78 @@
+// Copyright (C) 2021 The Android Open Source Project
+//
+// Licensed 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 com.googlesource.gerrit.plugins.replicationstatus;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Lists;
+import com.google.common.flogger.FluentLogger;
+import java.net.URISyntaxException;
+import java.util.Collections;
+import java.util.List;
+import java.util.Set;
+import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.lib.Config;
+import org.eclipse.jgit.transport.RemoteConfig;
+import org.eclipse.jgit.transport.URIish;
+
+class ConfigParser {
+
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+
+  List<RemoteURLConfiguration> parseRemotes(Config config) throws ConfigInvalidException {
+
+    if (config.getSections().isEmpty()) {
+      logger.atWarning().log("Replication config does not exist or it's empty");
+      return Collections.emptyList();
+    }
+
+    ImmutableList.Builder<RemoteURLConfiguration> confs = ImmutableList.builder();
+    for (RemoteConfig c : allRemotes(config)) {
+      if (c.getURIs().isEmpty()) {
+        continue;
+      }
+
+      RemoteURLConfiguration remoteURLConfiguration = new RemoteURLConfiguration(c, config);
+
+      if (!remoteURLConfiguration.isSingleProjectMatch()) {
+        for (URIish u : c.getURIs()) {
+          if (u.getPath() == null || !u.getPath().contains("${name}")) {
+            throw new ConfigInvalidException(
+                String.format(
+                    "remote.%s.url \"%s\" lacks ${name} placeholder in %s",
+                    c.getName(), u, config));
+          }
+        }
+      }
+
+      confs.add(remoteURLConfiguration);
+    }
+
+    return confs.build();
+  }
+
+  private static List<RemoteConfig> allRemotes(Config cfg) throws ConfigInvalidException {
+    Set<String> names = cfg.getSubsections("remote");
+    List<RemoteConfig> result = Lists.newArrayListWithCapacity(names.size());
+    for (String name : names) {
+      try {
+        result.add(new RemoteConfig(cfg, name));
+      } catch (URISyntaxException e) {
+        throw new ConfigInvalidException(
+            String.format("remote %s has invalid URL in %s", name, cfg), e);
+      }
+    }
+    return result;
+  }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replicationstatus/Module.java b/src/main/java/com/googlesource/gerrit/plugins/replicationstatus/Module.java
index 9109e55..dff59cc 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/replicationstatus/Module.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/replicationstatus/Module.java
@@ -16,13 +16,23 @@
 
 import com.google.gerrit.extensions.registration.DynamicSet;
 import com.google.gerrit.lifecycle.LifecycleModule;
+import com.google.gerrit.server.config.SitePaths;
 import com.google.gerrit.server.events.EventListener;
+import com.google.inject.Inject;
 
 class Module extends LifecycleModule {
+
+  private final SitePaths site;
+
+  @Inject
+  public Module(SitePaths site) {
+    this.site = site;
+  }
+
   @Override
   protected void configure() {
     DynamicSet.bind(binder(), EventListener.class).to(EventHandler.class);
-    install(new ReplicationStatusApiModule());
+    install(new ReplicationStatusApiModule(site));
     install(new ReplicationStatusCacheModule());
   }
 }
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replicationstatus/RemoteURLConfiguration.java b/src/main/java/com/googlesource/gerrit/plugins/replicationstatus/RemoteURLConfiguration.java
new file mode 100644
index 0000000..de97d73
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/replicationstatus/RemoteURLConfiguration.java
@@ -0,0 +1,66 @@
+// Copyright (C) 2021 The Android Open Source Project
+//
+// Licensed 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 com.googlesource.gerrit.plugins.replicationstatus;
+
+import com.google.common.base.MoreObjects;
+import com.google.common.collect.ImmutableList;
+import com.googlesource.gerrit.plugins.replication.ReplicationFilter;
+import org.eclipse.jgit.lib.Config;
+import org.eclipse.jgit.transport.RemoteConfig;
+
+class RemoteURLConfiguration {
+
+  private final ImmutableList<String> urls;
+  private final ImmutableList<String> projects;
+  private final String name;
+  private final String remoteNameStyle;
+
+  RemoteURLConfiguration(RemoteConfig remoteConfig, Config cfg) {
+    name = remoteConfig.getName();
+    urls = ImmutableList.copyOf(cfg.getStringList("remote", name, "url"));
+    projects = ImmutableList.copyOf(cfg.getStringList("remote", name, "projects"));
+    remoteNameStyle =
+        MoreObjects.firstNonNull(cfg.getString("remote", name, "remoteNameStyle"), "slash");
+  }
+
+  ImmutableList<String> getUrls() {
+    return urls;
+  }
+
+  boolean isSingleProjectMatch() {
+    boolean ret = (projects.size() == 1);
+    if (ret) {
+      String projectMatch = projects.get(0);
+      if (ReplicationFilter.getPatternType(projectMatch)
+          != ReplicationFilter.PatternType.EXACT_MATCH) {
+        // projectMatch is either regular expression, or wild-card.
+        //
+        // Even though they might refer to a single project now, they need not
+        // after new projects have been created. Hence, we do not treat them as
+        // matching a single project.
+        ret = false;
+      }
+    }
+    return ret;
+  }
+
+  String getName() {
+    return name;
+  }
+
+  String getRemoteNameStyle() {
+    return remoteNameStyle;
+  }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replicationstatus/ReplicationStatusAction.java b/src/main/java/com/googlesource/gerrit/plugins/replicationstatus/ReplicationStatusAction.java
index 7691897..9fc5ada 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/replicationstatus/ReplicationStatusAction.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/replicationstatus/ReplicationStatusAction.java
@@ -14,10 +14,15 @@
 
 package com.googlesource.gerrit.plugins.replicationstatus;
 
+import static com.googlesource.gerrit.plugins.replication.PushResultProcessing.resolveNodeName;
+import static com.googlesource.gerrit.plugins.replication.ReplicationFileBasedConfig.replaceName;
 import static com.googlesource.gerrit.plugins.replicationstatus.ReplicationStatus.CACHE_NAME;
 
 import com.google.common.cache.Cache;
+import com.google.common.collect.Maps;
+import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.entities.Project;
+import com.google.gerrit.entities.Project.NameKey;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
@@ -30,26 +35,44 @@
 import com.google.gerrit.server.permissions.ProjectPermission;
 import com.google.inject.Inject;
 import com.google.inject.name.Named;
+import com.googlesource.gerrit.plugins.replication.ReplicationConfig;
+import com.googlesource.gerrit.plugins.replicationstatus.ProjectReplicationStatus.ProjectReplicationStatusResult;
 import java.io.IOException;
-import java.util.HashMap;
+import java.io.UnsupportedEncodingException;
+import java.net.URISyntaxException;
+import java.net.URLEncoder;
+import java.util.Collections;
+import java.util.List;
 import java.util.Map;
+import java.util.Optional;
+import java.util.stream.Collectors;
+import org.apache.commons.io.FilenameUtils;
+import org.eclipse.jgit.errors.ConfigInvalidException;
 import org.eclipse.jgit.errors.RepositoryNotFoundException;
 import org.eclipse.jgit.lib.Ref;
 import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.transport.URIish;
 
 class ReplicationStatusAction implements RestReadView<ReplicationStatusProjectRemoteResource> {
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+
   private final PermissionBackend permissionBackend;
   private final GitRepositoryManager repoManager;
   private final Cache<ReplicationStatus.Key, ReplicationStatus> replicationStatusCache;
+  private final List<RemoteURLConfiguration> remoteConfigurations;
 
   @Inject
   ReplicationStatusAction(
       PermissionBackend permissionBackend,
       GitRepositoryManager repoManager,
-      @Named(CACHE_NAME) Cache<ReplicationStatus.Key, ReplicationStatus> replicationStatusCache) {
+      @Named(CACHE_NAME) Cache<ReplicationStatus.Key, ReplicationStatus> replicationStatusCache,
+      ReplicationConfig replicationConfig,
+      ConfigParser configParser)
+      throws ConfigInvalidException {
     this.permissionBackend = permissionBackend;
     this.repoManager = repoManager;
     this.replicationStatusCache = replicationStatusCache;
+    this.remoteConfigurations = configParser.parseRemotes(replicationConfig.getConfig());
   }
 
   @Override
@@ -58,30 +81,23 @@
           ResourceConflictException, IOException {
 
     Project.NameKey projectNameKey = resource.getProjectNameKey();
-    String remoteURL = resource.getRemoteUrl();
+    String remoteName = resource.getRemote();
+    Optional<RemoteURLConfiguration> remoteConfig =
+        remoteConfigurations.stream()
+            .filter(config -> config.getName().equals(remoteName))
+            .findFirst();
 
     checkIsOwnerOrAdmin(projectNameKey);
 
-    ProjectReplicationStatus.ProjectReplicationStatusResult overallStatus =
-        ProjectReplicationStatus.ProjectReplicationStatusResult.OK;
-    Map<String, RemoteReplicationStatus> remoteStatuses = new HashMap<>();
     try (Repository git = repoManager.openRepository(projectNameKey)) {
+      List<Ref> refs = git.getRefDatabase().getRefs();
+      Map<String, RemoteReplicationStatus> remoteStatuses =
+          remoteConfig
+              .map(config -> getRemoteReplicationStatuses(config, projectNameKey, refs))
+              .orElse(Collections.emptyMap());
 
-      Map<String, ReplicationStatus> refStatuses = new HashMap<>();
-      for (Ref r : git.getRefDatabase().getRefs()) {
-        ReplicationStatus replicationStatus =
-            replicationStatusCache.getIfPresent(
-                ReplicationStatus.Key.create(projectNameKey, remoteURL, r.getName()));
-
-        if (replicationStatus != null) {
-          refStatuses.put(r.getName(), replicationStatus);
-          if (replicationStatus.isFailure()) {
-            overallStatus = ProjectReplicationStatus.ProjectReplicationStatusResult.FAILED;
-          }
-        }
-      }
-      remoteStatuses.put(remoteURL, RemoteReplicationStatus.create(refStatuses));
-
+      ProjectReplicationStatus.ProjectReplicationStatusResult overallStatus =
+          getOverallStatus(remoteStatuses);
       ProjectReplicationStatus projectStatus =
           ProjectReplicationStatus.create(remoteStatuses, overallStatus, projectNameKey.get());
 
@@ -93,6 +109,106 @@
     }
   }
 
+  private Map<String, RemoteReplicationStatus> getRemoteReplicationStatuses(
+      RemoteURLConfiguration config, NameKey projectNameKey, List<Ref> refs) {
+    return config.getUrls().stream()
+        .map(url -> getTargetURL(projectNameKey, config, url))
+        .filter(Optional::isPresent)
+        .map(
+            targetUrl ->
+                Maps.immutableEntry(
+                    targetUrl.get(),
+                    RemoteReplicationStatus.create(
+                        getRefStatuses(projectNameKey, refs, targetUrl.get()))))
+        .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
+  }
+
+  private ProjectReplicationStatusResult getOverallStatus(
+      Map<String, RemoteReplicationStatus> remoteStatuses) {
+    return remoteStatuses.values().stream()
+        .flatMap(status -> status.status().values().stream())
+        .filter(ReplicationStatus::isFailure)
+        .findFirst()
+        .map(status -> ProjectReplicationStatus.ProjectReplicationStatusResult.FAILED)
+        .orElse(ProjectReplicationStatus.ProjectReplicationStatusResult.OK);
+  }
+
+  private Map<String, ReplicationStatus> getRefStatuses(
+      Project.NameKey projectNameKey, List<Ref> refs, String uri) {
+    return refs.stream()
+        .map(ref -> ReplicationStatus.Key.create(projectNameKey, uri, ref.getName()))
+        .map(
+            key ->
+                Maps.immutableEntry(
+                    key.ref(), Optional.ofNullable(replicationStatusCache.getIfPresent(key))))
+        .filter(e -> e.getValue().isPresent())
+        .collect(Collectors.toMap(Map.Entry::getKey, e -> e.getValue().get()));
+  }
+
+  private Optional<String> getTargetURL(
+      Project.NameKey projectNameKey, RemoteURLConfiguration config, String url) {
+    try {
+      return Optional.of(resolveNodeName(getURI(config, new URIish(url), projectNameKey)));
+    } catch (URISyntaxException e) {
+      logger.atSevere().withCause(e).log(
+          "Cannot resolve target URI for template: %s and project name: %s", url, projectNameKey);
+      return Optional.empty();
+    }
+  }
+
+  /**
+   * This method was copied from replication plugin where the method is in protected scope {@link
+   * com.googlesource.gerrit.plugins.replication.Destination#getURI(URIish, NameKey)}
+   */
+  @SuppressWarnings("javadoc")
+  private URIish getURI(RemoteURLConfiguration config, URIish template, Project.NameKey project) {
+    String name = project.get();
+    if (needsUrlEncoding(template)) {
+      name = encode(name);
+    }
+    String remoteNameStyle = config.getRemoteNameStyle();
+    if (remoteNameStyle.equals("dash")) {
+      name = name.replace("/", "-");
+    } else if (remoteNameStyle.equals("underscore")) {
+      name = name.replace("/", "_");
+    } else if (remoteNameStyle.equals("basenameOnly")) {
+      name = FilenameUtils.getBaseName(name);
+    } else if (!remoteNameStyle.equals("slash")) {
+      logger.atFine().log("Unknown remoteNameStyle: %s, falling back to slash", remoteNameStyle);
+    }
+    String replacedPath = replaceName(template.getPath(), name, config.isSingleProjectMatch());
+    return (replacedPath != null) ? template.setPath(replacedPath) : template;
+  }
+
+  /**
+   * This method was copied from replication plugin where the method is in protected scope {@link
+   * com.googlesource.gerrit.plugins.replication.Destination#needsUrlEncoding(URIish)}
+   */
+  @SuppressWarnings("javadoc")
+  private static boolean needsUrlEncoding(URIish uri) {
+    return "http".equalsIgnoreCase(uri.getScheme())
+        || "https".equalsIgnoreCase(uri.getScheme())
+        || "amazon-s3".equalsIgnoreCase(uri.getScheme());
+  }
+
+  /**
+   * This method was copied from replication plugin where the method is in protected scope {@link
+   * com.googlesource.gerrit.plugins.replication.Destination#encode(String)}
+   */
+  @SuppressWarnings("javadoc")
+  private static String encode(String str) {
+    try {
+      // Some cleanup is required. The '/' character is always encoded as %2F
+      // however remote servers will expect it to be not encoded as part of the
+      // path used to the repository. Space is incorrectly encoded as '+' for this
+      // context. In the path part of a URI space should be %20, but in form data
+      // space is '+'. Our cleanup replace fixes these two issues.
+      return URLEncoder.encode(str, "UTF-8").replaceAll("%2[fF]", "/").replace("+", "%20");
+    } catch (UnsupportedEncodingException e) {
+      throw new RuntimeException(e);
+    }
+  }
+
   private void checkIsOwnerOrAdmin(Project.NameKey project) throws AuthException {
     if (!permissionBackend.currentUser().testOrFalse(GlobalPermission.ADMINISTRATE_SERVER)
         && !permissionBackend
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replicationstatus/ReplicationStatusApiModule.java b/src/main/java/com/googlesource/gerrit/plugins/replicationstatus/ReplicationStatusApiModule.java
index adecc6d..7674c5b 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/replicationstatus/ReplicationStatusApiModule.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/replicationstatus/ReplicationStatusApiModule.java
@@ -19,9 +19,21 @@
 
 import com.google.gerrit.extensions.registration.DynamicMap;
 import com.google.gerrit.extensions.restapi.RestApiModule;
+import com.google.gerrit.server.config.SitePaths;
 import com.google.inject.Scopes;
+import com.googlesource.gerrit.plugins.replication.FanoutReplicationConfig;
+import com.googlesource.gerrit.plugins.replication.ReplicationConfig;
+import com.googlesource.gerrit.plugins.replication.ReplicationFileBasedConfig;
+import java.nio.file.Files;
 
 class ReplicationStatusApiModule extends RestApiModule {
+
+  private final SitePaths site;
+
+  public ReplicationStatusApiModule(SitePaths site) {
+    this.site = site;
+  }
+
   @Override
   protected void configure() {
     bind(ReplicationStatusAction.class).in(Scopes.SINGLETON);
@@ -29,5 +41,15 @@
     child(PROJECT_KIND, "remotes").to(ReplicationStatusProjectRemoteCollection.class);
     get(REPLICATION_STATUS_PROJECT_REMOTE_KIND, "replication-status")
         .to(ReplicationStatusAction.class);
+
+    bind(ConfigParser.class).in(Scopes.SINGLETON);
+    bind(ReplicationConfig.class).to(getReplicationConfigClass()).in(Scopes.SINGLETON);
+  }
+
+  private Class<? extends ReplicationConfig> getReplicationConfigClass() {
+    if (Files.exists(site.etc_dir.resolve("replication"))) {
+      return FanoutReplicationConfig.class;
+    }
+    return ReplicationFileBasedConfig.class;
   }
 }
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replicationstatus/ReplicationStatusProjectRemoteCollection.java b/src/main/java/com/googlesource/gerrit/plugins/replicationstatus/ReplicationStatusProjectRemoteCollection.java
index a8c0b0c..ad41189 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/replicationstatus/ReplicationStatusProjectRemoteCollection.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/replicationstatus/ReplicationStatusProjectRemoteCollection.java
@@ -46,9 +46,9 @@
   public ReplicationStatusProjectRemoteResource parse(ProjectResource parent, IdString id)
       throws ResourceNotFoundException, Exception {
     Project.NameKey projectNameKey = parent.getNameKey();
-    String remoteURL = id.get();
+    String remoteName = id.get();
 
-    return new ReplicationStatusProjectRemoteResource(projectNameKey, remoteURL, remoteURL);
+    return new ReplicationStatusProjectRemoteResource(projectNameKey, remoteName);
   }
 
   @Override
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replicationstatus/ReplicationStatusProjectRemoteResource.java b/src/main/java/com/googlesource/gerrit/plugins/replicationstatus/ReplicationStatusProjectRemoteResource.java
index 77b81cd..b7ba722 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/replicationstatus/ReplicationStatusProjectRemoteResource.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/replicationstatus/ReplicationStatusProjectRemoteResource.java
@@ -26,13 +26,10 @@
 
   private final Project.NameKey projectNameKey;
   private final String remote;
-  private final String remoteURL;
 
-  public ReplicationStatusProjectRemoteResource(
-      Project.NameKey projectNameKey, String remote, String remoteURL) {
+  public ReplicationStatusProjectRemoteResource(Project.NameKey projectNameKey, String remote) {
     this.projectNameKey = projectNameKey;
     this.remote = remote;
-    this.remoteURL = remoteURL;
   }
 
   public Project.NameKey getProjectNameKey() {
@@ -42,8 +39,4 @@
   public String getRemote() {
     return remote;
   }
-
-  public String getRemoteUrl() {
-    return remoteURL;
-  }
 }
diff --git a/src/test/java/com/googlesource/gerrit/plugins/replicationstatus/ReplicationStatusIT.java b/src/test/java/com/googlesource/gerrit/plugins/replicationstatus/ReplicationStatusIT.java
index 2049f53..109cc7c 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/replicationstatus/ReplicationStatusIT.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/replicationstatus/ReplicationStatusIT.java
@@ -24,7 +24,9 @@
 import com.google.gerrit.acceptance.RestResponse;
 import com.google.gerrit.acceptance.TestAccount;
 import com.google.gerrit.acceptance.TestPlugin;
+import com.google.gerrit.acceptance.UseLocalDisk;
 import com.google.gerrit.acceptance.config.GerritConfig;
+import com.google.gerrit.acceptance.config.GlobalPluginConfig;
 import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.common.data.AccessSection;
@@ -42,6 +44,7 @@
 import java.io.IOException;
 import java.net.URISyntaxException;
 import java.util.Collections;
+import java.util.Map;
 import org.eclipse.jgit.lib.Constants;
 import org.eclipse.jgit.transport.RemoteRefUpdate;
 import org.junit.Before;
@@ -50,9 +53,12 @@
 @TestPlugin(
     name = "replication-status",
     sysModule = "com.googlesource.gerrit.plugins.replicationstatus.Module")
+@UseLocalDisk
 public class ReplicationStatusIT extends LightweightPluginDaemonTest {
   private static final String REF_MASTER = Constants.R_HEADS + Constants.MASTER;
-  private static final String REMOTE = "ssh://some.remote.host";
+  private static final String REMOTE_TAGRET_NODE = "some.remote.host";
+  private static final String REMOTE_TARGET_URL_TEMPLATE = "ssh://some.remote.host/git/${name}.git";
+  private static final String REMOTE_NAME = "some-remote-host";
 
   private static final Gson gson = newGson();
 
@@ -68,27 +74,41 @@
 
   @Test
   @GerritConfig(name = "gerrit.instanceId", value = "testInstanceId-1")
+  @GlobalPluginConfig(
+      pluginName = "replication",
+      name = "remote.some-remote-host.url",
+      value = REMOTE_TARGET_URL_TEMPLATE)
   public void shouldBeOKSuccessForAdminUsers() throws Exception {
-    RestResponse result = adminRestSession.get(endpoint(project, REMOTE));
+    RestResponse result = adminRestSession.get(endpoint(project, REMOTE_NAME));
     result.assertOK();
 
-    assertThat(contentWithoutMagicJson(result)).isEqualTo(emptyReplicationStatus(project, REMOTE));
+    assertThat(contentWithoutMagicJson(result))
+        .isEqualTo(emptyReplicationStatus(project, REMOTE_TAGRET_NODE));
   }
 
   @Test
   @GerritConfig(name = "gerrit.instanceId", value = "testInstanceId-1")
+  @GlobalPluginConfig(
+      pluginName = "replication",
+      name = "remote.some-remote-host.url",
+      value = REMOTE_TARGET_URL_TEMPLATE)
   public void shouldBeOKSuccessForProjectOwners() throws Exception {
     makeProjectOwner(user, project);
-    RestResponse result = userRestSession.get(endpoint(project, REMOTE));
+    RestResponse result = userRestSession.get(endpoint(project, REMOTE_NAME));
     result.assertOK();
 
-    assertThat(contentWithoutMagicJson(result)).isEqualTo(emptyReplicationStatus(project, REMOTE));
+    assertThat(contentWithoutMagicJson(result))
+        .isEqualTo(emptyReplicationStatus(project, REMOTE_TAGRET_NODE));
   }
 
   @Test
   @GerritConfig(name = "gerrit.instanceId", value = "testInstanceId-1")
+  @GlobalPluginConfig(
+      pluginName = "replication",
+      name = "remote.some-remote-host.url",
+      value = REMOTE_TARGET_URL_TEMPLATE)
   public void shouldBeForbiddenForNonProjectOwners() throws Exception {
-    RestResponse result = userRestSession.get(endpoint(project, REMOTE));
+    RestResponse result = userRestSession.get(endpoint(project, REMOTE_NAME));
     result.assertForbidden();
 
     assertThat(result.getEntityContent()).contains("Administrate Server or Project owner required");
@@ -96,8 +116,12 @@
 
   @Test
   @GerritConfig(name = "gerrit.instanceId", value = "testInstanceId-1")
+  @GlobalPluginConfig(
+      pluginName = "replication",
+      name = "remote.some-remote-host.url",
+      value = REMOTE_TARGET_URL_TEMPLATE)
   public void shouldBeForbiddenForAnonymousUsers() throws Exception {
-    RestResponse result = anonymousRestSession.get(endpoint(project, REMOTE));
+    RestResponse result = anonymousRestSession.get(endpoint(project, REMOTE_NAME));
     result.assertForbidden();
 
     assertThat(result.getEntityContent()).contains("Administrate Server or Project owner required");
@@ -105,87 +129,170 @@
 
   @Test
   @GerritConfig(name = "gerrit.instanceId", value = "testInstanceId-1")
+  @GlobalPluginConfig(
+      pluginName = "replication",
+      name = "remote.some-remote-host.url",
+      value = REMOTE_TARGET_URL_TEMPLATE)
   public void shouldNotReportStatusOfReplicationsGeneratedOnDifferentNodes() throws Exception {
     eventHandler.onEvent(
-        successReplicatedEvent("testInstanceId-2", System.currentTimeMillis(), REMOTE));
+        successReplicatedEvent("testInstanceId-2", System.currentTimeMillis(), REMOTE_TAGRET_NODE));
 
-    RestResponse result = adminRestSession.get(endpoint(project, REMOTE));
+    RestResponse result = adminRestSession.get(endpoint(project, REMOTE_NAME));
     result.assertOK();
 
-    assertThat(contentWithoutMagicJson(result)).isEqualTo(emptyReplicationStatus(project, REMOTE));
+    assertThat(contentWithoutMagicJson(result))
+        .isEqualTo(emptyReplicationStatus(project, REMOTE_TAGRET_NODE));
   }
 
   @Test
   @GerritConfig(name = "gerrit.instanceId", value = "testInstanceId-1")
+  @GlobalPluginConfig(
+      pluginName = "replication",
+      name = "remote.some-remote-host.url",
+      value = REMOTE_TARGET_URL_TEMPLATE)
   public void shouldReturnSuccessfulProjectReplicationStatus() throws Exception {
     long eventCreatedOn = System.currentTimeMillis();
 
-    eventHandler.onEvent(successReplicatedEvent("testInstanceId-1", eventCreatedOn, REMOTE));
-    RestResponse result = adminRestSession.get(endpoint(project, REMOTE));
+    eventHandler.onEvent(
+        successReplicatedEvent("testInstanceId-1", eventCreatedOn, REMOTE_TAGRET_NODE));
+    RestResponse result = adminRestSession.get(endpoint(project, REMOTE_NAME));
 
     result.assertOK();
     assertThat(contentWithoutMagicJson(result))
-        .isEqualTo(successReplicationStatus(REMOTE, project, eventCreatedOn));
+        .isEqualTo(successReplicationStatus(REMOTE_TAGRET_NODE, project, eventCreatedOn));
   }
 
   @Test
+  @GlobalPluginConfig(
+      pluginName = "replication",
+      name = "remote.some-remote-host.url",
+      value = REMOTE_TARGET_URL_TEMPLATE)
   public void shouldReturnScheduledProjectReplicationStatus() throws Exception {
     long eventCreatedOn = System.currentTimeMillis();
 
-    eventHandler.onEvent(scheduledEvent(null, eventCreatedOn, REF_MASTER, REMOTE));
-    RestResponse result = adminRestSession.get(endpoint(project, REMOTE));
+    eventHandler.onEvent(scheduledEvent(null, eventCreatedOn, REF_MASTER, REMOTE_TAGRET_NODE));
+    RestResponse result = adminRestSession.get(endpoint(project, REMOTE_NAME));
 
     result.assertOK();
     assertThat(contentWithoutMagicJson(result))
-        .isEqualTo(scheduledReplicationStatus(REMOTE, project, eventCreatedOn));
+        .isEqualTo(scheduledReplicationStatus(REMOTE_TAGRET_NODE, project, eventCreatedOn));
   }
 
   @Test
+  @GlobalPluginConfig(
+      pluginName = "replication",
+      name = "remote.some-remote-host.url",
+      value = REMOTE_TARGET_URL_TEMPLATE)
   public void shouldConsumeEventsThatHaveNoInstanceId() throws Exception {
     long eventCreatedOn = System.currentTimeMillis();
 
-    eventHandler.onEvent(successReplicatedEvent(null, eventCreatedOn, REMOTE));
-    RestResponse result = adminRestSession.get(endpoint(project, REMOTE));
+    eventHandler.onEvent(successReplicatedEvent(null, eventCreatedOn, REMOTE_TAGRET_NODE));
+    RestResponse result = adminRestSession.get(endpoint(project, REMOTE_NAME));
 
     result.assertOK();
     assertThat(contentWithoutMagicJson(result))
-        .isEqualTo(successReplicationStatus(REMOTE, project, eventCreatedOn));
+        .isEqualTo(successReplicationStatus(REMOTE_TAGRET_NODE, project, eventCreatedOn));
   }
 
   @Test
+  @GlobalPluginConfig(
+      pluginName = "replication",
+      name = "remote.some-remote-host.url",
+      value = REMOTE_TARGET_URL_TEMPLATE)
   public void shouldNotConsumeEventsWhenNodeInstanceIdIsNullButEventHasIt() throws Exception {
     eventHandler.onEvent(
-        successReplicatedEvent("testInstanceId-2", System.currentTimeMillis(), REMOTE));
+        successReplicatedEvent("testInstanceId-2", System.currentTimeMillis(), REMOTE_TAGRET_NODE));
 
-    RestResponse result = adminRestSession.get(endpoint(project, REMOTE));
+    RestResponse result = adminRestSession.get(endpoint(project, REMOTE_NAME));
     result.assertOK();
 
-    assertThat(contentWithoutMagicJson(result)).isEqualTo(emptyReplicationStatus(project, REMOTE));
+    assertThat(contentWithoutMagicJson(result))
+        .isEqualTo(emptyReplicationStatus(project, REMOTE_TAGRET_NODE));
   }
 
   @Test
+  @GlobalPluginConfig(
+      pluginName = "replication",
+      name = "remote.some-remote-host.url",
+      value = REMOTE_TARGET_URL_TEMPLATE)
   public void shouldConsumeEventsWhenBothNodeAndEventHaveNoInstanceId() throws Exception {
     long eventCreatedOn = System.currentTimeMillis();
 
-    eventHandler.onEvent(successReplicatedEvent(null, eventCreatedOn, REMOTE));
-    RestResponse result = adminRestSession.get(endpoint(project, REMOTE));
+    eventHandler.onEvent(successReplicatedEvent(null, eventCreatedOn, REMOTE_TAGRET_NODE));
+    RestResponse result = adminRestSession.get(endpoint(project, REMOTE_NAME));
 
     result.assertOK();
     assertThat(contentWithoutMagicJson(result))
-        .isEqualTo(successReplicationStatus(REMOTE, project, eventCreatedOn));
+        .isEqualTo(successReplicationStatus(REMOTE_TAGRET_NODE, project, eventCreatedOn));
   }
 
   @Test
   @GerritConfig(name = "gerrit.instanceId", value = "testInstanceId-1")
+  @GlobalPluginConfig(
+      pluginName = "replication",
+      name = "remote.some-remote-host.url",
+      value = REMOTE_TARGET_URL_TEMPLATE)
   public void shouldShowFailedInPayloadWhenRefCouldntBeReplicated() throws Exception {
     long eventCreatedOn = System.currentTimeMillis();
 
-    eventHandler.onEvent(failedReplicatedEvent("testInstanceId-1", eventCreatedOn, REMOTE));
-    RestResponse result = adminRestSession.get(endpoint(project, REMOTE));
+    eventHandler.onEvent(
+        failedReplicatedEvent("testInstanceId-1", eventCreatedOn, REMOTE_TAGRET_NODE));
+    RestResponse result = adminRestSession.get(endpoint(project, REMOTE_NAME));
 
     result.assertOK();
     assertThat(contentWithoutMagicJson(result))
-        .isEqualTo(failedReplicationStatus(REMOTE, project, eventCreatedOn));
+        .isEqualTo(failedReplicationStatus(REMOTE_TAGRET_NODE, project, eventCreatedOn));
+  }
+
+  @Test
+  @GerritConfig(name = "gerrit.instanceId", value = "testInstanceId-1")
+  @GlobalPluginConfig(
+      pluginName = "replication",
+      name = "remote.some-remote-host.url",
+      value = REMOTE_TARGET_URL_TEMPLATE)
+  public void shouldBeOKSuccessForNonExistentRemoteName() throws Exception {
+    String nonExistingRemote = "non-existing-remote";
+    RestResponse result = adminRestSession.get(endpoint(project, nonExistingRemote));
+    result.assertOK();
+
+    assertThat(contentWithoutMagicJson(result)).isEqualTo(emptyReplicationStatus(project));
+  }
+
+  @Test
+  @GerritConfig(name = "gerrit.instanceId", value = "testInstanceId-1")
+  @GlobalPluginConfig(
+      pluginName = "replication",
+      name = "remote.some-remote-host.url",
+      values = {REMOTE_TARGET_URL_TEMPLATE, "ssh://some.remote.host2/git/${name}.git"})
+  public void shouldReturnSuccessfulProjectReplicationStatusForMultipeURLs() throws Exception {
+    long eventCreatedOn = System.currentTimeMillis();
+
+    eventHandler.onEvent(
+        successReplicatedEvent("testInstanceId-1", eventCreatedOn, REMOTE_TAGRET_NODE));
+    eventHandler.onEvent(
+        successReplicatedEvent("testInstanceId-1", eventCreatedOn, "some.remote.host2"));
+    RestResponse result = adminRestSession.get(endpoint(project, REMOTE_NAME));
+
+    result.assertOK();
+    String resultJson = contentWithoutMagicJson(result);
+    Map<String, RemoteReplicationStatus> statuses =
+        ImmutableMap.of(
+            REMOTE_TAGRET_NODE,
+            RemoteReplicationStatus.create(
+                ImmutableMap.of(
+                    REF_MASTER,
+                    ReplicationStatus.create(
+                        ReplicationStatus.ReplicationStatusResult.SUCCEEDED, eventCreatedOn))),
+            "some.remote.host2",
+            RemoteReplicationStatus.create(
+                ImmutableMap.of(
+                    REF_MASTER,
+                    ReplicationStatus.create(
+                        ReplicationStatus.ReplicationStatusResult.SUCCEEDED, eventCreatedOn))));
+    assertThat(resultJson)
+        .isEqualTo(
+            projectReplicationStatus(
+                statuses, project, ProjectReplicationStatus.ProjectReplicationStatusResult.OK));
   }
 
   private String contentWithoutMagicJson(RestResponse response) throws IOException {
@@ -264,13 +371,21 @@
         "/projects/%s/remotes/%s/replication-status", project.get(), encode(remote));
   }
 
-  private String emptyReplicationStatus(Project.NameKey project, String remoteUrl)
-      throws URISyntaxException {
+  private String emptyReplicationStatus(Project.NameKey project, String remoteUrl) {
+    return emptyReplicationStatus(
+        ImmutableMap.of(remoteUrl, RemoteReplicationStatus.create(Collections.emptyMap())),
+        project);
+  }
+
+  private String emptyReplicationStatus(Project.NameKey project) {
+    return emptyReplicationStatus(ImmutableMap.of(), project);
+  }
+
+  private String emptyReplicationStatus(
+      Map<String, RemoteReplicationStatus> statuses, Project.NameKey project) {
     return gson.toJson(
         ProjectReplicationStatus.create(
-            ImmutableMap.of(remoteUrl, RemoteReplicationStatus.create(Collections.emptyMap())),
-            ProjectReplicationStatus.ProjectReplicationStatusResult.OK,
-            project.get()));
+            statuses, ProjectReplicationStatus.ProjectReplicationStatusResult.OK, project.get()));
   }
 
   private String successReplicationStatus(String remote, Project.NameKey project, long when)
@@ -314,16 +429,22 @@
       Project.NameKey project,
       long when,
       ProjectReplicationStatus.ProjectReplicationStatusResult projectReplicationStatusResult,
-      ReplicationStatus.ReplicationStatusResult replicationStatusResult)
-      throws URISyntaxException {
+      ReplicationStatus.ReplicationStatusResult replicationStatusResult) {
+    return projectReplicationStatus(
+        ImmutableMap.of(
+            remoteUrl,
+            RemoteReplicationStatus.create(
+                ImmutableMap.of(
+                    REF_MASTER, ReplicationStatus.create(replicationStatusResult, when)))),
+        project,
+        projectReplicationStatusResult);
+  }
+
+  private String projectReplicationStatus(
+      Map<String, RemoteReplicationStatus> statuses,
+      Project.NameKey project,
+      ProjectReplicationStatus.ProjectReplicationStatusResult projectReplicationStatusResult) {
     return gson.toJson(
-        ProjectReplicationStatus.create(
-            ImmutableMap.of(
-                remoteUrl,
-                RemoteReplicationStatus.create(
-                    ImmutableMap.of(
-                        REF_MASTER, ReplicationStatus.create(replicationStatusResult, when)))),
-            projectReplicationStatusResult,
-            project.get()));
+        ProjectReplicationStatus.create(statuses, projectReplicationStatusResult, project.get()));
   }
 }