Add STATE field to ProjectIndex

This commit adds the ProjectState to the project index and makes it
queryable. V2 was added in this series, so adding a field does not
require creating V3.

Our post processors filter out hidden projects, so it doesn't make much
sense for users to query for them. As a future optimisation we can make
the default query exclude hidden projects by adding 'not state:hidden'
so that we have to do less post-filtering.

Change-Id: I98434e4b4ea232e257cca4b551fcaba21b3db656
diff --git a/Documentation/user-search-projects.txt b/Documentation/user-search-projects.txt
index ba20adb..11c1326 100644
--- a/Documentation/user-search-projects.txt
+++ b/Documentation/user-search-projects.txt
@@ -24,6 +24,11 @@
 Matches projects whose description contains 'DESCRIPTION', using a
 full-text search.
 
+[[state]]
+state:'STATE'::
++
+Matches project's state. Can be either 'active' or 'read-only'.
+
 == Magical Operators
 
 [[is-visible]]
diff --git a/java/com/google/gerrit/index/project/ProjectField.java b/java/com/google/gerrit/index/project/ProjectField.java
index 041813c..5388253 100644
--- a/java/com/google/gerrit/index/project/ProjectField.java
+++ b/java/com/google/gerrit/index/project/ProjectField.java
@@ -45,6 +45,9 @@
   public static final FieldDef<ProjectData, Iterable<String>> NAME_PART =
       prefix("name_part").buildRepeatable(p -> SchemaUtil.getNameParts(p.getProject().getName()));
 
+  public static final FieldDef<ProjectData, String> STATE =
+      exact("state").stored().build(p -> p.getProject().getState().name());
+
   public static final FieldDef<ProjectData, Iterable<String>> ANCESTOR_NAME =
       exact("ancestor_name").buildRepeatable(p -> p.getParentNames());
 
diff --git a/java/com/google/gerrit/index/project/ProjectSchemaDefinitions.java b/java/com/google/gerrit/index/project/ProjectSchemaDefinitions.java
index 4d212fb..07b5adb 100644
--- a/java/com/google/gerrit/index/project/ProjectSchemaDefinitions.java
+++ b/java/com/google/gerrit/index/project/ProjectSchemaDefinitions.java
@@ -30,7 +30,7 @@
           ProjectField.NAME_PART,
           ProjectField.ANCESTOR_NAME);
 
-  static final Schema<ProjectData> V2 = schema(V1, ProjectField.REF_STATE);
+  static final Schema<ProjectData> V2 = schema(V1, ProjectField.STATE, ProjectField.REF_STATE);
 
   public static final ProjectSchemaDefinitions INSTANCE = new ProjectSchemaDefinitions();
 
diff --git a/java/com/google/gerrit/server/query/project/ProjectPredicates.java b/java/com/google/gerrit/server/query/project/ProjectPredicates.java
index b4f56d4..2e406aa 100644
--- a/java/com/google/gerrit/server/query/project/ProjectPredicates.java
+++ b/java/com/google/gerrit/server/query/project/ProjectPredicates.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.server.query.project;
 
+import com.google.gerrit.extensions.client.ProjectState;
 import com.google.gerrit.index.project.ProjectData;
 import com.google.gerrit.index.project.ProjectField;
 import com.google.gerrit.index.project.ProjectPredicate;
@@ -34,5 +35,9 @@
     return new ProjectPredicate(ProjectField.DESCRIPTION, description);
   }
 
+  public static Predicate<ProjectData> state(ProjectState state) {
+    return new ProjectPredicate(ProjectField.STATE, state.name());
+  }
+
   private ProjectPredicates() {}
 }
diff --git a/java/com/google/gerrit/server/query/project/ProjectQueryBuilder.java b/java/com/google/gerrit/server/query/project/ProjectQueryBuilder.java
index be7ea22..872d3e0 100644
--- a/java/com/google/gerrit/server/query/project/ProjectQueryBuilder.java
+++ b/java/com/google/gerrit/server/query/project/ProjectQueryBuilder.java
@@ -17,6 +17,7 @@
 import com.google.common.base.Strings;
 import com.google.common.collect.Lists;
 import com.google.common.primitives.Ints;
+import com.google.gerrit.extensions.client.ProjectState;
 import com.google.gerrit.index.project.ProjectData;
 import com.google.gerrit.index.query.LimitPredicate;
 import com.google.gerrit.index.query.Predicate;
@@ -60,6 +61,23 @@
     return ProjectPredicates.description(description);
   }
 
+  @Operator
+  public Predicate<ProjectData> state(String state) throws QueryParseException {
+    if (Strings.isNullOrEmpty(state)) {
+      throw error("state operator requires a value");
+    }
+    ProjectState parsedState;
+    try {
+      parsedState = ProjectState.valueOf(state.replace('-', '_').toUpperCase());
+    } catch (IllegalArgumentException e) {
+      throw error("state operator must be either 'active' or 'read-only'");
+    }
+    if (parsedState == ProjectState.HIDDEN) {
+      throw error("state operator must be either 'active' or 'read-only'");
+    }
+    return ProjectPredicates.state(parsedState);
+  }
+
   @Override
   protected Predicate<ProjectData> defaultField(String query) throws QueryParseException {
     // Adapt the capacity of this list when adding more default predicates.
diff --git a/javatests/com/google/gerrit/server/query/project/AbstractQueryProjectsTest.java b/javatests/com/google/gerrit/server/query/project/AbstractQueryProjectsTest.java
index e34746c..2eec006 100644
--- a/javatests/com/google/gerrit/server/query/project/AbstractQueryProjectsTest.java
+++ b/javatests/com/google/gerrit/server/query/project/AbstractQueryProjectsTest.java
@@ -15,15 +15,21 @@
 package com.google.gerrit.server.query.project;
 
 import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.TruthJUnit.assume;
 import static java.util.stream.Collectors.toList;
 
 import com.google.common.base.CharMatcher;
 import com.google.gerrit.extensions.api.GerritApi;
+import com.google.gerrit.extensions.api.projects.ConfigInput;
 import com.google.gerrit.extensions.api.projects.ProjectInput;
 import com.google.gerrit.extensions.api.projects.Projects.QueryRequest;
+import com.google.gerrit.extensions.client.ProjectState;
 import com.google.gerrit.extensions.common.AccountInfo;
 import com.google.gerrit.extensions.common.ProjectInfo;
 import com.google.gerrit.extensions.restapi.BadRequestException;
+import com.google.gerrit.index.Schema;
+import com.google.gerrit.index.project.ProjectData;
+import com.google.gerrit.index.project.ProjectIndexCollection;
 import com.google.gerrit.lifecycle.LifecycleManager;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.Project;
@@ -38,7 +44,6 @@
 import com.google.gerrit.server.account.AccountsUpdate;
 import com.google.gerrit.server.account.AuthRequest;
 import com.google.gerrit.server.config.AllProjectsName;
-import com.google.gerrit.server.query.account.InternalAccountQuery;
 import com.google.gerrit.server.schema.SchemaCreator;
 import com.google.gerrit.server.util.ManualRequestContext;
 import com.google.gerrit.server.util.OneOffRequestContext;
@@ -83,7 +88,7 @@
 
   @Inject protected OneOffRequestContext oneOffRequestContext;
 
-  @Inject protected InternalAccountQuery internalAccountQuery;
+  @Inject protected ProjectIndexCollection indexes;
 
   @Inject protected AllProjectsName allProjects;
 
@@ -211,6 +216,30 @@
   }
 
   @Test
+  public void byState() throws Exception {
+    assume().that(getSchemaVersion() >= 2).isTrue();
+
+    ProjectInfo project1 = createProjectWithState(name("project1"), ProjectState.ACTIVE);
+    ProjectInfo project2 = createProjectWithState(name("project2"), ProjectState.READ_ONLY);
+    assertQuery("state:active", project1);
+    assertQuery("state:read-only", project2);
+  }
+
+  @Test
+  public void byState_emptyQuery() throws Exception {
+    exception.expect(BadRequestException.class);
+    exception.expectMessage("state operator requires a value");
+    assertQuery("state:\"\"");
+  }
+
+  @Test
+  public void byState_badQuery() throws Exception {
+    exception.expect(BadRequestException.class);
+    exception.expectMessage("state operator must be either 'active' or 'read-only'");
+    assertQuery("state:bla");
+  }
+
+  @Test
   public void byDefaultField() throws Exception {
     ProjectInfo project1 = createProject(name("foo-project"));
     ProjectInfo project2 = createProject(name("project2"));
@@ -291,6 +320,14 @@
     return gApi.projects().create(in).get();
   }
 
+  protected ProjectInfo createProjectWithState(String name, ProjectState state) throws Exception {
+    ProjectInfo info = createProject(name);
+    ConfigInput config = new ConfigInput();
+    config.state = state;
+    gApi.projects().name(info.name).config(config);
+    return info;
+  }
+
   protected ProjectInfo getProject(Project.NameKey nameKey) throws Exception {
     return gApi.projects().name(nameKey.get()).get();
   }
@@ -354,6 +391,14 @@
     return b.toString();
   }
 
+  protected int getSchemaVersion() {
+    return getSchema().getVersion();
+  }
+
+  protected Schema<ProjectData> getSchema() {
+    return indexes.getSearchIndex().getSchema();
+  }
+
   protected static Iterable<String> names(ProjectInfo... projects) {
     return names(Arrays.asList(projects));
   }
diff --git a/javatests/com/google/gerrit/server/query/project/BUILD b/javatests/com/google/gerrit/server/query/project/BUILD
index eaa3df3..f0c455e 100644
--- a/javatests/com/google/gerrit/server/query/project/BUILD
+++ b/javatests/com/google/gerrit/server/query/project/BUILD
@@ -9,6 +9,8 @@
     visibility = ["//visibility:public"],
     deps = [
         "//java/com/google/gerrit/extensions:api",
+        "//java/com/google/gerrit/index",
+        "//java/com/google/gerrit/index/project",
         "//java/com/google/gerrit/lifecycle",
         "//java/com/google/gerrit/reviewdb:server",
         "//java/com/google/gerrit/server",