Introduce events processor

Current webhooks implementation supports stream-events like (native)
events. However, enabling implementation of different formats may be
useful.

The following steps were performed to enable it:
* Configuration was changed to be extensible (this way adding more
configuration options is achievable by injecting specific
implementation)

* EventProcessor interface was introduced and it is responsible for
handling event according to provided remote configuration.

* EventProcessor's process method is performed by executor in dedicated
thread hence it could be resource consuming. In addition result of
processing is kept in Supplier with cache (memoize) so that processing
is not repeated when post gets repeated (for recoverable failure).

* Default implementation is provided and injected within ProcessorModule
that could be overwritten with different implementations without
necessity to re-bind core webhook classes (HttpSession,
HttpResponseHandler, etc) so that they can stay package-protected.

4 steps to introduce own webhook implementation and preserve plugin's
logic for HTTP and configuration handling:
1. implement EventProcessor or better AbstractEventProcessor as it
already checks if event is configured for remote
2. extend Configuration and inject it directly in case more
configuration options are required by implementation
3. extend ProcessorModule to bind EventProcessor to its new
implementation
4. add bindings for Configuration and ProcessorModule implementations
and install Module so that all necessary bits (related to webhooks
configuration and HTTP handling) are provided by Guice

Change-Id: Id575eba9b5aedd917e2441a70ae87d43276d2ab1
Signed-off-by: Jacek Centkowski <jcentkowski@collab.net>
diff --git a/src/main/java/com/googlesource/gerrit/plugins/webhooks/Configuration.java b/src/main/java/com/googlesource/gerrit/plugins/webhooks/Configuration.java
index b374bde..5dda6b4 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/webhooks/Configuration.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/webhooks/Configuration.java
@@ -38,7 +38,7 @@
   private final int threadPoolSize;
 
   @Inject
-  Configuration(PluginConfigFactory config, @PluginName String pluginName) {
+  protected Configuration(PluginConfigFactory config, @PluginName String pluginName) {
     PluginConfig cfg = config.getFromGerritConfig(pluginName, true);
     connectionTimeout = getInt(cfg, "connectionTimeout", DEFAULT_TIMEOUT_MS);
     socketTimeout = getInt(cfg, "socketTimeout", DEFAULT_TIMEOUT_MS);
@@ -47,7 +47,7 @@
     threadPoolSize = getInt(cfg, "threadPoolSize", DEFAULT_THREAD_POOL_SIZE);
   }
 
-  private int getInt(PluginConfig cfg, String name, int defaultValue) {
+  protected int getInt(PluginConfig cfg, String name, int defaultValue) {
     try {
       return cfg.getInt(name, defaultValue);
     } catch (IllegalArgumentException e) {
diff --git a/src/main/java/com/googlesource/gerrit/plugins/webhooks/EventHandler.java b/src/main/java/com/googlesource/gerrit/plugins/webhooks/EventHandler.java
index 0b83fe1..a29014c 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/webhooks/EventHandler.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/webhooks/EventHandler.java
@@ -14,39 +14,38 @@
 
 package com.googlesource.gerrit.plugins.webhooks;
 
+import static com.googlesource.gerrit.plugins.webhooks.RemoteConfig.REMOTE;
+
+import org.eclipse.jgit.lib.Config;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
 import com.google.common.base.Strings;
-import com.google.common.base.Supplier;
 import com.google.gerrit.common.EventListener;
 import com.google.gerrit.extensions.annotations.PluginName;
 import com.google.gerrit.server.config.PluginConfigFactory;
 import com.google.gerrit.server.events.Event;
 import com.google.gerrit.server.events.ProjectEvent;
-import com.google.gerrit.server.events.SupplierSerializer;
 import com.google.gerrit.server.project.NoSuchProjectException;
-import com.google.gson.Gson;
-import com.google.gson.GsonBuilder;
 import com.google.inject.Inject;
-import org.eclipse.jgit.lib.Config;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 class EventHandler implements EventListener {
   private static final Logger log = LoggerFactory.getLogger(EventHandler.class);
 
-  private static Gson GSON =
-      new GsonBuilder().registerTypeAdapter(Supplier.class, new SupplierSerializer()).create();
-
   private final PluginConfigFactory configFactory;
   private final String pluginName;
+  private final RemoteConfig.Factory remoteFactory;
   private final PostTask.Factory taskFactory;
 
   @Inject
   EventHandler(
       PluginConfigFactory configFactory,
       @PluginName String pluginName,
+      RemoteConfig.Factory remoteFactory,
       PostTask.Factory taskFactory) {
     this.configFactory = configFactory;
     this.pluginName = pluginName;
+    this.remoteFactory = remoteFactory;
     this.taskFactory = taskFactory;
   }
 
@@ -70,34 +69,14 @@
       return;
     }
 
-    for (String name : cfg.getSubsections("remote")) {
-      String url = cfg.getString("remote", name, "url");
-      if (Strings.isNullOrEmpty(url)) {
+    for (String name : cfg.getSubsections(REMOTE)) {
+      RemoteConfig remote = remoteFactory.create(cfg, name);
+      if (Strings.isNullOrEmpty(remote.getUrl())) {
         log.warn("remote.{}.url not defined, skipping this remote", name);
         continue;
       }
 
-      if (shouldPost(projectEvent, cfg.getStringList("remote", name, "event"))) {
-        post(url, projectEvent);
-      }
+      taskFactory.create(projectEvent, remote).schedule();
     }
   }
-
-  private boolean shouldPost(ProjectEvent projectEvent, String[] wantedEvents) {
-    if (wantedEvents.length == 0) {
-      return true;
-    }
-
-    for (String type : wantedEvents) {
-      if (!Strings.isNullOrEmpty(type) && type.equals(projectEvent.getType())) {
-        return true;
-      }
-    }
-
-    return false;
-  }
-
-  private void post(final String url, final ProjectEvent projectEvent) {
-    taskFactory.create(url, GSON.toJson(projectEvent)).schedule();
-  }
 }
diff --git a/src/main/java/com/googlesource/gerrit/plugins/webhooks/EventProcessor.java b/src/main/java/com/googlesource/gerrit/plugins/webhooks/EventProcessor.java
new file mode 100644
index 0000000..866df37
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/webhooks/EventProcessor.java
@@ -0,0 +1,21 @@
+// Copyright (C) 2017 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.webhooks;
+
+import com.google.gerrit.server.events.ProjectEvent;
+
+public interface EventProcessor {
+  String process(ProjectEvent event, RemoteConfig remote);
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/webhooks/Module.java b/src/main/java/com/googlesource/gerrit/plugins/webhooks/Module.java
index f59df7c..1b6c6af 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/webhooks/Module.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/webhooks/Module.java
@@ -14,14 +14,23 @@
 
 package com.googlesource.gerrit.plugins.webhooks;
 
+import java.util.concurrent.ScheduledExecutorService;
+
+import org.apache.http.impl.client.CloseableHttpClient;
+
 import com.google.gerrit.common.EventListener;
 import com.google.gerrit.extensions.config.FactoryModule;
 import com.google.gerrit.extensions.registration.DynamicSet;
+import com.google.inject.Inject;
 import com.google.inject.Scopes;
-import java.util.concurrent.ScheduledExecutorService;
-import org.apache.http.impl.client.CloseableHttpClient;
 
 public class Module extends FactoryModule {
+  private final ProcessorModule processors;
+
+  @Inject
+  public Module(ProcessorModule processors) {
+    this.processors = processors;
+  }
 
   @Override
   protected void configure() {
@@ -30,6 +39,9 @@
         .toProvider(ExecutorProvider.class);
     bind(CloseableHttpClient.class).toProvider(HttpClientProvider.class).in(Scopes.SINGLETON);
     factory(PostTask.Factory.class);
+    factory(RemoteConfig.Factory.class);
     DynamicSet.bind(binder(), EventListener.class).to(EventHandler.class);
+
+    install(processors);
   }
 }
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 eeb6d46..0154804 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/webhooks/PostTask.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/webhooks/PostTask.java
@@ -14,9 +14,14 @@
 
 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;
 import com.google.inject.assistedinject.Assisted;
 import com.google.inject.assistedinject.AssistedInject;
 import com.googlesource.gerrit.plugins.webhooks.HttpResponseHandler.HttpResult;
+
 import java.io.IOException;
 import java.util.concurrent.ScheduledExecutorService;
 import java.util.concurrent.TimeUnit;
@@ -28,14 +33,14 @@
   private static final Logger log = LoggerFactory.getLogger(PostTask.class);
 
   interface Factory {
-    PostTask create(@Assisted("url") String url, @Assisted("body") String body);
+    PostTask create(ProjectEvent event, RemoteConfig remote);
   }
 
   private final ScheduledExecutorService executor;
   private final HttpSession session;
   private final Configuration cfg;
   private final String url;
-  private final String body;
+  private final Supplier<String> body;
   private int execCnt;
 
   @AssistedInject
@@ -43,13 +48,14 @@
       @WebHooksExecutor ScheduledExecutorService executor,
       HttpSession session,
       Configuration cfg,
-      @Assisted("url") String url,
-      @Assisted("body") String body) {
+      EventProcessor processor,
+      @Assisted ProjectEvent event,
+      @Assisted RemoteConfig remote) {
     this.executor = executor;
     this.session = session;
     this.cfg = cfg;
-    this.url = url;
-    this.body = body;
+    this.url = remote.getUrl();
+    this.body = Suppliers.memoize(() -> processor.process(event, remote));
   }
 
   void schedule() {
@@ -63,8 +69,14 @@
   @Override
   public void run() {
     try {
+      String content = body.get();
+      if (Strings.isNullOrEmpty(content)) {
+        log.debug("No content. Webhook [{}] skipped.", url);
+        return;
+      }
+
       execCnt++;
-      HttpResult result = session.post(url, body);
+      HttpResult result = session.post(url, content);
       if (!result.successful && execCnt < cfg.getMaxTries()) {
         logRetry(result.message);
         reschedule();
@@ -74,7 +86,7 @@
         logRetry(e);
         reschedule();
       } else {
-        log.error("Failed to post: {}", body, e);
+        log.error("Failed to post: {}", toString(), e);
       }
     }
   }
@@ -97,6 +109,6 @@
 
   @Override
   public String toString() {
-    return body;
+    return body.get();
   }
 }
diff --git a/src/main/java/com/googlesource/gerrit/plugins/webhooks/ProcessorModule.java b/src/main/java/com/googlesource/gerrit/plugins/webhooks/ProcessorModule.java
new file mode 100644
index 0000000..8288327
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/webhooks/ProcessorModule.java
@@ -0,0 +1,25 @@
+// Copyright (C) 2017 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.webhooks;
+
+import com.google.inject.AbstractModule;
+import com.googlesource.gerrit.plugins.webhooks.processors.GerritEventProcessor;
+
+public class ProcessorModule extends AbstractModule {
+  @Override
+  protected void configure() {
+    bind(EventProcessor.class).to(GerritEventProcessor.class);
+  }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/webhooks/RemoteConfig.java b/src/main/java/com/googlesource/gerrit/plugins/webhooks/RemoteConfig.java
new file mode 100644
index 0000000..0763204
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/webhooks/RemoteConfig.java
@@ -0,0 +1,58 @@
+// Copyright (C) 2017 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.webhooks;
+
+import org.eclipse.jgit.lib.Config;
+
+import com.google.inject.Inject;
+import com.google.inject.assistedinject.Assisted;
+
+public class RemoteConfig {
+  public interface Factory {
+    RemoteConfig create(@Assisted("config") Config config, @Assisted("name") String name);
+  }
+
+  public static final String REMOTE = "remote";
+
+  private final Config config;
+  private final String url;
+  private final String name;
+
+  @Inject
+  RemoteConfig(@Assisted("config") Config config,
+      @Assisted("name") String name) {
+    this.config = config;
+    this.name = name;
+    this.url = config.getString(REMOTE, name, "url");
+  }
+
+  public String getUrl() {
+    return url;
+  }
+
+  public String[] getEvents() {
+    return config.getStringList(REMOTE, name, "event");
+  }
+
+  // methods were added in order to make configuration
+  // extensible in EvenptProcessor implementations
+  public Config getConfig() {
+    return config;
+  }
+
+  public String getName() {
+    return name;
+  }
+}
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
new file mode 100644
index 0000000..8c727e0
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/webhooks/processors/AbstractEventProcessor.java
@@ -0,0 +1,48 @@
+// Copyright (C) 2017 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.webhooks.processors;
+
+import com.google.common.base.Strings;
+import com.google.gerrit.server.events.ProjectEvent;
+import com.googlesource.gerrit.plugins.webhooks.EventProcessor;
+import com.googlesource.gerrit.plugins.webhooks.RemoteConfig;
+
+public abstract class AbstractEventProcessor implements EventProcessor {
+  @Override
+  public String process(ProjectEvent event, RemoteConfig remote) {
+    if (!shouldProcess(event, remote)) {
+      return null;
+    }
+
+    return doProcess(event, remote);
+  }
+
+  protected abstract String doProcess(ProjectEvent event, RemoteConfig remote);
+
+  protected boolean shouldProcess(ProjectEvent event, RemoteConfig remote) {
+    String[] wantedEvents = remote.getEvents();
+    if (wantedEvents.length == 0) {
+      return true;
+    }
+
+    for (String type : wantedEvents) {
+      if (!Strings.isNullOrEmpty(type) && type.equals(event.getType())) {
+        return true;
+      }
+    }
+
+    return false;
+  }
+}
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
new file mode 100644
index 0000000..b8532b4
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/webhooks/processors/GerritEventProcessor.java
@@ -0,0 +1,32 @@
+// Copyright (C) 2017 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.webhooks.processors;
+
+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.RemoteConfig;
+
+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);
+  }
+}
diff --git a/src/test/java/com/googlesource/gerrit/plugins/webhooks/EventHandlerTest.java b/src/test/java/com/googlesource/gerrit/plugins/webhooks/EventHandlerTest.java
index 56ef8c9..eb677e1 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/webhooks/EventHandlerTest.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/webhooks/EventHandlerTest.java
@@ -14,18 +14,17 @@
 
 package com.googlesource.gerrit.plugins.webhooks;
 
-import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.verifyZeroInteractions;
 import static org.mockito.Mockito.when;
 
+import com.google.common.collect.ImmutableSet;
 import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.reviewdb.client.Project.NameKey;
 import com.google.gerrit.server.config.PluginConfigFactory;
 import com.google.gerrit.server.events.Event;
 import com.google.gerrit.server.events.ProjectCreatedEvent;
-import com.google.gerrit.server.events.RefUpdatedEvent;
 import com.google.gerrit.server.project.NoSuchProjectException;
 import org.eclipse.jgit.lib.Config;
 import org.junit.Before;
@@ -36,89 +35,62 @@
 
 @RunWith(MockitoJUnitRunner.class)
 public class EventHandlerTest {
-
-  private static final String PROJECT = "p";
-  private static final Project.NameKey PROJECT_NAME = new Project.NameKey(PROJECT);
+  private static final Project.NameKey PROJECT_NAME = new Project.NameKey("p");
   private static final String PLUGIN = "webhooks";
   private static final String REMOTE = "remote";
   private static final String FOO = "foo";
-  private static final String URL = "url";
   private static final String FOO_URL = "foo-url";
-  private static final String EVENT = "event";
 
-  private static final ProjectCreatedEvent PROJECT_CREATED =
-      new ProjectCreatedEvent() {
-        @Override
-        public NameKey getProjectNameKey() {
-          return PROJECT_NAME;
-        }
-      };
-
-  private static final RefUpdatedEvent REF_UPDATED =
-      new RefUpdatedEvent() {
-        @Override
-        public NameKey getProjectNameKey() {
-          return PROJECT_NAME;
-        }
-      };
+  @Mock private ProjectCreatedEvent projectCreated;
 
   @Mock private PluginConfigFactory configFactory;
 
+  @Mock private RemoteConfig.Factory remoteFactory;
+
   @Mock private PostTask.Factory taskFactory;
 
   @Mock private PostTask postTask;
 
-  private Config config = new Config();
+  @Mock private RemoteConfig remote;
+
+  @Mock private Config config;
 
   private EventHandler eventHandler;
 
   @Before
   public void setup() throws NoSuchProjectException {
+    when(projectCreated.getProjectNameKey()).thenReturn(PROJECT_NAME);
     when(configFactory.getProjectPluginConfigWithInheritance(PROJECT_NAME, PLUGIN))
         .thenReturn(config);
-    when(taskFactory.create(anyString(), anyString())).thenReturn(postTask);
-    eventHandler = new EventHandler(configFactory, PLUGIN, taskFactory);
+    when(remoteFactory.create(eq(config), eq(FOO))).thenReturn(remote);
+    when(taskFactory.create(eq(projectCreated), eq(remote))).thenReturn(postTask);
+    eventHandler = new EventHandler(configFactory, PLUGIN, remoteFactory, taskFactory);
   }
 
   @Test
-  public void remoteUrlUndefinedEventsNotPosted() {
-    eventHandler.onEvent(PROJECT_CREATED);
+  public void remoteUrlUndefinedTaskNotScheduled() {
+    when(config.getSubsections(eq(REMOTE))).thenReturn(ImmutableSet.of(FOO));
+    eventHandler.onEvent(projectCreated);
+    verifyZeroInteractions(taskFactory);
     verifyZeroInteractions(postTask);
   }
 
   @Test
-  public void eventTypesNotSpecifiedAllEventsPosted() {
-    config.setString(REMOTE, FOO, URL, FOO_URL);
+  public void remoteUrlDefinedTaskScheduled() {
+    when(config.getSubsections(eq(REMOTE))).thenReturn(ImmutableSet.of(FOO));
+    when(remote.getUrl()).thenReturn(FOO_URL);
 
-    eventHandler.onEvent(PROJECT_CREATED);
-    eventHandler.onEvent(REF_UPDATED);
-    verify(postTask, times(2)).schedule();
-  }
-
-  @Test
-  public void specifiedEventTypesPosted() {
-    config.setString(REMOTE, FOO, URL, FOO_URL);
-    config.setString(REMOTE, FOO, EVENT, "project-created");
-
-    eventHandler.onEvent(PROJECT_CREATED);
+    eventHandler.onEvent(projectCreated);
+    verify(taskFactory, times(1)).create(eq(projectCreated), eq(remote));
     verify(postTask, times(1)).schedule();
   }
 
   @Test
-  public void nonSpecifiedProjectEventTypesNotPosted() {
-    config.setString(REMOTE, FOO, URL, FOO_URL);
-    config.setString(REMOTE, FOO, EVENT, "project-created");
-
-    eventHandler.onEvent(REF_UPDATED);
-    verifyZeroInteractions(postTask);
-  }
-
-  @Test
-  public void nonProjectEventNotPosted() {
-    config.setString(REMOTE, FOO, URL, FOO_URL);
-
+  public void nonProjectEventNotProcessed() {
     Event nonProjectEvent = new Event("non-project-event") {};
     eventHandler.onEvent(nonProjectEvent);
+    verifyZeroInteractions(remoteFactory);
+    verifyZeroInteractions(taskFactory);
     verifyZeroInteractions(postTask);
   }
 }
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 cf41a4d..043828f 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/webhooks/PostTaskTest.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/webhooks/PostTaskTest.java
@@ -14,11 +14,13 @@
 
 package com.googlesource.gerrit.plugins.webhooks;
 
+import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.verifyZeroInteractions;
 import static org.mockito.Mockito.when;
 
+import com.google.gerrit.server.events.ProjectCreatedEvent;
 import com.googlesource.gerrit.plugins.webhooks.HttpResponseHandler.HttpResult;
 import java.io.IOException;
 import java.util.concurrent.ScheduledThreadPoolExecutor;
@@ -42,19 +44,35 @@
   private static final int RETRY_INTERVAL = 100;
   private static final int MAX_TRIES = 3;
 
+  @Mock private ProjectCreatedEvent projectCreated;
+
+  @Mock private RemoteConfig remote;
+
   @Mock private Configuration cfg;
 
   @Mock private HttpSession session;
 
   @Mock private ScheduledThreadPoolExecutor executor;
 
+  @Mock private EventProcessor processor;
+
   private PostTask task;
 
   @Before
   public void setup() {
     when(cfg.getRetryInterval()).thenReturn(RETRY_INTERVAL);
     when(cfg.getMaxTries()).thenReturn(MAX_TRIES);
-    task = new PostTask(executor, session, cfg, WEBHOOK_URL, BODY);
+    when(remote.getUrl()).thenReturn(WEBHOOK_URL);
+    when(processor.process(eq(projectCreated), eq(remote))).thenReturn(BODY);
+    task = new PostTask(executor, session, cfg, processor, projectCreated, remote);
+  }
+
+  @Test
+  public void noScheduleOnEmptyBody() throws Exception {
+    when(processor.process(eq(projectCreated), eq(remote))).thenReturn(null);
+    task.run();
+    verifyZeroInteractions(session);
+    verifyZeroInteractions(executor);
   }
 
   @Test
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
new file mode 100644
index 0000000..d461487
--- /dev/null
+++ b/src/test/java/com/googlesource/gerrit/plugins/webhooks/processors/AbstractEventProcessorTest.java
@@ -0,0 +1,94 @@
+// Copyright (C) 2017 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.webhooks.processors;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.mockito.Mockito.when;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnitRunner;
+
+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.RemoteConfig;
+
+@RunWith(MockitoJUnitRunner.class)
+public class AbstractEventProcessorTest {
+  private static final String PROJECT = "p";
+  private static final Project.NameKey PROJECT_NAME = new Project.NameKey(PROJECT);
+
+  private static final ProjectCreatedEvent PROJECT_CREATED =
+      new ProjectCreatedEvent() {
+        @Override
+        public NameKey getProjectNameKey() {
+          return PROJECT_NAME;
+        }
+      };
+
+  private static final RefUpdatedEvent REF_UPDATED =
+      new RefUpdatedEvent() {
+        @Override
+        public NameKey getProjectNameKey() {
+          return PROJECT_NAME;
+        }
+      };
+
+  @Mock private RemoteConfig remote;
+
+  private TestEventProcessor processor;
+
+  @Before
+  public void setup() throws Exception {
+    processor = new TestEventProcessor();
+  }
+
+  @Test
+  public void eventsNotSpecifiedAllEventsShouldProcess() throws Exception {
+    when(remote.getEvents()).thenReturn(new String[] {});
+    boolean actual = processor.shouldProcess(PROJECT_CREATED, remote);
+    assertThat(actual).isTrue();
+
+    actual = processor.shouldProcess(REF_UPDATED, remote);
+    assertThat(actual).isTrue();
+  }
+
+  @Test
+  public void specifiedEventTypesShouldProcess() throws Exception {
+    when(remote.getEvents()).thenReturn(new String[] {"project-created"});
+    boolean actual = processor.shouldProcess(PROJECT_CREATED, remote);
+    assertThat(actual).isTrue();
+  }
+
+  @Test
+  public void nonSpecifiedProjectEventTypesNotProcess() throws Exception {
+    when(remote.getEvents()).thenReturn(new String[] {"project-created"});
+    boolean actual = processor.shouldProcess(REF_UPDATED, remote);
+    assertThat(actual).isFalse();
+  }
+
+  private class TestEventProcessor extends AbstractEventProcessor {
+    @Override
+    public String doProcess(ProjectEvent event, RemoteConfig remote) {
+      // do nothing
+      return null;
+    }
+  }
+}