com.google.gerrit.server.project.SubmitRequirementsEvaluatorImpl Maven / Gradle / Ivy
// 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.annotations.VisibleForTesting;
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.logging.Metadata;
import com.google.gerrit.server.logging.TraceContext;
import com.google.gerrit.server.logging.TraceContext.TraceTimer;
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 queryBuilder;
private final ProjectCache projectCache;
private final PluginSetContext globalSubmitRequirements;
// 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).
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 queryBuilder,
ProjectCache projectCache,
PluginSetContext globalSubmitRequirements,
OneOffRequestContext requestContext) {
this.queryBuilder = queryBuilder;
this.projectCache = projectCache;
this.globalSubmitRequirements = globalSubmitRequirements;
this.requestContext = requestContext;
}
@Override
public void validateExpression(SubmitRequirementExpression expression)
throws QueryParseException {
try (ManualRequestContext ignored = requestContext.open()) {
@SuppressWarnings("unused")
var unused = queryBuilder.get().parse(expression.expressionString());
}
}
@Override
public ImmutableMap evaluateAllRequirements(
ChangeData cd) {
try (ManualRequestContext ignored = requestContext.open()) {
return getRequirements(cd);
}
}
@Override
public SubmitRequirementResult evaluateRequirement(SubmitRequirement sr, ChangeData cd) {
try (ManualRequestContext ignored = requestContext.open()) {
return evaluateRequirementInternal(sr, cd);
}
}
/** Evaluate a {@link SubmitRequirementExpression} using change data. */
@VisibleForTesting
public SubmitRequirementExpressionResult evaluateExpression(
SubmitRequirementExpression expression, ChangeData changeData) {
try {
Predicate 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());
}
}
private SubmitRequirementResult evaluateRequirementInternal(SubmitRequirement sr, ChangeData cd) {
Optional applicabilityResult =
sr.applicabilityExpression().isPresent()
? Optional.of(evaluateExpression(sr.applicabilityExpression().get(), cd))
: Optional.empty();
Optional submittabilityResult =
Optional.of(SubmitRequirementExpressionResult.notEvaluated(sr.submittabilityExpression()));
Optional 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();
}
/**
* Evaluate and return all {@link SubmitRequirement}s.
*
* This includes all globally bound {@link SubmitRequirement}s, as well as requirements stored
* in this project's config and its parents.
*
*
The behaviour in case of the name match is controlled by {@link
* SubmitRequirement#allowOverrideInChildProjects} of global {@link SubmitRequirement}.
*/
private ImmutableMap getRequirements(ChangeData cd) {
try (TraceTimer timer =
TraceContext.newTimer(
"Get submit requirements",
Metadata.builder().changeId(cd.change().getId().get()).build())) {
ImmutableMap globalRequirements = getGlobalRequirements();
ProjectState state = projectCache.get(cd.project()).orElseThrow(illegalState(cd.project()));
Map projectConfigRequirements = state.getSubmitRequirements();
ImmutableMap 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 results =
ImmutableMap.builder();
for (SubmitRequirement requirement : requirements.values()) {
results.put(requirement, evaluateRequirementInternal(requirement, cd));
}
return results.build();
}
}
/**
* Returns a map of all global {@link SubmitRequirement}s, keyed by their lower-case name.
*
* The global {@link SubmitRequirement}s apply to all projects and can be bound by plugins.
*/
private ImmutableMap 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 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();
}
}