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