Merge "Reimplement is:watched to inline filter query trees"
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/ChangeField.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/ChangeField.java
index 513669c..b89cb7e 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/index/ChangeField.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/ChangeField.java
@@ -45,7 +45,7 @@
  */
 public class ChangeField {
   /** Increment whenever making schema changes. */
-  public static final int SCHEMA_VERSION = 9;
+  public static final int SCHEMA_VERSION = 10;
 
   /** Legacy change ID. */
   public static final FieldDef<ChangeData, Integer> LEGACY_ID =
@@ -203,6 +203,27 @@
         }
       };
 
+  /** List of labels on the current patch set. */
+  public static final FieldDef<ChangeData, Iterable<String>> LABEL =
+      new FieldDef.Repeatable<ChangeData, String>(
+          ChangeQueryBuilder.FIELD_LABEL, FieldType.EXACT, false) {
+        @Override
+        public Iterable<String> get(ChangeData input, FillArgs args)
+            throws OrmException {
+          Set<String> distinctApprovals = Sets.newHashSet();
+          for (PatchSetApproval a : input.currentApprovals(args.db)) {
+            if (a.getValue() != 0) {
+              distinctApprovals.add(formatLabel(a.getLabel(), a.getValue()));
+            }
+          }
+          return distinctApprovals;
+        }
+      };
+
+  public static String formatLabel(String label, int value) {
+    return label.toLowerCase() + (value >= 0 ? "+" : "") + value;
+  }
+
   public static final ImmutableMap<String, FieldDef<ChangeData, ?>> ALL;
 
   static {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/EqualsLabelPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/EqualsLabelPredicate.java
new file mode 100644
index 0000000..b838ca5
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/EqualsLabelPredicate.java
@@ -0,0 +1,139 @@
+// Copyright (C) 2013 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.common.data.LabelType;
+import com.google.gerrit.common.data.LabelTypes;
+import com.google.gerrit.common.data.Permission;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.PatchSetApproval;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.index.ChangeField;
+import com.google.gerrit.server.index.IndexPredicate;
+import com.google.gerrit.server.project.ChangeControl;
+import com.google.gerrit.server.project.NoSuchChangeException;
+import com.google.gerrit.server.project.ProjectCache;
+import com.google.gerrit.server.project.ProjectState;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Provider;
+
+class EqualsLabelPredicate extends IndexPredicate<ChangeData> {
+  private final ProjectCache projectCache;
+  private final ChangeControl.GenericFactory ccFactory;
+  private final IdentifiedUser.GenericFactory userFactory;
+  private final Provider<ReviewDb> dbProvider;
+  private final String label;
+  private final int expVal;
+
+  EqualsLabelPredicate(ProjectCache projectCache,
+      ChangeControl.GenericFactory ccFactory,
+      IdentifiedUser.GenericFactory userFactory, Provider<ReviewDb> dbProvider,
+      String label, int expVal) {
+    super(ChangeField.LABEL, ChangeField.formatLabel(label, expVal));
+    this.ccFactory = ccFactory;
+    this.projectCache = projectCache;
+    this.userFactory = userFactory;
+    this.dbProvider = dbProvider;
+    this.label = label;
+    this.expVal = expVal;
+  }
+
+  @Override
+  public boolean match(ChangeData object) throws OrmException {
+    Change c = object.change(dbProvider);
+    if (c == null) {
+      // The change has disappeared.
+      //
+      return false;
+    }
+    ProjectState project = projectCache.get(c.getDest().getParentKey());
+    if (project == null) {
+      // The project has disappeared.
+      //
+      return false;
+    }
+    LabelType labelType = type(project.getLabelTypes(), label);
+    boolean hasVote = false;
+    for (PatchSetApproval p : object.currentApprovals(dbProvider)) {
+      if (labelType.matches(p)) {
+        hasVote = true;
+        if (match(c, p.getValue(), p.getAccountId(), labelType)) {
+          return true;
+        }
+      }
+    }
+
+    if (!hasVote && expVal == 0) {
+      return true;
+    }
+
+    return false;
+  }
+
+  private static LabelType type(LabelTypes types, String toFind) {
+    if (types.byLabel(toFind) != null) {
+      return types.byLabel(toFind);
+    }
+
+    for (LabelType lt : types.getLabelTypes()) {
+      if (toFind.equalsIgnoreCase(lt.getName())) {
+        return lt;
+      }
+    }
+
+    for (LabelType lt : types.getLabelTypes()) {
+      if (toFind.equalsIgnoreCase(lt.getAbbreviation())) {
+        return lt;
+      }
+    }
+
+    return LabelType.withDefaultValues(toFind);
+  }
+
+  private boolean match(Change change, int value, Account.Id approver,
+      LabelType type) throws OrmException {
+    int psVal = value;
+    if (psVal == expVal) {
+      // Double check the value is still permitted for the user.
+      //
+      try {
+        ChangeControl cc = ccFactory.controlFor(change, //
+            userFactory.create(dbProvider, approver));
+        if (!cc.isVisible(dbProvider.get())) {
+          // The user can't see the change anymore.
+          //
+          return false;
+        }
+        psVal = cc.getRange(Permission.forLabel(type.getName())).squash(psVal);
+      } catch (NoSuchChangeException e) {
+        // The project has disappeared.
+        //
+        return false;
+      }
+
+      if (psVal == expVal) {
+        return true;
+      }
+    }
+    return false;
+  }
+
+  @Override
+  public int getCost() {
+    return 1;
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/LabelPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/LabelPredicate.java
index b0d02f8..a3c972a 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/LabelPredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/LabelPredicate.java
@@ -14,84 +14,106 @@
 
 package com.google.gerrit.server.query.change;
 
-import com.google.gerrit.common.data.LabelType;
-import com.google.gerrit.common.data.LabelTypes;
-import com.google.gerrit.common.data.Permission;
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.PatchSetApproval;
+import com.google.common.collect.Lists;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.project.ChangeControl;
-import com.google.gerrit.server.project.NoSuchChangeException;
 import com.google.gerrit.server.project.ProjectCache;
-import com.google.gerrit.server.project.ProjectState;
-import com.google.gerrit.server.query.OperatorPredicate;
-import com.google.gwtorm.server.OrmException;
+import com.google.gerrit.server.query.OrPredicate;
+import com.google.gerrit.server.query.Predicate;
 import com.google.inject.Provider;
 
-import java.util.HashSet;
-import java.util.Set;
+import java.util.List;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 
-class LabelPredicate extends OperatorPredicate<ChangeData> {
+public class LabelPredicate extends OrPredicate<ChangeData> {
+  private static final int MAX_LABEL_VALUE = 4;
+
   private static enum Test {
-    EQ {
-      @Override
-      public boolean match(int psValue, int expValue) {
-        return psValue == expValue;
-      }
-    },
-    GT_EQ {
-      @Override
-      public boolean match(int psValue, int expValue) {
-        return psValue >= expValue;
-      }
-    },
-    LT_EQ {
-      @Override
-      public boolean match(int psValue, int expValue) {
-        return psValue <= expValue;
-      }
-    };
+    EQ, GT_EQ, LT_EQ;
 
-    abstract boolean match(int psValue, int expValue);
+    boolean isEq() {
+      return EQ.equals(this);
+    }
+
+    boolean isGtEq() {
+      return GT_EQ.equals(this);
+    }
+
+    static Test op(String op) {
+      if ("=".equals(op)) {
+        return EQ;
+
+      } else if (">=".equals(op)) {
+        return GT_EQ;
+
+      } else if ("<=".equals(op)) {
+        return LT_EQ;
+
+      } else {
+        throw new IllegalArgumentException("Unsupported operation " + op);
+      }
+    }
   }
 
-  private static LabelType type(LabelTypes types, String toFind) {
-    if (types.byLabel(toFind) != null) {
-      return types.byLabel(toFind);
-    }
+  private final String value;
 
-    for (LabelType lt : types.getLabelTypes()) {
-      if (toFind.equalsIgnoreCase(lt.getName())) {
-        return lt;
-      }
-    }
-
-    for (LabelType lt : types.getLabelTypes()) {
-      if (toFind.equalsIgnoreCase(lt.getAbbreviation())) {
-        return lt;
-      }
-    }
-
-    return LabelType.withDefaultValues(toFind);
+  LabelPredicate(ProjectCache projectCache,
+      ChangeControl.GenericFactory ccFactory,
+      IdentifiedUser.GenericFactory userFactory, Provider<ReviewDb> dbProvider,
+      String value) {
+    super(predicates(projectCache, ccFactory, userFactory,
+          dbProvider, value));
+    this.value = value;
   }
 
-  private static Test op(String op) {
-    if ("=".equals(op)) {
-      return Test.EQ;
+  private static List<Predicate<ChangeData>> predicates(
+      ProjectCache projectCache, ChangeControl.GenericFactory ccFactory,
+      IdentifiedUser.GenericFactory userFactory, Provider<ReviewDb> dbProvider,
+      String value) {
+    String label;
+    Test test;
+    int expVal;
+    Matcher m1 = Pattern.compile("(=|>=|<=)([+-]?\\d+)$").matcher(value);
+    Matcher m2 = Pattern.compile("([+-]\\d+)$").matcher(value);
+    if (m1.find()) {
+      label = value.substring(0, m1.start());
+      test = Test.op(m1.group(1));
+      expVal = value(m1.group(2));
 
-    } else if (">=".equals(op)) {
-      return Test.GT_EQ;
-
-    } else if ("<=".equals(op)) {
-      return Test.LT_EQ;
+    } else if (m2.find()) {
+      label = value.substring(0, m2.start());
+      test = Test.EQ;
+      expVal = value(m2.group(1));
 
     } else {
-      throw new IllegalArgumentException("Unsupported operation " + op);
+      label = value;
+      test = Test.EQ;
+      expVal = 1;
     }
+
+    List<Predicate<ChangeData>> r = Lists.newArrayListWithCapacity(2 * MAX_LABEL_VALUE);
+    if (test.isEq()) {
+      if (expVal != 0) {
+        r.add(equalsLabelPredicate(projectCache, ccFactory, userFactory,
+            dbProvider, label, expVal));
+      } else {
+        r.add(noLabelQuery(projectCache, ccFactory, userFactory,
+            dbProvider, label));
+      }
+    } else {
+      for (int i = test.isGtEq() ? expVal : neg(expVal); i <= MAX_LABEL_VALUE; i++) {
+        if (i != 0) {
+          r.add(equalsLabelPredicate(projectCache, ccFactory, userFactory,
+              dbProvider, label, test.isGtEq() ? i : neg(i)));
+        } else {
+          r.add(noLabelQuery(projectCache, ccFactory, userFactory,
+              dbProvider, label));
+        }
+      }
+    }
+    return r;
   }
 
   private static int value(String value) {
@@ -101,113 +123,31 @@
     return Integer.parseInt(value);
   }
 
-  private final ProjectCache projectCache;
-  private final ChangeControl.GenericFactory ccFactory;
-  private final IdentifiedUser.GenericFactory userFactory;
-  private final Provider<ReviewDb> dbProvider;
-  private final Test test;
-  private final String type;
-  private final int expVal;
+  private static int neg(int value) {
+    return -1 * value;
+  }
 
-  LabelPredicate(ProjectCache projectCache,
-      ChangeControl.GenericFactory ccFactory,
-      IdentifiedUser.GenericFactory userFactory,
-      Provider<ReviewDb> dbProvider,
-      String value) {
-    super(ChangeQueryBuilder.FIELD_LABEL, value);
-    this.ccFactory = ccFactory;
-    this.projectCache = projectCache;
-    this.userFactory = userFactory;
-    this.dbProvider = dbProvider;
-
-    Matcher m1 = Pattern.compile("(=|>=|<=)([+-]?\\d+)$").matcher(value);
-    Matcher m2 = Pattern.compile("([+-]\\d+)$").matcher(value);
-    if (m1.find()) {
-      type = value.substring(0, m1.start());
-      test = op(m1.group(1));
-      expVal = value(m1.group(2));
-
-    } else if (m2.find()) {
-      type = value.substring(0, m2.start());
-      test = Test.EQ;
-      expVal = value(m2.group(1));
-
-    } else {
-      type = value;
-      test = Test.EQ;
-      expVal = 1;
+  private static Predicate<ChangeData> noLabelQuery(ProjectCache projectCache, ChangeControl.GenericFactory ccFactory,
+      IdentifiedUser.GenericFactory userFactory, Provider<ReviewDb> dbProvider, String label) {
+    List<Predicate<ChangeData>> r =
+        Lists.newArrayListWithCapacity(2 * MAX_LABEL_VALUE);
+    for (int i = 1; i <= MAX_LABEL_VALUE; i++) {
+      r.add(not(equalsLabelPredicate(projectCache, ccFactory, userFactory,
+          dbProvider, label, i)));
+      r.add(not(equalsLabelPredicate(projectCache, ccFactory, userFactory,
+          dbProvider, label, neg(i))));
     }
+    return and(r);
+  }
+
+  private static Predicate<ChangeData> equalsLabelPredicate(ProjectCache projectCache, ChangeControl.GenericFactory ccFactory,
+      IdentifiedUser.GenericFactory userFactory, Provider<ReviewDb> dbProvider, String label, int expVal) {
+    return new EqualsLabelPredicate(projectCache, ccFactory, userFactory,
+        dbProvider, label, expVal);
   }
 
   @Override
-  public boolean match(final ChangeData object) throws OrmException {
-    final Change c = object.change(dbProvider);
-    if (c == null) {
-      // The change has disappeared.
-      //
-      return false;
-    }
-    final ProjectState project = projectCache.get(c.getDest().getParentKey());
-    if (project == null) {
-      // The project has disappeared.
-      //
-      return false;
-    }
-    final LabelType labelType = type(project.getLabelTypes(), type);
-    final Set<Account.Id> allApprovers = new HashSet<Account.Id>();
-    final Set<Account.Id> approversThatVotedInCategory = new HashSet<Account.Id>();
-    for (PatchSetApproval p : object.currentApprovals(dbProvider)) {
-      allApprovers.add(p.getAccountId());
-      if (labelType.matches(p)) {
-        approversThatVotedInCategory.add(p.getAccountId());
-        if (match(c, p.getValue(), p.getAccountId(), labelType)) {
-          return true;
-        }
-      }
-    }
-
-    final Set<Account.Id> approversThatDidNotVoteInCategory = new HashSet<Account.Id>(allApprovers);
-    approversThatDidNotVoteInCategory.removeAll(approversThatVotedInCategory);
-    for (Account.Id a : approversThatDidNotVoteInCategory) {
-      if (match(c, 0, a, labelType)) {
-        return true;
-      }
-    }
-
-    return false;
-  }
-
-  private boolean match(final Change change, final int value,
-      final Account.Id approver, final LabelType type)
-      throws OrmException {
-    int psVal = value;
-    if (test.match(psVal, expVal)) {
-      // Double check the value is still permitted for the user.
-      //
-      try {
-        ChangeControl cc = ccFactory.controlFor(change, //
-            userFactory.create(dbProvider, approver));
-        if (!cc.isVisible(dbProvider.get())) {
-          // The user can't see the change anymore.
-          //
-          return false;
-        }
-        psVal = cc.getRange(Permission.forLabel(type.getName())).squash(psVal);
-      } catch (NoSuchChangeException e) {
-        // The project has disappeared.
-        //
-        return false;
-      }
-
-      if (test.match(psVal, expVal)) {
-        return true;
-      }
-    }
-    return false;
-  }
-
-  @Override
-  public int getCost() {
-    return 2;
+  public String toString() {
+    return ChangeQueryBuilder.FIELD_LABEL + ":" + value;
   }
 }