| // Copyright (C) 2021 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.project; |
| |
| import static com.google.common.collect.ImmutableMap.toImmutableMap; |
| import static com.google.gerrit.server.project.ProjectCache.illegalState; |
| |
| import com.google.common.collect.ImmutableMap; |
| import com.google.common.flogger.FluentLogger; |
| import com.google.gerrit.entities.SubmitRequirement; |
| import com.google.gerrit.entities.SubmitRequirementExpression; |
| import com.google.gerrit.entities.SubmitRequirementExpressionResult; |
| import com.google.gerrit.entities.SubmitRequirementExpressionResult.PredicateResult; |
| import com.google.gerrit.entities.SubmitRequirementResult; |
| import com.google.gerrit.index.query.Predicate; |
| import com.google.gerrit.index.query.QueryParseException; |
| import com.google.gerrit.server.plugincontext.PluginSetContext; |
| import com.google.gerrit.server.query.change.ChangeData; |
| import com.google.gerrit.server.query.change.SubmitRequirementChangeQueryBuilder; |
| import com.google.gerrit.server.util.ManualRequestContext; |
| import com.google.gerrit.server.util.OneOffRequestContext; |
| import com.google.inject.AbstractModule; |
| import com.google.inject.Inject; |
| import com.google.inject.Module; |
| import com.google.inject.Provider; |
| import com.google.inject.Scopes; |
| import java.util.Locale; |
| import java.util.Map; |
| import java.util.Optional; |
| import java.util.function.Function; |
| import java.util.stream.Stream; |
| |
| /** Evaluates submit requirements for different change data. */ |
| public class SubmitRequirementsEvaluatorImpl implements SubmitRequirementsEvaluator { |
| private static final FluentLogger logger = FluentLogger.forEnclosingClass(); |
| |
| private final Provider<SubmitRequirementChangeQueryBuilder> queryBuilder; |
| private final ProjectCache projectCache; |
| private final PluginSetContext<SubmitRequirement> globalSubmitRequirements; |
| private final OneOffRequestContext requestContext; |
| |
| public static Module module() { |
| return new AbstractModule() { |
| @Override |
| protected void configure() { |
| bind(SubmitRequirementsEvaluator.class) |
| .to(SubmitRequirementsEvaluatorImpl.class) |
| .in(Scopes.SINGLETON); |
| } |
| }; |
| } |
| |
| @Inject |
| private SubmitRequirementsEvaluatorImpl( |
| Provider<SubmitRequirementChangeQueryBuilder> queryBuilder, |
| ProjectCache projectCache, |
| PluginSetContext<SubmitRequirement> globalSubmitRequirements, |
| OneOffRequestContext requestContext) { |
| this.queryBuilder = queryBuilder; |
| this.projectCache = projectCache; |
| this.globalSubmitRequirements = globalSubmitRequirements; |
| this.requestContext = requestContext; |
| } |
| |
| @Override |
| public void validateExpression(SubmitRequirementExpression expression) |
| throws QueryParseException { |
| queryBuilder.get().parse(expression.expressionString()); |
| } |
| |
| @Override |
| public ImmutableMap<SubmitRequirement, SubmitRequirementResult> evaluateAllRequirements( |
| ChangeData cd) { |
| return getRequirements(cd); |
| } |
| |
| @Override |
| public SubmitRequirementResult evaluateRequirement(SubmitRequirement sr, ChangeData cd) { |
| try (ManualRequestContext ignored = requestContext.open()) { |
| // Use a request context to execute predicates as an internal user with expanded visibility. |
| // This is so that the evaluation does not depend on who is running the current request (e.g. |
| // a "ownerin" predicate with group that is not visible to the person making this request). |
| |
| Optional<SubmitRequirementExpressionResult> applicabilityResult = |
| sr.applicabilityExpression().isPresent() |
| ? Optional.of(evaluateExpression(sr.applicabilityExpression().get(), cd)) |
| : Optional.empty(); |
| Optional<SubmitRequirementExpressionResult> submittabilityResult = |
| Optional.of( |
| SubmitRequirementExpressionResult.notEvaluated(sr.submittabilityExpression())); |
| Optional<SubmitRequirementExpressionResult> overrideResult = |
| sr.overrideExpression().isPresent() |
| ? Optional.of( |
| SubmitRequirementExpressionResult.notEvaluated(sr.overrideExpression().get())) |
| : Optional.empty(); |
| if (!sr.applicabilityExpression().isPresent() |
| || SubmitRequirementResult.assertPass(applicabilityResult)) { |
| submittabilityResult = Optional.of(evaluateExpression(sr.submittabilityExpression(), cd)); |
| overrideResult = |
| sr.overrideExpression().isPresent() |
| ? Optional.of(evaluateExpression(sr.overrideExpression().get(), cd)) |
| : Optional.empty(); |
| } |
| |
| if (applicabilityResult.isPresent()) { |
| logger.atFine().log( |
| "Applicability expression result for SR name '%s':" |
| + " passing atoms: %s, failing atoms: %s", |
| sr.name(), |
| applicabilityResult.get().passingAtoms(), |
| applicabilityResult.get().failingAtoms()); |
| } |
| if (submittabilityResult.isPresent()) { |
| logger.atFine().log( |
| "Submittability expression result for SR name '%s':" |
| + " passing atoms: %s, failing atoms: %s", |
| sr.name(), |
| submittabilityResult.get().passingAtoms(), |
| submittabilityResult.get().failingAtoms()); |
| } |
| if (overrideResult.isPresent()) { |
| logger.atFine().log( |
| "Override expression result for SR name '%s':" |
| + " passing atoms: %s, failing atoms: %s", |
| sr.name(), overrideResult.get().passingAtoms(), overrideResult.get().failingAtoms()); |
| } |
| |
| return SubmitRequirementResult.builder() |
| .legacy(Optional.of(false)) |
| .submitRequirement(sr) |
| .patchSetCommitId(cd.currentPatchSet().commitId()) |
| .submittabilityExpressionResult(submittabilityResult) |
| .applicabilityExpressionResult(applicabilityResult) |
| .overrideExpressionResult(overrideResult) |
| .build(); |
| } |
| } |
| |
| @Override |
| public SubmitRequirementExpressionResult evaluateExpression( |
| SubmitRequirementExpression expression, ChangeData changeData) { |
| try { |
| Predicate<ChangeData> predicate = queryBuilder.get().parse(expression.expressionString()); |
| PredicateResult predicateResult = evaluatePredicateTree(predicate, changeData); |
| return SubmitRequirementExpressionResult.create(expression, predicateResult); |
| } catch (QueryParseException | SubmitRequirementEvaluationException e) { |
| logger.atWarning().withCause(e).log( |
| "Failed to evaluate submit requirement expression: %s", expression.expressionString()); |
| return SubmitRequirementExpressionResult.error(expression, e.getMessage()); |
| } |
| } |
| |
| /** |
| * Evaluate and return all {@link SubmitRequirement}s. |
| * |
| * <p>This includes all globally bound {@link SubmitRequirement}s, as well as requirements stored |
| * in this project's config and its parents. |
| * |
| * <p>The behaviour in case of the name match is controlled by {@link |
| * SubmitRequirement#allowOverrideInChildProjects} of global {@link SubmitRequirement}. |
| */ |
| private ImmutableMap<SubmitRequirement, SubmitRequirementResult> getRequirements(ChangeData cd) { |
| Map<String, SubmitRequirement> globalRequirements = getGlobalRequirements(); |
| |
| ProjectState state = projectCache.get(cd.project()).orElseThrow(illegalState(cd.project())); |
| Map<String, SubmitRequirement> projectConfigRequirements = state.getSubmitRequirements(); |
| |
| ImmutableMap<String, SubmitRequirement> requirements = |
| Stream.concat( |
| globalRequirements.entrySet().stream(), |
| projectConfigRequirements.entrySet().stream()) |
| .collect( |
| toImmutableMap( |
| Map.Entry::getKey, |
| Map.Entry::getValue, |
| (globalSubmitRequirement, projectConfigRequirement) -> |
| // Override with projectConfigRequirement if allowed by |
| // globalSubmitRequirement configuration |
| globalSubmitRequirement.allowOverrideInChildProjects() |
| ? projectConfigRequirement |
| : globalSubmitRequirement)); |
| ImmutableMap.Builder<SubmitRequirement, SubmitRequirementResult> results = |
| ImmutableMap.builder(); |
| for (SubmitRequirement requirement : requirements.values()) { |
| results.put(requirement, evaluateRequirement(requirement, cd)); |
| } |
| return results.build(); |
| } |
| |
| /** |
| * Returns a map of all global {@link SubmitRequirement}s, keyed by their lower-case name. |
| * |
| * <p>The global {@link SubmitRequirement}s apply to all projects and can be bound by plugins. |
| */ |
| private Map<String, SubmitRequirement> getGlobalRequirements() { |
| return globalSubmitRequirements.stream() |
| .collect( |
| toImmutableMap( |
| globalRequirement -> globalRequirement.name().toLowerCase(Locale.US), |
| Function.identity())); |
| } |
| |
| /** Evaluate the predicate recursively using change data. */ |
| private PredicateResult evaluatePredicateTree( |
| Predicate<ChangeData> predicate, ChangeData changeData) { |
| PredicateResult.Builder predicateResult = |
| PredicateResult.builder() |
| .predicateString(predicate.isLeaf() ? predicate.getPredicateString() : "") |
| .status(predicate.asMatchable().match(changeData)); |
| predicate |
| .getChildren() |
| .forEach( |
| c -> predicateResult.addChildPredicateResult(evaluatePredicateTree(c, changeData))); |
| return predicateResult.build(); |
| } |
| } |