Initialize sync-index plugin with first version
Sync-index plugin keeps the secondary index synchronized among two
Gerrit instances that share the same git repositories and Gerrit
database.
Using a new extension point in Gerrit core [1], this plugin is notified
every time a change has been indexed in or removed from the secondary
index. Then it propagates that information to the configured target
Gerrit instance. The sync-index plugin installed in the target instance
updates the corresponding secondary index accordingly.
[1] https://gerrit-review.googlesource.com/#/c/72607/
Change-Id: Id5c089cfe3263f23dd56880e47d609180aeb3edc
diff --git a/.buckconfig b/.buckconfig
new file mode 100644
index 0000000..2b818b6
--- /dev/null
+++ b/.buckconfig
@@ -0,0 +1,16 @@
+[alias]
+ sync-index = //:sync-index
+ plugin = //:sync-index
+ src = //:sync-index-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..7f7fd4b
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,8 @@
+/.buckversion
+/.classpath
+/.project
+/.settings/
+/.watchmanconfig
+/buck-out/
+/bucklets
+/eclipse-out/
diff --git a/BUCK b/BUCK
new file mode 100644
index 0000000..163c42f
--- /dev/null
+++ b/BUCK
@@ -0,0 +1,53 @@
+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 + [
+ ':sync-index__plugin',
+ ':wiremock',
+]
+
+gerrit_plugin(
+ name = 'sync-index',
+ srcs = SOURCES,
+ resources = RESOURCES,
+ manifest_entries = [
+ 'Gerrit-PluginName: sync-index',
+ 'Gerrit-ApiType: plugin',
+ 'Gerrit-Module: com.ericsson.gerrit.plugins.syncindex.Module',
+ 'Gerrit-HttpModule: com.ericsson.gerrit.plugins.syncindex.HttpModule',
+ 'Implementation-Title: sync-index plugin',
+ 'Implementation-URL: https://gerrit.ericsson.se/#/admin/projects/gerrit/plugins/sync-index',
+ 'Implementation-Vendor: Ericsson',
+ ],
+ provided_deps = GERRIT_TESTS + [':wiremock',],
+)
+
+java_sources(
+ name = 'sync-index-sources',
+ srcs = SOURCES + RESOURCES,
+)
+
+java_library(
+ name = 'classpath',
+ deps = TEST_DEPS,
+)
+
+java_test(
+ name = 'sync-index_tests',
+ srcs = glob(['src/test/java/**/*.java']),
+ labels = ['sync-index'],
+ source_under_test = [':sync-index__plugin'],
+ deps = TEST_DEPS,
+)
+
+maven_jar(
+ name = 'wiremock',
+ id = 'com.github.tomakehurst:wiremock:1.58:standalone',
+ sha1 = '21c8386a95c5dc54a9c55839c5a95083e42412ae',
+ license = 'Apache2.0',
+ attach_source = False,
+)
diff --git a/lib/gerrit/BUCK b/lib/gerrit/BUCK
new file mode 100644
index 0000000..f30d88f
--- /dev/null
+++ b/lib/gerrit/BUCK
@@ -0,0 +1,20 @@
+include_defs('//bucklets/maven_jar.bucklet')
+
+VER = '2.13-SNAPSHOT'
+REPO = MAVEN_LOCAL
+
+maven_jar(
+ name = 'acceptance-framework',
+ id = 'com.google.gerrit:gerrit-acceptance-framework:' + VER,
+ license = 'Apache2.0',
+ attach_source = False,
+ repository = REPO,
+)
+
+maven_jar(
+ name = 'plugin-api',
+ id = 'com.google.gerrit:gerrit-plugin-api:' + VER,
+ license = 'Apache2.0',
+ attach_source = False,
+ repository = REPO,
+)
diff --git a/src/main/java/com/ericsson/gerrit/plugins/syncindex/Configuration.java b/src/main/java/com/ericsson/gerrit/plugins/syncindex/Configuration.java
new file mode 100644
index 0000000..a93ba84
--- /dev/null
+++ b/src/main/java/com/ericsson/gerrit/plugins/syncindex/Configuration.java
@@ -0,0 +1,86 @@
+// Copyright (C) 2015 Ericsson
+//
+// 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.ericsson.gerrit.plugins.syncindex;
+
+import com.google.common.base.CharMatcher;
+import com.google.common.base.Strings;
+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
+class Configuration {
+ 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 String url;
+ private final String user;
+ private final String password;
+ 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);
+ url = Strings.nullToEmpty(cfg.getString("url"));
+ user = Strings.nullToEmpty(cfg.getString("user"));
+ password = Strings.nullToEmpty(cfg.getString("password"));
+ connectionTimeout = cfg.getInt("connectionTimeout", DEFAULT_TIMEOUT_MS);
+ socketTimeout = cfg.getInt("socketTimeout", DEFAULT_TIMEOUT_MS);
+ maxTries = cfg.getInt("maxTries", DEFAULT_MAX_TRIES);
+ retryInterval = cfg.getInt("retryInterval", DEFAULT_RETRY_INTERVAL);
+ threadPoolSize = cfg.getInt("threadPoolSize", DEFAULT_THREAD_POOL_SIZE);
+ }
+
+ int getConnectionTimeout() {
+ return connectionTimeout;
+ }
+
+ int getMaxTries() {
+ return maxTries;
+ }
+
+ int getRetryInterval() {
+ return retryInterval;
+ }
+
+ int getSocketTimeout() {
+ return socketTimeout;
+ }
+
+ String getUrl() {
+ return CharMatcher.is('/').trimTrailingFrom(url);
+ }
+
+ String getUser() {
+ return user;
+ }
+
+ String getPassword() {
+ return password;
+ }
+
+ public int getThreadPoolSize() {
+ return threadPoolSize;
+ }
+}
diff --git a/src/main/java/com/ericsson/gerrit/plugins/syncindex/Context.java b/src/main/java/com/ericsson/gerrit/plugins/syncindex/Context.java
new file mode 100644
index 0000000..67abc1e
--- /dev/null
+++ b/src/main/java/com/ericsson/gerrit/plugins/syncindex/Context.java
@@ -0,0 +1,43 @@
+// Copyright (C) 2015 Ericsson
+//
+// 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.ericsson.gerrit.plugins.syncindex;
+
+/**
+ * Allows to tag a forwarded event to avoid infinitely looping events.
+ */
+class Context {
+ private static final ThreadLocal<Boolean> FORWARDED_EVENT =
+ new ThreadLocal<Boolean>() {
+ @Override
+ protected Boolean initialValue() {
+ return false;
+ }
+ };
+
+ private Context() {
+ }
+
+ static Boolean isForwardedEvent() {
+ return FORWARDED_EVENT.get();
+ }
+
+ static void setForwardedEvent(Boolean b) {
+ FORWARDED_EVENT.set(b);
+ }
+
+ static void unsetForwardedEvent() {
+ FORWARDED_EVENT.remove();
+ }
+}
diff --git a/src/main/java/com/ericsson/gerrit/plugins/syncindex/HttpClientProvider.java b/src/main/java/com/ericsson/gerrit/plugins/syncindex/HttpClientProvider.java
new file mode 100644
index 0000000..c36370c
--- /dev/null
+++ b/src/main/java/com/ericsson/gerrit/plugins/syncindex/HttpClientProvider.java
@@ -0,0 +1,193 @@
+// Copyright (C) 2015 Ericsson
+//
+// 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.ericsson.gerrit.plugins.syncindex;
+
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+
+import org.apache.http.HttpResponse;
+import org.apache.http.auth.AuthScope;
+import org.apache.http.auth.UsernamePasswordCredentials;
+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.BasicCredentialsProvider;
+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 java.io.IOException;
+import java.net.URI;
+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;
+
+/**
+ * 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())
+ .setDefaultCredentialsProvider(buildCredentials())
+ .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());
+ try {
+ Thread.sleep(cfg.getRetryInterval());
+ } catch (InterruptedException e) {
+ log.debug("Ignoring InterruptedException", e);
+ }
+ 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());
+ return true;
+ }
+ return false;
+ }
+
+ @Override
+ public long getRetryInterval() {
+ return cfg.getRetryInterval();
+ }
+ };
+ }
+
+ private void logRetry(String cause) {
+ log.debug("Retrying request to '" + cfg.getUrl() + "' Cause: " + cause);
+ }
+
+ 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 BasicCredentialsProvider buildCredentials() {
+ URI uri = URI.create(cfg.getUrl());
+ BasicCredentialsProvider creds = new BasicCredentialsProvider();
+ creds.setCredentials(new AuthScope(uri.getHost(), uri.getPort()),
+ new UsernamePasswordCredentials(cfg.getUser(), cfg.getPassword()));
+ return creds;
+ }
+
+ 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/ericsson/gerrit/plugins/syncindex/HttpModule.java b/src/main/java/com/ericsson/gerrit/plugins/syncindex/HttpModule.java
new file mode 100644
index 0000000..334c195
--- /dev/null
+++ b/src/main/java/com/ericsson/gerrit/plugins/syncindex/HttpModule.java
@@ -0,0 +1,24 @@
+// Copyright (C) 2015 Ericsson
+//
+// 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.ericsson.gerrit.plugins.syncindex;
+
+import com.google.gerrit.httpd.plugins.HttpPluginModule;
+
+class HttpModule extends HttpPluginModule {
+ @Override
+ protected void configureServlets() {
+ serveRegex("/index/\\d+$").with(SyncIndexRestApiServlet.class);
+ }
+}
diff --git a/src/main/java/com/ericsson/gerrit/plugins/syncindex/HttpSession.java b/src/main/java/com/ericsson/gerrit/plugins/syncindex/HttpSession.java
new file mode 100644
index 0000000..159acad
--- /dev/null
+++ b/src/main/java/com/ericsson/gerrit/plugins/syncindex/HttpSession.java
@@ -0,0 +1,47 @@
+// Copyright (C) 2015 Ericsson
+//
+// 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.ericsson.gerrit.plugins.syncindex;
+
+import com.google.inject.Inject;
+
+import com.ericsson.gerrit.plugins.syncindex.IndexResponseHandler.IndexResult;
+
+import org.apache.http.client.methods.HttpDelete;
+import org.apache.http.client.methods.HttpPost;
+import org.apache.http.impl.client.CloseableHttpClient;
+
+import java.io.IOException;
+
+class HttpSession {
+ private final CloseableHttpClient httpClient;
+ private final String url;
+
+ @Inject
+ HttpSession(CloseableHttpClient httpClient,
+ @SyncUrl String url) {
+ this.httpClient = httpClient;
+ this.url = url;
+ }
+
+ IndexResult post(String endpoint) throws IOException {
+ return httpClient.execute(new HttpPost(url + endpoint),
+ new IndexResponseHandler());
+ }
+
+ IndexResult delete(String endpoint) throws IOException {
+ return httpClient.execute(new HttpDelete(url + endpoint),
+ new IndexResponseHandler());
+ }
+}
diff --git a/src/main/java/com/ericsson/gerrit/plugins/syncindex/IndexEventHandler.java b/src/main/java/com/ericsson/gerrit/plugins/syncindex/IndexEventHandler.java
new file mode 100644
index 0000000..f8e2f31
--- /dev/null
+++ b/src/main/java/com/ericsson/gerrit/plugins/syncindex/IndexEventHandler.java
@@ -0,0 +1,103 @@
+// Copyright (C) 2015 Ericsson
+//
+// 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.ericsson.gerrit.plugins.syncindex;
+
+import com.google.common.base.Objects;
+import com.google.gerrit.extensions.annotations.PluginName;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.server.extensions.events.ChangeIndexedListener;
+import com.google.gerrit.server.query.change.ChangeData;
+import com.google.inject.Inject;
+
+import java.util.Collections;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.Executor;
+
+class IndexEventHandler implements ChangeIndexedListener {
+ private final Executor executor;
+ private final RestSession restClient;
+ private final String pluginName;
+ private final Set<SyncIndexTask> queuedTasks = Collections
+ .newSetFromMap(new ConcurrentHashMap<SyncIndexTask, Boolean>());
+
+ @Inject
+ IndexEventHandler(@SyncIndexExecutor Executor executor,
+ @PluginName String pluginName,
+ RestSession restClient) {
+ this.restClient = restClient;
+ this.executor = executor;
+ this.pluginName = pluginName;
+ }
+
+ @Override
+ public void onChangeIndexed(ChangeData cd) {
+ executeIndexTask(cd.getId(), false);
+ }
+
+ @Override
+ public void onChangeDeleted(Change.Id id) {
+ executeIndexTask(id, true);
+ }
+
+ private void executeIndexTask(Change.Id id, boolean deleted) {
+ if (!Context.isForwardedEvent()) {
+ SyncIndexTask syncIndexTask = new SyncIndexTask(id.get(), deleted);
+ if (queuedTasks.add(syncIndexTask)) {
+ executor.execute(syncIndexTask);
+ }
+ }
+ }
+
+ class SyncIndexTask implements Runnable {
+ private int changeId;
+ private boolean deleted;
+
+ SyncIndexTask(int changeId, boolean deleted) {
+ this.changeId = changeId;
+ this.deleted = deleted;
+ }
+
+ @Override
+ public void run() {
+ queuedTasks.remove(this);
+ if (deleted) {
+ restClient.deleteFromIndex(changeId);
+ } else {
+ restClient.index(changeId);
+ }
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hashCode(changeId, deleted);
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (!(obj instanceof SyncIndexTask)) {
+ return false;
+ }
+ SyncIndexTask other = (SyncIndexTask) obj;
+ return changeId == other.changeId && deleted == other.deleted;
+ }
+
+ @Override
+ public String toString() {
+ return String.format("[%s] Index change %s in target instance",
+ pluginName, changeId);
+ }
+ }
+}
diff --git a/src/main/java/com/ericsson/gerrit/plugins/syncindex/IndexResponseHandler.java b/src/main/java/com/ericsson/gerrit/plugins/syncindex/IndexResponseHandler.java
new file mode 100644
index 0000000..2d549ae
--- /dev/null
+++ b/src/main/java/com/ericsson/gerrit/plugins/syncindex/IndexResponseHandler.java
@@ -0,0 +1,74 @@
+// Copyright (C) 2015 Ericsson
+//
+// 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.ericsson.gerrit.plugins.syncindex;
+
+import static javax.servlet.http.HttpServletResponse.SC_NO_CONTENT;
+
+import com.ericsson.gerrit.plugins.syncindex.IndexResponseHandler.IndexResult;
+
+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 IndexResponseHandler implements ResponseHandler<IndexResult> {
+
+ static class IndexResult {
+ private boolean successful;
+ private String message;
+
+ IndexResult(boolean successful, String message) {
+ this.successful = successful;
+ this.message = message;
+ }
+
+ boolean isSuccessful() {
+ return successful;
+ }
+
+ String getMessage() {
+ return message;
+ }
+ }
+
+ private static final Logger log = LoggerFactory
+ .getLogger(IndexResponseHandler.class);
+
+ @Override
+ public IndexResult handleResponse(HttpResponse response) {
+ return new IndexResult(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();
+ String asString = "";
+ if (entity != null) {
+ try {
+ asString = EntityUtils.toString(entity);
+ } catch (IOException e) {
+ log.error("Error parsing entity", e);
+ }
+ }
+ return asString;
+ }
+}
diff --git a/src/main/java/com/ericsson/gerrit/plugins/syncindex/Module.java b/src/main/java/com/ericsson/gerrit/plugins/syncindex/Module.java
new file mode 100644
index 0000000..5b278c3
--- /dev/null
+++ b/src/main/java/com/ericsson/gerrit/plugins/syncindex/Module.java
@@ -0,0 +1,49 @@
+// Copyright (C) 2015 Ericsson
+//
+// 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.ericsson.gerrit.plugins.syncindex;
+
+import com.google.gerrit.extensions.registration.DynamicSet;
+import com.google.gerrit.lifecycle.LifecycleModule;
+import com.google.gerrit.server.extensions.events.ChangeIndexedListener;
+import com.google.inject.Provides;
+import com.google.inject.Scopes;
+
+import org.apache.http.impl.client.CloseableHttpClient;
+
+import java.util.concurrent.Executor;
+
+class Module extends LifecycleModule {
+
+ @Override
+ protected void configure() {
+ bind(CloseableHttpClient.class).toProvider(HttpClientProvider.class)
+ .in(Scopes.SINGLETON);
+ bind(Configuration.class).in(Scopes.SINGLETON);
+ bind(HttpSession.class);
+ bind(RestSession.class);
+ bind(Executor.class)
+ .annotatedWith(SyncIndexExecutor.class)
+ .toProvider(SyncIndexExecutorProvider.class);
+ listener().to(SyncIndexExecutorProvider.class);
+ DynamicSet.bind(binder(), ChangeIndexedListener.class).to(
+ IndexEventHandler.class);
+ }
+
+ @Provides
+ @SyncUrl
+ String syncUrl(Configuration config) {
+ return config.getUrl();
+ }
+}
diff --git a/src/main/java/com/ericsson/gerrit/plugins/syncindex/RestSession.java b/src/main/java/com/ericsson/gerrit/plugins/syncindex/RestSession.java
new file mode 100644
index 0000000..9cdd4ea
--- /dev/null
+++ b/src/main/java/com/ericsson/gerrit/plugins/syncindex/RestSession.java
@@ -0,0 +1,71 @@
+// Copyright (C) 2015 Ericsson
+//
+// 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.ericsson.gerrit.plugins.syncindex;
+
+import com.google.common.base.Joiner;
+import com.google.gerrit.extensions.annotations.PluginName;
+import com.google.inject.Inject;
+
+import com.ericsson.gerrit.plugins.syncindex.IndexResponseHandler.IndexResult;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+
+class RestSession {
+ private static final Logger log = LoggerFactory.getLogger(RestSession.class);
+
+ private final HttpSession httpSession;
+ private final String pluginName;
+
+ @Inject
+ RestSession(HttpSession httpClient, @PluginName String pluginName) {
+ this.httpSession = httpClient;
+ this.pluginName = pluginName;
+ }
+
+ boolean index(int changeId) {
+ try {
+ IndexResult result = httpSession.post(buildEndpoint(changeId));
+ if (result.isSuccessful()) {
+ return true;
+ }
+ log.error("Unable to index change {}. Cause: {}", changeId,
+ result.getMessage());
+ } catch (IOException e) {
+ log.error("Error trying to index change " + changeId, e);
+ }
+ return false;
+ }
+
+ boolean deleteFromIndex(int changeId) {
+ try {
+ IndexResult result = httpSession.delete(buildEndpoint(changeId));
+ if (result.isSuccessful()) {
+ return true;
+ }
+ log.error("Unable to delete from index change {}. Cause: {}", changeId,
+ result.getMessage());
+ } catch (IOException e) {
+ log.error("Error trying to delete from index change " + changeId, e);
+ }
+ return false;
+ }
+
+ private String buildEndpoint(int changeId) {
+ return Joiner.on("/").join("/plugins", pluginName, "index", changeId);
+ }
+}
diff --git a/src/main/java/com/ericsson/gerrit/plugins/syncindex/SyncIndexExecutor.java b/src/main/java/com/ericsson/gerrit/plugins/syncindex/SyncIndexExecutor.java
new file mode 100644
index 0000000..75932e9
--- /dev/null
+++ b/src/main/java/com/ericsson/gerrit/plugins/syncindex/SyncIndexExecutor.java
@@ -0,0 +1,26 @@
+// Copyright (C) 2015 Ericsson
+//
+// 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.ericsson.gerrit.plugins.syncindex;
+
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+
+import com.google.inject.BindingAnnotation;
+
+import java.lang.annotation.Retention;
+
+@Retention(RUNTIME)
+@BindingAnnotation
+@interface SyncIndexExecutor {
+}
diff --git a/src/main/java/com/ericsson/gerrit/plugins/syncindex/SyncIndexExecutorProvider.java b/src/main/java/com/ericsson/gerrit/plugins/syncindex/SyncIndexExecutorProvider.java
new file mode 100644
index 0000000..0400506
--- /dev/null
+++ b/src/main/java/com/ericsson/gerrit/plugins/syncindex/SyncIndexExecutorProvider.java
@@ -0,0 +1,52 @@
+// Copyright (C) 2015 Ericsson
+//
+// 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.ericsson.gerrit.plugins.syncindex;
+
+import com.google.gerrit.extensions.events.LifecycleListener;
+import com.google.gerrit.server.git.WorkQueue;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.Singleton;
+
+import java.util.concurrent.Executor;
+
+@Singleton
+class SyncIndexExecutorProvider implements Provider<Executor>,
+ LifecycleListener {
+ private WorkQueue.Executor executor;
+
+ @Inject
+ SyncIndexExecutorProvider(WorkQueue workQueue, Configuration config) {
+ executor =
+ workQueue.createQueue(config.getThreadPoolSize(), "Sync remote index");
+ }
+
+ @Override
+ public void start() {
+ //do nothing
+ }
+
+ @Override
+ public void stop() {
+ executor.shutdown();
+ executor.unregisterWorkQueue();
+ executor = null;
+ }
+
+ @Override
+ public Executor get() {
+ return executor;
+ }
+}
diff --git a/src/main/java/com/ericsson/gerrit/plugins/syncindex/SyncIndexRestApiServlet.java b/src/main/java/com/ericsson/gerrit/plugins/syncindex/SyncIndexRestApiServlet.java
new file mode 100644
index 0000000..ff3e47b
--- /dev/null
+++ b/src/main/java/com/ericsson/gerrit/plugins/syncindex/SyncIndexRestApiServlet.java
@@ -0,0 +1,105 @@
+// Copyright (C) 2015 Ericsson
+//
+// 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.ericsson.gerrit.plugins.syncindex;
+
+import static javax.servlet.http.HttpServletResponse.SC_CONFLICT;
+import static javax.servlet.http.HttpServletResponse.SC_NOT_FOUND;
+import static javax.servlet.http.HttpServletResponse.SC_NO_CONTENT;
+
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.index.ChangeIndexer;
+import com.google.gerrit.server.project.NoSuchChangeException;
+import com.google.gwtorm.server.OrmException;
+import com.google.gwtorm.server.SchemaFactory;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+@Singleton
+class SyncIndexRestApiServlet extends HttpServlet {
+ private static final long serialVersionUID = -1L;
+ private static final Logger logger =
+ LoggerFactory.getLogger(SyncIndexRestApiServlet.class);
+
+ private final ChangeIndexer indexer;
+ private final SchemaFactory<ReviewDb> schemaFactory;
+
+ @Inject
+ SyncIndexRestApiServlet(ChangeIndexer indexer,
+ SchemaFactory<ReviewDb> schemaFactory) {
+ this.indexer = indexer;
+ this.schemaFactory = schemaFactory;
+ }
+
+ @Override
+ protected void doPost(HttpServletRequest req, HttpServletResponse rsp)
+ throws IOException, ServletException {
+ rsp.setContentType("text/plain");
+ rsp.setCharacterEncoding("UTF-8");
+ Change.Id id = getIdFromRequest(req.getPathInfo());
+
+ try (ReviewDb db = schemaFactory.open()) {
+ Context.setForwardedEvent(true);
+ Change change = db.changes().get(id);
+ if (change == null) {
+ throw new NoSuchChangeException(id);
+ }
+ indexer.index(db, change);
+ rsp.setStatus(SC_NO_CONTENT);
+ } catch (IOException e) {
+ rsp.sendError(SC_CONFLICT, e.getMessage());
+ logger.error("Unable to update index", e);
+ } catch (OrmException | NoSuchChangeException e) {
+ rsp.sendError(SC_NOT_FOUND, "Change not found\n");
+ logger.debug("Error trying to find a change ", e);
+ } finally {
+ Context.unsetForwardedEvent();
+ }
+ }
+
+ @Override
+ protected void doDelete(HttpServletRequest req, HttpServletResponse rsp)
+ throws IOException, ServletException {
+ rsp.setContentType("text/plain");
+ rsp.setCharacterEncoding("UTF-8");
+ Change.Id id = getIdFromRequest(req.getPathInfo());
+
+ try {
+ Context.setForwardedEvent(true);
+ indexer.delete(id);
+ rsp.setStatus(SC_NO_CONTENT);
+ } catch (IOException e) {
+ rsp.sendError(SC_CONFLICT, e.getMessage());
+ logger.error("Unable to update index", e);
+ } finally {
+ Context.unsetForwardedEvent();
+ }
+ }
+
+ private Change.Id getIdFromRequest(String path) {
+ String changeId = path.substring(path.lastIndexOf('/') + 1);
+ return Change.Id.parse(changeId);
+ }
+}
diff --git a/src/main/java/com/ericsson/gerrit/plugins/syncindex/SyncUrl.java b/src/main/java/com/ericsson/gerrit/plugins/syncindex/SyncUrl.java
new file mode 100644
index 0000000..8b3ac1a
--- /dev/null
+++ b/src/main/java/com/ericsson/gerrit/plugins/syncindex/SyncUrl.java
@@ -0,0 +1,26 @@
+// Copyright (C) 2015 Ericsson
+//
+// 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.ericsson.gerrit.plugins.syncindex;
+
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+
+import com.google.inject.BindingAnnotation;
+
+import java.lang.annotation.Retention;
+
+@Retention(RUNTIME)
+@BindingAnnotation
+@interface SyncUrl {
+}
diff --git a/src/main/resources/Documentation/about.md b/src/main/resources/Documentation/about.md
new file mode 100644
index 0000000..a711d86
--- /dev/null
+++ b/src/main/resources/Documentation/about.md
@@ -0,0 +1,11 @@
+The @PLUGIN@ plugin allows to synchronize secondary indexes between two Gerrit
+instances sharing the same git repositories and database.
+
+The plugin is installed in both instances and every time the secondary index
+is modified in one of the instances, i.e., a change is added, updated or removed
+from the index, the other instance index is updated accordingly. This way, both
+indexes are kept synchronized.
+
+For this secondary index synchronization to work, http must be enabled in both
+instances and the plugin must be configured with valid credentials. For further
+information, refer to [config](config.html) documentation.
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..48b9e94
--- /dev/null
+++ b/src/main/resources/Documentation/config.md
@@ -0,0 +1,51 @@
+@PLUGIN@ Configuration
+=========================
+
+In order for the synchronization to work, the @PLUGIN@ plugin must be installed
+in both instances and the following fields should be specified in the
+corresponding Gerrit configuration file:
+
+File 'gerrit.config'
+--------------------
+
+[plugin "@PLUGIN@"]
+: url = target_instance_url
+: user = username
+: password = password
+
+plugin.@PLUGIN@.url
+: Specify the URL for the secondary (target) instance.
+
+plugin.@PLUGIN@.user
+: Username to connect to the secondary (target) instance.
+
+plugin.@PLUGIN@.password
+: Password to connect to the secondary (target) instance. This value can
+ also be defined in secure.config.
+
+@PLUGIN@ plugin uses REST API calls to index changes in the target instance. It
+is possible to customize the parameters of the underlying http client doing these
+calls by specifying the following fields:
+
+@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 to index the event in the
+ target instance. 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 so that admins can re-index the change manually.
+
+@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 index events to the target instance.
+ Defaults to 1.
diff --git a/src/test/java/com/ericsson/gerrit/plugins/syncindex/ConfigurationTest.java b/src/test/java/com/ericsson/gerrit/plugins/syncindex/ConfigurationTest.java
new file mode 100644
index 0000000..fd39331
--- /dev/null
+++ b/src/test/java/com/ericsson/gerrit/plugins/syncindex/ConfigurationTest.java
@@ -0,0 +1,102 @@
+// Copyright (C) 2015 Ericsson
+//
+// 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.ericsson.gerrit.plugins.syncindex;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.easymock.EasyMock.expect;
+
+import org.easymock.EasyMockSupport;
+import org.junit.Before;
+import org.junit.Test;
+
+import com.google.gerrit.server.config.PluginConfig;
+import com.google.gerrit.server.config.PluginConfigFactory;
+
+public class ConfigurationTest extends EasyMockSupport {
+ private static final String PASS = "fakePass";
+ private static final String USER = "fakeUser";
+ private static final String URL = "fakeUrl";
+ private static final String EMPTY = "";
+ private static final int TIMEOUT = 5000;
+ private static final int MAX_TRIES = 5;
+ private static final int RETRY_INTERVAL = 1000;
+ private static final int THREAD_POOL_SIZE = 1;
+
+ private PluginConfigFactory cfgFactoryMock;
+ private PluginConfig configMock;
+ private Configuration configuration;
+ private String pluginName = "sync-index";
+
+ @Before
+ public void setUp() throws Exception {
+ configMock = createNiceMock(PluginConfig.class);
+ cfgFactoryMock = createMock(PluginConfigFactory.class);
+ expect(cfgFactoryMock.getFromGerritConfig(pluginName, true))
+ .andStubReturn(configMock);
+ }
+
+ @Test
+ public void testValuesPresentInGerritConfig() throws Exception {
+ buildMocks(true);
+ assertThat(configuration.getUrl()).isEqualTo(URL);
+ assertThat(configuration.getUser()).isEqualTo(USER);
+ assertThat(configuration.getPassword()).isEqualTo(PASS);
+ assertThat(configuration.getConnectionTimeout()).isEqualTo(TIMEOUT);
+ assertThat(configuration.getSocketTimeout()).isEqualTo(TIMEOUT);
+ assertThat(configuration.getMaxTries()).isEqualTo(MAX_TRIES);
+ assertThat(configuration.getRetryInterval()).isEqualTo(RETRY_INTERVAL);
+ assertThat(configuration.getThreadPoolSize()).isEqualTo(THREAD_POOL_SIZE);
+ }
+
+ @Test
+ public void testValuesNotPresentInGerritConfig() throws Exception {
+ buildMocks(false);
+ assertThat(configuration.getUrl()).isEqualTo(EMPTY);
+ assertThat(configuration.getUser()).isEqualTo(EMPTY);
+ assertThat(configuration.getPassword()).isEqualTo(EMPTY);
+ assertThat(configuration.getConnectionTimeout()).isEqualTo(0);
+ assertThat(configuration.getSocketTimeout()).isEqualTo(0);
+ assertThat(configuration.getMaxTries()).isEqualTo(0);
+ assertThat(configuration.getRetryInterval()).isEqualTo(0);
+ assertThat(configuration.getThreadPoolSize()).isEqualTo(0);
+ }
+
+ @Test
+ public void testUrlTrailingSlashIsDropped() throws Exception {
+ expect(configMock.getString("url")).andReturn(URL + "/");
+ replayAll();
+ configuration = new Configuration(cfgFactoryMock, pluginName);
+ assertThat(configuration).isNotNull();
+ assertThat(configuration.getUrl()).isEqualTo(URL);
+ }
+
+ private void buildMocks(boolean values) {
+ expect(configMock.getString("url")).andReturn(values ? URL : null);
+ expect(configMock.getString("user")).andReturn(values ? USER : null);
+ expect(configMock.getString("password")).andReturn(values ? PASS : null);
+ expect(configMock.getInt("connectionTimeout", TIMEOUT))
+ .andReturn(values ? TIMEOUT : 0);
+ expect(configMock.getInt("socketTimeout", TIMEOUT))
+ .andReturn(values ? TIMEOUT : 0);
+ expect(configMock.getInt("maxTries", MAX_TRIES))
+ .andReturn(values ? MAX_TRIES : 0);
+ expect(configMock.getInt("retryInterval", RETRY_INTERVAL))
+ .andReturn(values ? RETRY_INTERVAL : 0);
+ expect(configMock.getInt("threadPoolSize", THREAD_POOL_SIZE))
+ .andReturn(values ? THREAD_POOL_SIZE : 0);
+ replayAll();
+ configuration = new Configuration(cfgFactoryMock, pluginName);
+ }
+}
diff --git a/src/test/java/com/ericsson/gerrit/plugins/syncindex/ContextTest.java b/src/test/java/com/ericsson/gerrit/plugins/syncindex/ContextTest.java
new file mode 100644
index 0000000..f43439e
--- /dev/null
+++ b/src/test/java/com/ericsson/gerrit/plugins/syncindex/ContextTest.java
@@ -0,0 +1,46 @@
+// Copyright (C) 2015 Ericsson
+//
+// 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.ericsson.gerrit.plugins.syncindex;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import org.easymock.EasyMockSupport;
+import org.junit.Test;
+
+public class ContextTest extends EasyMockSupport {
+
+ @Test
+ public void testInitialValueNotNull() throws Exception {
+ assertThat(Context.isForwardedEvent()).isNotNull();
+ assertThat(Context.isForwardedEvent()).isFalse();
+ }
+
+ @Test
+ public void testSetForwardedEvent() throws Exception {
+ Context.setForwardedEvent(true);
+ try {
+ assertThat(Context.isForwardedEvent()).isTrue();
+ } finally {
+ Context.unsetForwardedEvent();
+ }
+ }
+
+ @Test
+ public void testUnsetForwardedEvent() throws Exception {
+ Context.setForwardedEvent(true);
+ Context.unsetForwardedEvent();
+ assertThat(Context.isForwardedEvent()).isFalse();
+ }
+}
diff --git a/src/test/java/com/ericsson/gerrit/plugins/syncindex/HttpClientProviderTest.java b/src/test/java/com/ericsson/gerrit/plugins/syncindex/HttpClientProviderTest.java
new file mode 100644
index 0000000..48af47a
--- /dev/null
+++ b/src/test/java/com/ericsson/gerrit/plugins/syncindex/HttpClientProviderTest.java
@@ -0,0 +1,68 @@
+// Copyright (C) 2015 Ericsson
+//
+// 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.ericsson.gerrit.plugins.syncindex;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.easymock.EasyMock.expect;
+
+import com.google.gerrit.lifecycle.LifecycleModule;
+import com.google.inject.Guice;
+import com.google.inject.Injector;
+import com.google.inject.Scopes;
+
+import org.apache.http.impl.client.CloseableHttpClient;
+import org.easymock.EasyMockSupport;
+import org.junit.Before;
+import org.junit.Test;
+
+public class HttpClientProviderTest extends EasyMockSupport {
+ private static final int TIME_INTERVAL = 1000;
+ private static final String EMPTY = "";
+
+ private Configuration config;
+
+ @Before
+ public void setUp() throws Exception {
+ config = createNiceMock(Configuration.class);
+ expect(config.getUrl()).andReturn(EMPTY).anyTimes();
+ expect(config.getUser()).andReturn(EMPTY).anyTimes();
+ expect(config.getPassword()).andReturn(EMPTY).anyTimes();
+ expect(config.getMaxTries()).andReturn(1).anyTimes();
+ expect(config.getConnectionTimeout()).andReturn(TIME_INTERVAL).anyTimes();
+ expect(config.getSocketTimeout()).andReturn(TIME_INTERVAL).anyTimes();
+ expect(config.getRetryInterval()).andReturn(TIME_INTERVAL).anyTimes();
+ replayAll();
+ }
+
+ @Test
+ public void testGet() throws Exception {
+ Injector injector = Guice.createInjector(new TestModule());
+ CloseableHttpClient httpClient1 =
+ injector.getInstance(CloseableHttpClient.class);
+ assertThat(httpClient1).isNotNull();
+ CloseableHttpClient httpClient2 =
+ injector.getInstance(CloseableHttpClient.class);
+ assertThat(httpClient1).isEqualTo(httpClient2);
+ }
+
+ class TestModule extends LifecycleModule {
+ @Override
+ protected void configure() {
+ bind(Configuration.class).toInstance(config);
+ bind(CloseableHttpClient.class).toProvider(HttpClientProvider.class)
+ .in(Scopes.SINGLETON);
+ }
+ }
+}
diff --git a/src/test/java/com/ericsson/gerrit/plugins/syncindex/HttpSessionTest.java b/src/test/java/com/ericsson/gerrit/plugins/syncindex/HttpSessionTest.java
new file mode 100644
index 0000000..27f30d5
--- /dev/null
+++ b/src/test/java/com/ericsson/gerrit/plugins/syncindex/HttpSessionTest.java
@@ -0,0 +1,154 @@
+// Copyright (C) 2015 Ericsson
+//
+// 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.ericsson.gerrit.plugins.syncindex;
+
+import static com.github.tomakehurst.wiremock.client.WireMock.aResponse;
+import static com.github.tomakehurst.wiremock.client.WireMock.post;
+import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo;
+import static com.google.common.truth.Truth.assertThat;
+import static org.easymock.EasyMock.expect;
+
+import com.ericsson.gerrit.plugins.syncindex.IndexResponseHandler.IndexResult;
+import com.github.tomakehurst.wiremock.http.Fault;
+import com.github.tomakehurst.wiremock.junit.WireMockRule;
+import com.github.tomakehurst.wiremock.stubbing.Scenario;
+
+import org.apache.http.impl.client.CloseableHttpClient;
+import org.easymock.EasyMockSupport;
+import org.junit.Before;
+import org.junit.ClassRule;
+import org.junit.Test;
+
+public class HttpSessionTest extends EasyMockSupport {
+ private static final int MAX_TRIES = 5;
+ private static final int RETRY_INTERVAL = 1000;
+ private static final int TIMEOUT = 1000;
+ private static final int ERROR = 500;
+ private static final int OK = 204;
+ private static final int NOT_FOUND = 404;
+ private static final int UNAUTHORIZED = 401;
+
+ private static final String ENDPOINT = "/plugins/sync-index/index/1";
+ private static final String ERROR_MESSAGE = "Error message";
+ private static final String REQUEST_MADE = "Request made";
+ private static final String RETRY_AT_ERROR = "Retry at error";
+ private static final String RETRY_AT_DELAY = "Retry at delay";
+
+ private Configuration cfg;
+ private CloseableHttpClient httpClient;
+ private HttpSession httpSession;
+
+ @ClassRule
+ public static WireMockRule wireMockRule = new WireMockRule(0);
+
+ @Before
+ public void setUp() throws Exception {
+ String url = "http://localhost:" + wireMockRule.port();
+ cfg = createMock(Configuration.class);
+ expect(cfg.getUrl()).andReturn(url).anyTimes();
+ expect(cfg.getUser()).andReturn("user");
+ expect(cfg.getPassword()).andReturn("pass");
+ expect(cfg.getMaxTries()).andReturn(MAX_TRIES).anyTimes();
+ expect(cfg.getConnectionTimeout()).andReturn(TIMEOUT).anyTimes();
+ expect(cfg.getSocketTimeout()).andReturn(TIMEOUT).anyTimes();
+ expect(cfg.getRetryInterval()).andReturn(RETRY_INTERVAL).anyTimes();
+ replayAll();
+ httpClient = new HttpClientProvider(cfg).get();
+ httpSession = new HttpSession(httpClient, url);
+
+ wireMockRule.resetRequests();
+ }
+
+ @Test
+ public void testResponseOK() throws Exception {
+ wireMockRule.givenThat(post(urlEqualTo(ENDPOINT))
+ .willReturn(aResponse().withStatus(OK)));
+
+ assertThat(httpSession.post(ENDPOINT).isSuccessful()).isTrue();
+ }
+
+ @Test
+ public void testNotAuthorized() throws Exception {
+ String expected = "unauthorized";
+ wireMockRule.givenThat(post(urlEqualTo(ENDPOINT))
+ .willReturn(aResponse().withStatus(UNAUTHORIZED).withBody(expected)));
+
+ IndexResult result = httpSession.post(ENDPOINT);
+ assertThat(result.isSuccessful()).isFalse();
+ assertThat(result.getMessage()).isEqualTo(expected);
+ }
+
+ @Test
+ public void testNotFound() throws Exception {
+ String expected = "not found";
+ wireMockRule.givenThat(post(urlEqualTo(ENDPOINT))
+ .willReturn(aResponse().withStatus(NOT_FOUND).withBody(expected)));
+
+ IndexResult result = httpSession.post(ENDPOINT);
+ assertThat(result.isSuccessful()).isFalse();
+ assertThat(result.getMessage()).isEqualTo(expected);
+ }
+
+ @Test
+ public void testBadResponseRetryThenOK() throws Exception {
+ wireMockRule.givenThat(post(urlEqualTo(ENDPOINT)).inScenario(RETRY_AT_ERROR)
+ .whenScenarioStateIs(Scenario.STARTED).willSetStateTo(REQUEST_MADE)
+ .willReturn(aResponse().withStatus(ERROR)));
+ wireMockRule.givenThat(post(urlEqualTo(ENDPOINT)).inScenario(RETRY_AT_ERROR)
+ .whenScenarioStateIs(REQUEST_MADE)
+ .willReturn(aResponse().withStatus(OK)));
+
+ assertThat(httpSession.post(ENDPOINT).isSuccessful()).isTrue();
+ }
+
+ @Test
+ public void testBadResponseRetryThenGiveUp() throws Exception {
+ wireMockRule.givenThat(post(urlEqualTo(ENDPOINT))
+ .willReturn(aResponse().withStatus(ERROR).withBody(ERROR_MESSAGE)));
+
+ IndexResult result = httpSession.post(ENDPOINT);
+ assertThat(result.isSuccessful()).isFalse();
+ assertThat(result.getMessage()).isEqualTo(ERROR_MESSAGE);
+ }
+
+ @Test
+ public void testRetryAfterDelay() throws Exception {
+ wireMockRule.givenThat(post(urlEqualTo(ENDPOINT)).inScenario(RETRY_AT_DELAY)
+ .whenScenarioStateIs(Scenario.STARTED).willSetStateTo(REQUEST_MADE)
+ .willReturn(aResponse().withStatus(ERROR).withFixedDelay(TIMEOUT / 2)));
+ wireMockRule.givenThat(post(urlEqualTo(ENDPOINT)).inScenario(RETRY_AT_DELAY)
+ .whenScenarioStateIs(REQUEST_MADE)
+ .willReturn(aResponse().withStatus(OK)));
+
+ assertThat(httpSession.post(ENDPOINT).isSuccessful()).isTrue();
+ }
+
+ @Test
+ public void testGiveUpAtTimeout() throws Exception {
+ wireMockRule.givenThat(post(urlEqualTo(ENDPOINT)).inScenario(RETRY_AT_DELAY)
+ .whenScenarioStateIs(Scenario.STARTED).willSetStateTo(REQUEST_MADE)
+ .willReturn(aResponse().withStatus(ERROR).withFixedDelay(TIMEOUT)));
+
+ assertThat(httpSession.post(ENDPOINT).isSuccessful()).isFalse();
+ }
+
+ @Test
+ public void testResponseWithMalformedResponse() throws Exception {
+ wireMockRule.givenThat(post(urlEqualTo(ENDPOINT))
+ .willReturn(aResponse().withFault(Fault.MALFORMED_RESPONSE_CHUNK)));
+
+ assertThat(httpSession.post(ENDPOINT).isSuccessful()).isFalse();
+ }
+}
diff --git a/src/test/java/com/ericsson/gerrit/plugins/syncindex/IndexEventHandlerTest.java b/src/test/java/com/ericsson/gerrit/plugins/syncindex/IndexEventHandlerTest.java
new file mode 100644
index 0000000..614d30f
--- /dev/null
+++ b/src/test/java/com/ericsson/gerrit/plugins/syncindex/IndexEventHandlerTest.java
@@ -0,0 +1,141 @@
+// Copyright (C) 2015 Ericsson
+//
+// 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.ericsson.gerrit.plugins.syncindex;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.easymock.EasyMock.anyObject;
+import static org.easymock.EasyMock.expect;
+import static org.easymock.EasyMock.expectLastCall;
+import static org.easymock.EasyMock.reset;
+
+import com.google.common.util.concurrent.MoreExecutors;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.server.query.change.ChangeData;
+import com.google.gwtorm.client.KeyUtil;
+import com.google.gwtorm.server.StandardKeyEncoder;
+
+import com.ericsson.gerrit.plugins.syncindex.IndexEventHandler.SyncIndexTask;
+
+import org.easymock.EasyMockSupport;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+import java.util.concurrent.Executor;
+
+public class IndexEventHandlerTest extends EasyMockSupport {
+ private static final String PLUGIN_NAME = "sync-index";
+ private static final int CHANGE_ID = 1;
+
+ private IndexEventHandler indexEventHandler;
+ private Executor poolMock;
+ private RestSession restClientMock;
+ private ChangeData cd;
+ private Change.Id id;
+
+ @BeforeClass
+ public static void setUp() {
+ KeyUtil.setEncoderImpl(new StandardKeyEncoder());
+ }
+
+ @Before
+ public void setUpMocks() {
+ cd = createNiceMock(ChangeData.class);
+ id = Change.Id.parse(Integer.toString(CHANGE_ID));
+ expect(cd.getId()).andReturn(id).anyTimes();
+ poolMock = createMock(Executor.class);
+ poolMock.execute(anyObject(SyncIndexTask.class));
+ expectLastCall().andDelegateTo(MoreExecutors.directExecutor());
+ restClientMock = createMock(RestSession.class);
+ indexEventHandler =
+ new IndexEventHandler(poolMock, PLUGIN_NAME, restClientMock);
+ }
+
+ @Test
+ public void shouldIndexInRemoteOnChangeIndexedEvent() throws Exception {
+ expect(restClientMock.index(CHANGE_ID)).andReturn(true);
+ replayAll();
+ indexEventHandler.onChangeIndexed(cd);
+ verifyAll();
+ }
+
+ @Test
+ public void shouldDeleteFromIndexInRemoteOnChangeDeletedEvent()
+ throws Exception {
+ reset(cd);
+ expect(restClientMock.deleteFromIndex(CHANGE_ID)).andReturn(true);
+ replayAll();
+ indexEventHandler.onChangeDeleted(id);
+ verifyAll();
+ }
+
+ @Test
+ public void shouldNotCallRemoteWhenEventIsForwarded() throws Exception {
+ reset(poolMock);
+ replayAll();
+ Context.setForwardedEvent(true);
+ indexEventHandler.onChangeIndexed(cd);
+ indexEventHandler.onChangeDeleted(id);
+ Context.unsetForwardedEvent();
+ verifyAll();
+ }
+
+ @Test
+ public void duplicateEventOfAQueuedEventShouldGetDiscarded() {
+ reset(poolMock);
+ poolMock.execute(indexEventHandler.new SyncIndexTask(CHANGE_ID, false));
+ expectLastCall().once();
+ replayAll();
+ indexEventHandler.onChangeIndexed(cd);
+ indexEventHandler.onChangeIndexed(cd);
+ verifyAll();
+ }
+
+ @Test
+ public void testSyncIndexTaskToString() throws Exception {
+ SyncIndexTask syncIndexTask =
+ indexEventHandler.new SyncIndexTask(CHANGE_ID, false);
+ assertThat(syncIndexTask.toString()).isEqualTo(
+ String.format("[%s] Index change %s in target instance", PLUGIN_NAME,
+ CHANGE_ID));
+ }
+
+ @Test
+ public void testSyncIndexTaskHashCodeAndEquals() {
+ SyncIndexTask task = indexEventHandler.new SyncIndexTask(CHANGE_ID, false);
+
+ assertThat(task.equals(task)).isTrue();
+ assertThat(task.hashCode()).isEqualTo(task.hashCode());
+
+ SyncIndexTask identicalTask =
+ indexEventHandler.new SyncIndexTask(CHANGE_ID, false);
+ assertThat(task.equals(identicalTask)).isTrue();
+ assertThat(task.hashCode()).isEqualTo(identicalTask.hashCode());
+
+ assertThat(task.equals(null)).isFalse();
+ assertThat(task.equals("test")).isFalse();
+ assertThat(task.hashCode()).isNotEqualTo("test".hashCode());
+
+ SyncIndexTask differentChangeIdTask =
+ indexEventHandler.new SyncIndexTask(123, false);
+ assertThat(task.equals(differentChangeIdTask)).isFalse();
+ assertThat(task.hashCode()).isNotEqualTo(differentChangeIdTask.hashCode());
+
+ SyncIndexTask removeTask =
+ indexEventHandler.new SyncIndexTask(CHANGE_ID, true);
+ assertThat(task.equals(removeTask)).isFalse();
+ assertThat(task.hashCode()).isNotEqualTo(removeTask.hashCode());
+ }
+}
diff --git a/src/test/java/com/ericsson/gerrit/plugins/syncindex/IndexResponseHandlerTest.java b/src/test/java/com/ericsson/gerrit/plugins/syncindex/IndexResponseHandlerTest.java
new file mode 100644
index 0000000..595cf7d
--- /dev/null
+++ b/src/test/java/com/ericsson/gerrit/plugins/syncindex/IndexResponseHandlerTest.java
@@ -0,0 +1,70 @@
+// Copyright (C) 2015 Ericsson
+//
+// 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.ericsson.gerrit.plugins.syncindex;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.easymock.EasyMock.expect;
+
+import com.ericsson.gerrit.plugins.syncindex.IndexResponseHandler.IndexResult;
+
+import org.apache.http.HttpResponse;
+import org.apache.http.StatusLine;
+import org.apache.http.entity.StringEntity;
+import org.easymock.EasyMockSupport;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.io.UnsupportedEncodingException;
+
+public class IndexResponseHandlerTest extends EasyMockSupport {
+ private static final int ERROR = 400;
+ private static final int OK = 204;
+ private static final String EMPTY_ENTITY = "";
+ private static final String ERROR_ENTITY = "Error";
+
+ private IndexResponseHandler handler;
+
+ @Before
+ public void setUp() throws Exception {
+ handler = new IndexResponseHandler();
+ }
+
+ @Test
+ public void testIsSuccessful() throws Exception {
+ HttpResponse response = setupMocks(OK, EMPTY_ENTITY);
+ IndexResult result = handler.handleResponse(response);
+ assertThat(result.isSuccessful()).isTrue();
+ assertThat(result.getMessage()).isEmpty();
+ }
+
+ @Test
+ public void testIsNotSuccessful() throws Exception {
+ HttpResponse response = setupMocks(ERROR, ERROR_ENTITY);
+ IndexResult result = handler.handleResponse(response);
+ assertThat(result.isSuccessful()).isFalse();
+ assertThat(result.getMessage()).contains(ERROR_ENTITY);
+ }
+
+ private HttpResponse setupMocks(int httpCode, String entity)
+ throws UnsupportedEncodingException {
+ StatusLine status = createNiceMock(StatusLine.class);
+ expect(status.getStatusCode()).andReturn(httpCode).anyTimes();
+ HttpResponse response = createNiceMock(HttpResponse.class);
+ expect(response.getStatusLine()).andReturn(status).anyTimes();
+ expect(response.getEntity()).andReturn(new StringEntity(entity)).anyTimes();
+ replayAll();
+ return response;
+ }
+}
diff --git a/src/test/java/com/ericsson/gerrit/plugins/syncindex/ModuleTest.java b/src/test/java/com/ericsson/gerrit/plugins/syncindex/ModuleTest.java
new file mode 100644
index 0000000..bfc42b4
--- /dev/null
+++ b/src/test/java/com/ericsson/gerrit/plugins/syncindex/ModuleTest.java
@@ -0,0 +1,34 @@
+// Copyright (C) 2015 Ericsson
+//
+// 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.ericsson.gerrit.plugins.syncindex;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.easymock.EasyMock.expect;
+
+import org.easymock.EasyMockSupport;
+import org.junit.Test;
+
+public class ModuleTest extends EasyMockSupport {
+
+ @Test
+ public void testSyncUrlProvider() {
+ Configuration configMock = createNiceMock(Configuration.class);
+ String expected = "someUrl";
+ expect(configMock.getUrl()).andReturn(expected);
+ replayAll();
+ Module module = new Module();
+ assertThat(module.syncUrl(configMock)).isEqualTo(expected);
+ }
+}
diff --git a/src/test/java/com/ericsson/gerrit/plugins/syncindex/RestSessionTest.java b/src/test/java/com/ericsson/gerrit/plugins/syncindex/RestSessionTest.java
new file mode 100644
index 0000000..31850c2
--- /dev/null
+++ b/src/test/java/com/ericsson/gerrit/plugins/syncindex/RestSessionTest.java
@@ -0,0 +1,102 @@
+// Copyright (C) 2015 Ericsson
+//
+// 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.ericsson.gerrit.plugins.syncindex;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.easymock.EasyMock.expect;
+
+import com.google.common.base.Joiner;
+
+import com.ericsson.gerrit.plugins.syncindex.IndexResponseHandler.IndexResult;
+
+import org.easymock.EasyMockSupport;
+import org.junit.Test;
+
+import java.io.IOException;
+
+public class RestSessionTest extends EasyMockSupport {
+ private static final int CHANGE_NUMBER = 1;
+ private static final String DELETE_OP = "delete";
+ private static final String INDEX_OP = "index";
+ private static final String PLUGIN_NAME = "sync-index";
+ private static final String EMPTY_MSG = "";
+ private static final String ERROR_MSG = "Error";
+ private static final String EXCEPTION_MSG = "Exception";
+ private static final boolean SUCCESSFUL = true;
+ private static final boolean FAILED = false;
+ private static final boolean DO_NOT_THROW_EXCEPTION = false;
+ private static final boolean THROW_EXCEPTION = true;
+
+ private RestSession restClient;
+
+ @Test
+ public void testIndexChangeOK() throws Exception {
+ setUpMocks(INDEX_OP, SUCCESSFUL, EMPTY_MSG, DO_NOT_THROW_EXCEPTION);
+ assertThat(restClient.index(CHANGE_NUMBER)).isTrue();
+ }
+
+ @Test
+ public void testIndexChangeFailed() throws Exception {
+ setUpMocks(INDEX_OP, FAILED, ERROR_MSG, DO_NOT_THROW_EXCEPTION);
+ assertThat(restClient.index(CHANGE_NUMBER)).isFalse();
+ }
+
+ @Test
+ public void testIndexChangeThrowsException() throws Exception {
+ setUpMocks(INDEX_OP, FAILED, EXCEPTION_MSG, THROW_EXCEPTION);
+ assertThat(restClient.index(CHANGE_NUMBER)).isFalse();
+ }
+
+ @Test
+ public void testChangeDeletedFromIndexOK() throws Exception {
+ setUpMocks(DELETE_OP, SUCCESSFUL, EMPTY_MSG, DO_NOT_THROW_EXCEPTION);
+ assertThat(restClient.deleteFromIndex(CHANGE_NUMBER)).isTrue();
+ }
+
+ @Test
+ public void testChangeDeletedFromIndexFailed() throws Exception {
+ setUpMocks(DELETE_OP, FAILED, ERROR_MSG, DO_NOT_THROW_EXCEPTION);
+ assertThat(restClient.deleteFromIndex(CHANGE_NUMBER)).isFalse();
+ }
+
+ @Test
+ public void testChangeDeletedFromThrowsException() throws Exception {
+ setUpMocks(DELETE_OP, FAILED, EXCEPTION_MSG, THROW_EXCEPTION);
+ assertThat(restClient.deleteFromIndex(CHANGE_NUMBER)).isFalse();
+ }
+
+ private void setUpMocks(String operation, boolean isOperationSuccessful,
+ String msg, boolean exception) throws Exception {
+ String request =
+ Joiner.on("/").join("/plugins", PLUGIN_NAME, INDEX_OP, CHANGE_NUMBER);
+ HttpSession httpSession = createNiceMock(HttpSession.class);
+ if (exception) {
+ if (operation.equals(INDEX_OP)) {
+ expect(httpSession.post(request)).andThrow(new IOException());
+ } else {
+ expect(httpSession.delete(request)).andThrow(new IOException());
+ }
+ } else {
+ IndexResult result = new IndexResult(isOperationSuccessful, msg);
+ if (operation.equals(INDEX_OP)) {
+ expect(httpSession.post(request)).andReturn(result);
+ } else {
+ expect(httpSession.delete(request)).andReturn(result);
+ }
+ }
+ restClient = new RestSession(httpSession, PLUGIN_NAME);
+ replayAll();
+ }
+}
diff --git a/src/test/java/com/ericsson/gerrit/plugins/syncindex/SyncIndexExecutorProviderTest.java b/src/test/java/com/ericsson/gerrit/plugins/syncindex/SyncIndexExecutorProviderTest.java
new file mode 100644
index 0000000..665b10a
--- /dev/null
+++ b/src/test/java/com/ericsson/gerrit/plugins/syncindex/SyncIndexExecutorProviderTest.java
@@ -0,0 +1,64 @@
+// Copyright (C) 2015 Ericsson
+//
+// 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.ericsson.gerrit.plugins.syncindex;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.easymock.EasyMock.expect;
+import static org.easymock.EasyMock.expectLastCall;
+
+import com.google.gerrit.server.git.WorkQueue;
+
+import org.easymock.EasyMockSupport;
+import org.junit.Before;
+import org.junit.Test;
+
+public class SyncIndexExecutorProviderTest extends EasyMockSupport {
+ private WorkQueue.Executor executorMock;
+ private SyncIndexExecutorProvider syncIndexExecutorProvider;
+
+ @Before
+ public void setUp() throws Exception {
+ executorMock = createStrictMock(WorkQueue.Executor.class);
+ WorkQueue workQueueMock = createNiceMock(WorkQueue.class);
+ expect(workQueueMock.createQueue(4, "Sync remote index")).andReturn(
+ executorMock);
+ Configuration configMock = createStrictMock(Configuration.class);
+ expect(configMock.getThreadPoolSize()).andReturn(4);
+ replayAll();
+ syncIndexExecutorProvider =
+ new SyncIndexExecutorProvider(workQueueMock, configMock);
+ }
+
+ @Test
+ public void shouldReturnExecutor() throws Exception {
+ assertThat(syncIndexExecutorProvider.get()).isEqualTo(executorMock);
+ }
+
+ @Test
+ public void testStop() throws Exception {
+ resetAll();
+ executorMock.shutdown();
+ expectLastCall().once();
+ executorMock.unregisterWorkQueue();
+ expectLastCall().once();
+ replayAll();
+
+ syncIndexExecutorProvider.start();
+ assertThat(syncIndexExecutorProvider.get()).isEqualTo(executorMock);
+ syncIndexExecutorProvider.stop();
+ verifyAll();
+ assertThat(syncIndexExecutorProvider.get()).isNull();
+ }
+}
diff --git a/src/test/java/com/ericsson/gerrit/plugins/syncindex/SyncIndexRestApiServletTest.java b/src/test/java/com/ericsson/gerrit/plugins/syncindex/SyncIndexRestApiServletTest.java
new file mode 100644
index 0000000..2dbdd83
--- /dev/null
+++ b/src/test/java/com/ericsson/gerrit/plugins/syncindex/SyncIndexRestApiServletTest.java
@@ -0,0 +1,168 @@
+// Copyright (C) 2016 Ericsson
+//
+// 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.ericsson.gerrit.plugins.syncindex;
+
+import static org.easymock.EasyMock.expect;
+import static org.easymock.EasyMock.expectLastCall;
+
+import com.google.gerrit.common.TimeUtil;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.server.ChangeAccess;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.index.ChangeIndexer;
+import com.google.gwtorm.client.KeyUtil;
+import com.google.gwtorm.server.OrmException;
+import com.google.gwtorm.server.SchemaFactory;
+import com.google.gwtorm.server.StandardKeyEncoder;
+
+import org.easymock.EasyMockSupport;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+import java.io.IOException;
+
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+public class SyncIndexRestApiServletTest extends EasyMockSupport {
+ private static final boolean CHANGE_EXISTS = true;
+ private static final boolean CHANGE_DOES_NOT_EXIST = false;
+ private static final boolean DO_NOT_THROW_IO_EXCEPTION = false;
+ private static final boolean DO_NOT_THROW_ORM_EXCEPTION = false;
+ private static final boolean THROW_IO_EXCEPTION = true;
+ private static final boolean THROW_ORM_EXCEPTION = true;
+ private static final String CHANGE_NUMBER = "1";
+
+ private ChangeIndexer indexer;
+ private SchemaFactory<ReviewDb> schemaFactory;
+ private SyncIndexRestApiServlet syncIndexRestApiServlet;
+ private HttpServletRequest req;
+ private HttpServletResponse rsp;
+ private Change.Id id;
+
+ @BeforeClass
+ public static void setup() {
+ KeyUtil.setEncoderImpl(new StandardKeyEncoder());
+ }
+
+ @Before
+ @SuppressWarnings("unchecked")
+ public void setUpMocks() {
+ indexer = createNiceMock(ChangeIndexer.class);
+ schemaFactory = createNiceMock(SchemaFactory.class);
+ req = createNiceMock(HttpServletRequest.class);
+ rsp = createNiceMock(HttpServletResponse.class);
+ syncIndexRestApiServlet =
+ new SyncIndexRestApiServlet(indexer, schemaFactory);
+ id = Change.Id.parse(CHANGE_NUMBER);
+
+ expect(req.getPathInfo()).andReturn("/index/" + CHANGE_NUMBER);
+ }
+
+ @Test
+ public void changeIsIndexed() throws Exception {
+ setupPostMocks(CHANGE_EXISTS);
+ verifyPost();
+ }
+
+ @Test
+ public void changeToIndexDoNotExist() throws Exception {
+ setupPostMocks(CHANGE_DOES_NOT_EXIST);
+ verifyPost();
+ }
+
+ @Test
+ public void schemaThrowsExceptionWhenLookingUpForChange() throws Exception {
+ setupPostMocks(CHANGE_EXISTS, THROW_ORM_EXCEPTION);
+ verifyPost();
+ }
+
+ @Test
+ public void indexerThrowsIOExceptionTryingToIndexChange() throws Exception {
+ setupPostMocks(CHANGE_EXISTS, DO_NOT_THROW_ORM_EXCEPTION,
+ THROW_IO_EXCEPTION);
+ verifyPost();
+ }
+
+ @Test
+ public void changeIsDeletedFromIndex() throws Exception {
+ setupDeleteMocks(DO_NOT_THROW_IO_EXCEPTION);
+ verifyDelete();
+ }
+
+ @Test
+ public void indexerThrowsExceptionTryingToDeleteChange() throws Exception {
+ setupDeleteMocks(THROW_IO_EXCEPTION);
+ verifyDelete();
+ }
+
+ private void setupPostMocks(boolean changeExist) throws Exception {
+ setupPostMocks(changeExist, DO_NOT_THROW_ORM_EXCEPTION,
+ DO_NOT_THROW_IO_EXCEPTION);
+ }
+
+ private void setupPostMocks(boolean changeExist, boolean ormException)
+ throws OrmException, IOException {
+ setupPostMocks(changeExist, ormException, DO_NOT_THROW_IO_EXCEPTION);
+ }
+
+ private void setupPostMocks(boolean changeExist, boolean ormException,
+ boolean ioException) throws OrmException, IOException {
+ if (ormException) {
+ expect(schemaFactory.open()).andThrow(new OrmException(""));
+ } else {
+ ReviewDb db = createNiceMock(ReviewDb.class);
+ expect(schemaFactory.open()).andReturn(db);
+ ChangeAccess ca = createNiceMock(ChangeAccess.class);
+ expect(db.changes()).andReturn(ca);
+
+ if (changeExist) {
+ Change change = new Change(null, id, null, null, TimeUtil.nowTs());
+ expect(ca.get(id)).andReturn(change);
+ indexer.index(db, change);
+ if (ioException) {
+ expectLastCall().andThrow(new IOException());
+ } else {
+ expectLastCall().once();
+ }
+ } else {
+ expect(ca.get(id)).andReturn(null);
+ }
+ }
+ replayAll();
+ }
+
+ private void verifyPost() throws IOException, ServletException {
+ syncIndexRestApiServlet.doPost(req, rsp);
+ verifyAll();
+ }
+
+ private void setupDeleteMocks(boolean exception) throws IOException {
+ indexer.delete(id);
+ if (exception) {
+ expectLastCall().andThrow(new IOException());
+ } else {
+ expectLastCall().once();
+ }
+ replayAll();
+ }
+
+ private void verifyDelete() throws IOException, ServletException {
+ syncIndexRestApiServlet.doDelete(req, rsp);
+ verifyAll();
+ }
+}