Add Submit Requirements predicate

Introduce the `has:approval_owners` predicate that can be used in
the project's submit requirement definition.

Notes:
* as of `stable-3.5` submit requirements is an experimental feature
  and one needs to add the following line to `gerrit.config`
  `experiments.enabled = GerritBackendRequestFeature__enable_submit_requirements`
* as of `stable-3.5` submit requirements coexist with legacy (label
  function's based) submit requirements and they are not taken into
  consideration when sbumittability is checked IOW they don't prevent
  allow changes to be submittable (yet the SubmitRule is still binding
  (all scenarios are re-verified in OwnersApprovalHasOperandIT).

Bug: Issue 15556
Change-Id: I9765649fb0e19d2de6add0c4c2235a4a0155ea66
diff --git a/owners/src/main/java/com/googlesource/gerrit/owners/OwnersApprovalHasOperand.java b/owners/src/main/java/com/googlesource/gerrit/owners/OwnersApprovalHasOperand.java
new file mode 100644
index 0000000..ff00cfd
--- /dev/null
+++ b/owners/src/main/java/com/googlesource/gerrit/owners/OwnersApprovalHasOperand.java
@@ -0,0 +1,52 @@
+// 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.googlesource.gerrit.owners;
+
+import com.google.gerrit.extensions.annotations.Exports;
+import com.google.gerrit.index.query.Predicate;
+import com.google.gerrit.index.query.QueryParseException;
+import com.google.gerrit.server.query.change.ChangeData;
+import com.google.gerrit.server.query.change.ChangeQueryBuilder;
+import com.google.gerrit.server.query.change.ChangeQueryBuilder.ChangeHasOperandFactory;
+import com.google.inject.AbstractModule;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+
+/** Class contributing an "approval_owners" operand to the "has" predicate. */
+@Singleton
+class OwnersApprovalHasOperand implements ChangeHasOperandFactory {
+  static final String OPERAND = "approval";
+
+  static class OwnerApprovalHasOperandModule extends AbstractModule {
+    @Override
+    protected void configure() {
+      bind(ChangeHasOperandFactory.class)
+          .annotatedWith(Exports.named(OPERAND))
+          .to(OwnersApprovalHasOperand.class);
+    }
+  }
+
+  private final OwnersApprovalHasPredicate ownersApprovalHasPredicate;
+
+  @Inject
+  OwnersApprovalHasOperand(OwnersApprovalHasPredicate ownersApprovalHasPredicate) {
+    this.ownersApprovalHasPredicate = ownersApprovalHasPredicate;
+  }
+
+  @Override
+  public Predicate<ChangeData> create(ChangeQueryBuilder builder) throws QueryParseException {
+    return ownersApprovalHasPredicate;
+  }
+}
diff --git a/owners/src/main/java/com/googlesource/gerrit/owners/OwnersApprovalHasPredicate.java b/owners/src/main/java/com/googlesource/gerrit/owners/OwnersApprovalHasPredicate.java
new file mode 100644
index 0000000..3e62023
--- /dev/null
+++ b/owners/src/main/java/com/googlesource/gerrit/owners/OwnersApprovalHasPredicate.java
@@ -0,0 +1,57 @@
+// 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.googlesource.gerrit.owners;
+
+import com.google.gerrit.entities.SubmitRecord;
+import com.google.gerrit.extensions.annotations.PluginName;
+import com.google.gerrit.server.query.change.ChangeData;
+import com.google.gerrit.server.query.change.SubmitRequirementPredicate;
+import com.google.gerrit.server.rules.SubmitRule;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+import java.util.Optional;
+
+/**
+ * A predicate that checks if a given change has all necessary owner approvals. Matches with changes
+ * that have an owner approval. This predicate wraps the existing {@link OwnersSubmitRequirement}
+ * (that implements the {@link SubmitRule}) to perform the logic.
+ */
+@Singleton
+class OwnersApprovalHasPredicate extends SubmitRequirementPredicate {
+
+  private final OwnersSubmitRequirement ownersSubmitRequirement;
+
+  @Inject
+  OwnersApprovalHasPredicate(
+      @PluginName String pluginName, OwnersSubmitRequirement ownersSubmitRequirement) {
+    super("has", OwnersApprovalHasOperand.OPERAND + "_" + pluginName);
+    this.ownersSubmitRequirement = ownersSubmitRequirement;
+  }
+
+  @Override
+  public boolean match(ChangeData cd) {
+    Optional<SubmitRecord> submitRecord = ownersSubmitRequirement.evaluate(cd);
+    return submitRecord.map(sr -> sr.status == SubmitRecord.Status.OK).orElse(false);
+  }
+
+  /**
+   * Assuming that it is similarly expensive to calculate this as the 'code-owners' plugin hence
+   * giving the same value.
+   */
+  @Override
+  public int getCost() {
+    return 10;
+  }
+}
diff --git a/owners/src/main/java/com/googlesource/gerrit/owners/OwnersModule.java b/owners/src/main/java/com/googlesource/gerrit/owners/OwnersModule.java
index fe6d325..bd2f074 100644
--- a/owners/src/main/java/com/googlesource/gerrit/owners/OwnersModule.java
+++ b/owners/src/main/java/com/googlesource/gerrit/owners/OwnersModule.java
@@ -41,6 +41,7 @@
 
     if (pluginSettings.enableSubmitRequirement()) {
       install(new OwnersSubmitRequirement.OwnersSubmitRequirementModule());
+      install(new OwnersApprovalHasOperand.OwnerApprovalHasOperandModule());
     } else {
       logger.atInfo().log(
           "OwnersSubmitRequirement is disabled therefore it will not be evaluated.");
diff --git a/owners/src/test/java/com/googlesource/gerrit/owners/OwnersApprovalHasOperandIT.java b/owners/src/test/java/com/googlesource/gerrit/owners/OwnersApprovalHasOperandIT.java
new file mode 100644
index 0000000..667227c
--- /dev/null
+++ b/owners/src/test/java/com/googlesource/gerrit/owners/OwnersApprovalHasOperandIT.java
@@ -0,0 +1,105 @@
+// 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.googlesource.gerrit.owners;
+
+import static com.google.common.collect.ImmutableList.toImmutableList;
+import static com.google.gerrit.extensions.common.SubmitRequirementResultInfo.Status.SATISFIED;
+import static com.google.gerrit.extensions.common.SubmitRequirementResultInfo.Status.UNSATISFIED;
+
+import com.google.gerrit.acceptance.PushOneCommit;
+import com.google.gerrit.acceptance.TestAccount;
+import com.google.gerrit.acceptance.config.GlobalPluginConfig;
+import com.google.gerrit.entities.SubmitRequirement;
+import com.google.gerrit.entities.SubmitRequirementExpression;
+import com.google.gerrit.extensions.api.changes.ChangeApi;
+import com.google.gerrit.extensions.api.changes.ReviewInput;
+import com.google.gerrit.extensions.client.ListChangesOption;
+import com.google.gerrit.extensions.common.ChangeInfo;
+import com.google.gerrit.extensions.common.SubmitRequirementResultInfo;
+import com.google.gerrit.extensions.common.SubmitRequirementResultInfo.Status;
+import com.google.gerrit.server.experiments.ExperimentFeaturesConstants;
+import com.google.gerrit.testing.ConfigSuite;
+import com.googlesource.gerrit.owners.common.LabelDefinition;
+import java.util.Collection;
+import org.eclipse.jgit.lib.Config;
+import org.junit.Before;
+import org.junit.Test;
+
+public class OwnersApprovalHasOperandIT extends OwnersSubmitRequirementIT {
+  private static final String REQUIREMENT_NAME = "Owner-Approval";
+
+  // This configuration is needed on 3.5 only and should be removed during/after the merge to
+  // stable-3.6 as it is enabled there by default.
+  @ConfigSuite.Default
+  public static Config defaultConfig() {
+    Config cfg = new Config();
+    cfg.setString(
+        "experiments",
+        null,
+        "enabled",
+        ExperimentFeaturesConstants.GERRIT_BACKEND_REQUEST_FEATURE_ENABLE_SUBMIT_REQUIREMENTS);
+    return cfg;
+  }
+
+  @Before
+  public void setup() throws Exception {
+    configSubmitRequirement(
+        project,
+        SubmitRequirement.builder()
+            .setName(REQUIREMENT_NAME)
+            .setSubmittabilityExpression(SubmitRequirementExpression.create("has:approval_owners"))
+            .setAllowOverrideInChildProjects(false)
+            .build());
+  }
+
+  @Test
+  @GlobalPluginConfig(
+      pluginName = "owners",
+      name = "owners.enableSubmitRequirement",
+      value = "true")
+  public void shouldOwnersRequirementBeSatisfied() throws Exception {
+    TestAccount admin2 = accountCreator.admin2();
+    addOwnerFileToRoot(true, LabelDefinition.parse("Code-Review,1").get(), admin2);
+
+    PushOneCommit.Result r = createChange("Add a file", "foo", "bar");
+    ChangeApi changeApi = forChange(r);
+    ChangeInfo changeNotReady = changeApi.get(ListChangesOption.SUBMIT_REQUIREMENTS);
+    verifySubmitRequirements(changeNotReady.submitRequirements, REQUIREMENT_NAME, UNSATISFIED);
+
+    requestScopeOperations.setApiUser(admin2.id());
+    forChange(r).current().review(ReviewInput.recommend());
+    ChangeInfo ownersVoteSufficient = forChange(r).get(ListChangesOption.SUBMIT_REQUIREMENTS);
+    verifySubmitRequirements(ownersVoteSufficient.submitRequirements, REQUIREMENT_NAME, SATISFIED);
+  }
+
+  private void verifySubmitRequirements(
+      Collection<SubmitRequirementResultInfo> requirements, String name, Status status) {
+    for (SubmitRequirementResultInfo requirement : requirements) {
+      if (requirement.name.equals(name) && requirement.status == status) {
+        return;
+      }
+    }
+
+    throw new AssertionError(
+        String.format(
+            "Could not find submit requirement %s with status %s (results = %s)",
+            name,
+            status,
+            requirements.stream()
+                .map(r -> String.format("%s=%s", r.name, r.status))
+                .collect(toImmutableList())));
+  }
+}
diff --git a/owners/src/test/java/com/googlesource/gerrit/owners/OwnersSubmitRequirementIT.java b/owners/src/test/java/com/googlesource/gerrit/owners/OwnersSubmitRequirementIT.java
index de389ba..fda7edb 100644
--- a/owners/src/test/java/com/googlesource/gerrit/owners/OwnersSubmitRequirementIT.java
+++ b/owners/src/test/java/com/googlesource/gerrit/owners/OwnersSubmitRequirementIT.java
@@ -56,7 +56,7 @@
   private static final LegacySubmitRequirementInfo READY =
       new LegacySubmitRequirementInfo("OK", "Owners", "owners");
 
-  @Inject private RequestScopeOperations requestScopeOperations;
+  @Inject protected RequestScopeOperations requestScopeOperations;
   @Inject private ProjectOperations projectOperations;
 
   @Test
@@ -396,7 +396,7 @@
     }
   }
 
-  private ChangeApi forChange(PushOneCommit.Result r) throws RestApiException {
+  protected ChangeApi forChange(PushOneCommit.Result r) throws RestApiException {
     return gApi.changes().id(r.getChangeId());
   }
 
@@ -443,7 +443,7 @@
             ""));
   }
 
-  private void addOwnerFileToRoot(boolean inherit, LabelDefinition label, TestAccount u)
+  protected void addOwnerFileToRoot(boolean inherit, LabelDefinition label, TestAccount u)
       throws Exception {
     // Add OWNERS file to root:
     //