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..ee6cd59 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,35 @@
 
 package com.googlesource.gerrit.plugins.webhooks;
 
+import java.util.Collections;
+import java.util.Map;
+
+import com.google.common.base.MoreObjects;
+import com.google.common.base.Optional;
 import com.google.gerrit.server.events.ProjectEvent;
 
 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.fromNullable(headers).or(Collections.<String, String>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 14c2edf..6d9bc6b 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/webhooks/HttpSession.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/webhooks/HttpSession.java
@@ -16,6 +16,7 @@
 
 import java.io.IOException;
 import java.nio.charset.StandardCharsets;
+import java.util.Map.Entry;
 
 import org.apache.http.client.methods.HttpPost;
 import org.apache.http.entity.StringEntity;
@@ -33,10 +34,13 @@
     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));
+    for (Entry<String, String> header : request.headers.entrySet()) {
+      post.addHeader(header.getKey(), header.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 2a8df46..1bd8d6c 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/webhooks/PostTask.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/webhooks/PostTask.java
@@ -23,7 +23,7 @@
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
-import com.google.common.base.Strings;
+import com.google.common.base.Optional;
 import com.google.common.base.Supplier;
 import com.google.common.base.Suppliers;
 import com.google.gerrit.server.events.ProjectEvent;
@@ -43,7 +43,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
@@ -57,9 +57,9 @@
     this.session = session;
     this.cfg = cfg;
     this.url = remote.getUrl();
-    this.body = Suppliers.memoize(new Supplier<String>() {
+    this.processor = Suppliers.memoize(new Supplier<Optional<EventProcessor.Request>>() {
       @Override
-      public String get() {
+      public Optional<EventProcessor.Request> get() {
         return processor.process(event, remote);
       }
     });
@@ -76,14 +76,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();
@@ -116,6 +116,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..5303180 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
@@ -14,6 +14,7 @@
 
 package com.googlesource.gerrit.plugins.webhooks.processors;
 
+import com.google.common.base.Optional;
 import com.google.common.base.Strings;
 import com.google.gerrit.server.events.ProjectEvent;
 import com.googlesource.gerrit.plugins.webhooks.EventProcessor;
@@ -21,7 +22,7 @@
 
 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..bf67ce9 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
@@ -14,11 +14,13 @@
 
 package com.googlesource.gerrit.plugins.webhooks.processors;
 
+import com.google.common.base.Optional;
 import com.google.common.base.Supplier;
 import com.google.gerrit.server.events.ProjectEvent;
 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;
 
 public class GerritEventProcessor extends AbstractEventProcessor {
@@ -26,7 +28,7 @@
       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 6acd767..1f0cf45 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/webhooks/PostTaskTest.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/webhooks/PostTaskTest.java
@@ -20,6 +20,7 @@
 import static org.mockito.Mockito.verifyZeroInteractions;
 import static org.mockito.Mockito.when;
 
+import com.google.common.base.Optional;
 import com.google.gerrit.server.events.ProjectCreatedEvent;
 import java.io.IOException;
 import java.util.concurrent.ScheduledThreadPoolExecutor;
@@ -41,7 +42,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;
@@ -65,6 +65,8 @@
   @Mock
   private EventProcessor processor;
 
+  @Mock private EventProcessor.Request content;
+
   private PostTask task;
 
   @Before
@@ -72,13 +74,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.<EventProcessor.Request>absent());
     task.run();
     verifyZeroInteractions(session);
     verifyZeroInteractions(executor);
@@ -86,35 +88,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>() {
           @Override
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 d461487..eee004a 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
@@ -23,11 +23,13 @@
 import org.mockito.Mock;
 import org.mockito.junit.MockitoJUnitRunner;
 
+import com.google.common.base.Optional;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.client.Project.NameKey;
 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;
 
 @RunWith(MockitoJUnitRunner.class)
@@ -86,9 +88,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.absent();
     }
   }
 }