Line data Source code
1 : // Copyright (C) 2021 The Android Open Source Project 2 : // 3 : // Licensed under the Apache License, Version 2.0 (the "License"); 4 : // you may not use this file except in compliance with the License. 5 : // You may obtain a copy of the License at 6 : // 7 : // http://www.apache.org/licenses/LICENSE-2.0 8 : // 9 : // Unless required by applicable law or agreed to in writing, software 10 : // distributed under the License is distributed on an "AS IS" BASIS, 11 : // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 : // See the License for the specific language governing permissions and 13 : // limitations under the License. 14 : 15 : package com.google.gerrit.server.query.change; 16 : 17 : import com.google.common.base.Splitter; 18 : import com.google.gerrit.index.SchemaFieldDefs.SchemaField; 19 : import com.google.gerrit.index.query.Predicate; 20 : import com.google.gerrit.index.query.QueryBuilder; 21 : import com.google.gerrit.index.query.QueryParseException; 22 : import com.google.gerrit.server.query.FileEditsPredicate; 23 : import com.google.gerrit.server.query.FileEditsPredicate.FileEditsArgs; 24 : import com.google.inject.Inject; 25 : import java.util.List; 26 : import java.util.Locale; 27 : import java.util.regex.Matcher; 28 : import java.util.regex.Pattern; 29 : import java.util.regex.PatternSyntaxException; 30 : 31 : /** 32 : * A query builder for submit requirement expressions that includes all {@link ChangeQueryBuilder} 33 : * operators, in addition to extra operators contributed by this class. 34 : * 35 : * <p>Operators defined in this class cannot be used in change queries. 36 : */ 37 : public class SubmitRequirementChangeQueryBuilder extends ChangeQueryBuilder { 38 : 39 5 : private static final QueryBuilder.Definition<ChangeData, ChangeQueryBuilder> def = 40 : new QueryBuilder.Definition<>(SubmitRequirementChangeQueryBuilder.class); 41 : 42 : private final DistinctVotersPredicate.Factory distinctVotersPredicateFactory; 43 : private final HasSubmoduleUpdatePredicate.Factory hasSubmoduleUpdateFactory; 44 : 45 : /** 46 : * Regular expression for the {@link #file(String)} operator. Field value is of the form: 47 : * 48 : * <p>'$fileRegex',withDiffContaining='$contentRegex' 49 : * 50 : * <p>Both $fileRegex and $contentRegex may contain escaped single or double quotes. 51 : */ 52 5 : private static final Pattern FILE_EDITS_PATTERN = 53 5 : Pattern.compile("'((?:(?:\\\\')|(?:[^']))*)',withDiffContaining='((?:(?:\\\\')|(?:[^']))*)'"); 54 : 55 : public static final String SUBMODULE_UPDATE_HAS_ARG = "submodule-update"; 56 5 : private static final Splitter SUBMODULE_UPDATE_SPLITTER = Splitter.on(","); 57 : 58 : private final FileEditsPredicate.Factory fileEditsPredicateFactory; 59 : 60 : @Inject 61 : SubmitRequirementChangeQueryBuilder( 62 : Arguments args, 63 : DistinctVotersPredicate.Factory distinctVotersPredicateFactory, 64 : FileEditsPredicate.Factory fileEditsPredicateFactory, 65 : HasSubmoduleUpdatePredicate.Factory hasSubmoduleUpdateFactory) { 66 5 : super(def, args); 67 5 : this.distinctVotersPredicateFactory = distinctVotersPredicateFactory; 68 5 : this.fileEditsPredicateFactory = fileEditsPredicateFactory; 69 5 : this.hasSubmoduleUpdateFactory = hasSubmoduleUpdateFactory; 70 5 : } 71 : 72 : @Override 73 : protected void checkFieldAvailable(SchemaField<ChangeData, ?> field, String operator) { 74 : // Submit requirements don't rely on the index, so they can be used regardless of index schema 75 : // version. 76 1 : } 77 : 78 : @Override 79 : public Predicate<ChangeData> is(String value) throws QueryParseException { 80 2 : if ("submittable".equalsIgnoreCase(value)) { 81 1 : throw new QueryParseException( 82 1 : String.format( 83 : "Operator 'is:submittable' cannot be used in submit requirement expressions.")); 84 : } 85 2 : if ("true".equalsIgnoreCase(value) || "false".equalsIgnoreCase(value)) { 86 2 : return new ConstantPredicate(value); 87 : } 88 1 : return super.is(value); 89 : } 90 : 91 : @Override 92 : public Predicate<ChangeData> has(String value) throws QueryParseException { 93 2 : if (value.toLowerCase(Locale.US).startsWith(SUBMODULE_UPDATE_HAS_ARG)) { 94 1 : List<String> args = SUBMODULE_UPDATE_SPLITTER.splitToList(value); 95 1 : if (args.size() > 2) { 96 1 : throw error( 97 1 : String.format( 98 : "wrong number of arguments for the has:%s operator", SUBMODULE_UPDATE_HAS_ARG)); 99 1 : } else if (args.size() == 2) { 100 1 : List<String> baseValue = Splitter.on("=").splitToList(args.get(1)); 101 1 : if (baseValue.size() != 2) { 102 1 : throw error("unexpected base value format"); 103 : } 104 1 : if (!baseValue.get(0).toLowerCase(Locale.US).equals("base")) { 105 0 : throw error("unexpected base value format"); 106 : } 107 : try { 108 1 : int base = Integer.parseInt(baseValue.get(1)); 109 1 : return hasSubmoduleUpdateFactory.create(base); 110 1 : } catch (NumberFormatException e) { 111 1 : throw error( 112 1 : String.format( 113 1 : "failed to parse the parent number %s: %s", baseValue.get(1), e.getMessage())); 114 : } 115 : } else { 116 1 : return hasSubmoduleUpdateFactory.create(0); 117 : } 118 : } 119 1 : return super.has(value); 120 : } 121 : 122 : @Operator 123 : public Predicate<ChangeData> authoremail(String who) throws QueryParseException { 124 1 : return new RegexAuthorEmailPredicate(who); 125 : } 126 : 127 : @Operator 128 : public Predicate<ChangeData> distinctvoters(String value) throws QueryParseException { 129 1 : return distinctVotersPredicateFactory.create(value); 130 : } 131 : 132 : /** 133 : * A SR operator that can match with file path and content pattern. The value should be of the 134 : * form: 135 : * 136 : * <p>file:"'$filePattern',withDiffContaining='$contentPattern'" 137 : * 138 : * <p>The operator matches with changes that have their latest PS vs. base diff containing a file 139 : * path matching the {@code filePattern} with an edit (added, deleted, modified) matching the 140 : * {@code contentPattern}. {@code filePattern} and {@code contentPattern} can start with "^" to 141 : * use regular expression matching. 142 : * 143 : * <p>If the specified value does not match this form, we fall back to the operator's 144 : * implementation in {@link ChangeQueryBuilder}. 145 : */ 146 : @Override 147 : public Predicate<ChangeData> file(String value) throws QueryParseException { 148 1 : Matcher matcher = FILE_EDITS_PATTERN.matcher(value); 149 1 : if (!matcher.find()) { 150 1 : return super.file(value); 151 : } 152 1 : String filePattern = matcher.group(1); 153 1 : String contentPattern = matcher.group(2); 154 1 : if (filePattern.startsWith("^")) { 155 1 : validateRegularExpression(filePattern, "Invalid file pattern."); 156 : } 157 1 : if (contentPattern.startsWith("^")) { 158 1 : validateRegularExpression(contentPattern, "Invalid content pattern."); 159 : } 160 1 : return fileEditsPredicateFactory.create(FileEditsArgs.create(filePattern, contentPattern)); 161 : } 162 : 163 : private static void validateRegularExpression(String pattern, String errorMessage) 164 : throws QueryParseException { 165 : try { 166 1 : Pattern.compile(pattern); 167 1 : } catch (PatternSyntaxException e) { 168 1 : throw new QueryParseException(errorMessage, e); 169 1 : } 170 1 : } 171 : }