blob: 04cef70dba9750de8641e2c6559a23587afb17d5 [file] [log] [blame]
// Copyright (C) 2025 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.restapi.change;
import static com.google.common.base.Preconditions.checkState;
import static com.google.common.collect.ImmutableMap.toImmutableMap;
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Iterables;
import com.google.common.flogger.FluentLogger;
import com.google.gerrit.entities.PredicateResult;
import com.google.gerrit.extensions.common.EvaluateChangeQueryExpressionResultInfo;
import com.google.gerrit.extensions.restapi.BadRequestException;
import com.google.gerrit.extensions.restapi.Response;
import com.google.gerrit.extensions.restapi.RestReadView;
import com.google.gerrit.index.query.Predicate;
import com.google.gerrit.index.query.QueryParseException;
import com.google.gerrit.server.change.ChangeResource;
import com.google.gerrit.server.query.change.ChangeData;
import com.google.gerrit.server.query.change.ChangeQueryBuilder;
import com.google.gerrit.server.query.change.InternalChangeQuery;
import com.google.inject.Inject;
import com.google.inject.Provider;
import java.util.List;
import java.util.Map;
import org.kohsuke.args4j.Option;
/** REST endpoint to evaluate whether a change query expression matches the change. */
public class EvaluateChangeQueryExpression implements RestReadView<ChangeResource> {
@Option(
name = "--expression",
usage = "Change query expression for which it should be checked if the change matches.")
public String expression;
@Option(
name = "--use-index",
usage =
"Whether the change query expression should be evaluated against the change state in the"
+ " index.")
public boolean useIndex;
private final Provider<ChangeQueryBuilder> queryBuilder;
private final Provider<InternalChangeQuery> internalChangeQuery;
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
@Inject
EvaluateChangeQueryExpression(
Provider<ChangeQueryBuilder> queryBuilder,
Provider<InternalChangeQuery> internalChangeQuery) {
this.queryBuilder = queryBuilder;
this.internalChangeQuery = internalChangeQuery;
}
@Override
public Response<EvaluateChangeQueryExpressionResultInfo> apply(ChangeResource rsrc)
throws BadRequestException {
if (Strings.isNullOrEmpty(expression)) {
throw new BadRequestException("expression is required");
}
logger.atFine().log("parsing expression %s", expression);
Predicate<ChangeData> predicate = parseExpression(expression);
logger.atFine().log("evaluating predicate string %s", predicate.getPredicateString());
PredicateResult predicateResult = getChangeData(rsrc).evaluatePredicateTree(predicate);
return Response.ok(toInfo(predicateResult));
}
private ChangeData getChangeData(ChangeResource rsrc) {
if (useIndex) {
// Loading the change from the index populates ChangeData with the data that is stored in the
// index, including submit requirement results.
List<ChangeData> changeDatas =
internalChangeQuery.get().byProjectChangeNumber(rsrc.getProject(), rsrc.getId());
checkState(
changeDatas.size() == 1,
"Got %s matches for change %s, expected 1",
changeDatas.size() == 1,
rsrc.getId());
return Iterables.getOnlyElement(changeDatas);
}
// The ChangeData in ChangeResource has been loaded from NoteDb. It doesn't contain submit
// requirement results yet. Submit requirement results are loaded lazily by executing the submit
// requirements. Executing the submit requirements is rather expensive. This means if an
// expression is evaluated that requires checking if the change is submittable (e.g.
// "is:submittable") this is slower than using the change data from the index where submit
// requirement results are already present.
return rsrc.getChangeData();
}
private Predicate<ChangeData> parseExpression(String expression) throws BadRequestException {
try {
return queryBuilder.get().parse(expression);
} catch (QueryParseException e) {
throw new BadRequestException(String.format("invalid query expression: %s", e.getMessage()));
}
}
private static EvaluateChangeQueryExpressionResultInfo toInfo(PredicateResult predicateResult) {
EvaluateChangeQueryExpressionResultInfo info = new EvaluateChangeQueryExpressionResultInfo();
info.status = predicateResult.status();
info.passingAtoms = predicateResult.getPassingAtoms();
info.failingAtoms = predicateResult.getFailingAtoms();
ImmutableMap<String, String> atomExplanations =
predicateResult.getAtomExplanations().entrySet().stream()
.filter(e -> !Strings.isNullOrEmpty(e.getValue()))
.collect(toImmutableMap(Map.Entry::getKey, Map.Entry::getValue));
info.atomExplanations = !atomExplanations.isEmpty() ? atomExplanations : null;
return info;
}
}