| // 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.query.change; |
| |
| import com.google.common.base.Splitter; |
| import com.google.gerrit.entities.Account; |
| import com.google.gerrit.index.SchemaFieldDefs.SchemaField; |
| import com.google.gerrit.index.query.Predicate; |
| import com.google.gerrit.index.query.QueryBuilder; |
| import com.google.gerrit.index.query.QueryParseException; |
| import com.google.gerrit.server.submitrequirement.predicate.ConstantPredicate; |
| import com.google.gerrit.server.submitrequirement.predicate.DistinctVotersPredicate; |
| import com.google.gerrit.server.submitrequirement.predicate.FileEditsPredicate; |
| import com.google.gerrit.server.submitrequirement.predicate.FileEditsPredicate.FileEditsArgs; |
| import com.google.gerrit.server.submitrequirement.predicate.HasSubmoduleUpdatePredicate; |
| import com.google.gerrit.server.submitrequirement.predicate.RegexAuthorEmailPredicate; |
| import com.google.gerrit.server.submitrequirement.predicate.RegexCommitterEmailPredicate; |
| import com.google.gerrit.server.submitrequirement.predicate.RegexUploaderEmailPredicateFactory; |
| import com.google.inject.Inject; |
| import java.util.List; |
| import java.util.Locale; |
| import java.util.Set; |
| import java.util.regex.Matcher; |
| import java.util.regex.Pattern; |
| import java.util.regex.PatternSyntaxException; |
| |
| /** |
| * A query builder for submit requirement expressions that includes all {@link ChangeQueryBuilder} |
| * operators, in addition to extra operators contributed by this class. |
| * |
| * <p>Operators defined in this class cannot be used in change queries. |
| */ |
| public class SubmitRequirementChangeQueryBuilder extends ChangeQueryBuilder { |
| |
| private static final QueryBuilder.Definition<ChangeData, ChangeQueryBuilder> def = |
| new QueryBuilder.Definition<>(SubmitRequirementChangeQueryBuilder.class); |
| |
| private final DistinctVotersPredicate.Factory distinctVotersPredicateFactory; |
| private final HasSubmoduleUpdatePredicate.Factory hasSubmoduleUpdateFactory; |
| |
| /** |
| * Regular expression for the {@link #file(String)} operator. Field value is of the form: |
| * |
| * <p>'$fileRegex',withDiffContaining='$contentRegex' |
| * |
| * <p>Both $fileRegex and $contentRegex may contain escaped single or double quotes. |
| */ |
| private static final Pattern FILE_EDITS_PATTERN = |
| Pattern.compile("'((?:(?:\\\\')|(?:[^']))*)',withDiffContaining='((?:(?:\\\\')|(?:[^']))*)'"); |
| |
| public static final String SUBMODULE_UPDATE_HAS_ARG = "submodule-update"; |
| private static final Splitter SUBMODULE_UPDATE_SPLITTER = Splitter.on(","); |
| |
| private final FileEditsPredicate.Factory fileEditsPredicateFactory; |
| private final RegexUploaderEmailPredicateFactory regexUploaderEmailPredicateFactory; |
| |
| @Inject |
| SubmitRequirementChangeQueryBuilder( |
| Arguments args, |
| DistinctVotersPredicate.Factory distinctVotersPredicateFactory, |
| FileEditsPredicate.Factory fileEditsPredicateFactory, |
| HasSubmoduleUpdatePredicate.Factory hasSubmoduleUpdateFactory, |
| RegexUploaderEmailPredicateFactory regexUploaderEmailPredicateFactory) { |
| super(def, args); |
| this.distinctVotersPredicateFactory = distinctVotersPredicateFactory; |
| this.fileEditsPredicateFactory = fileEditsPredicateFactory; |
| this.hasSubmoduleUpdateFactory = hasSubmoduleUpdateFactory; |
| this.regexUploaderEmailPredicateFactory = regexUploaderEmailPredicateFactory; |
| } |
| |
| @Override |
| protected void checkFieldAvailable(SchemaField<ChangeData, ?> field, String operator) { |
| // Submit requirements don't rely on the index, so they can be used regardless of index schema |
| // version. |
| } |
| |
| @Override |
| public Predicate<ChangeData> is(String value) throws QueryParseException { |
| if ("submittable".equalsIgnoreCase(value)) { |
| throw new QueryParseException( |
| String.format( |
| "Operator 'is:submittable' cannot be used in submit requirement expressions.")); |
| } |
| if ("true".equalsIgnoreCase(value) || "false".equalsIgnoreCase(value)) { |
| return new ConstantPredicate(value); |
| } |
| return super.is(value); |
| } |
| |
| @Override |
| public Predicate<ChangeData> has(String value) throws QueryParseException { |
| if (value.toLowerCase(Locale.US).startsWith(SUBMODULE_UPDATE_HAS_ARG)) { |
| List<String> args = SUBMODULE_UPDATE_SPLITTER.splitToList(value); |
| if (args.size() > 2) { |
| throw error( |
| String.format( |
| "wrong number of arguments for the has:%s operator", SUBMODULE_UPDATE_HAS_ARG)); |
| } else if (args.size() == 2) { |
| List<String> baseValue = Splitter.on("=").splitToList(args.get(1)); |
| if (baseValue.size() != 2) { |
| throw error("unexpected base value format"); |
| } |
| if (!baseValue.get(0).toLowerCase(Locale.US).equals("base")) { |
| throw error("unexpected base value format"); |
| } |
| try { |
| int base = Integer.parseInt(baseValue.get(1)); |
| return hasSubmoduleUpdateFactory.create(base); |
| } catch (NumberFormatException e) { |
| throw error( |
| String.format( |
| "failed to parse the parent number %s: %s", baseValue.get(1), e.getMessage())); |
| } |
| } else { |
| return hasSubmoduleUpdateFactory.create(0); |
| } |
| } |
| return super.has(value); |
| } |
| |
| @Operator |
| public Predicate<ChangeData> authoremail(String who) throws QueryParseException { |
| return new RegexAuthorEmailPredicate(who); |
| } |
| |
| @Operator |
| public Predicate<ChangeData> committerEmail(String who) throws QueryParseException { |
| return new RegexCommitterEmailPredicate(who); |
| } |
| |
| @Operator |
| public Predicate<ChangeData> uploaderEmail(String who) throws QueryParseException { |
| return regexUploaderEmailPredicateFactory.create(who); |
| } |
| |
| @Operator |
| public Predicate<ChangeData> distinctvoters(String value) throws QueryParseException { |
| return distinctVotersPredicateFactory.create(value); |
| } |
| |
| /** |
| * A SR operator that can match with file path and content pattern. The value should be of the |
| * form: |
| * |
| * <p>file:"'$filePattern',withDiffContaining='$contentPattern'" |
| * |
| * <p>The operator matches with changes that have their latest PS vs. base diff containing a file |
| * path matching the {@code filePattern} with an edit (added, deleted, modified) matching the |
| * {@code contentPattern}. {@code filePattern} and {@code contentPattern} can start with "^" to |
| * use regular expression matching. |
| * |
| * <p>If the specified value does not match this form, we fall back to the operator's |
| * implementation in {@link ChangeQueryBuilder}. |
| */ |
| @Override |
| public Predicate<ChangeData> file(String value) throws QueryParseException { |
| Matcher matcher = FILE_EDITS_PATTERN.matcher(value); |
| if (!matcher.find()) { |
| return super.file(value); |
| } |
| String filePattern = matcher.group(1); |
| String contentPattern = matcher.group(2); |
| if (filePattern.startsWith("^")) { |
| validateRegularExpression(filePattern, "Invalid file pattern."); |
| } |
| if (contentPattern.startsWith("^")) { |
| validateRegularExpression(contentPattern, "Invalid content pattern."); |
| } |
| return fileEditsPredicateFactory.create(FileEditsArgs.create(filePattern, contentPattern)); |
| } |
| |
| @Override |
| protected void validateLabelArgs(Set<Account.Id> accountIds) throws QueryParseException {} |
| |
| private static void validateRegularExpression(String pattern, String errorMessage) |
| throws QueryParseException { |
| try { |
| Pattern.compile(pattern); |
| } catch (PatternSyntaxException e) { |
| throw new QueryParseException(errorMessage, e); |
| } |
| } |
| } |