Add headers to EventProcessor

Some webhooks require additional information as part of request headers
e.g. GitHub event type is provided in header:
X-GitHub-Event: push.

This patch extends EventProcessor processing result with headers and
wraps it into Request class.

Change-Id: I6eb42092c663bce475a52c0aa4f30ce3163399aa
Signed-off-by: Jacek Centkowski <jcentkowski@collab.net>
diff --git a/src/main/java/com/googlesource/gerrit/plugins/webhooks/EventProcessor.java b/src/main/java/com/googlesource/gerrit/plugins/webhooks/EventProcessor.java
index 866df37..1d52300 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/webhooks/EventProcessor.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/webhooks/EventProcessor.java
@@ -14,8 +14,31 @@
 
 package com.googlesource.gerrit.plugins.webhooks;
 
+import com.google.common.base.MoreObjects;
 import com.google.gerrit.server.events.ProjectEvent;
+import java.util.Collections;
+import java.util.Map;
+import java.util.Optional;
 
 public interface EventProcessor {
-  String process(ProjectEvent event, RemoteConfig remote);
+  public class Request {
+    public final String body;
+    public final Map<String, String> headers;
+
+    public Request(String body) {
+      this(body, null);
+    }
+
+    public Request(String body, Map<String, String> headers) {
+      this.body = body;
+      this.headers = Optional.ofNullable(headers).orElse(Collections.emptyMap());
+    }
+
+    @Override
+    public String toString() {
+      return MoreObjects.toStringHelper(this).add("headers", headers).add("body", body).toString();
+    }
+  }
+
+  Optional<Request> process(ProjectEvent event, RemoteConfig remote);
 }
diff --git a/src/main/java/com/googlesource/gerrit/plugins/webhooks/HttpSession.java b/src/main/java/com/googlesource/gerrit/plugins/webhooks/HttpSession.java
index 8809f4d..f6b2b93 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/webhooks/HttpSession.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/webhooks/HttpSession.java
@@ -31,10 +31,18 @@
     this.httpClient = httpClient;
   }
 
-  HttpResult post(String endpoint, String content) throws IOException {
+  HttpResult post(String endpoint, EventProcessor.Request request) throws IOException {
     HttpPost post = new HttpPost(endpoint);
     post.addHeader("Content-Type", MediaType.JSON_UTF_8.toString());
-    post.setEntity(new StringEntity(content, StandardCharsets.UTF_8));
+    request
+        .headers
+        .entrySet()
+        .stream()
+        .forEach(
+            e -> {
+              post.addHeader(e.getKey(), e.getValue());
+            });
+    post.setEntity(new StringEntity(request.body, StandardCharsets.UTF_8));
     return httpClient.execute(post, new HttpResponseHandler());
   }
 }
diff --git a/src/main/java/com/googlesource/gerrit/plugins/webhooks/PostTask.java b/src/main/java/com/googlesource/gerrit/plugins/webhooks/PostTask.java
index abdaf38..fbc386a 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/webhooks/PostTask.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/webhooks/PostTask.java
@@ -14,7 +14,6 @@
 
 package com.googlesource.gerrit.plugins.webhooks;
 
-import com.google.common.base.Strings;
 import com.google.common.base.Supplier;
 import com.google.common.base.Suppliers;
 import com.google.gerrit.server.events.ProjectEvent;
@@ -22,6 +21,7 @@
 import com.google.inject.assistedinject.AssistedInject;
 import com.googlesource.gerrit.plugins.webhooks.HttpResponseHandler.HttpResult;
 import java.io.IOException;
+import java.util.Optional;
 import java.util.concurrent.ScheduledExecutorService;
 import java.util.concurrent.TimeUnit;
 import javax.net.ssl.SSLException;
@@ -39,7 +39,7 @@
   private final HttpSession session;
   private final Configuration cfg;
   private final String url;
-  private final Supplier<String> body;
+  private final Supplier<Optional<EventProcessor.Request>> processor;
   private int execCnt;
 
   @AssistedInject
@@ -54,7 +54,7 @@
     this.session = session;
     this.cfg = cfg;
     this.url = remote.getUrl();
-    this.body = Suppliers.memoize(() -> processor.process(event, remote));
+    this.processor = Suppliers.memoize(() -> processor.process(event, remote));
   }
 
   void schedule() {
@@ -68,14 +68,14 @@
   @Override
   public void run() {
     try {
-      String content = body.get();
-      if (Strings.isNullOrEmpty(content)) {
+      Optional<EventProcessor.Request> content = processor.get();
+      if (!content.isPresent()) {
         log.debug("No content. Webhook [{}] skipped.", url);
         return;
       }
 
       execCnt++;
-      HttpResult result = session.post(url, content);
+      HttpResult result = session.post(url, content.get());
       if (!result.successful && execCnt < cfg.getMaxTries()) {
         logRetry(result.message);
         reschedule();
@@ -108,6 +108,7 @@
 
   @Override
   public String toString() {
-    return body.get();
+    Optional<EventProcessor.Request> content = processor.get();
+    return content.isPresent() ? content.get().toString() : "no content";
   }
 }
diff --git a/src/main/java/com/googlesource/gerrit/plugins/webhooks/processors/AbstractEventProcessor.java b/src/main/java/com/googlesource/gerrit/plugins/webhooks/processors/AbstractEventProcessor.java
index 8c727e0..54ee74d 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/webhooks/processors/AbstractEventProcessor.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/webhooks/processors/AbstractEventProcessor.java
@@ -18,10 +18,11 @@
 import com.google.gerrit.server.events.ProjectEvent;
 import com.googlesource.gerrit.plugins.webhooks.EventProcessor;
 import com.googlesource.gerrit.plugins.webhooks.RemoteConfig;
+import java.util.Optional;
 
 public abstract class AbstractEventProcessor implements EventProcessor {
   @Override
-  public String process(ProjectEvent event, RemoteConfig remote) {
+  public Optional<EventProcessor.Request> process(ProjectEvent event, RemoteConfig remote) {
     if (!shouldProcess(event, remote)) {
       return null;
     }
@@ -29,7 +30,8 @@
     return doProcess(event, remote);
   }
 
-  protected abstract String doProcess(ProjectEvent event, RemoteConfig remote);
+  protected abstract Optional<EventProcessor.Request> doProcess(
+      ProjectEvent event, RemoteConfig remote);
 
   protected boolean shouldProcess(ProjectEvent event, RemoteConfig remote) {
     String[] wantedEvents = remote.getEvents();
diff --git a/src/main/java/com/googlesource/gerrit/plugins/webhooks/processors/GerritEventProcessor.java b/src/main/java/com/googlesource/gerrit/plugins/webhooks/processors/GerritEventProcessor.java
index b8532b4..e9c373c 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/webhooks/processors/GerritEventProcessor.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/webhooks/processors/GerritEventProcessor.java
@@ -19,14 +19,16 @@
 import com.google.gerrit.server.events.SupplierSerializer;
 import com.google.gson.Gson;
 import com.google.gson.GsonBuilder;
+import com.googlesource.gerrit.plugins.webhooks.EventProcessor;
 import com.googlesource.gerrit.plugins.webhooks.RemoteConfig;
+import java.util.Optional;
 
 public class GerritEventProcessor extends AbstractEventProcessor {
   private static Gson GSON =
       new GsonBuilder().registerTypeAdapter(Supplier.class, new SupplierSerializer()).create();
 
   @Override
-  public String doProcess(ProjectEvent event, RemoteConfig remote) {
-    return GSON.toJson(event);
+  public Optional<EventProcessor.Request> doProcess(ProjectEvent event, RemoteConfig remote) {
+    return Optional.of(new EventProcessor.Request(GSON.toJson(event)));
   }
 }
diff --git a/src/test/java/com/googlesource/gerrit/plugins/webhooks/PostTaskTest.java b/src/test/java/com/googlesource/gerrit/plugins/webhooks/PostTaskTest.java
index 043828f..f52969a 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/webhooks/PostTaskTest.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/webhooks/PostTaskTest.java
@@ -23,6 +23,7 @@
 import com.google.gerrit.server.events.ProjectCreatedEvent;
 import com.googlesource.gerrit.plugins.webhooks.HttpResponseHandler.HttpResult;
 import java.io.IOException;
+import java.util.Optional;
 import java.util.concurrent.ScheduledThreadPoolExecutor;
 import java.util.concurrent.TimeUnit;
 import javax.net.ssl.SSLException;
@@ -38,7 +39,6 @@
 public class PostTaskTest {
 
   private static final String WEBHOOK_URL = "webhook-url";
-  private static final String BODY = "body";
   private static final HttpResult OK_RESULT = new HttpResult(true, "");
   private static final HttpResult ERR_RESULT = new HttpResult(false, "");
   private static final int RETRY_INTERVAL = 100;
@@ -56,6 +56,8 @@
 
   @Mock private EventProcessor processor;
 
+  @Mock private EventProcessor.Request content;
+
   private PostTask task;
 
   @Before
@@ -63,13 +65,13 @@
     when(cfg.getRetryInterval()).thenReturn(RETRY_INTERVAL);
     when(cfg.getMaxTries()).thenReturn(MAX_TRIES);
     when(remote.getUrl()).thenReturn(WEBHOOK_URL);
-    when(processor.process(eq(projectCreated), eq(remote))).thenReturn(BODY);
+    when(processor.process(eq(projectCreated), eq(remote))).thenReturn(Optional.of(content));
     task = new PostTask(executor, session, cfg, processor, projectCreated, remote);
   }
 
   @Test
   public void noScheduleOnEmptyBody() throws Exception {
-    when(processor.process(eq(projectCreated), eq(remote))).thenReturn(null);
+    when(processor.process(eq(projectCreated), eq(remote))).thenReturn(Optional.empty());
     task.run();
     verifyZeroInteractions(session);
     verifyZeroInteractions(executor);
@@ -77,35 +79,35 @@
 
   @Test
   public void noRescheduleOnSuccess() throws IOException {
-    when(session.post(WEBHOOK_URL, BODY)).thenReturn(OK_RESULT);
+    when(session.post(eq(WEBHOOK_URL), eq(content))).thenReturn(OK_RESULT);
     task.run();
     verifyZeroInteractions(executor);
   }
 
   @Test
   public void noRescheduleOnNonRecoverableException() throws IOException {
-    when(session.post(WEBHOOK_URL, BODY)).thenThrow(SSLException.class);
+    when(session.post(eq(WEBHOOK_URL), eq(content))).thenThrow(SSLException.class);
     task.run();
     verifyZeroInteractions(executor);
   }
 
   @Test
   public void rescheduleOnError() throws IOException {
-    when(session.post(WEBHOOK_URL, BODY)).thenReturn(ERR_RESULT);
+    when(session.post(eq(WEBHOOK_URL), eq(content))).thenReturn(ERR_RESULT);
     task.run();
     verify(executor, times(1)).schedule(task, RETRY_INTERVAL, TimeUnit.MILLISECONDS);
   }
 
   @Test
   public void rescheduleOnRecoverableException() throws IOException {
-    when(session.post(WEBHOOK_URL, BODY)).thenThrow(IOException.class);
+    when(session.post(eq(WEBHOOK_URL), eq(content))).thenThrow(IOException.class);
     task.run();
     verify(executor, times(1)).schedule(task, RETRY_INTERVAL, TimeUnit.MILLISECONDS);
   }
 
   @Test
   public void keepReschedulingMaxTriesTimes() throws IOException {
-    when(session.post(WEBHOOK_URL, BODY)).thenThrow(IOException.class);
+    when(session.post(eq(WEBHOOK_URL), eq(content))).thenThrow(IOException.class);
     when(executor.schedule(task, RETRY_INTERVAL, TimeUnit.MILLISECONDS))
         .then(
             new Answer<Void>() {
diff --git a/src/test/java/com/googlesource/gerrit/plugins/webhooks/processors/AbstractEventProcessorTest.java b/src/test/java/com/googlesource/gerrit/plugins/webhooks/processors/AbstractEventProcessorTest.java
index e8c3391..f4c2ba0 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/webhooks/processors/AbstractEventProcessorTest.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/webhooks/processors/AbstractEventProcessorTest.java
@@ -22,7 +22,9 @@
 import com.google.gerrit.server.events.ProjectCreatedEvent;
 import com.google.gerrit.server.events.ProjectEvent;
 import com.google.gerrit.server.events.RefUpdatedEvent;
+import com.googlesource.gerrit.plugins.webhooks.EventProcessor;
 import com.googlesource.gerrit.plugins.webhooks.RemoteConfig;
+import java.util.Optional;
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -85,9 +87,9 @@
 
   private class TestEventProcessor extends AbstractEventProcessor {
     @Override
-    public String doProcess(ProjectEvent event, RemoteConfig remote) {
+    public Optional<EventProcessor.Request> doProcess(ProjectEvent event, RemoteConfig remote) {
       // do nothing
-      return null;
+      return Optional.empty();
     }
   }
 }