Merge changes from topic "boolcond"

* changes:
  UiActions: defer GlobalPermission checks on views
  CherryPick: use BooleanCondition for visible
  PermissionBackend: support bulk evaluation of UiAction
  PermissionBackend: support testCond for UiAction
  Delay UiAction visible and enabled with BooleanCondition
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/conditions/BooleanCondition.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/conditions/BooleanCondition.java
new file mode 100644
index 0000000..950365a
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/conditions/BooleanCondition.java
@@ -0,0 +1,217 @@
+// Copyright (C) 2017 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.extensions.conditions;
+
+import com.google.common.collect.Iterables;
+import java.util.Collections;
+
+/** Delayed evaluation of a boolean condition. */
+public abstract class BooleanCondition {
+  public static final BooleanCondition TRUE = new Value(true);
+  public static final BooleanCondition FALSE = new Value(false);
+
+  public static BooleanCondition valueOf(boolean a) {
+    return a ? TRUE : FALSE;
+  }
+
+  public static BooleanCondition and(BooleanCondition a, BooleanCondition b) {
+    return a == FALSE || b == FALSE ? FALSE : new And(a, b);
+  }
+
+  public static BooleanCondition and(boolean a, BooleanCondition b) {
+    return and(valueOf(a), b);
+  }
+
+  public static BooleanCondition or(BooleanCondition a, BooleanCondition b) {
+    return a == TRUE || b == TRUE ? TRUE : new Or(a, b);
+  }
+
+  public static BooleanCondition or(boolean a, BooleanCondition b) {
+    return or(valueOf(a), b);
+  }
+
+  public static BooleanCondition not(BooleanCondition bc) {
+    return bc == TRUE ? FALSE : bc == FALSE ? TRUE : new Not(bc);
+  }
+
+  BooleanCondition() {}
+
+  /** @return evaluate the condition and return its value. */
+  public abstract boolean value();
+
+  /**
+   * Recursively collect all children of type {@code type}.
+   *
+   * @param type implementation type of the conditions to collect and return.
+   * @return non-null, unmodifiable iteration of children of type {@code type}.
+   */
+  public abstract <T> Iterable<T> children(Class<T> type);
+
+  private static final class And extends BooleanCondition {
+    private final BooleanCondition a;
+    private final BooleanCondition b;
+
+    And(BooleanCondition a, BooleanCondition b) {
+      this.a = a;
+      this.b = b;
+    }
+
+    @Override
+    public boolean value() {
+      return a.value() && b.value();
+    }
+
+    @Override
+    public <T> Iterable<T> children(Class<T> type) {
+      return Iterables.concat(a.children(type), b.children(type));
+    }
+
+    @Override
+    public int hashCode() {
+      return a.hashCode() * 31 + b.hashCode();
+    }
+
+    @Override
+    public boolean equals(Object other) {
+      if (other instanceof And) {
+        And o = (And) other;
+        return a.equals(o.a) && b.equals(o.b);
+      }
+      return false;
+    }
+
+    @Override
+    public String toString() {
+      return "(" + maybeTrim(a, getClass()) + " && " + maybeTrim(a, getClass()) + ")";
+    }
+  }
+
+  private static final class Or extends BooleanCondition {
+    private final BooleanCondition a;
+    private final BooleanCondition b;
+
+    Or(BooleanCondition a, BooleanCondition b) {
+      this.a = a;
+      this.b = b;
+    }
+
+    @Override
+    public boolean value() {
+      return a.value() || b.value();
+    }
+
+    @Override
+    public <T> Iterable<T> children(Class<T> type) {
+      return Iterables.concat(a.children(type), b.children(type));
+    }
+
+    @Override
+    public int hashCode() {
+      return a.hashCode() * 31 + b.hashCode();
+    }
+
+    @Override
+    public boolean equals(Object other) {
+      if (other instanceof Or) {
+        Or o = (Or) other;
+        return a.equals(o.a) && b.equals(o.b);
+      }
+      return false;
+    }
+
+    @Override
+    public String toString() {
+      return "(" + maybeTrim(a, getClass()) + " || " + maybeTrim(a, getClass()) + ")";
+    }
+  }
+
+  private static final class Not extends BooleanCondition {
+    private final BooleanCondition cond;
+
+    Not(BooleanCondition bc) {
+      cond = bc;
+    }
+
+    @Override
+    public boolean value() {
+      return !cond.value();
+    }
+
+    @Override
+    public <T> Iterable<T> children(Class<T> type) {
+      return cond.children(type);
+    }
+
+    @Override
+    public int hashCode() {
+      return cond.hashCode() * 31;
+    }
+
+    @Override
+    public boolean equals(Object other) {
+      return other instanceof Not ? cond.equals(((Not) other).cond) : false;
+    }
+
+    @Override
+    public String toString() {
+      return "!" + cond;
+    }
+  }
+
+  private static final class Value extends BooleanCondition {
+    private final boolean value;
+
+    Value(boolean v) {
+      value = v;
+    }
+
+    @Override
+    public boolean value() {
+      return value;
+    }
+
+    @Override
+    public <T> Iterable<T> children(Class<T> type) {
+      return Collections.emptyList();
+    }
+
+    @Override
+    public int hashCode() {
+      return value ? 1 : 0;
+    }
+
+    @Override
+    public boolean equals(Object other) {
+      return other instanceof Value ? value == ((Value) other).value : false;
+    }
+
+    @Override
+    public String toString() {
+      return Boolean.toString(value);
+    }
+  }
+
+  /** Remove leading '(' and trailing ')' if the type is the same as the parent. */
+  static String maybeTrim(BooleanCondition cond, Class<? extends BooleanCondition> type) {
+    String s = cond.toString();
+    if (cond.getClass() == type
+        && s.length() > 2
+        && s.charAt(0) == '('
+        && s.charAt(s.length() - 1) == ')') {
+      s = s.substring(1, s.length() - 1);
+    }
+    return s;
+  }
+}
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/conditions/PrivateInternals_BooleanCondition.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/conditions/PrivateInternals_BooleanCondition.java
new file mode 100644
index 0000000..4fa932a
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/conditions/PrivateInternals_BooleanCondition.java
@@ -0,0 +1,33 @@
+// Copyright (C) 2017 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.extensions.conditions;
+
+import java.util.Collections;
+
+/** <b>DO NOT USE</b> */
+public final class PrivateInternals_BooleanCondition {
+  private PrivateInternals_BooleanCondition() {}
+
+  public abstract static class SubclassOnlyInCoreServer extends BooleanCondition {
+    @SuppressWarnings("unchecked")
+    @Override
+    public <T> Iterable<T> children(Class<T> type) {
+      if (type.isAssignableFrom(getClass())) {
+        return Collections.singleton((T) this);
+      }
+      return Collections.emptyList();
+    }
+  }
+}
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/webui/UiAction.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/webui/UiAction.java
index 62c074e..5f6dec3 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/webui/UiAction.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/webui/UiAction.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.extensions.webui;
 
+import com.google.gerrit.extensions.conditions.BooleanCondition;
 import com.google.gerrit.extensions.restapi.RestResource;
 import com.google.gerrit.extensions.restapi.RestView;
 
@@ -35,8 +36,8 @@
     private String id;
     private String label;
     private String title;
-    private boolean visible = true;
-    private boolean enabled = true;
+    private BooleanCondition visible = BooleanCondition.TRUE;
+    private BooleanCondition enabled = BooleanCondition.TRUE;
 
     public String getMethod() {
       return method;
@@ -77,6 +78,10 @@
     }
 
     public boolean isVisible() {
+      return getVisibleCondition().value();
+    }
+
+    public BooleanCondition getVisibleCondition() {
       return visible;
     }
 
@@ -85,16 +90,33 @@
      * action description may not be sent to the client.
      */
     public Description setVisible(boolean visible) {
+      return setVisible(BooleanCondition.valueOf(visible));
+    }
+
+    /**
+     * Set if the action's button is visible on screen for the current client. If not visible the
+     * action description may not be sent to the client.
+     */
+    public Description setVisible(BooleanCondition visible) {
       this.visible = visible;
       return this;
     }
 
     public boolean isEnabled() {
-      return enabled && isVisible();
+      return getEnabledCondition().value();
+    }
+
+    public BooleanCondition getEnabledCondition() {
+      return BooleanCondition.and(enabled, visible);
     }
 
     /** Set if the button should be invokable (true), or greyed out (false). */
     public Description setEnabled(boolean enabled) {
+      return setEnabled(BooleanCondition.valueOf(enabled));
+    }
+
+    /** Set if the button should be invokable (true), or greyed out (false). */
+    public Description setEnabled(BooleanCondition enabled) {
       this.enabled = enabled;
       return this;
     }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/ActionJson.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/ActionJson.java
index 19fdcfb..a1deb89 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/ActionJson.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/ActionJson.java
@@ -16,7 +16,6 @@
 
 import static com.google.common.base.Preconditions.checkNotNull;
 
-import com.google.common.collect.FluentIterable;
 import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.Iterables;
 import com.google.common.collect.Lists;
@@ -36,6 +35,7 @@
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 import java.util.ArrayList;
+import java.util.Collections;
 import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
@@ -162,7 +162,7 @@
       return out;
     }
 
-    FluentIterable<UiAction.Description> descs =
+    Iterable<UiAction.Description> descs =
         uiActions.from(changeViews, changeResourceFactory.create(ctl));
 
     // The followup action is a client-side only operation that does not
@@ -174,7 +174,7 @@
       PrivateInternals_UiActionDescription.setMethod(descr, "POST");
       descr.setTitle("Create follow-up change");
       descr.setLabel("Follow-Up");
-      descs = descs.append(descr);
+      descs = Iterables.concat(descs, Collections.singleton(descr));
     }
 
     ACTION:
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/CherryPick.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/CherryPick.java
index 993148e..930cb8b 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/CherryPick.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/CherryPick.java
@@ -14,6 +14,8 @@
 
 package com.google.gerrit.server.change;
 
+import static com.google.gerrit.extensions.conditions.BooleanCondition.and;
+
 import com.google.gerrit.extensions.api.changes.CherryPickInput;
 import com.google.gerrit.extensions.common.ChangeInfo;
 import com.google.gerrit.extensions.restapi.BadRequestException;
@@ -106,10 +108,11 @@
         .setLabel("Cherry Pick")
         .setTitle("Cherry pick change to a different branch")
         .setVisible(
-            rsrc.isCurrent()
-                && permissionBackend
+            and(
+                rsrc.isCurrent(),
+                permissionBackend
                     .user(user)
                     .project(rsrc.getProject())
-                    .testOrFalse(ProjectPermission.CREATE_CHANGE));
+                    .testCond(ProjectPermission.CREATE_CHANGE)));
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/extensions/webui/UiActions.java b/gerrit-server/src/main/java/com/google/gerrit/server/extensions/webui/UiActions.java
index ae15cfd..79a5d4c 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/extensions/webui/UiActions.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/extensions/webui/UiActions.java
@@ -14,23 +14,32 @@
 
 package com.google.gerrit.server.extensions.webui;
 
+import static com.google.gerrit.extensions.conditions.BooleanCondition.and;
+import static com.google.gerrit.extensions.conditions.BooleanCondition.or;
+import static java.util.stream.Collectors.toList;
+
 import com.google.common.base.Predicate;
-import com.google.common.collect.FluentIterable;
+import com.google.common.collect.Streams;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.extensions.api.access.GlobalOrPluginPermission;
+import com.google.gerrit.extensions.conditions.BooleanCondition;
 import com.google.gerrit.extensions.registration.DynamicMap;
 import com.google.gerrit.extensions.restapi.RestCollection;
 import com.google.gerrit.extensions.restapi.RestResource;
 import com.google.gerrit.extensions.restapi.RestView;
 import com.google.gerrit.extensions.webui.PrivateInternals_UiActionDescription;
 import com.google.gerrit.extensions.webui.UiAction;
+import com.google.gerrit.extensions.webui.UiAction.Description;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.permissions.GlobalPermission;
 import com.google.gerrit.server.permissions.PermissionBackend;
+import com.google.gerrit.server.permissions.PermissionBackendCondition;
 import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
+import java.util.Iterator;
+import java.util.List;
 import java.util.Objects;
 import java.util.Set;
 import org.slf4j.Logger;
@@ -53,16 +62,35 @@
     this.userProvider = userProvider;
   }
 
-  public <R extends RestResource> FluentIterable<UiAction.Description> from(
+  public <R extends RestResource> Iterable<UiAction.Description> from(
       RestCollection<?, R> collection, R resource) {
     return from(collection.views(), resource);
   }
 
-  public <R extends RestResource> FluentIterable<UiAction.Description> from(
+  public <R extends RestResource> Iterable<UiAction.Description> from(
       DynamicMap<RestView<R>> views, R resource) {
-    return FluentIterable.from(views)
-        .transform((e) -> describe(e, resource))
-        .filter(Objects::nonNull);
+    List<UiAction.Description> descs =
+        Streams.stream(views)
+            .map(e -> describe(e, resource))
+            .filter(Objects::nonNull)
+            .collect(toList());
+
+    List<PermissionBackendCondition> conds =
+        Streams.concat(
+                descs.stream().flatMap(u -> Streams.stream(visibleCondition(u))),
+                descs.stream().flatMap(u -> Streams.stream(enabledCondition(u))))
+            .collect(toList());
+    permissionBackend.bulkEvaluateTest(conds);
+
+    return descs.stream().filter(u -> u.isVisible()).collect(toList());
+  }
+
+  private static Iterable<PermissionBackendCondition> visibleCondition(Description u) {
+    return u.getVisibleCondition().children(PermissionBackendCondition.class);
+  }
+
+  private static Iterable<PermissionBackendCondition> enabledCondition(Description u) {
+    return u.getEnabledCondition().children(PermissionBackendCondition.class);
   }
 
   @Nullable
@@ -86,22 +114,27 @@
       return null;
     }
 
+    UiAction.Description dsc = ((UiAction<R>) view).getDescription(resource);
+    if (dsc == null) {
+      return null;
+    }
+
+    Set<GlobalOrPluginPermission> globalRequired;
     try {
-      Set<GlobalOrPluginPermission> need =
-          GlobalPermission.fromAnnotation(e.getPluginName(), view.getClass());
-      if (!need.isEmpty() && permissionBackend.user(userProvider).test(need).isEmpty()) {
-        // A permission is required, but test returned no candidates.
-        return null;
-      }
+      globalRequired = GlobalPermission.fromAnnotation(e.getPluginName(), view.getClass());
     } catch (PermissionBackendException err) {
       log.error(
           String.format("exception testing view %s.%s", e.getPluginName(), e.getExportName()), err);
       return null;
     }
-
-    UiAction.Description dsc = ((UiAction<R>) view).getDescription(resource);
-    if (dsc == null || !dsc.isVisible()) {
-      return null;
+    if (!globalRequired.isEmpty()) {
+      PermissionBackend.WithUser withUser = permissionBackend.user(userProvider);
+      Iterator<GlobalOrPluginPermission> i = globalRequired.iterator();
+      BooleanCondition p = withUser.testCond(i.next());
+      while (i.hasNext()) {
+        p = or(p, withUser.testCond(i.next()));
+      }
+      dsc.setVisible(and(p, dsc.getVisibleCondition()));
     }
 
     String name = e.getExportName().substring(d + 1);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/permissions/PermissionBackend.java b/gerrit-server/src/main/java/com/google/gerrit/server/permissions/PermissionBackend.java
index 522eccb..5c0cf44 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/permissions/PermissionBackend.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/permissions/PermissionBackend.java
@@ -20,6 +20,7 @@
 import com.google.common.collect.Sets;
 import com.google.gerrit.common.data.LabelType;
 import com.google.gerrit.extensions.api.access.GlobalOrPluginPermission;
+import com.google.gerrit.extensions.conditions.BooleanCondition;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.reviewdb.client.Branch;
 import com.google.gerrit.reviewdb.client.Project;
@@ -78,7 +79,7 @@
  *   public UiAction.Description getDescription(ChangeResource rsrc) {
  *     return new UiAction.Description()
  *       .setLabel("Submit")
- *       .setVisible(rsrc.permissions().testOrFalse(ChangePermission.SUBMIT));
+ *       .setVisible(rsrc.permissions().testCond(ChangePermission.SUBMIT));
  * }
  * </pre>
  */
@@ -94,6 +95,24 @@
     return user(checkNotNull(user, "Provider<CurrentUser>").get());
   }
 
+  /**
+   * Bulk evaluate a collection of {@link PermissionBackendCondition} for view handling.
+   *
+   * <p>Overridden implementations should call {@link PermissionBackendCondition#set(boolean)} to
+   * cache the result of {@code testOrFalse} in the condition for later evaluation. Caching the
+   * result will bypass the usual invocation of {@code testOrFalse}.
+   *
+   * <p>{@code conds} may contain duplicate entries (such as same user, resource, permission
+   * triplet). When duplicates exist, implementations should set a result into all instances to
+   * ensure {@code testOrFalse} does not get invoked during evaluation of the containing condition.
+   *
+   * @param conds conditions to consider.
+   */
+  public void bulkEvaluateTest(Collection<PermissionBackendCondition> conds) {
+    // Do nothing by default. The default implementation of PermissionBackendCondition
+    // delegates to the appropriate testOrFalse method in PermissionBackend.
+  }
+
   /** PermissionBackend with an optional per-request ReviewDb handle. */
   public abstract static class AcceptsReviewDb<T> {
     protected Provider<ReviewDb> db;
@@ -198,6 +217,10 @@
       }
     }
 
+    public BooleanCondition testCond(GlobalOrPluginPermission perm) {
+      return new PermissionBackendCondition.WithUser(this, perm);
+    }
+
     /**
      * Filter a set of projects using {@code check(perm)}.
      *
@@ -265,6 +288,10 @@
         return false;
       }
     }
+
+    public BooleanCondition testCond(ProjectPermission perm) {
+      return new PermissionBackendCondition.ForProject(this, perm);
+    }
   }
 
   /** PermissionBackend scoped to a user, project and reference. */
@@ -313,6 +340,10 @@
         return false;
       }
     }
+
+    public BooleanCondition testCond(RefPermission perm) {
+      return new PermissionBackendCondition.ForRef(this, perm);
+    }
   }
 
   /** PermissionBackend scoped to a user, project, reference and change. */
@@ -354,6 +385,10 @@
       }
     }
 
+    public BooleanCondition testCond(ChangePermissionOrLabel perm) {
+      return new PermissionBackendCondition.ForChange(this, perm);
+    }
+
     /**
      * Test which values of a label the user may be able to set.
      *
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/permissions/PermissionBackendCondition.java b/gerrit-server/src/main/java/com/google/gerrit/server/permissions/PermissionBackendCondition.java
new file mode 100644
index 0000000..8d66e50
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/permissions/PermissionBackendCondition.java
@@ -0,0 +1,152 @@
+// Copyright (C) 2017 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.permissions;
+
+import com.google.gerrit.extensions.api.access.GlobalOrPluginPermission;
+import com.google.gerrit.extensions.conditions.BooleanCondition;
+import com.google.gerrit.extensions.conditions.PrivateInternals_BooleanCondition;
+
+/** {@link BooleanCondition} to evaluate a permission. */
+public abstract class PermissionBackendCondition
+    extends PrivateInternals_BooleanCondition.SubclassOnlyInCoreServer {
+  Boolean value;
+
+  /**
+   * Assign a specific {@code testOrFalse} result to this condition.
+   *
+   * <p>By setting the condition to a specific value the condition will bypass calling {@link
+   * PermissionBackend} during {@code value()}, and immediately return the set value instead.
+   *
+   * @param val value to return from {@code value()}.
+   */
+  public void set(boolean val) {
+    value = val;
+  }
+
+  @Override
+  public abstract String toString();
+
+  public static class WithUser extends PermissionBackendCondition {
+    private final PermissionBackend.WithUser impl;
+    private final GlobalOrPluginPermission perm;
+
+    WithUser(PermissionBackend.WithUser impl, GlobalOrPluginPermission perm) {
+      this.impl = impl;
+      this.perm = perm;
+    }
+
+    public PermissionBackend.WithUser withUser() {
+      return impl;
+    }
+
+    public GlobalOrPluginPermission permission() {
+      return perm;
+    }
+
+    @Override
+    public boolean value() {
+      return value != null ? value : impl.testOrFalse(perm);
+    }
+
+    @Override
+    public String toString() {
+      return "PermissionBackendCondition.WithUser(" + perm + ")";
+    }
+  }
+
+  public static class ForProject extends PermissionBackendCondition {
+    private final PermissionBackend.ForProject impl;
+    private final ProjectPermission perm;
+
+    ForProject(PermissionBackend.ForProject impl, ProjectPermission perm) {
+      this.impl = impl;
+      this.perm = perm;
+    }
+
+    public PermissionBackend.ForProject project() {
+      return impl;
+    }
+
+    public ProjectPermission permission() {
+      return perm;
+    }
+
+    @Override
+    public boolean value() {
+      return value != null ? value : impl.testOrFalse(perm);
+    }
+
+    @Override
+    public String toString() {
+      return "PermissionBackendCondition.ForProject(" + perm + ")";
+    }
+  }
+
+  public static class ForRef extends PermissionBackendCondition {
+    private final PermissionBackend.ForRef impl;
+    private final RefPermission perm;
+
+    ForRef(PermissionBackend.ForRef impl, RefPermission perm) {
+      this.impl = impl;
+      this.perm = perm;
+    }
+
+    public PermissionBackend.ForRef ref() {
+      return impl;
+    }
+
+    public RefPermission permission() {
+      return perm;
+    }
+
+    @Override
+    public boolean value() {
+      return value != null ? value : impl.testOrFalse(perm);
+    }
+
+    @Override
+    public String toString() {
+      return "PermissionBackendCondition.ForRef(" + perm + ")";
+    }
+  }
+
+  public static class ForChange extends PermissionBackendCondition {
+    private final PermissionBackend.ForChange impl;
+    private final ChangePermissionOrLabel perm;
+
+    ForChange(PermissionBackend.ForChange impl, ChangePermissionOrLabel perm) {
+      this.impl = impl;
+      this.perm = perm;
+    }
+
+    public PermissionBackend.ForChange change() {
+      return impl;
+    }
+
+    public ChangePermissionOrLabel permission() {
+      return perm;
+    }
+
+    @Override
+    public boolean value() {
+      return value != null ? value : impl.testOrFalse(perm);
+    }
+
+    @Override
+    public String toString() {
+      return "PermissionBackendCondition.ForChange(" + perm + ")";
+    }
+  }
+}