webhooks plugin, initial commit

This plugin sends HTTP POST request to configured URL(s) when a project
event happens.  The target URL(s) are configurable on project level.
The configuration is inheritable.

This can be used as an alternative to the SSH stream-events.

Motivated by Github's webhooks [1].

Most of code was copied from the high-availability plugin [2].

[1] https://developer.github.com/webhooks/
[2] https://gerrit-review.googlesource.com/#/admin/projects/plugins/high-availability

Change-Id: I902c8d25a25dd54a0b929c6e383837135737e6cb
diff --git a/.buckconfig b/.buckconfig
new file mode 100644
index 0000000..81f137b
--- /dev/null
+++ b/.buckconfig
@@ -0,0 +1,16 @@
+[alias]
+  webhooks = //:webhooks
+  plugin = //:webhooks
+  src = //:webhooks-sources
+
+[java]
+  jar_spool_mode = direct_to_jar
+  src_roots = java, resources
+
+[project]
+  ignore = .git, eclipse-out/
+  parallel_parsing = true
+
+[cache]
+  mode = dir
+  dir = buck-out/cache
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..c27e17f
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,9 @@
+/.buckd/
+/.buckversion
+/.classpath
+/.project
+/.settings/
+/.watchmanconfig
+/buck-out/
+/bucklets
+/eclipse-out/
diff --git a/BUCK b/BUCK
new file mode 100644
index 0000000..aa5ea84
--- /dev/null
+++ b/BUCK
@@ -0,0 +1,42 @@
+include_defs('//bucklets/gerrit_plugin.bucklet')
+include_defs('//bucklets/java_sources.bucklet')
+include_defs('//bucklets/maven_jar.bucklet')
+
+SOURCES = glob(['src/main/java/**/*.java'])
+RESOURCES = glob(['src/main/resources/**/*'])
+
+TEST_DEPS = GERRIT_PLUGIN_API + GERRIT_TESTS + [
+  ':webhooks__plugin',
+]
+
+gerrit_plugin(
+  name = 'webhooks',
+  srcs = SOURCES,
+  resources = RESOURCES,
+  manifest_entries = [
+    'Gerrit-PluginName: webhooks',
+    'Gerrit-ApiType: plugin',
+    'Gerrit-Module: com.googlesource.gerrit.plugins.webhooks.Module',
+    'Implementation-Title: webhooks plugin',
+    'Implementation-URL: https://gerrit-review.googlesource.com/#/admin/projects/plugins/webhooks',
+    'Implementation-Vendor: Gerrit Code Review',
+  ],
+  provided_deps = GERRIT_TESTS,
+)
+
+java_sources(
+  name = 'webhooks-sources',
+  srcs = SOURCES + RESOURCES,
+)
+
+java_library(
+  name = 'classpath',
+  deps = TEST_DEPS,
+)
+
+java_test(
+  name = 'webhooks_tests',
+  srcs = glob(['src/test/java/**/*.java']),
+  labels = ['webhooks'],
+  deps = TEST_DEPS,
+)
diff --git a/lib/gerrit/BUCK b/lib/gerrit/BUCK
new file mode 100644
index 0000000..8a21820
--- /dev/null
+++ b/lib/gerrit/BUCK
@@ -0,0 +1,22 @@
+include_defs('//bucklets/maven_jar.bucklet')
+
+VER = '2.13.6'
+REPO = MAVEN_CENTRAL
+
+maven_jar(
+  name = 'acceptance-framework',
+  id = 'com.google.gerrit:gerrit-acceptance-framework:' + VER,
+  sha1 = '53a5ffbc3ce6842b7145fd11abcc1dc8503b124f',
+  license = 'Apache2.0',
+  attach_source = False,
+  repository = REPO,
+)
+
+maven_jar(
+  name = 'plugin-api',
+  id = 'com.google.gerrit:gerrit-plugin-api:' + VER,
+  sha1 = '1a5d650c72ebc36f4ad522d5481d5ed690bec8bf',
+  license = 'Apache2.0',
+  attach_source = False,
+  repository = REPO,
+)
diff --git a/src/main/java/com/googlesource/gerrit/plugins/webhooks/Configuration.java b/src/main/java/com/googlesource/gerrit/plugins/webhooks/Configuration.java
new file mode 100644
index 0000000..1045a1b
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/webhooks/Configuration.java
@@ -0,0 +1,82 @@
+// 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.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.google.gerrit.extensions.annotations.PluginName;
+import com.google.gerrit.server.config.PluginConfig;
+import com.google.gerrit.server.config.PluginConfigFactory;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+
+@Singleton
+public class Configuration {
+  private static final Logger log = LoggerFactory.getLogger(Configuration.class);
+
+  private static final int DEFAULT_TIMEOUT_MS = 5000;
+  private static final int DEFAULT_MAX_TRIES = 5;
+  private static final int DEFAULT_RETRY_INTERVAL = 1000;
+  private static final int DEFAULT_THREAD_POOL_SIZE = 1;
+
+  private final int connectionTimeout;
+  private final int socketTimeout;
+  private final int maxTries;
+  private final int retryInterval;
+  private final int threadPoolSize;
+
+  @Inject
+  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);
+    maxTries = getInt(cfg, "maxTries", DEFAULT_MAX_TRIES);
+    retryInterval = getInt(cfg, "retryInterval", DEFAULT_RETRY_INTERVAL);
+    threadPoolSize = getInt(cfg, "threadPoolSize", DEFAULT_THREAD_POOL_SIZE);
+  }
+
+  private int getInt(PluginConfig cfg, String name, int defaultValue) {
+    try {
+      return cfg.getInt(name, defaultValue);
+    } catch (IllegalArgumentException e) {
+      log.error(String.format(
+          "invalid value for %s; using default value %d", name, defaultValue));
+      log.debug("Failed retrieve integer value: " + e.getMessage(), e);
+      return defaultValue;
+    }
+  }
+
+  public int getConnectionTimeout() {
+    return connectionTimeout;
+  }
+
+  public int getMaxTries() {
+    return maxTries;
+  }
+
+  public int getRetryInterval() {
+    return retryInterval;
+  }
+
+  public int getSocketTimeout() {
+    return socketTimeout;
+  }
+
+  public int getThreadPoolSize() {
+    return threadPoolSize;
+  }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/webhooks/EventHandler.java b/src/main/java/com/googlesource/gerrit/plugins/webhooks/EventHandler.java
new file mode 100644
index 0000000..0947f38
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/webhooks/EventHandler.java
@@ -0,0 +1,121 @@
+// 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 java.io.IOException;
+import java.util.concurrent.Executor;
+
+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;
+
+class EventHandler implements EventListener {
+  private static final Logger log = LoggerFactory
+      .getLogger(EventHandler.class);
+
+  private final HttpSession session;
+  private final PluginConfigFactory configFactory;
+  private final String pluginName;
+  private final Executor executor;
+
+  private final Gson gson;
+
+  @Inject
+  EventHandler(HttpSession session,
+      PluginConfigFactory configFactory,
+      @PluginName String pluginName,
+      @WebHooksExecutor Executor executor) {
+    this.session = session;
+    this.configFactory = configFactory;
+    this.pluginName = pluginName;
+    this.executor = executor;
+    this.gson = new GsonBuilder()
+        .registerTypeAdapter(Supplier.class, new SupplierSerializer())
+        .create();
+  }
+
+  @Override
+  public void onEvent(Event event) {
+    if (!(event instanceof ProjectEvent)) {
+      return;
+    }
+
+    ProjectEvent projectEvent = (ProjectEvent) event;
+    Config cfg;
+    try {
+      cfg = configFactory.getProjectPluginConfigWithInheritance(
+          projectEvent.getProjectNameKey(), pluginName);
+    } catch (NoSuchProjectException e) {
+      log.warn("Ignoring event for a non-existing project {}, {}",
+          projectEvent.getProjectNameKey().get(), projectEvent);
+      return;
+    }
+
+    for (String name : cfg.getSubsections("remote")) {
+      String url = cfg.getString("remote", name, "url");
+      if (Strings.isNullOrEmpty(url)) {
+        continue;
+      }
+
+      String[] eventTypes = cfg.getStringList("remote", name, "event");
+
+      if (eventTypes.length == 0) {
+        post(url, projectEvent);
+      }
+
+      for (String type : eventTypes) {
+        if (Strings.isNullOrEmpty(type)) {
+          continue;
+        }
+        if (type.equals(projectEvent.getType())) {
+          post(url, projectEvent);
+        }
+      }
+    }
+  }
+
+  private void post(final String url, final ProjectEvent projectEvent) {
+    executor.execute(new Runnable() {
+      @Override
+      public void run() {
+        String serializedEvent = gson.toJson(projectEvent);
+        try {
+          session.post(url, serializedEvent);
+        } catch (IOException e) {
+          log.error("Coulnd't post event: " + projectEvent, e);
+        }
+      }
+
+      @Override
+      public String toString() {
+        return String.format("%s:%s > %s",
+            projectEvent.type, projectEvent.getProjectNameKey().get(), url);
+      }
+    });
+  }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/webhooks/ExecutorProvider.java b/src/main/java/com/googlesource/gerrit/plugins/webhooks/ExecutorProvider.java
new file mode 100644
index 0000000..8a5a630
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/webhooks/ExecutorProvider.java
@@ -0,0 +1,52 @@
+// 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 java.util.concurrent.ScheduledThreadPoolExecutor;
+
+import com.google.gerrit.extensions.annotations.PluginName;
+import com.google.gerrit.extensions.events.LifecycleListener;
+import com.google.gerrit.server.git.WorkQueue;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+
+class ExecutorProvider
+    implements Provider<ScheduledThreadPoolExecutor>, LifecycleListener {
+  private WorkQueue.Executor executor;
+
+  @Inject
+  ExecutorProvider(WorkQueue workQueue,
+      Configuration cfg,
+      @PluginName String name) {
+    executor = workQueue.createQueue(cfg.getThreadPoolSize(), name);
+  }
+
+  @Override
+  public void start() {
+    // do nothing
+  }
+
+  @Override
+  public void stop() {
+    executor.shutdown();
+    executor.unregisterWorkQueue();
+    executor = null;
+  }
+
+  @Override
+  public ScheduledThreadPoolExecutor get() {
+    return executor;
+  }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/webhooks/HttpClientProvider.java b/src/main/java/com/googlesource/gerrit/plugins/webhooks/HttpClientProvider.java
new file mode 100644
index 0000000..047105e
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/webhooks/HttpClientProvider.java
@@ -0,0 +1,183 @@
+// 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 java.io.IOException;
+import java.security.KeyManagementException;
+import java.security.NoSuchAlgorithmException;
+import java.security.cert.X509Certificate;
+
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.SSLException;
+import javax.net.ssl.TrustManager;
+import javax.net.ssl.X509TrustManager;
+
+import org.apache.http.HttpResponse;
+import org.apache.http.client.HttpRequestRetryHandler;
+import org.apache.http.client.ServiceUnavailableRetryStrategy;
+import org.apache.http.client.config.RequestConfig;
+import org.apache.http.config.Registry;
+import org.apache.http.config.RegistryBuilder;
+import org.apache.http.conn.HttpClientConnectionManager;
+import org.apache.http.conn.socket.ConnectionSocketFactory;
+import org.apache.http.conn.socket.PlainConnectionSocketFactory;
+import org.apache.http.conn.ssl.NoopHostnameVerifier;
+import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
+import org.apache.http.impl.client.CloseableHttpClient;
+import org.apache.http.impl.client.HttpClients;
+import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
+import org.apache.http.protocol.HttpContext;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+
+/**
+ * Provides an HTTP client with SSL capabilities.
+ */
+class HttpClientProvider implements Provider<CloseableHttpClient> {
+  private static final Logger log = LoggerFactory
+      .getLogger(HttpClientProvider.class);
+  private static final int CONNECTIONS_PER_ROUTE = 100;
+  // Up to 2 target instances with the max number of connections per host:
+  private static final int MAX_CONNECTIONS = 2 * CONNECTIONS_PER_ROUTE;
+  private static final int ERROR_CODES = 500;
+  private static final int MAX_CONNECTION_INACTIVITY = 10000;
+
+  private final Configuration cfg;
+  private final SSLConnectionSocketFactory sslSocketFactory;
+
+  @Inject
+  HttpClientProvider(Configuration cfg) {
+    this.cfg = cfg;
+    this.sslSocketFactory = buildSslSocketFactory();
+  }
+
+  @Override
+  public CloseableHttpClient get() {
+    return HttpClients.custom().setSSLSocketFactory(sslSocketFactory)
+        .setConnectionManager(customConnectionManager())
+        .setDefaultRequestConfig(customRequestConfig())
+        .setRetryHandler(customRetryHandler())
+        .setServiceUnavailableRetryStrategy(customServiceUnavailRetryStrategy())
+        .build();
+  }
+
+  private RequestConfig customRequestConfig() {
+    return RequestConfig.custom().setConnectTimeout(cfg.getConnectionTimeout())
+        .setSocketTimeout(cfg.getSocketTimeout())
+        .setConnectionRequestTimeout(cfg.getConnectionTimeout())
+        .build();
+  }
+
+  private HttpRequestRetryHandler customRetryHandler() {
+    return new HttpRequestRetryHandler() {
+
+      @Override
+      public boolean retryRequest(IOException exception, int executionCount,
+          HttpContext context) {
+        if (executionCount > cfg.getMaxTries()
+            || exception instanceof SSLException) {
+          return false;
+        }
+        logRetry(exception.getMessage(), context);
+        try {
+          Thread.sleep(cfg.getRetryInterval());
+        } catch (InterruptedException e) {
+          Thread.currentThread().interrupt();
+          return false;
+        }
+        return true;
+      }
+    };
+  }
+
+  private ServiceUnavailableRetryStrategy customServiceUnavailRetryStrategy() {
+    return new ServiceUnavailableRetryStrategy() {
+      @Override
+      public boolean retryRequest(HttpResponse response, int executionCount,
+          HttpContext context) {
+        if (executionCount > cfg.getMaxTries()) {
+          return false;
+        }
+        if (response.getStatusLine().getStatusCode() >= ERROR_CODES) {
+          logRetry(response.getStatusLine().getReasonPhrase(), context);
+          return true;
+        }
+        return false;
+      }
+
+      @Override
+      public long getRetryInterval() {
+        return cfg.getRetryInterval();
+      }
+    };
+  }
+
+  private void logRetry(String cause, HttpContext context) {
+    if (log.isDebugEnabled()){
+      log.debug("Retrying request caused by '" + cause + "', request: '"
+          + context.getAttribute("http.request") + "'");
+    }
+  }
+
+  private HttpClientConnectionManager customConnectionManager() {
+    Registry<ConnectionSocketFactory> socketFactoryRegistry = RegistryBuilder
+        .<ConnectionSocketFactory> create().register("https", sslSocketFactory)
+        .register("http", PlainConnectionSocketFactory.INSTANCE).build();
+    PoolingHttpClientConnectionManager connManager =
+        new PoolingHttpClientConnectionManager(socketFactoryRegistry);
+    connManager.setDefaultMaxPerRoute(CONNECTIONS_PER_ROUTE);
+    connManager.setMaxTotal(MAX_CONNECTIONS);
+    connManager.setValidateAfterInactivity(MAX_CONNECTION_INACTIVITY);
+    return connManager;
+  }
+
+  private SSLConnectionSocketFactory buildSslSocketFactory() {
+    return new SSLConnectionSocketFactory(buildSslContext(),
+        NoopHostnameVerifier.INSTANCE);
+  }
+
+  private SSLContext buildSslContext() {
+    try {
+      TrustManager[] trustAllCerts =
+          new TrustManager[] {new DummyX509TrustManager()};
+      SSLContext context = SSLContext.getInstance("TLS");
+      context.init(null, trustAllCerts, null);
+      return context;
+    } catch (KeyManagementException | NoSuchAlgorithmException e) {
+      log.warn("Error building SSLContext object", e);
+      return null;
+    }
+  }
+
+  private static class DummyX509TrustManager implements X509TrustManager {
+    @Override
+    public X509Certificate[] getAcceptedIssuers() {
+      return new X509Certificate[0];
+    }
+
+    @Override
+    public void checkClientTrusted(X509Certificate[] certs, String authType) {
+      // no check
+    }
+
+    @Override
+    public void checkServerTrusted(X509Certificate[] certs, String authType) {
+      // no check
+    }
+  }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/webhooks/HttpResponseHandler.java b/src/main/java/com/googlesource/gerrit/plugins/webhooks/HttpResponseHandler.java
new file mode 100644
index 0000000..73d9f28
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/webhooks/HttpResponseHandler.java
@@ -0,0 +1,65 @@
+// 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 static javax.servlet.http.HttpServletResponse.SC_NO_CONTENT;
+
+import com.googlesource.gerrit.plugins.webhooks.HttpResponseHandler.HttpResult;
+
+import org.apache.http.HttpEntity;
+import org.apache.http.HttpResponse;
+import org.apache.http.client.ResponseHandler;
+import org.apache.http.util.EntityUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+
+class HttpResponseHandler implements ResponseHandler<HttpResult> {
+
+  static class HttpResult {
+    final boolean successful;
+    final String message;
+
+    HttpResult(boolean successful, String message) {
+      this.successful = successful;
+      this.message = message;
+    }
+  }
+
+  private static final Logger log =
+      LoggerFactory.getLogger(HttpResponseHandler.class);
+
+  @Override
+  public HttpResult handleResponse(HttpResponse response) {
+    return new HttpResult(isSuccessful(response), parseResponse(response));
+  }
+
+  private boolean isSuccessful(HttpResponse response) {
+    return response.getStatusLine().getStatusCode() == SC_NO_CONTENT;
+  }
+
+  private String parseResponse(HttpResponse response) {
+    HttpEntity entity = response.getEntity();
+    if (entity != null) {
+      try {
+        return EntityUtils.toString(entity);
+      } catch (IOException e) {
+        log.error("Error parsing entity", e);
+      }
+    }
+    return "";
+  }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/webhooks/HttpSession.java b/src/main/java/com/googlesource/gerrit/plugins/webhooks/HttpSession.java
new file mode 100644
index 0000000..14c2edf
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/webhooks/HttpSession.java
@@ -0,0 +1,42 @@
+// 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 java.io.IOException;
+import java.nio.charset.StandardCharsets;
+
+import org.apache.http.client.methods.HttpPost;
+import org.apache.http.entity.StringEntity;
+import org.apache.http.impl.client.CloseableHttpClient;
+
+import com.google.common.net.MediaType;
+import com.google.inject.Inject;
+import com.googlesource.gerrit.plugins.webhooks.HttpResponseHandler.HttpResult;
+
+class HttpSession {
+  private final CloseableHttpClient httpClient;
+
+  @Inject
+  HttpSession(CloseableHttpClient httpClient) {
+    this.httpClient = httpClient;
+  }
+
+  HttpResult post(String endpoint, String content) throws IOException {
+    HttpPost post = new HttpPost(endpoint);
+    post.addHeader("Content-Type", MediaType.JSON_UTF_8.toString());
+    post.setEntity(new StringEntity(content, StandardCharsets.UTF_8));
+    return httpClient.execute(post, new HttpResponseHandler());
+  }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/webhooks/Module.java b/src/main/java/com/googlesource/gerrit/plugins/webhooks/Module.java
new file mode 100644
index 0000000..d749e9c
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/webhooks/Module.java
@@ -0,0 +1,38 @@
+// 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 java.util.concurrent.Executor;
+
+import org.apache.http.impl.client.CloseableHttpClient;
+
+import com.google.gerrit.common.EventListener;
+import com.google.gerrit.extensions.registration.DynamicSet;
+import com.google.inject.AbstractModule;
+import com.google.inject.Scopes;
+
+public class Module extends AbstractModule {
+
+  @Override
+  protected void configure() {
+    bind(Executor.class)
+        .annotatedWith(WebHooksExecutor.class)
+        .toProvider(ExecutorProvider.class);
+    bind(Configuration.class).in(Scopes.SINGLETON);
+    bind(CloseableHttpClient.class).toProvider(HttpClientProvider.class)
+        .in(Scopes.SINGLETON);
+    DynamicSet.bind(binder(), EventListener.class).to(EventHandler.class);
+  }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/webhooks/WebHooksExecutor.java b/src/main/java/com/googlesource/gerrit/plugins/webhooks/WebHooksExecutor.java
new file mode 100644
index 0000000..45b25cd
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/webhooks/WebHooksExecutor.java
@@ -0,0 +1,26 @@
+// 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 static java.lang.annotation.RetentionPolicy.RUNTIME;
+
+import java.lang.annotation.Retention;
+
+import com.google.inject.BindingAnnotation;
+
+@Retention(RUNTIME)
+@BindingAnnotation
+@interface WebHooksExecutor {
+}
diff --git a/src/main/resources/Documentation/about.md b/src/main/resources/Documentation/about.md
new file mode 100644
index 0000000..62783c9
--- /dev/null
+++ b/src/main/resources/Documentation/about.md
@@ -0,0 +1 @@
+The @PLUGIN@ plugin allows to propagate Gerrit events to remote http endpoints.
diff --git a/src/main/resources/Documentation/build.md b/src/main/resources/Documentation/build.md
new file mode 100644
index 0000000..3babed2
--- /dev/null
+++ b/src/main/resources/Documentation/build.md
@@ -0,0 +1,106 @@
+Build
+=====
+
+This plugin is built with Buck.
+
+Two build modes are supported: Standalone and in Gerrit tree. Standalone
+build mode is recommended, as this mode doesn't require local Gerrit
+tree to exist.
+
+Build standalone
+----------------
+
+Clone bucklets library:
+
+```
+  git clone https://gerrit.googlesource.com/bucklets
+
+```
+and link it to @PLUGIN@ directory:
+
+```
+  cd @PLUGIN@ && ln -s ../bucklets .
+```
+
+Add link to the .buckversion file:
+
+```
+  cd @PLUGIN@ && ln -s bucklets/buckversion .buckversion
+```
+
+Add link to the .watchmanconfig file:
+
+```
+  cd @PLUGIN@ && ln -s bucklets/watchmanconfig .watchmanconfig
+```
+
+To build the plugin, issue the following command:
+
+```
+  buck build plugin
+```
+
+The output is created in:
+
+```
+  buck-out/gen/@PLUGIN@.jar
+```
+
+This project can be imported into the Eclipse IDE:
+
+```
+  ./bucklets/tools/eclipse.py
+```
+
+To execute the tests run:
+
+```
+  buck test
+```
+
+To build plugin sources run:
+
+```
+  buck build src
+```
+
+The output is created in:
+
+```
+  buck-out/gen/@PLUGIN@-sources.jar
+```
+
+Build in Gerrit tree
+--------------------
+
+Clone or link this plugin to the plugins directory of Gerrit's source
+tree, and issue the command:
+
+```
+  buck build plugins/@PLUGIN@
+```
+
+The output is created in:
+
+```
+  buck-out/gen/plugins/@PLUGIN@/@PLUGIN@.jar
+```
+
+This project can be imported into the Eclipse IDE:
+
+```
+  ./tools/eclipse/project.py
+```
+
+To execute the tests run:
+
+```
+  buck test --include @PLUGIN@
+```
+
+How to build the Gerrit Plugin API is described in the [Gerrit
+documentation](../../../Documentation/dev-buck.html#_extension_and_plugin_api_jar_files).
+
+[Back to @PLUGIN@ documentation index][index]
+
+[index]: index.html
diff --git a/src/main/resources/Documentation/config.md b/src/main/resources/Documentation/config.md
new file mode 100644
index 0000000..137c260
--- /dev/null
+++ b/src/main/resources/Documentation/config.md
@@ -0,0 +1,45 @@
+@PLUGIN@ Configuration
+=========================
+
+The @PLUGIN@ plugin's per project configuration is stored in the
+`webhooks.config` file in project's `refs/meta/config` branch.
+The configuration is inheritable.
+
+Global @PLUGIN@ plugin configuration is stored in the `gerrit.config` file.
+
+File 'webhooks.config'
+----------------------
+
+remote.NAME.url
+: Address of the remote server to post events to.
+
+remote.NAME.event
+: Type of the event which will be posted to the remote url. Multiple event
+  types can be specified, listing event types which should be posted.
+  When no event type is configured, all events will be posted.
+
+File 'gerrit.config'
+--------------------
+
+@PLUGIN@.connectionTimeout
+:   Maximum interval of time in milliseconds the plugin waits for a connection
+    to the target instance. When not specified, the default value is set to 5000ms.
+
+@PLUGIN@.socketTimeout
+:   Maximum interval of time in milliseconds the plugin waits for a response from the
+    target instance once the connection has been established. When not specified,
+    the default value is set to 5000ms.
+
+@PLUGIN@.maxTries
+:   Maximum number of times the plugin should attempt when posting an event to
+    the target url. Setting this value to 0 will disable retries. When not
+    specified, the default value is 5. After this number of failed tries, an
+    error is logged.
+
+@PLUGIN@.retryInterval
+:   The interval of time in milliseconds between the subsequent auto-retries.
+    When not specified, the default value is set to 1000ms.
+
+@PLUGIN@.threadPoolSize
+:   Maximum number of threads used to send events to the target instance.
+    Defaults to 1.
\ No newline at end of file