Merge branch 'stable-3.3' into master

* stable-3.3:
  Bazel: Perform some clean ups after rules_closure removal
  Update JGit to 5d925ecbb
  Move java_tools javac argument into tools parameter.
  Exempt commons-io from library compliance
  Bump commons-io version to 2.4
  Update git submodules
  Update git submodules
  Update git submodules
  Update git submodules
  Compact the REST-API output JSON unconditionally
  Elasticsearch: Discontinue EOL versions 7.0 and 7.1 support
  Elasticsearch: Discontinue EOL version 6.8 thus whole V6 support
  Update git submodules
  Avoid logging "length=0" exception

Change-Id: I28ee5f258c2072886d1b2674f017b76d9da0757d
diff --git a/java/com/google/gerrit/elasticsearch/AbstractElasticIndex.java b/java/com/google/gerrit/elasticsearch/AbstractElasticIndex.java
index e56f470..44a377a 100644
--- a/java/com/google/gerrit/elasticsearch/AbstractElasticIndex.java
+++ b/java/com/google/gerrit/elasticsearch/AbstractElasticIndex.java
@@ -84,6 +84,9 @@
   protected static final String BULK = "_bulk";
   protected static final String MAPPINGS = "mappings";
   protected static final String ORDER = "order";
+  protected static final String DESC_SORT_ORDER = "desc";
+  protected static final String ASC_SORT_ORDER = "asc";
+  protected static final String UNMAPPED_TYPE = "unmapped_type";
   protected static final String SEARCH = "_search";
   protected static final String SETTINGS = "settings";
 
@@ -288,7 +291,7 @@
 
   protected JsonArray getSortArray(String idFieldName) {
     JsonObject properties = new JsonObject();
-    properties.addProperty(ORDER, "asc");
+    properties.addProperty(ORDER, ASC_SORT_ORDER);
 
     JsonArray sortArray = new JsonArray();
     addNamedElement(idFieldName, properties, sortArray);
diff --git a/java/com/google/gerrit/elasticsearch/ElasticChangeIndex.java b/java/com/google/gerrit/elasticsearch/ElasticChangeIndex.java
index 625a598..969ffa5 100644
--- a/java/com/google/gerrit/elasticsearch/ElasticChangeIndex.java
+++ b/java/com/google/gerrit/elasticsearch/ElasticChangeIndex.java
@@ -57,6 +57,9 @@
 import com.google.gson.JsonObject;
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
+import java.sql.Timestamp;
+import java.time.Instant;
+import java.time.format.DateTimeFormatter;
 import java.util.Collections;
 import java.util.Optional;
 import java.util.Set;
@@ -133,14 +136,24 @@
 
   private JsonArray getSortArray() {
     JsonObject properties = new JsonObject();
-    properties.addProperty(ORDER, "desc");
+    properties.addProperty(ORDER, DESC_SORT_ORDER);
 
     JsonArray sortArray = new JsonArray();
     addNamedElement(ChangeField.UPDATED.getName(), properties, sortArray);
+    addNamedElement(ChangeField.MERGED_ON.getName(), getMergedOnSortOptions(), sortArray);
     addNamedElement(idField.getName(), properties, sortArray);
     return sortArray;
   }
 
+  private JsonObject getMergedOnSortOptions() {
+    JsonObject sortOptions = new JsonObject();
+    sortOptions.addProperty(ORDER, DESC_SORT_ORDER);
+    // Ignore the sort field if it does not exist in index. Otherwise the search would fail on open
+    // changes, because the corresponding documents do not have mergedOn field.
+    sortOptions.addProperty(UNMAPPED_TYPE, ElasticMapping.TIMESTAMP_FIELD_TYPE);
+    return sortOptions;
+  }
+
   @Override
   protected String getDeleteActions(Change.Id c) {
     return getDeleteRequest(c);
@@ -361,6 +374,10 @@
           cd);
     }
 
+    if (fields.contains(ChangeField.MERGED_ON.getName())) {
+      decodeMergedOn(source, cd);
+    }
+
     return cd;
   }
 
@@ -396,4 +413,18 @@
     }
     out.setUnresolvedCommentCount(count.getAsInt());
   }
+
+  private void decodeMergedOn(JsonObject doc, ChangeData out) {
+    JsonElement mergedOnField = doc.get(ChangeField.MERGED_ON.getName());
+
+    Timestamp mergedOn = null;
+    if (mergedOnField != null) {
+      // Parse from ElasticMapping.TIMESTAMP_FIELD_FORMAT.
+      // We currently use built-in ISO-based dateOptionalTime.
+      // https://www.elastic.co/guide/en/elasticsearch/reference/current/mapping-date-format.html#built-in-date-formats
+      DateTimeFormatter isoFormatter = DateTimeFormatter.ISO_INSTANT;
+      mergedOn = Timestamp.from(Instant.from(isoFormatter.parse(mergedOnField.getAsString())));
+    }
+    out.setMergedOn(mergedOn);
+  }
 }
diff --git a/java/com/google/gerrit/elasticsearch/ElasticMapping.java b/java/com/google/gerrit/elasticsearch/ElasticMapping.java
index f8c4168..edd05c9 100644
--- a/java/com/google/gerrit/elasticsearch/ElasticMapping.java
+++ b/java/com/google/gerrit/elasticsearch/ElasticMapping.java
@@ -21,6 +21,10 @@
 import java.util.Map;
 
 class ElasticMapping {
+
+  protected static final String TIMESTAMP_FIELD_TYPE = "date";
+  protected static final String TIMESTAMP_FIELD_FORMAT = "dateOptionalTime";
+
   static MappingProperties createMapping(Schema<?> schema, ElasticQueryAdapter adapter) {
     ElasticMapping.Builder mapping = new ElasticMapping.Builder(adapter);
     for (FieldDef<?, ?> field : schema.getFields().values()) {
@@ -71,9 +75,9 @@
     }
 
     Builder addTimestamp(String name) {
-      FieldProperties properties = new FieldProperties("date");
-      properties.type = "date";
-      properties.format = "dateOptionalTime";
+      FieldProperties properties = new FieldProperties(TIMESTAMP_FIELD_TYPE);
+      properties.type = TIMESTAMP_FIELD_TYPE;
+      properties.format = TIMESTAMP_FIELD_FORMAT;
       fields.put(name, properties);
       return this;
     }
diff --git a/java/com/google/gerrit/elasticsearch/ElasticQueryBuilder.java b/java/com/google/gerrit/elasticsearch/ElasticQueryBuilder.java
index d05e91c..40ac603 100644
--- a/java/com/google/gerrit/elasticsearch/ElasticQueryBuilder.java
+++ b/java/com/google/gerrit/elasticsearch/ElasticQueryBuilder.java
@@ -29,7 +29,6 @@
 import com.google.gerrit.index.query.QueryParseException;
 import com.google.gerrit.index.query.RegexPredicate;
 import com.google.gerrit.index.query.TimestampRangePredicate;
-import com.google.gerrit.server.query.change.AfterPredicate;
 import java.time.Instant;
 
 public class ElasticQueryBuilder {
@@ -130,7 +129,9 @@
   private <T> QueryBuilder timestampQuery(IndexPredicate<T> p) throws QueryParseException {
     if (p instanceof TimestampRangePredicate) {
       TimestampRangePredicate<T> r = (TimestampRangePredicate<T>) p;
-      if (p instanceof AfterPredicate) {
+      if (r.getMaxTimestamp().getTime() == Long.MAX_VALUE) {
+        // The time range only has the start value, search from the start to the max supported value
+        // Long.MAX_VALUE
         return QueryBuilders.rangeQuery(r.getField().getName())
             .gte(Instant.ofEpochMilli(r.getMinTimestamp().getTime()));
       }