| // 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(); | |
| } | |
| } |