Merge branch 'stable-2.16' into stable-3.0

* stable-2.16:
  Documentation/user-inline-edit: Remove reference to missing image
  e2e-tests: Add support for automated project names
  e2e-tests: Support non-core JAVA_OPTS overriding
  ElasticChangeIndex: Fix typo in comment

Change-Id: I6a78090f800bad97e8541d3288087890e51e74d5
diff --git a/java/com/google/gerrit/elasticsearch/AbstractElasticIndex.java b/java/com/google/gerrit/elasticsearch/AbstractElasticIndex.java
index ac60ddc..6ca3a49 100644
--- a/java/com/google/gerrit/elasticsearch/AbstractElasticIndex.java
+++ b/java/com/google/gerrit/elasticsearch/AbstractElasticIndex.java
@@ -15,14 +15,16 @@
 package com.google.gerrit.elasticsearch;
 
 import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.collect.ImmutableList.toImmutableList;
 import static com.google.gson.FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES;
 import static java.nio.charset.StandardCharsets.UTF_8;
 
 import com.google.common.collect.ArrayListMultimap;
-import com.google.common.collect.FluentIterable;
+import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.ListMultimap;
 import com.google.common.collect.Lists;
+import com.google.common.collect.Streams;
 import com.google.common.flogger.FluentLogger;
 import com.google.common.io.CharStreams;
 import com.google.gerrit.common.Nullable;
@@ -30,6 +32,7 @@
 import com.google.gerrit.elasticsearch.builders.QueryBuilder;
 import com.google.gerrit.elasticsearch.builders.SearchSourceBuilder;
 import com.google.gerrit.elasticsearch.bulk.DeleteRequest;
+import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.index.FieldDef;
 import com.google.gerrit.index.FieldType;
 import com.google.gerrit.index.Index;
@@ -37,8 +40,12 @@
 import com.google.gerrit.index.Schema;
 import com.google.gerrit.index.query.DataSource;
 import com.google.gerrit.index.query.FieldBundle;
+import com.google.gerrit.index.query.ListResultSet;
 import com.google.gerrit.index.query.Predicate;
 import com.google.gerrit.index.query.QueryParseException;
+import com.google.gerrit.index.query.ResultSet;
+import com.google.gerrit.proto.Protos;
+import com.google.gerrit.reviewdb.converter.ProtoConverter;
 import com.google.gerrit.server.config.SitePaths;
 import com.google.gerrit.server.index.IndexUtils;
 import com.google.gson.Gson;
@@ -47,9 +54,7 @@
 import com.google.gson.JsonElement;
 import com.google.gson.JsonObject;
 import com.google.gson.JsonParser;
-import com.google.gwtorm.protobuf.ProtobufCodec;
-import com.google.gwtorm.server.OrmException;
-import com.google.gwtorm.server.ResultSet;
+import com.google.protobuf.MessageLite;
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.InputStreamReader;
@@ -59,7 +64,6 @@
 import java.sql.Timestamp;
 import java.util.Collections;
 import java.util.HashMap;
-import java.util.Iterator;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
@@ -88,14 +92,22 @@
   }
 
   protected static <T> List<T> decodeProtos(
-      JsonObject doc, String fieldName, ProtobufCodec<T> codec) {
+      JsonObject doc, String fieldName, ProtoConverter<?, T> converter) {
     JsonArray field = doc.getAsJsonArray(fieldName);
     if (field == null) {
       return null;
     }
-    return FluentIterable.from(field)
-        .transform(i -> codec.decode(decodeBase64(i.getAsString())))
-        .toList();
+    return Streams.stream(field)
+        .map(JsonElement::getAsString)
+        .map(AbstractElasticIndex::decodeBase64)
+        .map(bytes -> parseProtoFrom(bytes, converter))
+        .collect(toImmutableList());
+  }
+
+  protected static <P extends MessageLite, T> T parseProtoFrom(
+      byte[] bytes, ProtoConverter<P, T> converter) {
+    P message = Protos.parseUnchecked(converter.getParser(), bytes);
+    return converter.fromProto(message);
   }
 
   static String getContent(Response response) throws IOException {
@@ -159,23 +171,23 @@
   }
 
   @Override
-  public void markReady(boolean ready) throws IOException {
+  public void markReady(boolean ready) {
     IndexUtils.setReady(sitePaths, indexNameRaw, schema.getVersion(), ready);
   }
 
   @Override
-  public void delete(K id) throws IOException {
+  public void delete(K id) {
     String uri = getURI(type, BULK);
     Response response = postRequest(uri, getDeleteActions(id), getRefreshParam());
     int statusCode = response.getStatusLine().getStatusCode();
     if (statusCode != HttpStatus.SC_OK) {
-      throw new IOException(
+      throw new StorageException(
           String.format("Failed to delete %s from index %s: %s", id, indexName, statusCode));
     }
   }
 
   @Override
-  public void deleteAll() throws IOException {
+  public void deleteAll() {
     // Delete the index, if it exists.
     String endpoint = indexName + client.adapter().indicesExistParams();
     Response response = performRequest("HEAD", endpoint);
@@ -184,7 +196,7 @@
       response = performRequest("DELETE", indexName);
       statusCode = response.getStatusLine().getStatusCode();
       if (statusCode != HttpStatus.SC_OK) {
-        throw new IOException(
+        throw new StorageException(
             String.format("Failed to delete index %s: %s", indexName, statusCode));
       }
     }
@@ -197,7 +209,7 @@
     statusCode = response.getStatusLine().getStatusCode();
     if (statusCode != HttpStatus.SC_OK) {
       String error = String.format("Failed to create index %s: %s", indexName, statusCode);
-      throw new IOException(error);
+      throw new StorageException(error);
     }
   }
 
@@ -303,22 +315,25 @@
     return sortArray;
   }
 
-  protected String getURI(String type, String request) throws UnsupportedEncodingException {
-    String encodedIndexName = URLEncoder.encode(indexName, UTF_8.toString());
-    if (SEARCH.equals(request) && client.adapter().omitType()) {
-      return encodedIndexName + "/" + request;
+  protected String getURI(String type, String request) {
+    try {
+      String encodedIndexName = URLEncoder.encode(indexName, UTF_8.toString());
+      if (SEARCH.equals(request) && client.adapter().omitType()) {
+        return encodedIndexName + "/" + request;
+      }
+      String encodedTypeIfAny =
+          client.adapter().omitType() ? "" : "/" + URLEncoder.encode(type, UTF_8.toString());
+      return encodedIndexName + encodedTypeIfAny + "/" + request;
+    } catch (UnsupportedEncodingException e) {
+      throw new StorageException(e);
     }
-    String encodedTypeIfAny =
-        client.adapter().omitType() ? "" : "/" + URLEncoder.encode(type, UTF_8.toString());
-    return encodedIndexName + encodedTypeIfAny + "/" + request;
   }
 
-  protected Response postRequest(String uri, Object payload) throws IOException {
+  protected Response postRequest(String uri, Object payload) {
     return performRequest("POST", uri, payload);
   }
 
-  protected Response postRequest(String uri, Object payload, Map<String, String> params)
-      throws IOException {
+  protected Response postRequest(String uri, Object payload, Map<String, String> params) {
     return performRequest("POST", uri, payload, params);
   }
 
@@ -326,18 +341,16 @@
     return target.substring(0, target.length() - 1) + "," + addition.substring(1);
   }
 
-  private Response performRequest(String method, String uri) throws IOException {
+  private Response performRequest(String method, String uri) {
     return performRequest(method, uri, null);
   }
 
-  private Response performRequest(String method, String uri, @Nullable Object payload)
-      throws IOException {
+  private Response performRequest(String method, String uri, @Nullable Object payload) {
     return performRequest(method, uri, payload, Collections.emptyMap());
   }
 
   private Response performRequest(
-      String method, String uri, @Nullable Object payload, Map<String, String> params)
-      throws IOException {
+      String method, String uri, @Nullable Object payload, Map<String, String> params) {
     Request request = new Request(method, uri.startsWith("/") ? uri : "/" + uri);
     if (payload != null) {
       String payloadStr = payload instanceof String ? (String) payload : payload.toString();
@@ -346,7 +359,11 @@
     for (Map.Entry<String, String> entry : params.entrySet()) {
       request.addParameter(entry.getKey(), entry.getValue());
     }
-    return client.get().performRequest(request);
+    try {
+      return client.get().performRequest(request);
+    } catch (IOException e) {
+      throw new StorageException(e);
+    }
   }
 
   protected class ElasticQuerySource implements DataSource<V> {
@@ -374,18 +391,17 @@
     }
 
     @Override
-    public ResultSet<V> read() throws OrmException {
+    public ResultSet<V> read() {
       return readImpl(doc -> AbstractElasticIndex.this.fromDocument(doc, opts.fields()));
     }
 
     @Override
-    public ResultSet<FieldBundle> readRaw() throws OrmException {
+    public ResultSet<FieldBundle> readRaw() {
       return readImpl(AbstractElasticIndex.this::toFieldBundle);
     }
 
-    private <T> ResultSet<T> readImpl(Function<JsonObject, T> mapper) throws OrmException {
+    private <T> ResultSet<T> readImpl(Function<JsonObject, T> mapper) {
       try {
-        List<T> results = Collections.emptyList();
         String uri = getURI(index, SEARCH);
         Response response =
             performRequest(HttpPost.METHOD_NAME, uri, search, Collections.emptyMap());
@@ -396,36 +412,21 @@
               new JsonParser().parse(content).getAsJsonObject().getAsJsonObject("hits");
           if (obj.get("hits") != null) {
             JsonArray json = obj.getAsJsonArray("hits");
-            results = Lists.newArrayListWithCapacity(json.size());
+            ImmutableList.Builder<T> results = ImmutableList.builderWithExpectedSize(json.size());
             for (int i = 0; i < json.size(); i++) {
               T mapperResult = mapper.apply(json.get(i).getAsJsonObject());
               if (mapperResult != null) {
                 results.add(mapperResult);
               }
             }
+            return new ListResultSet<>(results.build());
           }
         } else {
           logger.atSevere().log(statusLine.getReasonPhrase());
         }
-        final List<T> r = Collections.unmodifiableList(results);
-        return new ResultSet<T>() {
-          @Override
-          public Iterator<T> iterator() {
-            return r.iterator();
-          }
-
-          @Override
-          public List<T> toList() {
-            return r;
-          }
-
-          @Override
-          public void close() {
-            // Do nothing.
-          }
-        };
+        return new ListResultSet<>(ImmutableList.of());
       } catch (IOException e) {
-        throw new OrmException(e);
+        throw new StorageException(e);
       }
     }
   }
diff --git a/java/com/google/gerrit/elasticsearch/BUILD b/java/com/google/gerrit/elasticsearch/BUILD
index c71d536..a9b145b 100644
--- a/java/com/google/gerrit/elasticsearch/BUILD
+++ b/java/com/google/gerrit/elasticsearch/BUILD
@@ -6,16 +6,17 @@
     visibility = ["//visibility:public"],
     deps = [
         "//java/com/google/gerrit/common:annotations",
+        "//java/com/google/gerrit/exceptions",
         "//java/com/google/gerrit/extensions:api",
         "//java/com/google/gerrit/index",
         "//java/com/google/gerrit/index:query_exception",
         "//java/com/google/gerrit/index/project",
         "//java/com/google/gerrit/lifecycle",
+        "//java/com/google/gerrit/proto",
         "//java/com/google/gerrit/reviewdb:server",
         "//java/com/google/gerrit/server",
         "//lib:gson",
         "//lib:guava",
-        "//lib:gwtorm",
         "//lib:protobuf",
         "//lib/commons:codec",
         "//lib/commons:lang",
diff --git a/java/com/google/gerrit/elasticsearch/ElasticAccountIndex.java b/java/com/google/gerrit/elasticsearch/ElasticAccountIndex.java
index d0b70ae..c25aa90 100644
--- a/java/com/google/gerrit/elasticsearch/ElasticAccountIndex.java
+++ b/java/com/google/gerrit/elasticsearch/ElasticAccountIndex.java
@@ -20,6 +20,7 @@
 import com.google.gerrit.elasticsearch.bulk.BulkRequest;
 import com.google.gerrit.elasticsearch.bulk.IndexRequest;
 import com.google.gerrit.elasticsearch.bulk.UpdateRequest;
+import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.index.QueryOptions;
 import com.google.gerrit.index.Schema;
 import com.google.gerrit.index.query.DataSource;
@@ -38,7 +39,6 @@
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.assistedinject.Assisted;
-import java.io.IOException;
 import java.util.Set;
 import org.apache.http.HttpStatus;
 import org.elasticsearch.client.Response;
@@ -73,7 +73,7 @@
   }
 
   @Override
-  public void replace(AccountState as) throws IOException {
+  public void replace(AccountState as) {
     BulkRequest bulk =
         new IndexRequest(getId(as), indexName, type, client.adapter())
             .add(new UpdateRequest<>(schema, as));
@@ -82,7 +82,7 @@
     Response response = postRequest(uri, bulk, getRefreshParam());
     int statusCode = response.getStatusLine().getStatusCode();
     if (statusCode != HttpStatus.SC_OK) {
-      throw new IOException(
+      throw new StorageException(
           String.format(
               "Failed to replace account %s in index %s: %s",
               as.getAccount().getId(), indexName, statusCode));
diff --git a/java/com/google/gerrit/elasticsearch/ElasticChangeIndex.java b/java/com/google/gerrit/elasticsearch/ElasticChangeIndex.java
index f64896f..480a9c1 100644
--- a/java/com/google/gerrit/elasticsearch/ElasticChangeIndex.java
+++ b/java/com/google/gerrit/elasticsearch/ElasticChangeIndex.java
@@ -14,9 +14,6 @@
 
 package com.google.gerrit.elasticsearch;
 
-import static com.google.gerrit.reviewdb.server.ReviewDbCodecs.APPROVAL_CODEC;
-import static com.google.gerrit.reviewdb.server.ReviewDbCodecs.CHANGE_CODEC;
-import static com.google.gerrit.reviewdb.server.ReviewDbCodecs.PATCH_SET_CODEC;
 import static com.google.gerrit.server.index.change.ChangeIndexRewriter.CLOSED_STATUSES;
 import static com.google.gerrit.server.index.change.ChangeIndexRewriter.OPEN_STATUSES;
 import static java.nio.charset.StandardCharsets.UTF_8;
@@ -35,6 +32,7 @@
 import com.google.gerrit.elasticsearch.bulk.DeleteRequest;
 import com.google.gerrit.elasticsearch.bulk.IndexRequest;
 import com.google.gerrit.elasticsearch.bulk.UpdateRequest;
+import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.index.QueryOptions;
 import com.google.gerrit.index.Schema;
 import com.google.gerrit.index.query.DataSource;
@@ -42,9 +40,10 @@
 import com.google.gerrit.index.query.QueryParseException;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.Change.Id;
 import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.reviewdb.converter.ChangeProtoConverter;
+import com.google.gerrit.reviewdb.converter.PatchSetApprovalProtoConverter;
+import com.google.gerrit.reviewdb.converter.PatchSetProtoConverter;
 import com.google.gerrit.server.ReviewerByEmailSet;
 import com.google.gerrit.server.ReviewerSet;
 import com.google.gerrit.server.StarredChangesUtil;
@@ -58,11 +57,8 @@
 import com.google.gson.JsonArray;
 import com.google.gson.JsonElement;
 import com.google.gson.JsonObject;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
-import com.google.inject.Provider;
 import com.google.inject.assistedinject.Assisted;
-import java.io.IOException;
 import java.util.Collections;
 import java.util.List;
 import java.util.Optional;
@@ -91,40 +87,33 @@
   private static final String CLOSED_CHANGES = "closed_" + CHANGES;
 
   private final ChangeMapping mapping;
-  private final Provider<ReviewDb> db;
   private final ChangeData.Factory changeDataFactory;
   private final Schema<ChangeData> schema;
 
   @Inject
   ElasticChangeIndex(
       ElasticConfiguration cfg,
-      Provider<ReviewDb> db,
       ChangeData.Factory changeDataFactory,
       SitePaths sitePaths,
       ElasticRestClientProvider clientBuilder,
       @Assisted Schema<ChangeData> schema) {
     super(cfg, sitePaths, schema, clientBuilder, CHANGES);
-    this.db = db;
     this.changeDataFactory = changeDataFactory;
     this.schema = schema;
     mapping = new ChangeMapping(schema, client.adapter());
   }
 
   @Override
-  public void replace(ChangeData cd) throws IOException {
+  public void replace(ChangeData cd) {
     String deleteIndex;
     String insertIndex;
 
-    try {
-      if (cd.change().getStatus().isOpen()) {
-        insertIndex = OPEN_CHANGES;
-        deleteIndex = CLOSED_CHANGES;
-      } else {
-        insertIndex = CLOSED_CHANGES;
-        deleteIndex = OPEN_CHANGES;
-      }
-    } catch (OrmException e) {
-      throw new IOException(e);
+    if (cd.change().isNew()) {
+      insertIndex = OPEN_CHANGES;
+      deleteIndex = CLOSED_CHANGES;
+    } else {
+      insertIndex = CLOSED_CHANGES;
+      deleteIndex = OPEN_CHANGES;
     }
 
     ElasticQueryAdapter adapter = client.adapter();
@@ -139,7 +128,7 @@
     Response response = postRequest(uri, bulk, getRefreshParam());
     int statusCode = response.getStatusLine().getStatusCode();
     if (statusCode != HttpStatus.SC_OK) {
-      throw new IOException(
+      throw new StorageException(
           String.format(
               "Failed to replace change %s in index %s: %s", cd.getId(), indexName, statusCode));
     }
@@ -185,7 +174,7 @@
   }
 
   @Override
-  protected String getDeleteActions(Id c) {
+  protected String getDeleteActions(Change.Id c) {
     if (!client.adapter().useV5Type()) {
       return delete(client.adapter().getType(), c);
     }
@@ -218,21 +207,24 @@
       int id = source.get(ChangeField.LEGACY_ID.getName()).getAsInt();
       // IndexUtils#changeFields ensures either CHANGE or PROJECT is always present.
       String projectName = requireNonNull(source.get(ChangeField.PROJECT.getName()).getAsString());
-      return changeDataFactory.create(
-          db.get(), new Project.NameKey(projectName), new Change.Id(id));
+      return changeDataFactory.create(new Project.NameKey(projectName), new Change.Id(id));
     }
 
     ChangeData cd =
-        changeDataFactory.create(db.get(), CHANGE_CODEC.decode(decodeBase64(c.getAsString())));
+        changeDataFactory.create(
+            parseProtoFrom(decodeBase64(c.getAsString()), ChangeProtoConverter.INSTANCE));
 
     // Any decoding that is done here must also be done in {@link LuceneChangeIndex}.
 
     // Patch sets.
-    cd.setPatchSets(decodeProtos(source, ChangeField.PATCH_SET.getName(), PATCH_SET_CODEC));
+    cd.setPatchSets(
+        decodeProtos(source, ChangeField.PATCH_SET.getName(), PatchSetProtoConverter.INSTANCE));
 
     // Approvals.
     if (source.get(ChangeField.APPROVAL.getName()) != null) {
-      cd.setCurrentApprovals(decodeProtos(source, ChangeField.APPROVAL.getName(), APPROVAL_CODEC));
+      cd.setCurrentApprovals(
+          decodeProtos(
+              source, ChangeField.APPROVAL.getName(), PatchSetApprovalProtoConverter.INSTANCE));
     } else if (fields.contains(ChangeField.APPROVAL.getName())) {
       cd.setCurrentApprovals(Collections.emptyList());
     }
diff --git a/java/com/google/gerrit/elasticsearch/ElasticGroupIndex.java b/java/com/google/gerrit/elasticsearch/ElasticGroupIndex.java
index e74f208..ecda1ee 100644
--- a/java/com/google/gerrit/elasticsearch/ElasticGroupIndex.java
+++ b/java/com/google/gerrit/elasticsearch/ElasticGroupIndex.java
@@ -18,6 +18,7 @@
 import com.google.gerrit.elasticsearch.bulk.BulkRequest;
 import com.google.gerrit.elasticsearch.bulk.IndexRequest;
 import com.google.gerrit.elasticsearch.bulk.UpdateRequest;
+import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.index.QueryOptions;
 import com.google.gerrit.index.Schema;
 import com.google.gerrit.index.query.DataSource;
@@ -36,7 +37,6 @@
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.assistedinject.Assisted;
-import java.io.IOException;
 import java.util.Set;
 import org.apache.http.HttpStatus;
 import org.elasticsearch.client.Response;
@@ -71,7 +71,7 @@
   }
 
   @Override
-  public void replace(InternalGroup group) throws IOException {
+  public void replace(InternalGroup group) {
     BulkRequest bulk =
         new IndexRequest(getId(group), indexName, type, client.adapter())
             .add(new UpdateRequest<>(schema, group));
@@ -80,7 +80,7 @@
     Response response = postRequest(uri, bulk, getRefreshParam());
     int statusCode = response.getStatusLine().getStatusCode();
     if (statusCode != HttpStatus.SC_OK) {
-      throw new IOException(
+      throw new StorageException(
           String.format(
               "Failed to replace group %s in index %s: %s",
               group.getGroupUUID().get(), indexName, statusCode));
diff --git a/java/com/google/gerrit/elasticsearch/ElasticIndexVersionManager.java b/java/com/google/gerrit/elasticsearch/ElasticIndexVersionManager.java
index 8011efa..b9d86d5 100644
--- a/java/com/google/gerrit/elasticsearch/ElasticIndexVersionManager.java
+++ b/java/com/google/gerrit/elasticsearch/ElasticIndexVersionManager.java
@@ -17,7 +17,6 @@
 import com.google.common.base.Strings;
 import com.google.common.flogger.FluentLogger;
 import com.google.common.primitives.Ints;
-import com.google.gerrit.extensions.registration.DynamicSet;
 import com.google.gerrit.index.Index;
 import com.google.gerrit.index.IndexDefinition;
 import com.google.gerrit.index.Schema;
@@ -26,6 +25,7 @@
 import com.google.gerrit.server.index.GerritIndexStatus;
 import com.google.gerrit.server.index.OnlineUpgradeListener;
 import com.google.gerrit.server.index.VersionManager;
+import com.google.gerrit.server.plugincontext.PluginSetContext;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 import java.io.IOException;
@@ -45,7 +45,7 @@
   ElasticIndexVersionManager(
       @GerritServerConfig Config cfg,
       SitePaths sitePaths,
-      DynamicSet<OnlineUpgradeListener> listeners,
+      PluginSetContext<OnlineUpgradeListener> listeners,
       Collection<IndexDefinition<?, ?, ?>> defs,
       ElasticIndexVersionDiscovery versionDiscovery) {
     super(sitePaths, listeners, defs, VersionManager.getOnlineUpgrade(cfg));
diff --git a/java/com/google/gerrit/elasticsearch/ElasticProjectIndex.java b/java/com/google/gerrit/elasticsearch/ElasticProjectIndex.java
index 8510559..daf3702 100644
--- a/java/com/google/gerrit/elasticsearch/ElasticProjectIndex.java
+++ b/java/com/google/gerrit/elasticsearch/ElasticProjectIndex.java
@@ -18,6 +18,7 @@
 import com.google.gerrit.elasticsearch.bulk.BulkRequest;
 import com.google.gerrit.elasticsearch.bulk.IndexRequest;
 import com.google.gerrit.elasticsearch.bulk.UpdateRequest;
+import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.index.QueryOptions;
 import com.google.gerrit.index.Schema;
 import com.google.gerrit.index.project.ProjectData;
@@ -36,7 +37,6 @@
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.assistedinject.Assisted;
-import java.io.IOException;
 import java.util.Set;
 import org.apache.http.HttpStatus;
 import org.elasticsearch.client.Response;
@@ -71,7 +71,7 @@
   }
 
   @Override
-  public void replace(ProjectData projectState) throws IOException {
+  public void replace(ProjectData projectState) {
     BulkRequest bulk =
         new IndexRequest(projectState.getProject().getName(), indexName, type, client.adapter())
             .add(new UpdateRequest<>(schema, projectState));
@@ -80,7 +80,7 @@
     Response response = postRequest(uri, bulk, getRefreshParam());
     int statusCode = response.getStatusLine().getStatusCode();
     if (statusCode != HttpStatus.SC_OK) {
-      throw new IOException(
+      throw new StorageException(
           String.format(
               "Failed to replace project %s in index %s: %s",
               projectState.getProject().getName(), indexName, statusCode));
diff --git a/java/com/google/gerrit/server/query/change/FileWithNoExtensionInElasticPredicate.java b/java/com/google/gerrit/server/query/change/FileWithNoExtensionInElasticPredicate.java
new file mode 100644
index 0000000..d886baf
--- /dev/null
+++ b/java/com/google/gerrit/server/query/change/FileWithNoExtensionInElasticPredicate.java
@@ -0,0 +1,37 @@
+// Copyright (C) 2019 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.google.gerrit.server.query.change;
+
+import com.google.gerrit.index.query.PostFilterPredicate;
+import com.google.gerrit.server.index.change.ChangeField;
+
+public class FileWithNoExtensionInElasticPredicate extends PostFilterPredicate<ChangeData> {
+
+  private static final String NO_EXT = "";
+
+  public FileWithNoExtensionInElasticPredicate() {
+    super(ChangeField.EXTENSION.getName(), NO_EXT);
+  }
+
+  @Override
+  public boolean match(ChangeData cd) {
+    return ChangeField.getExtensions(cd).contains(NO_EXT);
+  }
+
+  @Override
+  public int getCost() {
+    return 1;
+  }
+}
diff --git a/javatests/com/google/gerrit/acceptance/pgm/ElasticReindexIT.java b/javatests/com/google/gerrit/acceptance/pgm/ElasticReindexIT.java
index 181f6f0..5bbaf40 100644
--- a/javatests/com/google/gerrit/acceptance/pgm/ElasticReindexIT.java
+++ b/javatests/com/google/gerrit/acceptance/pgm/ElasticReindexIT.java
@@ -41,7 +41,7 @@
   }
 
   @Override
-  public void configureIndex(Injector injector) throws Exception {
+  public void configureIndex(Injector injector) {
     createAllIndexes(injector);
   }
 
diff --git a/javatests/com/google/gerrit/acceptance/ssh/ElasticIndexIT.java b/javatests/com/google/gerrit/acceptance/ssh/ElasticIndexIT.java
index 0d55d7a..0d16305 100644
--- a/javatests/com/google/gerrit/acceptance/ssh/ElasticIndexIT.java
+++ b/javatests/com/google/gerrit/acceptance/ssh/ElasticIndexIT.java
@@ -40,7 +40,7 @@
   }
 
   @Override
-  public void configureIndex(Injector injector) throws Exception {
+  public void configureIndex(Injector injector) {
     createAllIndexes(injector);
   }
 }
diff --git a/javatests/com/google/gerrit/elasticsearch/BUILD b/javatests/com/google/gerrit/elasticsearch/BUILD
index 8bc738f..ecd5c11 100644
--- a/javatests/com/google/gerrit/elasticsearch/BUILD
+++ b/javatests/com/google/gerrit/elasticsearch/BUILD
@@ -94,8 +94,10 @@
     tags = ["elastic"],
     deps = [
         "//java/com/google/gerrit/elasticsearch",
+        "//java/com/google/gerrit/testing:gerrit-test-util",
         "//lib:guava",
         "//lib/guice",
+        "//lib/httpcomponents:httpcore",
         "//lib/jgit/org.eclipse.jgit:jgit",
         "//lib/truth",
     ],
diff --git a/javatests/com/google/gerrit/elasticsearch/ElasticConfigurationTest.java b/javatests/com/google/gerrit/elasticsearch/ElasticConfigurationTest.java
index ff7b5ca..9ce1456 100644
--- a/javatests/com/google/gerrit/elasticsearch/ElasticConfigurationTest.java
+++ b/javatests/com/google/gerrit/elasticsearch/ElasticConfigurationTest.java
@@ -24,16 +24,14 @@
 import static java.util.stream.Collectors.toList;
 
 import com.google.common.collect.ImmutableList;
+import com.google.gerrit.testing.GerritBaseTests;
 import com.google.inject.ProvisionException;
 import java.util.Arrays;
+import org.apache.http.HttpHost;
 import org.eclipse.jgit.lib.Config;
-import org.junit.Rule;
 import org.junit.Test;
-import org.junit.rules.ExpectedException;
 
-public class ElasticConfigurationTest {
-  @Rule public ExpectedException exception = ExpectedException.none();
-
+public class ElasticConfigurationTest extends GerritBaseTests {
   @Test
   public void singleServerNoOtherConfig() throws Exception {
     Config cfg = newConfig();
@@ -119,7 +117,7 @@
   }
 
   private void assertHosts(ElasticConfiguration cfg, Object... hostURIs) throws Exception {
-    assertThat(Arrays.asList(cfg.getHosts()).stream().map(h -> h.toURI()).collect(toList()))
+    assertThat(Arrays.asList(cfg.getHosts()).stream().map(HttpHost::toURI).collect(toList()))
         .containsExactly(hostURIs);
   }
 
diff --git a/javatests/com/google/gerrit/elasticsearch/ElasticTestUtils.java b/javatests/com/google/gerrit/elasticsearch/ElasticTestUtils.java
index 2d99cbf..2c73149 100644
--- a/javatests/com/google/gerrit/elasticsearch/ElasticTestUtils.java
+++ b/javatests/com/google/gerrit/elasticsearch/ElasticTestUtils.java
@@ -19,7 +19,6 @@
 import com.google.inject.Injector;
 import com.google.inject.Key;
 import com.google.inject.TypeLiteral;
-import java.io.IOException;
 import java.util.Collection;
 import java.util.UUID;
 import org.eclipse.jgit.lib.Config;
@@ -43,7 +42,7 @@
     configure(config, container, prefix, null);
   }
 
-  public static void createAllIndexes(Injector injector) throws IOException {
+  public static void createAllIndexes(Injector injector) {
     Collection<IndexDefinition<?, ?, ?>> indexDefs =
         injector.getInstance(Key.get(new TypeLiteral<Collection<IndexDefinition<?, ?, ?>>>() {}));
     for (IndexDefinition<?, ?, ?> indexDef : indexDefs) {
diff --git a/javatests/com/google/gerrit/elasticsearch/ElasticV5QueryAccountsTest.java b/javatests/com/google/gerrit/elasticsearch/ElasticV5QueryAccountsTest.java
index cb192cd..e34fd21 100644
--- a/javatests/com/google/gerrit/elasticsearch/ElasticV5QueryAccountsTest.java
+++ b/javatests/com/google/gerrit/elasticsearch/ElasticV5QueryAccountsTest.java
@@ -59,6 +59,6 @@
     InMemoryModule.setDefaults(elasticsearchConfig);
     String indicesPrefix = getSanitizedMethodName();
     ElasticTestUtils.configure(elasticsearchConfig, container, indicesPrefix, ElasticVersion.V5_6);
-    return Guice.createInjector(new InMemoryModule(elasticsearchConfig, notesMigration));
+    return Guice.createInjector(new InMemoryModule(elasticsearchConfig));
   }
 }
diff --git a/javatests/com/google/gerrit/elasticsearch/ElasticV5QueryChangesTest.java b/javatests/com/google/gerrit/elasticsearch/ElasticV5QueryChangesTest.java
index c4d0c18..1efaa7e 100644
--- a/javatests/com/google/gerrit/elasticsearch/ElasticV5QueryChangesTest.java
+++ b/javatests/com/google/gerrit/elasticsearch/ElasticV5QueryChangesTest.java
@@ -59,6 +59,6 @@
     InMemoryModule.setDefaults(elasticsearchConfig);
     String indicesPrefix = getSanitizedMethodName();
     ElasticTestUtils.configure(elasticsearchConfig, container, indicesPrefix, ElasticVersion.V5_6);
-    return Guice.createInjector(new InMemoryModule(elasticsearchConfig, notesMigration));
+    return Guice.createInjector(new InMemoryModule(elasticsearchConfig));
   }
 }
diff --git a/javatests/com/google/gerrit/elasticsearch/ElasticV5QueryGroupsTest.java b/javatests/com/google/gerrit/elasticsearch/ElasticV5QueryGroupsTest.java
index f1fb60c..3c20cf0 100644
--- a/javatests/com/google/gerrit/elasticsearch/ElasticV5QueryGroupsTest.java
+++ b/javatests/com/google/gerrit/elasticsearch/ElasticV5QueryGroupsTest.java
@@ -59,6 +59,6 @@
     InMemoryModule.setDefaults(elasticsearchConfig);
     String indicesPrefix = getSanitizedMethodName();
     ElasticTestUtils.configure(elasticsearchConfig, container, indicesPrefix, ElasticVersion.V5_6);
-    return Guice.createInjector(new InMemoryModule(elasticsearchConfig, notesMigration));
+    return Guice.createInjector(new InMemoryModule(elasticsearchConfig));
   }
 }
diff --git a/javatests/com/google/gerrit/elasticsearch/ElasticV5QueryProjectsTest.java b/javatests/com/google/gerrit/elasticsearch/ElasticV5QueryProjectsTest.java
index 4dd0e98..4bf4067 100644
--- a/javatests/com/google/gerrit/elasticsearch/ElasticV5QueryProjectsTest.java
+++ b/javatests/com/google/gerrit/elasticsearch/ElasticV5QueryProjectsTest.java
@@ -59,6 +59,6 @@
     InMemoryModule.setDefaults(elasticsearchConfig);
     String indicesPrefix = getSanitizedMethodName();
     ElasticTestUtils.configure(elasticsearchConfig, container, indicesPrefix, ElasticVersion.V5_6);
-    return Guice.createInjector(new InMemoryModule(elasticsearchConfig, notesMigration));
+    return Guice.createInjector(new InMemoryModule(elasticsearchConfig));
   }
 }
diff --git a/javatests/com/google/gerrit/elasticsearch/ElasticV6QueryAccountsTest.java b/javatests/com/google/gerrit/elasticsearch/ElasticV6QueryAccountsTest.java
index bb79655..d4d321d 100644
--- a/javatests/com/google/gerrit/elasticsearch/ElasticV6QueryAccountsTest.java
+++ b/javatests/com/google/gerrit/elasticsearch/ElasticV6QueryAccountsTest.java
@@ -59,6 +59,6 @@
     InMemoryModule.setDefaults(elasticsearchConfig);
     String indicesPrefix = getSanitizedMethodName();
     ElasticTestUtils.configure(elasticsearchConfig, container, indicesPrefix);
-    return Guice.createInjector(new InMemoryModule(elasticsearchConfig, notesMigration));
+    return Guice.createInjector(new InMemoryModule(elasticsearchConfig));
   }
 }
diff --git a/javatests/com/google/gerrit/elasticsearch/ElasticV6QueryChangesTest.java b/javatests/com/google/gerrit/elasticsearch/ElasticV6QueryChangesTest.java
index 3f59d30..68c4b71 100644
--- a/javatests/com/google/gerrit/elasticsearch/ElasticV6QueryChangesTest.java
+++ b/javatests/com/google/gerrit/elasticsearch/ElasticV6QueryChangesTest.java
@@ -82,6 +82,6 @@
     InMemoryModule.setDefaults(elasticsearchConfig);
     String indicesPrefix = getSanitizedMethodName();
     ElasticTestUtils.configure(elasticsearchConfig, container, indicesPrefix);
-    return Guice.createInjector(new InMemoryModule(elasticsearchConfig, notesMigration));
+    return Guice.createInjector(new InMemoryModule(elasticsearchConfig));
   }
 }
diff --git a/javatests/com/google/gerrit/elasticsearch/ElasticV6QueryGroupsTest.java b/javatests/com/google/gerrit/elasticsearch/ElasticV6QueryGroupsTest.java
index a966dfa..99c07f4 100644
--- a/javatests/com/google/gerrit/elasticsearch/ElasticV6QueryGroupsTest.java
+++ b/javatests/com/google/gerrit/elasticsearch/ElasticV6QueryGroupsTest.java
@@ -59,6 +59,6 @@
     InMemoryModule.setDefaults(elasticsearchConfig);
     String indicesPrefix = getSanitizedMethodName();
     ElasticTestUtils.configure(elasticsearchConfig, container, indicesPrefix);
-    return Guice.createInjector(new InMemoryModule(elasticsearchConfig, notesMigration));
+    return Guice.createInjector(new InMemoryModule(elasticsearchConfig));
   }
 }
diff --git a/javatests/com/google/gerrit/elasticsearch/ElasticV6QueryProjectsTest.java b/javatests/com/google/gerrit/elasticsearch/ElasticV6QueryProjectsTest.java
index 40bee58..89c7774 100644
--- a/javatests/com/google/gerrit/elasticsearch/ElasticV6QueryProjectsTest.java
+++ b/javatests/com/google/gerrit/elasticsearch/ElasticV6QueryProjectsTest.java
@@ -59,6 +59,6 @@
     InMemoryModule.setDefaults(elasticsearchConfig);
     String indicesPrefix = getSanitizedMethodName();
     ElasticTestUtils.configure(elasticsearchConfig, container, indicesPrefix);
-    return Guice.createInjector(new InMemoryModule(elasticsearchConfig, notesMigration));
+    return Guice.createInjector(new InMemoryModule(elasticsearchConfig));
   }
 }
diff --git a/javatests/com/google/gerrit/elasticsearch/ElasticV7QueryAccountsTest.java b/javatests/com/google/gerrit/elasticsearch/ElasticV7QueryAccountsTest.java
index 4a5345a..047e420 100644
--- a/javatests/com/google/gerrit/elasticsearch/ElasticV7QueryAccountsTest.java
+++ b/javatests/com/google/gerrit/elasticsearch/ElasticV7QueryAccountsTest.java
@@ -59,6 +59,6 @@
     InMemoryModule.setDefaults(elasticsearchConfig);
     String indicesPrefix = getSanitizedMethodName();
     ElasticTestUtils.configure(elasticsearchConfig, container, indicesPrefix);
-    return Guice.createInjector(new InMemoryModule(elasticsearchConfig, notesMigration));
+    return Guice.createInjector(new InMemoryModule(elasticsearchConfig));
   }
 }
diff --git a/javatests/com/google/gerrit/elasticsearch/ElasticV7QueryChangesTest.java b/javatests/com/google/gerrit/elasticsearch/ElasticV7QueryChangesTest.java
index 979a7f6..b61bc72 100644
--- a/javatests/com/google/gerrit/elasticsearch/ElasticV7QueryChangesTest.java
+++ b/javatests/com/google/gerrit/elasticsearch/ElasticV7QueryChangesTest.java
@@ -82,6 +82,6 @@
     InMemoryModule.setDefaults(elasticsearchConfig);
     String indicesPrefix = getSanitizedMethodName();
     ElasticTestUtils.configure(elasticsearchConfig, container, indicesPrefix);
-    return Guice.createInjector(new InMemoryModule(elasticsearchConfig, notesMigration));
+    return Guice.createInjector(new InMemoryModule(elasticsearchConfig));
   }
 }
diff --git a/javatests/com/google/gerrit/elasticsearch/ElasticV7QueryGroupsTest.java b/javatests/com/google/gerrit/elasticsearch/ElasticV7QueryGroupsTest.java
index 6b979b3..590a994 100644
--- a/javatests/com/google/gerrit/elasticsearch/ElasticV7QueryGroupsTest.java
+++ b/javatests/com/google/gerrit/elasticsearch/ElasticV7QueryGroupsTest.java
@@ -59,6 +59,6 @@
     InMemoryModule.setDefaults(elasticsearchConfig);
     String indicesPrefix = getSanitizedMethodName();
     ElasticTestUtils.configure(elasticsearchConfig, container, indicesPrefix);
-    return Guice.createInjector(new InMemoryModule(elasticsearchConfig, notesMigration));
+    return Guice.createInjector(new InMemoryModule(elasticsearchConfig));
   }
 }
diff --git a/javatests/com/google/gerrit/elasticsearch/ElasticV7QueryProjectsTest.java b/javatests/com/google/gerrit/elasticsearch/ElasticV7QueryProjectsTest.java
index 4106d8b..cf12d96 100644
--- a/javatests/com/google/gerrit/elasticsearch/ElasticV7QueryProjectsTest.java
+++ b/javatests/com/google/gerrit/elasticsearch/ElasticV7QueryProjectsTest.java
@@ -59,6 +59,6 @@
     InMemoryModule.setDefaults(elasticsearchConfig);
     String indicesPrefix = getSanitizedMethodName();
     ElasticTestUtils.configure(elasticsearchConfig, container, indicesPrefix);
-    return Guice.createInjector(new InMemoryModule(elasticsearchConfig, notesMigration));
+    return Guice.createInjector(new InMemoryModule(elasticsearchConfig));
   }
 }
diff --git a/javatests/com/google/gerrit/elasticsearch/ElasticVersionTest.java b/javatests/com/google/gerrit/elasticsearch/ElasticVersionTest.java
index 9786e37..18114e6 100644
--- a/javatests/com/google/gerrit/elasticsearch/ElasticVersionTest.java
+++ b/javatests/com/google/gerrit/elasticsearch/ElasticVersionTest.java
@@ -16,13 +16,10 @@
 
 import static com.google.common.truth.Truth.assertThat;
 
-import org.junit.Rule;
+import com.google.gerrit.testing.GerritBaseTests;
 import org.junit.Test;
-import org.junit.rules.ExpectedException;
 
-public class ElasticVersionTest {
-  @Rule public ExpectedException exception = ExpectedException.none();
-
+public class ElasticVersionTest extends GerritBaseTests {
   @Test
   public void supportedVersion() throws Exception {
     assertThat(ElasticVersion.forVersion("5.6.0")).isEqualTo(ElasticVersion.V5_6);