Add an isCacheableByBranch() to the PredicateCache

Add an API to determine if a Predicate's output is assumed to be
constant given any Change destined for the same Branch.NameKey.

Change-Id: I32b52fe04742a344e71a4eee9bc2b69133d112ff
diff --git a/src/main/java/com/googlesource/gerrit/plugins/task/PredicateCache.java b/src/main/java/com/googlesource/gerrit/plugins/task/PredicateCache.java
index aa3da13..f6d6259 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/task/PredicateCache.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/task/PredicateCache.java
@@ -14,27 +14,50 @@
 
 package com.googlesource.gerrit.plugins.task;
 
+import com.google.gerrit.extensions.annotations.PluginName;
+import com.google.gerrit.index.query.AndPredicate;
+import com.google.gerrit.index.query.NotPredicate;
+import com.google.gerrit.index.query.OrPredicate;
 import com.google.gerrit.index.query.Predicate;
 import com.google.gerrit.index.query.QueryParseException;
 import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.query.change.ChangeData;
 import com.google.gerrit.server.query.change.ChangeQueryBuilder;
+import com.google.gerrit.server.query.change.DestinationPredicate;
+import com.google.gerrit.server.query.change.ProjectPredicate;
+import com.google.gerrit.server.query.change.RefPredicate;
+import com.google.gerrit.server.query.change.RegexProjectPredicate;
+import com.google.gerrit.server.query.change.RegexRefPredicate;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
+import java.util.Arrays;
 import java.util.HashMap;
+import java.util.HashSet;
 import java.util.Map;
+import java.util.Set;
+import org.eclipse.jgit.lib.Config;
 
 public class PredicateCache {
   protected final ChangeQueryBuilder cqb;
+  protected final Set<String> cacheableByBranchPredicateClassNames;
   protected final CurrentUser user;
 
   protected final Map<String, ThrowingProvider<Predicate<ChangeData>, QueryParseException>>
       predicatesByQuery = new HashMap<>();
 
   @Inject
-  public PredicateCache(CurrentUser user, ChangeQueryBuilder cqb) {
+  public PredicateCache(
+      @GerritServerConfig Config config,
+      @PluginName String pluginName,
+      CurrentUser user,
+      ChangeQueryBuilder cqb) {
     this.user = user;
     this.cqb = cqb;
+    cacheableByBranchPredicateClassNames =
+        new HashSet<>(
+            Arrays.asList(
+                config.getStringList(pluginName, "cacheable-predicates", "byBranch-className")));
   }
 
   public boolean match(ChangeData c, String query) throws OrmException, QueryParseException {
@@ -78,4 +101,39 @@
       throw e;
     }
   }
+
+  /**
+   * Can this query's output be assumed to be constant given any Change destined for the same
+   * Branch.NameKey?
+   */
+  public boolean isCacheableByBranch(String query) throws QueryParseException {
+    if (query == null
+        || "".equals(query)
+        || "false".equalsIgnoreCase(query)
+        || "true".equalsIgnoreCase(query)) {
+      return true;
+    }
+    return isCacheableByBranch(getPredicate(query));
+  }
+
+  protected boolean isCacheableByBranch(Predicate<ChangeData> predicate) {
+    if (predicate instanceof AndPredicate
+        || predicate instanceof NotPredicate
+        || predicate instanceof OrPredicate) {
+      for (Predicate<ChangeData> subPred : predicate.getChildren()) {
+        if (!isCacheableByBranch(subPred)) {
+          return false;
+        }
+      }
+      return true;
+    }
+    if (predicate instanceof DestinationPredicate
+        || predicate instanceof ProjectPredicate
+        || predicate instanceof RefPredicate
+        || predicate instanceof RegexProjectPredicate
+        || predicate instanceof RegexRefPredicate) {
+      return true;
+    }
+    return cacheableByBranchPredicateClassNames.contains(predicate.getClass().getName());
+  }
 }
diff --git a/src/main/resources/Documentation/config-gerrit.md b/src/main/resources/Documentation/config-gerrit.md
new file mode 100644
index 0000000..6f0b0a5
--- /dev/null
+++ b/src/main/resources/Documentation/config-gerrit.md
@@ -0,0 +1,25 @@
+# Admin User Guide - Configuration
+
+## File `etc/gerrit.config`
+
+The file `'$site_path'/etc/gerrit.config` is a Git-style config file
+that controls many host specific settings for Gerrit.
+
+### Section @PLUGIN@ "cacheable-predicates"
+
+The @PLUGIN@.cacheable-predicates section configures Change Predicate
+optimizations which the @PLUGIN@ plugin may use when evaluating tasks.
+
+#### @PLUGIN@.cacheable-predicates.byBranch-className
+
+The value set with this key specifies a fully qualified class name
+of a Predicate which can be assumed to always return the same match
+result to all Changes destined for the same project/branch
+combinations. This key may be specified more than once.
+
+Example:
+
+```
+[@PLUGIN@ "cacheable-predicates"]
+        byBranch-className = com.google.gerrit.server.query.change.BranchSetPredicate
+```
\ No newline at end of file