Add REST API to list indexes and their versions

This change is a preparation to implement a REST API endpoint to
take snapshots of indexes.

Release-Notes: REST API to list indexes and their versions
Change-Id: Ib1e354a8abe5149826d94bca9aaecfea99a2e6ff
diff --git a/Documentation/rest-api-config.txt b/Documentation/rest-api-config.txt
index ec1ac03..70c4b4d 100644
--- a/Documentation/rest-api-config.txt
+++ b/Documentation/rest-api-config.txt
@@ -1420,6 +1420,73 @@
 When `delete_missing` is set to `true` changes to be reindexed which are missing in NoteDb
 will be deleted in the index.
 
+[[list-indexes]]
+=== List Indexes
+--
+'GET /config/server/indexes'
+--
+
+Lists the indexes used by Gerrit. It provides details about the index versions,
+which index version is used to search and which versions are written to.
+
+This endpoint requires the
+link:access-control.html#capability_maintainServer[Maintain Server]
+capability.
+
+.Request
+----
+  GET /config/server/indexes/ HTTP/1.0
+----
+
+.Response
+----
+  HTTP/1.1 200 OK
+  Content-Type: application/json; charset=UTF-8
+
+  )]}'
+  {
+    "accounts": {
+      "name": "accounts",
+      "versions": {
+        "13": {
+          "write": true,
+          "search": true
+        }
+      }
+    },
+    "changes": {
+      "name": "changes",
+      "versions": {
+        "83": {
+          "write": true,
+          "search": true
+        },
+        "84": {
+          "write": true,
+          "search": false
+        }
+      }
+    },
+    "groups": {
+      "name": "groups",
+      "versions": {
+        "10": {
+          "write": true,
+          "search": true
+        }
+      }
+    },
+    "projects": {
+      "name": "projects",
+      "versions": {
+        "8": {
+          "write": true,
+          "search": true
+        }
+      }
+    }
+  }
+----
 
 [[ids]]
 == IDs
diff --git a/java/com/google/gerrit/server/config/IndexResource.java b/java/com/google/gerrit/server/config/IndexResource.java
new file mode 100644
index 0000000..ea1b57a
--- /dev/null
+++ b/java/com/google/gerrit/server/config/IndexResource.java
@@ -0,0 +1,36 @@
+// Copyright (C) 2023 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.config;
+
+import com.google.gerrit.extensions.restapi.RestView;
+import com.google.gerrit.index.Index;
+import com.google.gerrit.index.IndexCollection;
+import com.google.inject.TypeLiteral;
+import java.util.Collection;
+
+public class IndexResource extends ConfigResource {
+  public static final TypeLiteral<RestView<IndexResource>> INDEX_KIND = new TypeLiteral<>() {};
+
+  private final IndexCollection<?, ?, ?> indexes;
+
+  public IndexResource(IndexCollection<?, ?, ?> indexes) {
+    this.indexes = indexes;
+  }
+
+  @SuppressWarnings("unchecked")
+  public Collection<Index<?, ?>> getWriteIndexes() {
+    return (Collection<Index<?, ?>>) indexes.getWriteIndexes();
+  }
+}
diff --git a/java/com/google/gerrit/server/restapi/config/ConfigRestApiModule.java b/java/com/google/gerrit/server/restapi/config/ConfigRestApiModule.java
index 3de05e9..f34a4a3 100644
--- a/java/com/google/gerrit/server/restapi/config/ConfigRestApiModule.java
+++ b/java/com/google/gerrit/server/restapi/config/ConfigRestApiModule.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.server.restapi.config;
 
 import static com.google.gerrit.server.config.ConfigResource.CONFIG_KIND;
+import static com.google.gerrit.server.config.IndexResource.INDEX_KIND;
 import static com.google.gerrit.server.config.TaskResource.TASK_KIND;
 
 import com.google.gerrit.extensions.registration.DynamicMap;
@@ -29,6 +30,7 @@
     DynamicMap.mapOf(binder(), CONFIG_KIND);
     DynamicMap.mapOf(binder(), TASK_KIND);
     DynamicMap.mapOf(binder(), TopMenuResource.TOP_MENU_KIND);
+    DynamicMap.mapOf(binder(), INDEX_KIND);
 
     child(CONFIG_KIND, "capabilities").to(CapabilitiesCollection.class);
     post(CONFIG_KIND, "check.consistency").to(CheckConsistency.class);
@@ -50,6 +52,7 @@
     child(CONFIG_KIND, "top-menus").to(TopMenuCollection.class);
     get(CONFIG_KIND, "version").to(GetVersion.class);
 
+    child(CONFIG_KIND, "indexes").to(IndexCollection.class);
     // The caches and summary REST endpoints are bound via RestCacheAdminModule.
   }
 }
diff --git a/java/com/google/gerrit/server/restapi/config/IndexCollection.java b/java/com/google/gerrit/server/restapi/config/IndexCollection.java
new file mode 100644
index 0000000..3e00ebd
--- /dev/null
+++ b/java/com/google/gerrit/server/restapi/config/IndexCollection.java
@@ -0,0 +1,72 @@
+// Copyright (C) 2023 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.restapi.config;
+
+import static com.google.gerrit.common.data.GlobalCapability.MAINTAIN_SERVER;
+
+import com.google.gerrit.extensions.annotations.RequiresCapability;
+import com.google.gerrit.extensions.registration.DynamicMap;
+import com.google.gerrit.extensions.restapi.ChildCollection;
+import com.google.gerrit.extensions.restapi.IdString;
+import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.extensions.restapi.RestView;
+import com.google.gerrit.index.IndexDefinition;
+import com.google.gerrit.server.config.ConfigResource;
+import com.google.gerrit.server.config.IndexResource;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.Singleton;
+import java.util.Collection;
+
+@RequiresCapability(MAINTAIN_SERVER)
+@Singleton
+public class IndexCollection implements ChildCollection<ConfigResource, IndexResource> {
+  private final DynamicMap<RestView<IndexResource>> views;
+  private final Provider<ListIndexes> list;
+  private final Collection<IndexDefinition<?, ?, ?>> defs;
+
+  @Inject
+  IndexCollection(
+      DynamicMap<RestView<IndexResource>> views,
+      Provider<ListIndexes> list,
+      Collection<IndexDefinition<?, ?, ?>> defs) {
+    this.views = views;
+    this.list = list;
+    this.defs = defs;
+  }
+
+  @Override
+  public IndexResource parse(ConfigResource parent, IdString id) throws ResourceNotFoundException {
+    String indexName = id.get();
+
+    for (IndexDefinition<?, ?, ?> def : defs) {
+      if (def.getName().equals(indexName)) {
+        return new IndexResource(def.getIndexCollection());
+      }
+    }
+    throw new ResourceNotFoundException("Unknown index requested: " + indexName);
+  }
+
+  @Override
+  public RestView<ConfigResource> list() throws RestApiException {
+    return list.get();
+  }
+
+  @Override
+  public DynamicMap<RestView<IndexResource>> views() {
+    return views;
+  }
+}
diff --git a/java/com/google/gerrit/server/restapi/config/IndexInfo.java b/java/com/google/gerrit/server/restapi/config/IndexInfo.java
new file mode 100644
index 0000000..4a6bd3e53
--- /dev/null
+++ b/java/com/google/gerrit/server/restapi/config/IndexInfo.java
@@ -0,0 +1,58 @@
+// Copyright (C) 2023 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.restapi.config;
+
+import com.google.auto.value.AutoValue;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSortedMap;
+import com.google.gerrit.index.Index;
+import com.google.gerrit.index.IndexCollection;
+
+@AutoValue
+public abstract class IndexInfo {
+
+  public static IndexInfo fromIndexCollection(
+      String name, IndexCollection<?, ?, ?> indexCollection) {
+    ImmutableSortedMap.Builder<Integer, IndexVersionInfo> versions =
+        ImmutableSortedMap.naturalOrder();
+    int searchIndexVersion = indexCollection.getSearchIndex().getSchema().getVersion();
+    boolean searchIndexAdded = false;
+    for (Index<?, ?> index : indexCollection.getWriteIndexes()) {
+      boolean isSearchIndex = index.getSchema().getVersion() == searchIndexVersion;
+      versions.put(index.getSchema().getVersion(), IndexVersionInfo.create(true, isSearchIndex));
+      searchIndexAdded = searchIndexAdded || isSearchIndex;
+    }
+    if (!searchIndexAdded) {
+      versions.put(searchIndexVersion, IndexVersionInfo.create(false, true));
+    }
+
+    return new AutoValue_IndexInfo(name, versions.build());
+  }
+
+  public abstract String getName();
+
+  public abstract ImmutableMap<Integer, IndexVersionInfo> getVersions();
+
+  @AutoValue
+  public abstract static class IndexVersionInfo {
+    static IndexVersionInfo create(boolean write, boolean search) {
+      return new AutoValue_IndexInfo_IndexVersionInfo(write, search);
+    }
+
+    abstract boolean isWrite();
+
+    abstract boolean isSearch();
+  }
+}
diff --git a/java/com/google/gerrit/server/restapi/config/ListIndexes.java b/java/com/google/gerrit/server/restapi/config/ListIndexes.java
new file mode 100644
index 0000000..9710000
--- /dev/null
+++ b/java/com/google/gerrit/server/restapi/config/ListIndexes.java
@@ -0,0 +1,51 @@
+// Copyright (C) 2014 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.restapi.config;
+
+import static com.google.gerrit.common.data.GlobalCapability.MAINTAIN_SERVER;
+
+import com.google.gerrit.extensions.annotations.RequiresCapability;
+import com.google.gerrit.extensions.restapi.Response;
+import com.google.gerrit.extensions.restapi.RestReadView;
+import com.google.gerrit.index.IndexDefinition;
+import com.google.gerrit.server.config.ConfigResource;
+import com.google.inject.Inject;
+import java.util.Collection;
+import java.util.Map;
+import java.util.TreeMap;
+
+@RequiresCapability(MAINTAIN_SERVER)
+public class ListIndexes implements RestReadView<ConfigResource> {
+  private final Collection<IndexDefinition<?, ?, ?>> defs;
+
+  @Inject
+  public ListIndexes(Collection<IndexDefinition<?, ?, ?>> defs) {
+    this.defs = defs;
+  }
+
+  private Map<String, IndexInfo> getIndexInfos() {
+    Map<String, IndexInfo> indexInfos = new TreeMap<>();
+    for (IndexDefinition<?, ?, ?> def : defs) {
+      String name = def.getName();
+      indexInfos.put(name, IndexInfo.fromIndexCollection(name, def.getIndexCollection()));
+    }
+    return indexInfos;
+  }
+
+  @Override
+  public Response<Object> apply(ConfigResource rsrc) {
+    return Response.ok(getIndexInfos());
+  }
+}