Handle batch-fetch requests in the replica

Add support for the `batch-fetch` endpoint in the
`PullReplicationFilter` class, which is used by the replica node in a
pull-replication setup.
Deserialise the json message into the appopriate class, and pass the
message for further processing to the relevant "action" class.

Bug: Issue 40015567
Change-Id: Id62a671f6436571846256d7dc4dac7c94416233c
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/PullReplicationEndpoints.java b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/PullReplicationEndpoints.java
index 253af58..9fdf5d9 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/PullReplicationEndpoints.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/PullReplicationEndpoints.java
@@ -36,6 +36,7 @@
   public static final String BATCH_APPLY_OBJECT_API_ENDPOINT = "batch-apply-object";
 
   public static final String FETCH_ENDPOINT = "fetch";
+  public static final String BATCH_FETCH_ENDPOINT = "batch-fetch";
   public static final String INIT_PROJECT_ENDPOINT = "init-project";
   public static final String DELETE_PROJECT_ENDPOINT = "delete-project";
 }
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/PullReplicationFilter.java b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/PullReplicationFilter.java
index bfe51a2..207456a 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/PullReplicationFilter.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/PullReplicationFilter.java
@@ -86,6 +86,7 @@
       Pattern.compile(".*/init-project/([^/]+.git)");
 
   private FetchAction fetchAction;
+  private BatchFetchAction batchFetchAction;
   private ApplyObjectAction applyObjectAction;
   private ApplyObjectsAction applyObjectsAction;
   private BatchApplyObjectAction batchApplyObjectAction;
@@ -100,6 +101,7 @@
   @Inject
   public PullReplicationFilter(
       FetchAction fetchAction,
+      BatchFetchAction batchFetchAction,
       ApplyObjectAction applyObjectAction,
       ApplyObjectsAction applyObjectsAction,
       BatchApplyObjectAction batchApplyObjectAction,
@@ -110,6 +112,7 @@
       @PluginName String pluginName,
       Provider<CurrentUser> currentUserProvider) {
     this.fetchAction = fetchAction;
+    this.batchFetchAction = batchFetchAction;
     this.applyObjectAction = applyObjectAction;
     this.applyObjectsAction = applyObjectsAction;
     this.batchApplyObjectAction = batchApplyObjectAction;
@@ -136,6 +139,9 @@
       if (isFetchAction(httpRequest)) {
         failIfcurrentUserIsAnonymous();
         writeResponse(httpResponse, doFetch(httpRequest));
+      } else if (isBatchFetchAction(httpRequest)) {
+        failIfcurrentUserIsAnonymous();
+        writeResponse(httpResponse, doBatchFetch(httpRequest));
       } else if (isApplyObjectAction(httpRequest)) {
         failIfcurrentUserIsAnonymous();
         writeResponse(httpResponse, doApplyObject(httpRequest));
@@ -281,6 +287,16 @@
     return new ProjectResource(project.get(), currentUserProvider.get());
   }
 
+  @SuppressWarnings("unchecked")
+  private Response<Map<String, Object>> doBatchFetch(HttpServletRequest httpRequest)
+      throws IOException, RestApiException, PermissionBackendException {
+    TypeToken<List<Input>> collectionType = new TypeToken<>() {};
+    List<Input> inputs = readJson(httpRequest, collectionType.getType());
+    IdString id = getProjectName(httpRequest).get();
+
+    return (Response<Map<String, Object>>) batchFetchAction.apply(parseProjectResource(id), inputs);
+  }
+
   private <T> void writeResponse(HttpServletResponse httpResponse, Response<T> response)
       throws IOException {
     String responseJson = gson.toJson(response);
@@ -373,6 +389,12 @@
     return httpRequest.getRequestURI().endsWith(String.format("/%s~" + FETCH_ENDPOINT, pluginName));
   }
 
+  private boolean isBatchFetchAction(HttpServletRequest httpRequest) {
+    return httpRequest
+        .getRequestURI()
+        .endsWith(String.format("/%s~" + BATCH_FETCH_ENDPOINT, pluginName));
+  }
+
   private boolean isInitProjectAction(HttpServletRequest httpRequest) {
     return httpRequest
         .getRequestURI()
diff --git a/src/test/java/com/googlesource/gerrit/plugins/replication/pull/api/PullReplicationFilterTest.java b/src/test/java/com/googlesource/gerrit/plugins/replication/pull/api/PullReplicationFilterTest.java
index 850d04a..e2afe92 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/replication/pull/api/PullReplicationFilterTest.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/replication/pull/api/PullReplicationFilterTest.java
@@ -39,6 +39,7 @@
   @Mock HttpServletResponse response;
   @Mock FilterChain filterChain;
   @Mock private FetchAction fetchAction;
+  @Mock private BatchFetchAction batchFetchAction;
   @Mock private ApplyObjectAction applyObjectAction;
   @Mock private ApplyObjectsAction applyObjectsAction;
   @Mock private BatchApplyObjectAction batchApplyObjectAction;
@@ -56,6 +57,8 @@
   private final String PROJECT_NAME_GIT = "some-project.git";
   private final String FETCH_URI =
       String.format("any-prefix/projects/%s/%s~fetch", PROJECT_NAME, PLUGIN_NAME);
+  private final String BATCH_FETCH_URI =
+      String.format("any-prefix/projects/%s/%s~batch-fetch", PROJECT_NAME, PLUGIN_NAME);
   private final String APPLY_OBJECT_URI =
       String.format("any-prefix/projects/%s/%s~apply-object", PROJECT_NAME, PLUGIN_NAME);
   private final String APPLY_OBJECTS_URI =
@@ -79,6 +82,7 @@
   private PullReplicationFilter createPullReplicationFilter(CurrentUser currentUser) {
     return new PullReplicationFilter(
         fetchAction,
+        batchFetchAction,
         applyObjectAction,
         applyObjectsAction,
         batchApplyObjectAction,
@@ -129,6 +133,31 @@
   }
 
   @Test
+  public void shouldFilterBatchFetchAction() throws Exception {
+    byte[] payloadBatchFetch =
+        ("[{"
+                + "\"label\":\"Replication\", "
+                + "\"ref_name\": \"refs/heads/master\", "
+                + "\"async\":false"
+                + "},"
+                + "{"
+                + "\"label\":\"Replication\", "
+                + "\"ref_name\": \"refs/heads/test\", "
+                + "\"async\":false"
+                + "}]")
+            .getBytes(StandardCharsets.UTF_8);
+
+    defineBehaviours(payloadBatchFetch, BATCH_FETCH_URI);
+    when(batchFetchAction.apply(any(), any())).thenReturn(OK_RESPONSE);
+
+    PullReplicationFilter pullReplicationFilter = createPullReplicationFilter();
+    pullReplicationFilter.doFilter(request, response, filterChain);
+
+    verifyBehaviours();
+    verify(batchFetchAction).apply(any(ProjectResource.class), any());
+  }
+
+  @Test
   public void shouldFilterApplyObjectAction() throws Exception {
 
     byte[] payloadApplyObject =