LCOV - code coverage report
Current view: top level - server/query/change - ChangeQueryBuilder.java (source / functions) Hit Total Coverage
Test: _coverage_report.dat Lines: 578 665 86.9 %
Date: 2022-11-19 15:00:39 Functions: 118 125 94.4 %

          Line data    Source code
       1             : // Copyright (C) 2009 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 static com.google.common.collect.ImmutableSet.toImmutableSet;
      18             : import static com.google.gerrit.entities.Change.CHANGE_ID_PATTERN;
      19             : import static com.google.gerrit.server.account.AccountResolver.isSelf;
      20             : import static com.google.gerrit.server.query.change.ChangeData.asChanges;
      21             : import static java.util.stream.Collectors.toList;
      22             : import static java.util.stream.Collectors.toSet;
      23             : 
      24             : import com.google.common.annotations.VisibleForTesting;
      25             : import com.google.common.base.Enums;
      26             : import com.google.common.base.Splitter;
      27             : import com.google.common.collect.Iterables;
      28             : import com.google.common.collect.Lists;
      29             : import com.google.common.collect.Sets;
      30             : import com.google.common.flogger.FluentLogger;
      31             : import com.google.common.primitives.Ints;
      32             : import com.google.gerrit.common.Nullable;
      33             : import com.google.gerrit.entities.Account;
      34             : import com.google.gerrit.entities.AccountGroup;
      35             : import com.google.gerrit.entities.Address;
      36             : import com.google.gerrit.entities.BranchNameKey;
      37             : import com.google.gerrit.entities.Change;
      38             : import com.google.gerrit.entities.GroupDescription;
      39             : import com.google.gerrit.entities.GroupReference;
      40             : import com.google.gerrit.entities.PatchSet;
      41             : import com.google.gerrit.entities.Project;
      42             : import com.google.gerrit.entities.RefNames;
      43             : import com.google.gerrit.entities.SubmitRecord;
      44             : import com.google.gerrit.exceptions.NotSignedInException;
      45             : import com.google.gerrit.exceptions.StorageException;
      46             : import com.google.gerrit.extensions.registration.DynamicMap;
      47             : import com.google.gerrit.index.IndexConfig;
      48             : import com.google.gerrit.index.Schema;
      49             : import com.google.gerrit.index.SchemaFieldDefs.SchemaField;
      50             : import com.google.gerrit.index.SchemaUtil;
      51             : import com.google.gerrit.index.query.LimitPredicate;
      52             : import com.google.gerrit.index.query.Predicate;
      53             : import com.google.gerrit.index.query.QueryBuilder;
      54             : import com.google.gerrit.index.query.QueryParseException;
      55             : import com.google.gerrit.index.query.QueryRequiresAuthException;
      56             : import com.google.gerrit.server.CommentsUtil;
      57             : import com.google.gerrit.server.CurrentUser;
      58             : import com.google.gerrit.server.IdentifiedUser;
      59             : import com.google.gerrit.server.StarredChangesUtil;
      60             : import com.google.gerrit.server.account.AccountCache;
      61             : import com.google.gerrit.server.account.AccountResolver;
      62             : import com.google.gerrit.server.account.AccountResolver.UnresolvableAccountException;
      63             : import com.google.gerrit.server.account.AccountState;
      64             : import com.google.gerrit.server.account.DestinationList;
      65             : import com.google.gerrit.server.account.GroupBackend;
      66             : import com.google.gerrit.server.account.GroupBackends;
      67             : import com.google.gerrit.server.account.GroupMembers;
      68             : import com.google.gerrit.server.account.VersionedAccountDestinations;
      69             : import com.google.gerrit.server.account.VersionedAccountQueries;
      70             : import com.google.gerrit.server.change.ChangeTriplet;
      71             : import com.google.gerrit.server.change.MergeabilityComputationBehavior;
      72             : import com.google.gerrit.server.config.AllProjectsName;
      73             : import com.google.gerrit.server.config.AllUsersName;
      74             : import com.google.gerrit.server.config.GerritServerConfig;
      75             : import com.google.gerrit.server.config.HasOperandAliasConfig;
      76             : import com.google.gerrit.server.config.OperatorAliasConfig;
      77             : import com.google.gerrit.server.experiments.ExperimentFeatures;
      78             : import com.google.gerrit.server.git.GitRepositoryManager;
      79             : import com.google.gerrit.server.index.change.ChangeField;
      80             : import com.google.gerrit.server.index.change.ChangeIndex;
      81             : import com.google.gerrit.server.index.change.ChangeIndexCollection;
      82             : import com.google.gerrit.server.index.change.ChangeIndexRewriter;
      83             : import com.google.gerrit.server.notedb.ReviewerStateInternal;
      84             : import com.google.gerrit.server.patch.PatchListCache;
      85             : import com.google.gerrit.server.permissions.PermissionBackend;
      86             : import com.google.gerrit.server.plugincontext.PluginSetContext;
      87             : import com.google.gerrit.server.project.ChildProjects;
      88             : import com.google.gerrit.server.project.ProjectCache;
      89             : import com.google.gerrit.server.query.change.PredicateArgs.ValOp;
      90             : import com.google.gerrit.server.rules.SubmitRule;
      91             : import com.google.gerrit.server.submit.SubmitDryRun;
      92             : import com.google.inject.Inject;
      93             : import com.google.inject.Provider;
      94             : import com.google.inject.ProvisionException;
      95             : import com.google.inject.util.Providers;
      96             : import java.io.IOException;
      97             : import java.util.ArrayList;
      98             : import java.util.Arrays;
      99             : import java.util.Collection;
     100             : import java.util.Collections;
     101             : import java.util.HashMap;
     102             : import java.util.HashSet;
     103             : import java.util.List;
     104             : import java.util.Map;
     105             : import java.util.Objects;
     106             : import java.util.Optional;
     107             : import java.util.Set;
     108             : import java.util.function.Function;
     109             : import java.util.regex.Pattern;
     110             : import org.eclipse.jgit.errors.ConfigInvalidException;
     111             : import org.eclipse.jgit.errors.RepositoryNotFoundException;
     112             : import org.eclipse.jgit.lib.Config;
     113             : import org.eclipse.jgit.lib.Repository;
     114             : import org.eclipse.jgit.revwalk.RevCommit;
     115             : import org.eclipse.jgit.revwalk.RevWalk;
     116             : 
     117             : /** Parses a query string meant to be applied to change objects. */
     118             : public class ChangeQueryBuilder extends QueryBuilder<ChangeData, ChangeQueryBuilder> {
     119         150 :   private static final FluentLogger logger = FluentLogger.forEnclosingClass();
     120             : 
     121             :   public interface ChangeOperatorFactory extends OperatorFactory<ChangeData, ChangeQueryBuilder> {}
     122             : 
     123             :   /**
     124             :    * Converts a operand (operator value) passed to an operator into a {@link Predicate}.
     125             :    *
     126             :    * <p>Register a ChangeOperandFactory in a config Module like this (note, for an example we are
     127             :    * using the has predicate, when other predicate plugin operands are created they can be
     128             :    * registered in a similar manner):
     129             :    *
     130             :    * <p>bind(ChangeHasOperandFactory.class) .annotatedWith(Exports.named("your has operand"))
     131             :    * .to(YourClass.class);
     132             :    */
     133             :   public interface ChangeOperandFactory {
     134             :     Predicate<ChangeData> create(ChangeQueryBuilder builder) throws QueryParseException;
     135             :   }
     136             : 
     137             :   public interface ChangeHasOperandFactory extends ChangeOperandFactory {}
     138             : 
     139             :   public interface ChangeIsOperandFactory extends ChangeOperandFactory {}
     140             : 
     141         150 :   private static final Pattern PAT_LEGACY_ID = Pattern.compile("^[1-9][0-9]*$");
     142         150 :   private static final Pattern PAT_CHANGE_ID = Pattern.compile(CHANGE_ID_PATTERN);
     143         150 :   private static final Pattern DEF_CHANGE =
     144         150 :       Pattern.compile("^(?:[1-9][0-9]*|(?:[^~]+~[^~]+~)?[iI][0-9a-f]{4,}.*)$");
     145             : 
     146             :   static final int MAX_ACCOUNTS_PER_DEFAULT_FIELD = 10;
     147             : 
     148             :   // NOTE: As new search operations are added, please keep the suggestions in
     149             :   // gr-search-bar.ts up to date.
     150             : 
     151             :   public static final String FIELD_ADDED = "added";
     152             :   public static final String FIELD_AGE = "age";
     153             :   public static final String FIELD_ATTENTION_SET_USERS = "attentionusers";
     154             :   public static final String FIELD_ATTENTION_SET_USERS_COUNT = "attentionuserscount";
     155             :   public static final String FIELD_ATTENTION_SET_FULL = "attentionfull";
     156             :   public static final String FIELD_ASSIGNEE = "assignee";
     157             :   public static final String FIELD_AUTHOR = "author";
     158             :   public static final String FIELD_EXACTAUTHOR = "exactauthor";
     159             : 
     160             :   public static final String FIELD_CHANGE = "change";
     161             :   public static final String FIELD_CHANGE_ID = "change_id";
     162             :   public static final String FIELD_COMMENT = "comment";
     163             :   public static final String FIELD_COMMENTBY = "commentby";
     164             :   public static final String FIELD_COMMIT = "commit";
     165             :   public static final String FIELD_COMMITTER = "committer";
     166             :   public static final String FIELD_DIRECTORY = "directory";
     167             :   public static final String FIELD_EXACTCOMMITTER = "exactcommitter";
     168             :   public static final String FIELD_EXTENSION = "extension";
     169             :   public static final String FIELD_ONLY_EXTENSIONS = "onlyextensions";
     170             :   public static final String FIELD_FOOTER = "footer";
     171             :   public static final String FIELD_FOOTER_NAME = "footernames";
     172             :   public static final String FIELD_CONFLICTS = "conflicts";
     173             :   public static final String FIELD_DELETED = "deleted";
     174             :   public static final String FIELD_DELTA = "delta";
     175             :   public static final String FIELD_DESTINATION = "destination";
     176             :   public static final String FIELD_DRAFTBY = "draftby";
     177             :   public static final String FIELD_EDITBY = "editby";
     178             :   public static final String FIELD_EXACTCOMMIT = "exactcommit";
     179             :   public static final String FIELD_FILE = "file";
     180             :   public static final String FIELD_FILEPART = "filepart";
     181             :   public static final String FIELD_GROUP = "group";
     182             :   public static final String FIELD_HASHTAG = "hashtag";
     183             :   public static final String FIELD_LABEL = "label";
     184             :   public static final String FIELD_LIMIT = "limit";
     185             :   public static final String FIELD_MERGE = "merge";
     186             :   public static final String FIELD_MERGEABLE = "mergeable2";
     187             :   public static final String FIELD_MERGED_ON = "mergedon";
     188             :   public static final String FIELD_MESSAGE = "message";
     189             :   public static final String FIELD_MESSAGE_EXACT = "messageexact";
     190             :   public static final String FIELD_OWNER = "owner";
     191             :   public static final String FIELD_OWNERIN = "ownerin";
     192             :   public static final String FIELD_PARENTOF = "parentof";
     193             :   public static final String FIELD_PARENTPROJECT = "parentproject";
     194             :   public static final String FIELD_PENDING_REVIEWER = "pendingreviewer";
     195             :   public static final String FIELD_PENDING_REVIEWER_BY_EMAIL = "pendingreviewerbyemail";
     196             :   public static final String FIELD_PRIVATE = "private";
     197             :   public static final String FIELD_PROJECT = "project";
     198             :   public static final String FIELD_PROJECTS = "projects";
     199             :   public static final String FIELD_REF = "ref";
     200             :   public static final String FIELD_REVIEWEDBY = "reviewedby";
     201             :   public static final String FIELD_REVIEWERIN = "reviewerin";
     202             :   public static final String FIELD_STAR = "star";
     203             :   public static final String FIELD_STARBY = "starby";
     204             :   public static final String FIELD_STARTED = "started";
     205             :   public static final String FIELD_STATUS = "status";
     206             :   public static final String FIELD_SUBMISSIONID = "submissionid";
     207             :   public static final String FIELD_TR = "tr";
     208             :   public static final String FIELD_UNRESOLVED_COMMENT_COUNT = "unresolved";
     209             :   public static final String FIELD_UPLOADER = "uploader";
     210             :   public static final String FIELD_UPLOADERIN = "uploaderin";
     211             :   public static final String FIELD_VISIBLETO = "visibleto";
     212             :   public static final String FIELD_WATCHEDBY = "watchedby";
     213             :   public static final String FIELD_WIP = "wip";
     214             :   public static final String FIELD_REVERTOF = "revertof";
     215             :   public static final String FIELD_PURE_REVERT = "ispurerevert";
     216             :   public static final String FIELD_CHERRYPICK = "cherrypick";
     217             :   public static final String FIELD_CHERRY_PICK_OF_CHANGE = "cherrypickofchange";
     218             :   public static final String FIELD_CHERRY_PICK_OF_PATCHSET = "cherrypickofpatchset";
     219             :   public static final String FIELD_IS_SUBMITTABLE = "issubmittable";
     220             : 
     221             :   public static final String ARG_ID_NAME = "name";
     222             :   public static final String ARG_ID_USER = "user";
     223             :   public static final String ARG_ID_GROUP = "group";
     224             :   public static final String ARG_ID_OWNER = "owner";
     225             :   public static final String ARG_ID_NON_UPLOADER = "non_uploader";
     226             :   public static final String ARG_COUNT = "count";
     227         150 :   public static final Account.Id OWNER_ACCOUNT_ID = Account.id(0);
     228         150 :   public static final Account.Id NON_UPLOADER_ACCOUNT_ID = Account.id(-1);
     229             : 
     230             :   public static final String OPERATOR_MERGED_BEFORE = "mergedbefore";
     231             :   public static final String OPERATOR_MERGED_AFTER = "mergedafter";
     232             : 
     233             :   // Operators to match on the last time the change was updated. Naming for legacy reasons.
     234             :   public static final String OPERATOR_BEFORE = "before";
     235             :   public static final String OPERATOR_AFTER = "after";
     236             : 
     237         150 :   private static final QueryBuilder.Definition<ChangeData, ChangeQueryBuilder> mydef =
     238             :       new QueryBuilder.Definition<>(ChangeQueryBuilder.class);
     239             : 
     240             :   @VisibleForTesting
     241             :   public static class Arguments {
     242             :     final AccountCache accountCache;
     243             :     final AccountResolver accountResolver;
     244             :     final AllProjectsName allProjectsName;
     245             :     final AllUsersName allUsersName;
     246             :     final PermissionBackend permissionBackend;
     247             :     final ChangeData.Factory changeDataFactory;
     248             :     final ChangeIndex index;
     249             :     final ChangeIndexRewriter rewriter;
     250             :     final CommentsUtil commentsUtil;
     251             :     final ConflictsCache conflictsCache;
     252             :     final DynamicMap<ChangeHasOperandFactory> hasOperands;
     253             :     final DynamicMap<ChangeIsOperandFactory> isOperands;
     254             :     final DynamicMap<ChangeOperatorFactory> opFactories;
     255             :     final GitRepositoryManager repoManager;
     256             :     final GroupBackend groupBackend;
     257             :     final IdentifiedUser.GenericFactory userFactory;
     258             :     final IndexConfig indexConfig;
     259             :     final PatchListCache patchListCache;
     260             :     final ProjectCache projectCache;
     261             :     final Provider<InternalChangeQuery> queryProvider;
     262             :     final ChildProjects childProjects;
     263             :     final StarredChangesUtil starredChangesUtil;
     264             :     final SubmitDryRun submitDryRun;
     265             :     final GroupMembers groupMembers;
     266             :     final ChangeIsVisibleToPredicate.Factory changeIsVisbleToPredicateFactory;
     267             :     final OperatorAliasConfig operatorAliasConfig;
     268             :     final boolean indexMergeable;
     269             :     final boolean conflictsPredicateEnabled;
     270             :     final ExperimentFeatures experimentFeatures;
     271             :     final HasOperandAliasConfig hasOperandAliasConfig;
     272             :     final PluginSetContext<SubmitRule> submitRules;
     273             : 
     274             :     private final Provider<CurrentUser> self;
     275             : 
     276             :     @Inject
     277             :     @VisibleForTesting
     278             :     public Arguments(
     279             :         Provider<InternalChangeQuery> queryProvider,
     280             :         ChangeIndexRewriter rewriter,
     281             :         DynamicMap<ChangeOperatorFactory> opFactories,
     282             :         DynamicMap<ChangeHasOperandFactory> hasOperands,
     283             :         DynamicMap<ChangeIsOperandFactory> isOperands,
     284             :         IdentifiedUser.GenericFactory userFactory,
     285             :         Provider<CurrentUser> self,
     286             :         PermissionBackend permissionBackend,
     287             :         ChangeData.Factory changeDataFactory,
     288             :         CommentsUtil commentsUtil,
     289             :         AccountResolver accountResolver,
     290             :         GroupBackend groupBackend,
     291             :         AllProjectsName allProjectsName,
     292             :         AllUsersName allUsersName,
     293             :         PatchListCache patchListCache,
     294             :         GitRepositoryManager repoManager,
     295             :         ProjectCache projectCache,
     296             :         ChildProjects childProjects,
     297             :         ChangeIndexCollection indexes,
     298             :         SubmitDryRun submitDryRun,
     299             :         ConflictsCache conflictsCache,
     300             :         IndexConfig indexConfig,
     301             :         StarredChangesUtil starredChangesUtil,
     302             :         AccountCache accountCache,
     303             :         GroupMembers groupMembers,
     304             :         OperatorAliasConfig operatorAliasConfig,
     305             :         @GerritServerConfig Config gerritConfig,
     306             :         ExperimentFeatures experimentFeatures,
     307             :         HasOperandAliasConfig hasOperandAliasConfig,
     308             :         ChangeIsVisibleToPredicate.Factory changeIsVisbleToPredicateFactory,
     309             :         PluginSetContext<SubmitRule> submitRules) {
     310         149 :       this(
     311             :           queryProvider,
     312             :           rewriter,
     313             :           opFactories,
     314             :           hasOperands,
     315             :           isOperands,
     316             :           userFactory,
     317             :           self,
     318             :           permissionBackend,
     319             :           changeDataFactory,
     320             :           commentsUtil,
     321             :           accountResolver,
     322             :           groupBackend,
     323             :           allProjectsName,
     324             :           allUsersName,
     325             :           patchListCache,
     326             :           repoManager,
     327             :           projectCache,
     328             :           childProjects,
     329             :           submitDryRun,
     330             :           conflictsCache,
     331         149 :           indexes != null ? indexes.getSearchIndex() : null,
     332             :           indexConfig,
     333             :           starredChangesUtil,
     334             :           accountCache,
     335             :           groupMembers,
     336             :           operatorAliasConfig,
     337         149 :           MergeabilityComputationBehavior.fromConfig(gerritConfig).includeInIndex(),
     338         149 :           gerritConfig.getBoolean("change", null, "conflictsPredicateEnabled", true),
     339             :           experimentFeatures,
     340             :           hasOperandAliasConfig,
     341             :           changeIsVisbleToPredicateFactory,
     342             :           submitRules);
     343         149 :     }
     344             : 
     345             :     private Arguments(
     346             :         Provider<InternalChangeQuery> queryProvider,
     347             :         ChangeIndexRewriter rewriter,
     348             :         DynamicMap<ChangeOperatorFactory> opFactories,
     349             :         DynamicMap<ChangeHasOperandFactory> hasOperands,
     350             :         DynamicMap<ChangeIsOperandFactory> isOperands,
     351             :         IdentifiedUser.GenericFactory userFactory,
     352             :         Provider<CurrentUser> self,
     353             :         PermissionBackend permissionBackend,
     354             :         ChangeData.Factory changeDataFactory,
     355             :         CommentsUtil commentsUtil,
     356             :         AccountResolver accountResolver,
     357             :         GroupBackend groupBackend,
     358             :         AllProjectsName allProjectsName,
     359             :         AllUsersName allUsersName,
     360             :         PatchListCache patchListCache,
     361             :         GitRepositoryManager repoManager,
     362             :         ProjectCache projectCache,
     363             :         ChildProjects childProjects,
     364             :         SubmitDryRun submitDryRun,
     365             :         ConflictsCache conflictsCache,
     366             :         ChangeIndex index,
     367             :         IndexConfig indexConfig,
     368             :         StarredChangesUtil starredChangesUtil,
     369             :         AccountCache accountCache,
     370             :         GroupMembers groupMembers,
     371             :         OperatorAliasConfig operatorAliasConfig,
     372             :         boolean indexMergeable,
     373             :         boolean conflictsPredicateEnabled,
     374             :         ExperimentFeatures experimentFeatures,
     375             :         HasOperandAliasConfig hasOperandAliasConfig,
     376             :         ChangeIsVisibleToPredicate.Factory changeIsVisbleToPredicateFactory,
     377         149 :         PluginSetContext<SubmitRule> submitRules) {
     378         149 :       this.queryProvider = queryProvider;
     379         149 :       this.rewriter = rewriter;
     380         149 :       this.opFactories = opFactories;
     381         149 :       this.userFactory = userFactory;
     382         149 :       this.self = self;
     383         149 :       this.permissionBackend = permissionBackend;
     384         149 :       this.changeDataFactory = changeDataFactory;
     385         149 :       this.commentsUtil = commentsUtil;
     386         149 :       this.accountResolver = accountResolver;
     387         149 :       this.groupBackend = groupBackend;
     388         149 :       this.allProjectsName = allProjectsName;
     389         149 :       this.allUsersName = allUsersName;
     390         149 :       this.patchListCache = patchListCache;
     391         149 :       this.repoManager = repoManager;
     392         149 :       this.projectCache = projectCache;
     393         149 :       this.childProjects = childProjects;
     394         149 :       this.submitDryRun = submitDryRun;
     395         149 :       this.conflictsCache = conflictsCache;
     396         149 :       this.index = index;
     397         149 :       this.indexConfig = indexConfig;
     398         149 :       this.starredChangesUtil = starredChangesUtil;
     399         149 :       this.accountCache = accountCache;
     400         149 :       this.hasOperands = hasOperands;
     401         149 :       this.isOperands = isOperands;
     402         149 :       this.groupMembers = groupMembers;
     403         149 :       this.changeIsVisbleToPredicateFactory = changeIsVisbleToPredicateFactory;
     404         149 :       this.operatorAliasConfig = operatorAliasConfig;
     405         149 :       this.indexMergeable = indexMergeable;
     406         149 :       this.conflictsPredicateEnabled = conflictsPredicateEnabled;
     407         149 :       this.experimentFeatures = experimentFeatures;
     408         149 :       this.hasOperandAliasConfig = hasOperandAliasConfig;
     409         149 :       this.submitRules = submitRules;
     410         149 :     }
     411             : 
     412             :     Arguments asUser(CurrentUser otherUser) {
     413           9 :       return new Arguments(
     414             :           queryProvider,
     415             :           rewriter,
     416             :           opFactories,
     417             :           hasOperands,
     418             :           isOperands,
     419             :           userFactory,
     420           9 :           Providers.of(otherUser),
     421             :           permissionBackend,
     422             :           changeDataFactory,
     423             :           commentsUtil,
     424             :           accountResolver,
     425             :           groupBackend,
     426             :           allProjectsName,
     427             :           allUsersName,
     428             :           patchListCache,
     429             :           repoManager,
     430             :           projectCache,
     431             :           childProjects,
     432             :           submitDryRun,
     433             :           conflictsCache,
     434             :           index,
     435             :           indexConfig,
     436             :           starredChangesUtil,
     437             :           accountCache,
     438             :           groupMembers,
     439             :           operatorAliasConfig,
     440             :           indexMergeable,
     441             :           conflictsPredicateEnabled,
     442             :           experimentFeatures,
     443             :           hasOperandAliasConfig,
     444             :           changeIsVisbleToPredicateFactory,
     445             :           submitRules);
     446             :     }
     447             : 
     448             :     Arguments asUser(Account.Id otherId) {
     449             :       try {
     450           0 :         CurrentUser u = self.get();
     451           0 :         if (u.isIdentifiedUser() && otherId.equals(u.getAccountId())) {
     452           0 :           return this;
     453             :         }
     454           0 :       } catch (ProvisionException e) {
     455             :         // Doesn't match current user, continue.
     456           0 :       }
     457           0 :       return asUser(userFactory.create(otherId));
     458             :     }
     459             : 
     460             :     IdentifiedUser getIdentifiedUser() throws QueryRequiresAuthException {
     461             :       try {
     462          10 :         CurrentUser u = getUser();
     463          10 :         if (u.isIdentifiedUser()) {
     464          10 :           return u.asIdentifiedUser();
     465             :         }
     466           4 :         throw new QueryRequiresAuthException(NotSignedInException.MESSAGE);
     467           0 :       } catch (ProvisionException e) {
     468           0 :         throw new QueryRequiresAuthException(NotSignedInException.MESSAGE, e);
     469             :       }
     470             :     }
     471             : 
     472             :     CurrentUser getUser() throws QueryRequiresAuthException {
     473             :       try {
     474          15 :         return self.get();
     475           0 :       } catch (ProvisionException e) {
     476           0 :         throw new QueryRequiresAuthException(NotSignedInException.MESSAGE, e);
     477             :       }
     478             :     }
     479             : 
     480             :     @Nullable
     481             :     Schema<ChangeData> getSchema() {
     482          11 :       return index != null ? index.getSchema() : null;
     483             :     }
     484             :   }
     485             : 
     486             :   private final Arguments args;
     487         149 :   protected Map<String, String> hasOperandAliases = Collections.emptyMap();
     488         149 :   private Map<Account.Id, DestinationList> destinationListByAccount = new HashMap<>();
     489             : 
     490         150 :   private static final Splitter RULE_SPLITTER = Splitter.on("=");
     491         150 :   private static final Splitter PLUGIN_SPLITTER = Splitter.on("_");
     492         150 :   private static final Splitter LABEL_SPLITTER = Splitter.on(",");
     493             : 
     494             :   @Inject
     495             :   ChangeQueryBuilder(Arguments args) {
     496         149 :     this(mydef, args);
     497         149 :     setupAliases();
     498         149 :   }
     499             : 
     500             :   @VisibleForTesting
     501             :   protected ChangeQueryBuilder(Definition<ChangeData, ChangeQueryBuilder> def, Arguments args) {
     502         149 :     super(def, args.opFactories);
     503         149 :     this.args = args;
     504         149 :   }
     505             : 
     506             :   private void setupAliases() {
     507         149 :     setOperatorAliases(args.operatorAliasConfig.getChangeQueryOperatorAliases());
     508         149 :     hasOperandAliases = args.hasOperandAliasConfig.getChangeQueryHasOperandAliases();
     509         149 :   }
     510             : 
     511             :   public ChangeQueryBuilder asUser(CurrentUser user) {
     512           9 :     return new ChangeQueryBuilder(builderDef, args.asUser(user));
     513             :   }
     514             : 
     515             :   @Operator
     516             :   public Predicate<ChangeData> age(String value) {
     517           5 :     return new AgePredicate(value);
     518             :   }
     519             : 
     520             :   @Operator
     521             :   public Predicate<ChangeData> before(String value) throws QueryParseException {
     522           4 :     return new BeforePredicate(ChangeField.UPDATED, ChangeQueryBuilder.OPERATOR_BEFORE, value);
     523             :   }
     524             : 
     525             :   @Operator
     526             :   public Predicate<ChangeData> until(String value) throws QueryParseException {
     527           4 :     return before(value);
     528             :   }
     529             : 
     530             :   @Operator
     531             :   public Predicate<ChangeData> after(String value) throws QueryParseException {
     532           4 :     return new AfterPredicate(ChangeField.UPDATED, ChangeQueryBuilder.OPERATOR_AFTER, value);
     533             :   }
     534             : 
     535             :   @Operator
     536             :   public Predicate<ChangeData> since(String value) throws QueryParseException {
     537           4 :     return after(value);
     538             :   }
     539             : 
     540             :   @Operator
     541             :   public Predicate<ChangeData> mergedBefore(String value) throws QueryParseException {
     542           4 :     checkFieldAvailable(ChangeField.MERGED_ON_SPEC, OPERATOR_MERGED_BEFORE);
     543           4 :     return new BeforePredicate(
     544             :         ChangeField.MERGED_ON_SPEC, ChangeQueryBuilder.OPERATOR_MERGED_BEFORE, value);
     545             :   }
     546             : 
     547             :   @Operator
     548             :   public Predicate<ChangeData> mergedAfter(String value) throws QueryParseException {
     549           4 :     checkFieldAvailable(ChangeField.MERGED_ON_SPEC, OPERATOR_MERGED_AFTER);
     550           4 :     return new AfterPredicate(
     551             :         ChangeField.MERGED_ON_SPEC, ChangeQueryBuilder.OPERATOR_MERGED_AFTER, value);
     552             :   }
     553             : 
     554             :   @Operator
     555             :   public Predicate<ChangeData> change(String query) throws QueryParseException {
     556          19 :     Optional<ChangeTriplet> triplet = ChangeTriplet.parse(query);
     557          19 :     if (triplet.isPresent()) {
     558           4 :       return Predicate.and(
     559           4 :           project(triplet.get().project().get()),
     560           4 :           branch(triplet.get().branch().branch()),
     561           4 :           ChangePredicates.idPrefix(parseChangeId(triplet.get().id().get())));
     562             :     }
     563          19 :     if (PAT_LEGACY_ID.matcher(query).matches()) {
     564           9 :       Integer id = Ints.tryParse(query);
     565           9 :       if (id != null) {
     566           9 :         return ChangePredicates.idStr(Change.id(id));
     567             :       }
     568          16 :     } else if (PAT_CHANGE_ID.matcher(query).matches()) {
     569          16 :       return ChangePredicates.idPrefix(parseChangeId(query));
     570             :     }
     571             : 
     572           4 :     throw new QueryParseException("Invalid change format");
     573             :   }
     574             : 
     575             :   @Operator
     576             :   public Predicate<ChangeData> comment(String value) {
     577          10 :     return ChangePredicates.comment(value);
     578             :   }
     579             : 
     580             :   @Operator
     581             :   public Predicate<ChangeData> status(String statusName) throws QueryParseException {
     582          17 :     if ("reviewed".equalsIgnoreCase(statusName)) {
     583           4 :       return ChangePredicates.unreviewed();
     584             :     }
     585          17 :     return ChangeStatusPredicate.parse(statusName);
     586             :   }
     587             : 
     588             :   public Predicate<ChangeData> statusOpen() {
     589           0 :     return ChangeStatusPredicate.open();
     590             :   }
     591             : 
     592             :   @Operator
     593             :   public Predicate<ChangeData> rule(String value) throws QueryParseException {
     594           4 :     String ruleNameArg = value;
     595           4 :     String statusArg = null;
     596           4 :     List<String> queryArgs = RULE_SPLITTER.splitToList(value);
     597           4 :     if (queryArgs.size() > 2) {
     598           0 :       throw new QueryParseException(
     599             :           "Invalid query arguments. Correct format is 'rule:<rule_name>=<status>' "
     600             :               + "with <rule_name> in the form of <plugin>~<rule>. For Gerrit core rules, "
     601             :               + "rule name should be specified as gerrit~<rule>.");
     602             :     }
     603           4 :     if (queryArgs.size() == 2) {
     604           4 :       ruleNameArg = queryArgs.get(0);
     605           4 :       statusArg = queryArgs.get(1);
     606             :     }
     607             : 
     608           4 :     return statusArg == null
     609           4 :         ? Predicate.or(
     610           4 :             Arrays.asList(
     611           4 :                 ChangePredicates.submitRuleStatus(ruleNameArg + "=" + SubmitRecord.Status.OK),
     612           4 :                 ChangePredicates.submitRuleStatus(ruleNameArg + "=" + SubmitRecord.Status.FORCED)))
     613           4 :         : ChangePredicates.submitRuleStatus(ruleNameArg + "=" + statusArg);
     614             :   }
     615             : 
     616             :   @Operator
     617             :   public Predicate<ChangeData> has(String value) throws QueryParseException {
     618          12 :     value = hasOperandAliases.getOrDefault(value, value);
     619          12 :     if ("star".equalsIgnoreCase(value)) {
     620           4 :       return starredBySelf();
     621             :     }
     622             : 
     623          12 :     if ("draft".equalsIgnoreCase(value)) {
     624           6 :       return draftBySelf();
     625             :     }
     626             : 
     627          10 :     if ("edit".equalsIgnoreCase(value)) {
     628           8 :       return ChangePredicates.editBy(self());
     629             :     }
     630             : 
     631           6 :     if ("attention".equalsIgnoreCase(value)) {
     632           4 :       checkFieldAvailable(ChangeField.ATTENTION_SET_USERS, "has:attention");
     633           4 :       return new IsAttentionPredicate();
     634             :     }
     635             : 
     636           6 :     if ("unresolved".equalsIgnoreCase(value)) {
     637           6 :       return new IsUnresolvedPredicate();
     638             :     }
     639             : 
     640             :     // for plugins the value will be operandName_pluginName
     641           0 :     List<String> names = PLUGIN_SPLITTER.splitToList(value);
     642           0 :     if (names.size() == 2) {
     643           0 :       ChangeHasOperandFactory op = args.hasOperands.get(names.get(1), names.get(0));
     644           0 :       if (op != null) {
     645           0 :         return op.create(this);
     646             :       }
     647             :     }
     648             : 
     649           0 :     throw new IllegalArgumentException();
     650             :   }
     651             : 
     652             :   @Operator
     653             :   public Predicate<ChangeData> is(String value) throws QueryParseException {
     654          11 :     if ("starred".equalsIgnoreCase(value)) {
     655           4 :       return starredBySelf();
     656             :     }
     657             : 
     658          11 :     if ("watched".equalsIgnoreCase(value)) {
     659           4 :       return new IsWatchedByPredicate(args);
     660             :     }
     661             : 
     662          11 :     if ("visible".equalsIgnoreCase(value)) {
     663           4 :       return isVisible();
     664             :     }
     665             : 
     666          11 :     if ("reviewed".equalsIgnoreCase(value)) {
     667           4 :       return ChangePredicates.unreviewed();
     668             :     }
     669             : 
     670          11 :     if ("owner".equalsIgnoreCase(value)) {
     671           4 :       return ChangePredicates.owner(self());
     672             :     }
     673             : 
     674          11 :     if ("uploader".equalsIgnoreCase(value)) {
     675           4 :       checkFieldAvailable(ChangeField.UPLOADER_SPEC, "is:uploader");
     676           4 :       return ChangePredicates.uploader(self());
     677             :     }
     678             : 
     679          11 :     if ("reviewer".equalsIgnoreCase(value)) {
     680           4 :       return Predicate.and(
     681           4 :           Predicate.not(new BooleanPredicate(ChangeField.WIP)), ReviewerPredicate.reviewer(self()));
     682             :     }
     683             : 
     684          11 :     if ("cc".equalsIgnoreCase(value)) {
     685           4 :       return ReviewerPredicate.cc(self());
     686             :     }
     687             : 
     688          11 :     if ("mergeable".equalsIgnoreCase(value)) {
     689           6 :       if (!args.indexMergeable) {
     690           5 :         throw new QueryParseException("'is:mergeable' operator is not supported by server");
     691             :       }
     692           6 :       return new BooleanPredicate(ChangeField.MERGEABLE);
     693             :     }
     694             : 
     695          10 :     if ("merge".equalsIgnoreCase(value)) {
     696           4 :       checkFieldAvailable(ChangeField.MERGE, "is:merge");
     697           4 :       return new BooleanPredicate(ChangeField.MERGE);
     698             :     }
     699             : 
     700          10 :     if ("private".equalsIgnoreCase(value)) {
     701           4 :       return new BooleanPredicate(ChangeField.PRIVATE);
     702             :     }
     703             : 
     704          10 :     if ("attention".equalsIgnoreCase(value)) {
     705           4 :       checkFieldAvailable(ChangeField.ATTENTION_SET_USERS, "is:attention");
     706           4 :       return new IsAttentionPredicate();
     707             :     }
     708             : 
     709          10 :     if ("assigned".equalsIgnoreCase(value)) {
     710           4 :       return Predicate.not(ChangePredicates.assignee(Account.id(ChangeField.NO_ASSIGNEE)));
     711             :     }
     712             : 
     713          10 :     if ("unassigned".equalsIgnoreCase(value)) {
     714           4 :       return ChangePredicates.assignee(Account.id(ChangeField.NO_ASSIGNEE));
     715             :     }
     716             : 
     717          10 :     if ("pure-revert".equalsIgnoreCase(value)) {
     718           5 :       checkFieldAvailable(ChangeField.IS_PURE_REVERT_SPEC, "is:pure-revert");
     719           5 :       return ChangePredicates.pureRevert("1");
     720             :     }
     721             : 
     722          10 :     if ("submittable".equalsIgnoreCase(value)) {
     723           5 :       if (!args.index.getSchema().hasField(ChangeField.IS_SUBMITTABLE_SPEC)) {
     724             :         // SubmittablePredicate will match if *any* of the submit records are OK,
     725             :         // but we need to check that they're *all* OK, so check that none of the
     726             :         // submit records match any of the negative cases. To avoid checking yet
     727             :         // more negative cases for CLOSED and FORCED, instead make sure at least
     728             :         // one submit record is OK.
     729           0 :         return Predicate.and(
     730             :             new SubmittablePredicate(SubmitRecord.Status.OK),
     731           0 :             Predicate.not(new SubmittablePredicate(SubmitRecord.Status.NOT_READY)),
     732           0 :             Predicate.not(new SubmittablePredicate(SubmitRecord.Status.RULE_ERROR)));
     733             :       }
     734           5 :       checkFieldAvailable(ChangeField.IS_SUBMITTABLE_SPEC, "is:submittable");
     735           5 :       return new IsSubmittablePredicate();
     736             :     }
     737             : 
     738           9 :     if ("started".equalsIgnoreCase(value)) {
     739           4 :       checkFieldAvailable(ChangeField.STARTED, "is:started");
     740           4 :       return new BooleanPredicate(ChangeField.STARTED);
     741             :     }
     742             : 
     743           9 :     if ("wip".equalsIgnoreCase(value)) {
     744           5 :       return new BooleanPredicate(ChangeField.WIP);
     745             :     }
     746             : 
     747           9 :     if ("cherrypick".equalsIgnoreCase(value)) {
     748           4 :       checkFieldAvailable(ChangeField.CHERRY_PICK, "is:cherrypick");
     749           4 :       return new BooleanPredicate(ChangeField.CHERRY_PICK);
     750             :     }
     751             : 
     752             :     // for plugins the value will be operandName_pluginName
     753           9 :     List<String> names = PLUGIN_SPLITTER.splitToList(value);
     754           9 :     if (names.size() == 2) {
     755           2 :       ChangeIsOperandFactory op = args.isOperands.get(names.get(1), names.get(0));
     756           2 :       if (op != null) {
     757           2 :         return op.create(this);
     758             :       }
     759             :     }
     760           7 :     return status(value);
     761             :   }
     762             : 
     763             :   @Operator
     764             :   public Predicate<ChangeData> commit(String id) {
     765          11 :     return ChangePredicates.commitPrefix(id);
     766             :   }
     767             : 
     768             :   @Operator
     769             :   public Predicate<ChangeData> conflicts(String value) throws QueryParseException {
     770           4 :     if (!args.conflictsPredicateEnabled) {
     771           0 :       throw new QueryParseException("'conflicts:' operator is not supported by server");
     772             :     }
     773           4 :     List<Change> changes = parseChange(value);
     774           4 :     List<Predicate<ChangeData>> or = new ArrayList<>(changes.size());
     775           4 :     for (Change c : changes) {
     776           4 :       or.add(ConflictsPredicate.create(args, value, c));
     777           4 :     }
     778           4 :     return Predicate.or(or);
     779             :   }
     780             : 
     781             :   @Operator
     782             :   public Predicate<ChangeData> p(String name) {
     783           0 :     return project(name);
     784             :   }
     785             : 
     786             :   @Operator
     787             :   public Predicate<ChangeData> project(String name) {
     788          13 :     if (name.startsWith("^")) {
     789           0 :       return new RegexProjectPredicate(name);
     790             :     }
     791          13 :     return ChangePredicates.project(Project.nameKey(name));
     792             :   }
     793             : 
     794             :   @Operator
     795             :   public Predicate<ChangeData> projects(String name) {
     796          10 :     return ChangePredicates.projectPrefix(name);
     797             :   }
     798             : 
     799             :   @Operator
     800             :   public Predicate<ChangeData> parentof(String value) throws QueryParseException {
     801           4 :     List<ChangeData> changes = parseChangeData(value);
     802           4 :     List<Predicate<ChangeData>> or = new ArrayList<>(changes.size());
     803           4 :     for (ChangeData c : changes) {
     804           4 :       for (RevCommit revCommit : getParents(c)) {
     805           4 :         or.add(ChangePredicates.commitPrefix(revCommit.getId().getName()));
     806           4 :       }
     807           4 :     }
     808           4 :     return Predicate.or(or);
     809             :   }
     810             : 
     811             :   private Set<RevCommit> getParents(ChangeData change) {
     812           4 :     PatchSet ps = change.currentPatchSet();
     813           4 :     try (Repository repo = args.repoManager.openRepository(change.project());
     814           4 :         RevWalk walk = new RevWalk(repo)) {
     815           4 :       RevCommit c = walk.parseCommit(ps.commitId());
     816           4 :       return Sets.newHashSet(c.getParents());
     817           0 :     } catch (IOException e) {
     818           0 :       throw new StorageException(
     819           0 :           String.format(
     820             :               "Loading commit %s for ps %d of change %d failed.",
     821           0 :               ps.commitId(), ps.id().get(), ps.id().changeId().get()),
     822             :           e);
     823             :     }
     824             :   }
     825             : 
     826             :   @Operator
     827             :   public Predicate<ChangeData> parentproject(String name) {
     828           4 :     return new ParentProjectPredicate(args.projectCache, args.childProjects, name);
     829             :   }
     830             : 
     831             :   @Operator
     832             :   public Predicate<ChangeData> repository(String name) {
     833           4 :     return project(name);
     834             :   }
     835             : 
     836             :   @Operator
     837             :   public Predicate<ChangeData> repositories(String name) {
     838           4 :     return projects(name);
     839             :   }
     840             : 
     841             :   @Operator
     842             :   public Predicate<ChangeData> parentrepository(String name) {
     843           4 :     return parentproject(name);
     844             :   }
     845             : 
     846             :   @Operator
     847             :   public Predicate<ChangeData> repo(String name) {
     848           5 :     return project(name);
     849             :   }
     850             : 
     851             :   @Operator
     852             :   public Predicate<ChangeData> repos(String name) {
     853           4 :     return projects(name);
     854             :   }
     855             : 
     856             :   @Operator
     857             :   public Predicate<ChangeData> parentrepo(String name) {
     858           4 :     return parentproject(name);
     859             :   }
     860             : 
     861             :   @Operator
     862             :   public Predicate<ChangeData> branch(String name) throws QueryParseException {
     863          12 :     if (name.startsWith("^")) {
     864           0 :       return ref("^" + RefNames.fullName(name.substring(1)));
     865             :     }
     866          12 :     return ref(RefNames.fullName(name));
     867             :   }
     868             : 
     869             :   @Operator
     870             :   public Predicate<ChangeData> hashtag(String hashtag) {
     871           4 :     return ChangePredicates.hashtag(hashtag);
     872             :   }
     873             : 
     874             :   @Operator
     875             :   public Predicate<ChangeData> inhashtag(String hashtag) throws QueryParseException {
     876           4 :     if (hashtag.startsWith("^")) {
     877           4 :       return new RegexHashtagPredicate(hashtag);
     878             :     }
     879           4 :     if (hashtag.isEmpty()) {
     880           0 :       return ChangePredicates.hashtag(hashtag);
     881             :     }
     882             : 
     883           4 :     checkFieldAvailable(ChangeField.FUZZY_HASHTAG, "inhashtag");
     884           4 :     return ChangePredicates.fuzzyHashtag(hashtag);
     885             :   }
     886             : 
     887             :   @Operator
     888             :   public Predicate<ChangeData> prefixhashtag(String hashtag) throws QueryParseException {
     889           4 :     if (hashtag.isEmpty()) {
     890           0 :       return ChangePredicates.hashtag(hashtag);
     891             :     }
     892             : 
     893           4 :     checkFieldAvailable(ChangeField.PREFIX_HASHTAG, "prefixhashtag");
     894           4 :     return ChangePredicates.prefixHashtag(hashtag);
     895             :   }
     896             : 
     897             :   @Operator
     898             :   public Predicate<ChangeData> topic(String name) {
     899          12 :     return ChangePredicates.exactTopic(name);
     900             :   }
     901             : 
     902             :   @Operator
     903             :   public Predicate<ChangeData> intopic(String name) {
     904           4 :     if (name.startsWith("^")) {
     905           4 :       return new RegexTopicPredicate(name);
     906             :     }
     907           4 :     if (name.isEmpty()) {
     908           4 :       return ChangePredicates.exactTopic(name);
     909             :     }
     910           4 :     return ChangePredicates.fuzzyTopic(name);
     911             :   }
     912             : 
     913             :   @Operator
     914             :   public Predicate<ChangeData> prefixtopic(String name) throws QueryParseException {
     915           4 :     if (name.isEmpty()) {
     916           0 :       return ChangePredicates.exactTopic(name);
     917             :     }
     918             : 
     919           4 :     checkFieldAvailable(ChangeField.PREFIX_TOPIC, "prefixtopic");
     920           4 :     return ChangePredicates.prefixTopic(name);
     921             :   }
     922             : 
     923             :   @Operator
     924             :   public Predicate<ChangeData> ref(String ref) throws QueryParseException {
     925          12 :     if (ref.startsWith("^")) {
     926           0 :       return new RegexRefPredicate(ref);
     927             :     }
     928          12 :     return ChangePredicates.ref(ref);
     929             :   }
     930             : 
     931             :   @Operator
     932             :   public Predicate<ChangeData> f(String file) throws QueryParseException {
     933           0 :     return file(file);
     934             :   }
     935             : 
     936             :   /**
     937             :    * Creates a predicate to match changes by file.
     938             :    *
     939             :    * @param file the value of the {@code file} query operator
     940             :    * @throws QueryParseException thrown if parsing the value fails (may be thrown by subclasses)
     941             :    */
     942             :   @Operator
     943             :   public Predicate<ChangeData> file(String file) throws QueryParseException {
     944          11 :     if (file.startsWith("^")) {
     945           5 :       return new RegexPathPredicate(file);
     946             :     }
     947          11 :     return ChangePredicates.file(args, file);
     948             :   }
     949             : 
     950             :   @Operator
     951             :   public Predicate<ChangeData> path(String path) {
     952           4 :     if (path.startsWith("^")) {
     953           4 :       return new RegexPathPredicate(path);
     954             :     }
     955           4 :     return ChangePredicates.path(path);
     956             :   }
     957             : 
     958             :   @Operator
     959             :   public Predicate<ChangeData> ext(String ext) {
     960           4 :     return extension(ext);
     961             :   }
     962             : 
     963             :   @Operator
     964             :   public Predicate<ChangeData> extension(String ext) {
     965           4 :     return new FileExtensionPredicate(ext);
     966             :   }
     967             : 
     968             :   @Operator
     969             :   public Predicate<ChangeData> onlyexts(String extList) {
     970           4 :     return onlyextensions(extList);
     971             :   }
     972             : 
     973             :   @Operator
     974             :   public Predicate<ChangeData> onlyextensions(String extList) {
     975           4 :     return new FileExtensionListPredicate(extList);
     976             :   }
     977             : 
     978             :   @Operator
     979             :   public Predicate<ChangeData> footer(String footer) {
     980           4 :     return ChangePredicates.footer(footer);
     981             :   }
     982             : 
     983             :   @Operator
     984             :   public Predicate<ChangeData> hasfooter(String footerName) throws QueryParseException {
     985           4 :     checkFieldAvailable(ChangeField.FOOTER_NAME, "hasfooter");
     986           4 :     return ChangePredicates.hasFooter(footerName);
     987             :   }
     988             : 
     989             :   @Operator
     990             :   public Predicate<ChangeData> dir(String directory) {
     991           5 :     return directory(directory);
     992             :   }
     993             : 
     994             :   @Operator
     995             :   public Predicate<ChangeData> directory(String directory) {
     996           5 :     if (directory.startsWith("^")) {
     997           4 :       return new RegexDirectoryPredicate(directory);
     998             :     }
     999           5 :     return ChangePredicates.directory(directory);
    1000             :   }
    1001             : 
    1002             :   @Operator
    1003             :   public Predicate<ChangeData> label(String name)
    1004             :       throws QueryParseException, IOException, ConfigInvalidException {
    1005          14 :     Set<Account.Id> accounts = null;
    1006          14 :     AccountGroup.UUID group = null;
    1007          14 :     Integer count = null;
    1008          14 :     PredicateArgs.Operator countOp = null;
    1009             : 
    1010             :     // Parse for:
    1011             :     // label:Code-Review=1,user=jsmith or
    1012             :     // label:Code-Review=1,jsmith or
    1013             :     // label:Code-Review=1,group=android_approvers or
    1014             :     // label:Code-Review=1,android_approvers
    1015             :     // user/groups without a label will first attempt to match user
    1016             :     // Special case: votes by owners can be tracked with ",owner":
    1017             :     // label:Code-Review+2,owner
    1018             :     // label:Code-Review+2,user=owner
    1019             :     // label:Code-Review+1,count=2
    1020          14 :     List<String> splitReviewer = LABEL_SPLITTER.limit(2).splitToList(name);
    1021          14 :     name = splitReviewer.get(0); // remove all but the vote piece, e.g.'CodeReview=1'
    1022             : 
    1023          14 :     if (splitReviewer.size() == 2) {
    1024             :       // process the user/group piece
    1025           5 :       PredicateArgs lblArgs = new PredicateArgs(splitReviewer.get(1));
    1026             : 
    1027             :       // Disallow using the "count=" arg in conjunction with the "user=" or "group=" args. to avoid
    1028             :       // unnecessary complexity.
    1029           5 :       assertDisjunctive(lblArgs, ARG_COUNT, ARG_ID_USER);
    1030           5 :       assertDisjunctive(lblArgs, ARG_COUNT, ARG_ID_GROUP);
    1031             : 
    1032           5 :       for (Map.Entry<String, ValOp> pair : lblArgs.keyValue.entrySet()) {
    1033           5 :         String key = pair.getKey();
    1034           5 :         String value = pair.getValue().value();
    1035           5 :         PredicateArgs.Operator operator = pair.getValue().operator();
    1036           5 :         if (key.equalsIgnoreCase(ARG_ID_USER)) {
    1037           5 :           if (value.equals(ARG_ID_OWNER)) {
    1038           4 :             accounts = Collections.singleton(OWNER_ACCOUNT_ID);
    1039           5 :           } else if (value.equals(ARG_ID_NON_UPLOADER)) {
    1040           5 :             accounts = Collections.singleton(NON_UPLOADER_ACCOUNT_ID);
    1041             :           } else {
    1042           4 :             accounts = parseAccount(value);
    1043             :           }
    1044           4 :         } else if (key.equalsIgnoreCase(ARG_ID_GROUP)) {
    1045           4 :           group = parseGroup(value).getUUID();
    1046           4 :         } else if (key.equalsIgnoreCase(ARG_COUNT)) {
    1047           4 :           if (!isInt(value)) {
    1048           0 :             throw new QueryParseException("Invalid count argument. Value should be an integer");
    1049             :           }
    1050           4 :           count = Integer.parseInt(value);
    1051           4 :           countOp = operator;
    1052           4 :           if (count == 0) {
    1053           4 :             throw new QueryParseException("Argument count=0 is not allowed.");
    1054             :           }
    1055           4 :           if (count > LabelPredicate.MAX_COUNT) {
    1056           4 :             throw new QueryParseException(
    1057           4 :                 String.format(
    1058             :                     "count=%d is not allowed. Maximum allowed value for count is %d.",
    1059           4 :                     count, LabelPredicate.MAX_COUNT));
    1060             :           }
    1061             :         } else {
    1062           0 :           throw new QueryParseException("Invalid argument identifier '" + pair.getKey() + "'");
    1063             :         }
    1064           5 :       }
    1065             : 
    1066           5 :       for (String value : lblArgs.positional) {
    1067           4 :         if (accounts != null || group != null) {
    1068           0 :           throw new QueryParseException("more than one user/group specified (" + value + ")");
    1069             :         }
    1070             :         try {
    1071           4 :           if (value.equals(ARG_ID_OWNER)) {
    1072           4 :             accounts = Collections.singleton(OWNER_ACCOUNT_ID);
    1073           4 :           } else if (value.equals(ARG_ID_NON_UPLOADER)) {
    1074           4 :             accounts = Collections.singleton(NON_UPLOADER_ACCOUNT_ID);
    1075             :           } else {
    1076           4 :             accounts = parseAccount(value);
    1077             :           }
    1078           4 :         } catch (QueryParseException qpex) {
    1079             :           // If it doesn't match an account, see if it matches a group
    1080             :           // (accounts get precedence)
    1081             :           try {
    1082           4 :             group = parseGroup(value).getUUID();
    1083           0 :           } catch (QueryParseException e) {
    1084           0 :             throw error("Neither user nor group " + value + " found", e);
    1085           4 :           }
    1086           4 :         }
    1087           4 :       }
    1088             :     }
    1089             : 
    1090          14 :     if (group != null) {
    1091           4 :       accounts = getMembers(group);
    1092             :     }
    1093             : 
    1094             :     // If the vote piece looks like Code-Review=NEED with a valid non-numeric
    1095             :     // submit record status, interpret as a submit record query.
    1096          14 :     int eq = name.indexOf('=');
    1097          14 :     if (eq > 0) {
    1098           9 :       String statusName = name.substring(eq + 1).toUpperCase();
    1099           9 :       if (!isInt(statusName) && !MagicLabelValue.tryParse(statusName).isPresent()) {
    1100           5 :         SubmitRecord.Label.Status status =
    1101           5 :             Enums.getIfPresent(SubmitRecord.Label.Status.class, statusName).orNull();
    1102           5 :         if (status == null) {
    1103           0 :           throw error("Invalid label status " + statusName + " in " + name);
    1104             :         }
    1105           5 :         return SubmitRecordPredicate.create(name.substring(0, eq), status, accounts);
    1106             :       }
    1107             :     }
    1108             : 
    1109          14 :     return new LabelPredicate(args, name, accounts, group, count, countOp);
    1110             :   }
    1111             : 
    1112             :   /** Assert that keys {@code k1} and {@code k2} do not exist in {@code labelArgs} together. */
    1113             :   private void assertDisjunctive(PredicateArgs labelArgs, String k1, String k2)
    1114             :       throws QueryParseException {
    1115           5 :     Map<String, ValOp> keyValArgs = labelArgs.keyValue;
    1116           5 :     if (keyValArgs.containsKey(k1) && keyValArgs.containsKey(k2)) {
    1117           4 :       throw new QueryParseException(
    1118           4 :           String.format(
    1119             :               "Cannot use the '%s' argument in conjunction with the '%s' argument", k1, k2));
    1120             :     }
    1121           5 :   }
    1122             : 
    1123             :   private static boolean isInt(String s) {
    1124           9 :     if (s == null) {
    1125           0 :       return false;
    1126             :     }
    1127           9 :     if (s.startsWith("+")) {
    1128           7 :       s = s.substring(1);
    1129             :     }
    1130           9 :     return Ints.tryParse(s) != null;
    1131             :   }
    1132             : 
    1133             :   @Operator
    1134             :   public Predicate<ChangeData> message(String text) throws QueryParseException {
    1135          13 :     if (text.startsWith("^")) {
    1136           5 :       checkFieldAvailable(ChangeField.COMMIT_MESSAGE_EXACT, "messageexact");
    1137           4 :       return new RegexMessagePredicate(text);
    1138             :     }
    1139          13 :     return ChangePredicates.message(text);
    1140             :   }
    1141             : 
    1142             :   private Predicate<ChangeData> starredBySelf() throws QueryParseException {
    1143           4 :     return ChangePredicates.starBy(
    1144           4 :         args.starredChangesUtil, self(), StarredChangesUtil.DEFAULT_LABEL);
    1145             :   }
    1146             : 
    1147             :   private Predicate<ChangeData> draftBySelf() throws QueryParseException {
    1148           6 :     return ChangePredicates.draftBy(args.commentsUtil, self());
    1149             :   }
    1150             : 
    1151             :   @Operator
    1152             :   public Predicate<ChangeData> visibleto(String who)
    1153             :       throws QueryParseException, IOException, ConfigInvalidException {
    1154           4 :     if (isSelf(who)) {
    1155           4 :       return isVisible();
    1156             :     }
    1157           4 :     Set<Account.Id> accounts = null;
    1158             :     try {
    1159           4 :       accounts = parseAccount(who);
    1160           4 :     } catch (QueryParseException e) {
    1161           4 :       if (e instanceof QueryRequiresAuthException) {
    1162           0 :         throw e;
    1163             :       }
    1164           4 :     }
    1165           4 :     if (accounts != null) {
    1166           4 :       if (accounts.size() == 1) {
    1167           4 :         return visibleto(args.userFactory.create(Iterables.getOnlyElement(accounts)));
    1168           4 :       } else if (accounts.size() > 1) {
    1169           4 :         throw error(String.format("\"%s\" resolves to multiple accounts", who));
    1170             :       }
    1171             :     }
    1172             : 
    1173             :     // If its not an account, maybe its a group?
    1174           4 :     Collection<GroupReference> suggestions = args.groupBackend.suggest(who, null);
    1175           4 :     if (!suggestions.isEmpty()) {
    1176           4 :       HashSet<AccountGroup.UUID> ids = new HashSet<>();
    1177           4 :       for (GroupReference ref : suggestions) {
    1178           4 :         ids.add(ref.getUUID());
    1179           4 :       }
    1180           4 :       return visibleto(new GroupBackedUser(ids));
    1181             :     }
    1182             : 
    1183           4 :     throw error("No user or group matches \"" + who + "\".");
    1184             :   }
    1185             : 
    1186             :   public Predicate<ChangeData> visibleto(CurrentUser user) {
    1187          12 :     return args.changeIsVisbleToPredicateFactory.forUser(user);
    1188             :   }
    1189             : 
    1190             :   public Predicate<ChangeData> isVisible() throws QueryParseException {
    1191          12 :     return visibleto(args.getUser());
    1192             :   }
    1193             : 
    1194             :   @Operator
    1195             :   public Predicate<ChangeData> o(String who)
    1196             :       throws QueryParseException, IOException, ConfigInvalidException {
    1197           0 :     return owner(who);
    1198             :   }
    1199             : 
    1200             :   @Operator
    1201             :   public Predicate<ChangeData> owner(String who)
    1202             :       throws QueryParseException, IOException, ConfigInvalidException {
    1203          10 :     return owner(parseAccount(who, (AccountState s) -> true));
    1204             :   }
    1205             : 
    1206             :   private Predicate<ChangeData> owner(Set<Account.Id> who) {
    1207          11 :     List<Predicate<ChangeData>> p = Lists.newArrayListWithCapacity(who.size());
    1208          11 :     for (Account.Id id : who) {
    1209          11 :       p.add(ChangePredicates.owner(id));
    1210          11 :     }
    1211          11 :     return Predicate.or(p);
    1212             :   }
    1213             : 
    1214             :   private Predicate<ChangeData> ownerDefaultField(String who)
    1215             :       throws QueryParseException, IOException, ConfigInvalidException {
    1216           5 :     Set<Account.Id> accounts = parseAccount(who);
    1217           5 :     if (accounts.size() > MAX_ACCOUNTS_PER_DEFAULT_FIELD) {
    1218           4 :       return Predicate.any();
    1219             :     }
    1220           5 :     return owner(accounts);
    1221             :   }
    1222             : 
    1223             :   @Operator
    1224             :   public Predicate<ChangeData> uploader(String who)
    1225             :       throws QueryParseException, IOException, ConfigInvalidException {
    1226           4 :     checkFieldAvailable(ChangeField.UPLOADER_SPEC, "uploader");
    1227           4 :     return uploader(parseAccount(who, (AccountState s) -> true));
    1228             :   }
    1229             : 
    1230             :   private Predicate<ChangeData> uploader(Set<Account.Id> who) {
    1231           4 :     List<Predicate<ChangeData>> p = Lists.newArrayListWithCapacity(who.size());
    1232           4 :     for (Account.Id id : who) {
    1233           4 :       p.add(ChangePredicates.uploader(id));
    1234           4 :     }
    1235           4 :     return Predicate.or(p);
    1236             :   }
    1237             : 
    1238             :   @Operator
    1239             :   public Predicate<ChangeData> attention(String who)
    1240             :       throws QueryParseException, IOException, ConfigInvalidException {
    1241           4 :     checkFieldAvailable(ChangeField.ATTENTION_SET_USERS, "attention");
    1242           4 :     return attention(parseAccount(who, (AccountState s) -> true));
    1243             :   }
    1244             : 
    1245             :   private Predicate<ChangeData> attention(Set<Account.Id> who) {
    1246           4 :     return Predicate.or(who.stream().map(ChangePredicates::attentionSet).collect(toImmutableSet()));
    1247             :   }
    1248             : 
    1249             :   @Operator
    1250             :   public Predicate<ChangeData> assignee(String who)
    1251             :       throws QueryParseException, IOException, ConfigInvalidException {
    1252           4 :     return assignee(parseAccount(who, (AccountState s) -> true));
    1253             :   }
    1254             : 
    1255             :   private Predicate<ChangeData> assignee(Set<Account.Id> who) {
    1256           4 :     List<Predicate<ChangeData>> p = Lists.newArrayListWithCapacity(who.size());
    1257           4 :     for (Account.Id id : who) {
    1258           4 :       p.add(ChangePredicates.assignee(id));
    1259           4 :     }
    1260           4 :     return Predicate.or(p);
    1261             :   }
    1262             : 
    1263             :   @Operator
    1264             :   public Predicate<ChangeData> ownerin(String group) throws QueryParseException, IOException {
    1265           5 :     GroupReference g = GroupBackends.findBestSuggestion(args.groupBackend, group);
    1266           5 :     if (g == null) {
    1267           0 :       throw error("Group " + group + " not found");
    1268             :     }
    1269             : 
    1270           5 :     AccountGroup.UUID groupId = g.getUUID();
    1271           5 :     GroupDescription.Basic groupDescription = args.groupBackend.get(groupId);
    1272           5 :     if (!(groupDescription instanceof GroupDescription.Internal)) {
    1273           4 :       return new OwnerinPredicate(args.userFactory, groupId);
    1274             :     }
    1275             : 
    1276           5 :     Set<Account.Id> accounts = getMembers(groupId);
    1277           5 :     List<Predicate<ChangeData>> p = Lists.newArrayListWithCapacity(accounts.size());
    1278           5 :     for (Account.Id id : accounts) {
    1279           4 :       p.add(ChangePredicates.owner(id));
    1280           4 :     }
    1281           5 :     return Predicate.or(p);
    1282             :   }
    1283             : 
    1284             :   @Operator
    1285             :   public Predicate<ChangeData> uploaderin(String group) throws QueryParseException, IOException {
    1286           4 :     checkFieldAvailable(ChangeField.UPLOADER_SPEC, "uploaderin");
    1287             : 
    1288           4 :     GroupReference g = GroupBackends.findBestSuggestion(args.groupBackend, group);
    1289           4 :     if (g == null) {
    1290           0 :       throw error("Group " + group + " not found");
    1291             :     }
    1292             : 
    1293           4 :     AccountGroup.UUID groupId = g.getUUID();
    1294           4 :     GroupDescription.Basic groupDescription = args.groupBackend.get(groupId);
    1295           4 :     if (!(groupDescription instanceof GroupDescription.Internal)) {
    1296           0 :       return new UploaderinPredicate(args.userFactory, groupId);
    1297             :     }
    1298             : 
    1299           4 :     Set<Account.Id> accounts = getMembers(groupId);
    1300           4 :     List<Predicate<ChangeData>> p = Lists.newArrayListWithCapacity(accounts.size());
    1301           4 :     for (Account.Id id : accounts) {
    1302           4 :       p.add(ChangePredicates.uploader(id));
    1303           4 :     }
    1304           4 :     return Predicate.or(p);
    1305             :   }
    1306             : 
    1307             :   @Operator
    1308             :   public Predicate<ChangeData> r(String who)
    1309             :       throws QueryParseException, IOException, ConfigInvalidException {
    1310           0 :     return reviewer(who);
    1311             :   }
    1312             : 
    1313             :   @Operator
    1314             :   public Predicate<ChangeData> reviewer(String who)
    1315             :       throws QueryParseException, IOException, ConfigInvalidException {
    1316           5 :     return reviewer(who, false);
    1317             :   }
    1318             : 
    1319             :   private Predicate<ChangeData> reviewerDefaultField(String who)
    1320             :       throws QueryParseException, IOException, ConfigInvalidException {
    1321           5 :     return reviewer(who, true);
    1322             :   }
    1323             : 
    1324             :   private Predicate<ChangeData> reviewer(String who, boolean forDefaultField)
    1325             :       throws QueryParseException, IOException, ConfigInvalidException {
    1326          10 :     Predicate<ChangeData> byState =
    1327           6 :         reviewerByState(who, ReviewerStateInternal.REVIEWER, forDefaultField);
    1328           6 :     if (Objects.equals(byState, Predicate.<ChangeData>any())) {
    1329           4 :       return Predicate.any();
    1330             :     }
    1331           6 :     return Predicate.and(Predicate.not(new BooleanPredicate(ChangeField.WIP)), byState);
    1332             :   }
    1333             : 
    1334             :   @Operator
    1335             :   public Predicate<ChangeData> cc(String who)
    1336             :       throws QueryParseException, IOException, ConfigInvalidException {
    1337           4 :     return reviewerByState(who, ReviewerStateInternal.CC, false);
    1338             :   }
    1339             : 
    1340             :   @Operator
    1341             :   public Predicate<ChangeData> reviewerin(String group) throws QueryParseException {
    1342           4 :     GroupReference g = GroupBackends.findBestSuggestion(args.groupBackend, group);
    1343           4 :     if (g == null) {
    1344           0 :       throw error("Group " + group + " not found");
    1345             :     }
    1346           4 :     return new ReviewerinPredicate(args.userFactory, g.getUUID());
    1347             :   }
    1348             : 
    1349             :   @Operator
    1350             :   public Predicate<ChangeData> tr(String trackingId) {
    1351           4 :     return ChangePredicates.trackingId(trackingId);
    1352             :   }
    1353             : 
    1354             :   @Operator
    1355             :   public Predicate<ChangeData> bug(String trackingId) {
    1356           4 :     return tr(trackingId);
    1357             :   }
    1358             : 
    1359             :   @Operator
    1360             :   public Predicate<ChangeData> limit(String query) throws QueryParseException {
    1361           6 :     Integer limit = Ints.tryParse(query);
    1362           6 :     if (limit == null) {
    1363           0 :       throw error("Invalid limit: " + query);
    1364             :     }
    1365           6 :     return new LimitPredicate<>(FIELD_LIMIT, limit);
    1366             :   }
    1367             : 
    1368             :   @Operator
    1369             :   public Predicate<ChangeData> added(String value) throws QueryParseException {
    1370           4 :     return new AddedPredicate(value);
    1371             :   }
    1372             : 
    1373             :   @Operator
    1374             :   public Predicate<ChangeData> deleted(String value) throws QueryParseException {
    1375           4 :     return new DeletedPredicate(value);
    1376             :   }
    1377             : 
    1378             :   @Operator
    1379             :   public Predicate<ChangeData> size(String value) throws QueryParseException {
    1380           4 :     return delta(value);
    1381             :   }
    1382             : 
    1383             :   @Operator
    1384             :   public Predicate<ChangeData> delta(String value) throws QueryParseException {
    1385           4 :     return new DeltaPredicate(value);
    1386             :   }
    1387             : 
    1388             :   @Operator
    1389             :   public Predicate<ChangeData> commentby(String who)
    1390             :       throws QueryParseException, IOException, ConfigInvalidException {
    1391           4 :     return commentby(parseAccount(who));
    1392             :   }
    1393             : 
    1394             :   private Predicate<ChangeData> commentby(Set<Account.Id> who) {
    1395           4 :     List<Predicate<ChangeData>> p = Lists.newArrayListWithCapacity(who.size());
    1396           4 :     for (Account.Id id : who) {
    1397           4 :       p.add(ChangePredicates.commentBy(id));
    1398           4 :     }
    1399           4 :     return Predicate.or(p);
    1400             :   }
    1401             : 
    1402             :   @Operator
    1403             :   public Predicate<ChangeData> from(String who)
    1404             :       throws QueryParseException, IOException, ConfigInvalidException {
    1405           4 :     Set<Account.Id> ownerIds = parseAccount(who);
    1406           4 :     return Predicate.or(owner(ownerIds), commentby(ownerIds));
    1407             :   }
    1408             : 
    1409             :   @Operator
    1410             :   public Predicate<ChangeData> query(String value) throws QueryParseException {
    1411             :     // [name=]<name>[,user=<user>] || [user=<user>,][name=]<name>
    1412           4 :     PredicateArgs inputArgs = new PredicateArgs(value);
    1413           4 :     String name = null;
    1414           4 :     Account.Id account = null;
    1415             : 
    1416           4 :     try (Repository git = args.repoManager.openRepository(args.allUsersName)) {
    1417             :       // [name=]<name>
    1418           4 :       if (inputArgs.keyValue.containsKey(ARG_ID_NAME)) {
    1419           4 :         name = inputArgs.keyValue.get(ARG_ID_NAME).value();
    1420           4 :       } else if (inputArgs.positional.size() == 1) {
    1421           4 :         name = Iterables.getOnlyElement(inputArgs.positional);
    1422           0 :       } else if (inputArgs.positional.size() > 1) {
    1423           0 :         throw new QueryParseException("Error parsing named query: " + value);
    1424             :       }
    1425             : 
    1426             :       // [,user=<user>]
    1427           4 :       if (inputArgs.keyValue.containsKey(ARG_ID_USER)) {
    1428           4 :         Set<Account.Id> accounts = parseAccount(inputArgs.keyValue.get(ARG_ID_USER).value());
    1429           4 :         if (accounts != null && accounts.size() > 1) {
    1430           0 :           throw error(
    1431           0 :               String.format(
    1432           0 :                   "\"%s\" resolves to multiple accounts", inputArgs.keyValue.get(ARG_ID_USER)));
    1433             :         }
    1434           4 :         account = (accounts == null ? self() : Iterables.getOnlyElement(accounts));
    1435           4 :       } else {
    1436           4 :         account = self();
    1437             :       }
    1438             : 
    1439           4 :       VersionedAccountQueries q = VersionedAccountQueries.forUser(account);
    1440           4 :       q.load(args.allUsersName, git);
    1441           4 :       String query = q.getQueryList().getQuery(name);
    1442           4 :       if (query != null) {
    1443           4 :         return parse(query);
    1444             :       }
    1445           4 :     } catch (RepositoryNotFoundException e) {
    1446           0 :       throw new QueryParseException(
    1447             :           "Unknown named query (no " + args.allUsersName + " repo): " + name, e);
    1448           0 :     } catch (IOException | ConfigInvalidException e) {
    1449           0 :       throw new QueryParseException("Error parsing named query: " + value, e);
    1450           4 :     }
    1451           4 :     throw new QueryParseException("Unknown named query: " + name);
    1452             :   }
    1453             : 
    1454             :   @Operator
    1455             :   public Predicate<ChangeData> reviewedby(String who)
    1456             :       throws QueryParseException, IOException, ConfigInvalidException {
    1457           4 :     return ChangePredicates.reviewedBy(parseAccount(who));
    1458             :   }
    1459             : 
    1460             :   @Operator
    1461             :   public Predicate<ChangeData> destination(String value) throws QueryParseException {
    1462             :     // [name=]<name>[,user=<user>] || [user=<user>,][name=]<name>
    1463           4 :     PredicateArgs inputArgs = new PredicateArgs(value);
    1464           4 :     String name = null;
    1465           4 :     Account.Id account = null;
    1466             : 
    1467           4 :     try (Repository git = args.repoManager.openRepository(args.allUsersName)) {
    1468             :       // [name=]<name>
    1469           4 :       if (inputArgs.keyValue.containsKey(ARG_ID_NAME)) {
    1470           4 :         name = inputArgs.keyValue.get(ARG_ID_NAME).value();
    1471           4 :       } else if (inputArgs.positional.size() == 1) {
    1472           4 :         name = Iterables.getOnlyElement(inputArgs.positional);
    1473           0 :       } else if (inputArgs.positional.size() > 1) {
    1474           0 :         throw new QueryParseException("Error parsing named destination: " + value);
    1475             :       }
    1476             : 
    1477             :       // [,user=<user>]
    1478           4 :       if (inputArgs.keyValue.containsKey(ARG_ID_USER)) {
    1479           4 :         Set<Account.Id> accounts = parseAccount(inputArgs.keyValue.get(ARG_ID_USER).value());
    1480           4 :         if (accounts != null && accounts.size() > 1) {
    1481           0 :           throw error(
    1482           0 :               String.format(
    1483           0 :                   "\"%s\" resolves to multiple accounts", inputArgs.keyValue.get(ARG_ID_USER)));
    1484             :         }
    1485           4 :         account = (accounts == null ? self() : Iterables.getOnlyElement(accounts));
    1486           4 :       } else {
    1487           4 :         account = self();
    1488             :       }
    1489             : 
    1490           4 :       Set<BranchNameKey> destinations = getDestinationList(git, account).getDestinations(name);
    1491           4 :       if (destinations != null && !destinations.isEmpty()) {
    1492           4 :         return new DestinationPredicate(destinations, value);
    1493             :       }
    1494           4 :     } catch (RepositoryNotFoundException e) {
    1495           0 :       throw new QueryParseException(
    1496             :           "Unknown named destination (no " + args.allUsersName + " repo): " + name, e);
    1497           0 :     } catch (IOException | ConfigInvalidException e) {
    1498           0 :       throw new QueryParseException("Error parsing named destination: " + value, e);
    1499           4 :     }
    1500           4 :     throw new QueryParseException("Unknown named destination: " + name);
    1501             :   }
    1502             : 
    1503             :   protected DestinationList getDestinationList(Repository git, Account.Id account)
    1504             :       throws ConfigInvalidException, RepositoryNotFoundException, IOException {
    1505           4 :     DestinationList dl = destinationListByAccount.get(account);
    1506           4 :     if (dl == null) {
    1507           4 :       dl = loadDestinationList(git, account);
    1508           4 :       destinationListByAccount.put(account, dl);
    1509             :     }
    1510           4 :     return dl;
    1511             :   }
    1512             : 
    1513             :   protected DestinationList loadDestinationList(Repository git, Account.Id account)
    1514             :       throws ConfigInvalidException, RepositoryNotFoundException, IOException {
    1515           4 :     VersionedAccountDestinations d = VersionedAccountDestinations.forUser(account);
    1516           4 :     d.load(args.allUsersName, git);
    1517           4 :     return d.getDestinationList();
    1518             :   }
    1519             : 
    1520             :   @Operator
    1521             :   public Predicate<ChangeData> author(String who) throws QueryParseException {
    1522           4 :     return getAuthorOrCommitterPredicate(
    1523           4 :         who.trim(), ChangePredicates::exactAuthor, ChangePredicates::author);
    1524             :   }
    1525             : 
    1526             :   @Operator
    1527             :   public Predicate<ChangeData> committer(String who) throws QueryParseException {
    1528           4 :     return getAuthorOrCommitterPredicate(
    1529           4 :         who.trim(), ChangePredicates::exactCommitter, ChangePredicates::committer);
    1530             :   }
    1531             : 
    1532             :   @Operator
    1533             :   public Predicate<ChangeData> unresolved(String value) throws QueryParseException {
    1534           4 :     return new IsUnresolvedPredicate(value);
    1535             :   }
    1536             : 
    1537             :   @Operator
    1538             :   public Predicate<ChangeData> revertof(String value) throws QueryParseException {
    1539           4 :     if (value == null || Ints.tryParse(value) == null) {
    1540           0 :       throw new QueryParseException("'revertof' must be an integer");
    1541             :     }
    1542           4 :     return ChangePredicates.revertOf(Change.id(Ints.tryParse(value)));
    1543             :   }
    1544             : 
    1545             :   @Operator
    1546             :   public Predicate<ChangeData> submissionId(String value) {
    1547           4 :     return ChangePredicates.submissionId(value);
    1548             :   }
    1549             : 
    1550             :   @Operator
    1551             :   public Predicate<ChangeData> cherryPickOf(String value) throws QueryParseException {
    1552           0 :     checkFieldAvailable(ChangeField.CHERRY_PICK_OF_CHANGE, "cherryPickOf");
    1553           0 :     checkFieldAvailable(ChangeField.CHERRY_PICK_OF_PATCHSET, "cherryPickOf");
    1554           0 :     if (Ints.tryParse(value) != null) {
    1555           0 :       return ChangePredicates.cherryPickOf(Change.id(Ints.tryParse(value)));
    1556             :     }
    1557             :     try {
    1558           0 :       PatchSet.Id patchSetId = PatchSet.Id.parse(value);
    1559           0 :       return ChangePredicates.cherryPickOf(patchSetId);
    1560           0 :     } catch (IllegalArgumentException e) {
    1561           0 :       throw new QueryParseException(
    1562             :           "'"
    1563             :               + value
    1564             :               + "' is not a valid input. It must be in the 'ChangeNumber[,PatchsetNumber]' format.",
    1565             :           e);
    1566             :     }
    1567             :   }
    1568             : 
    1569             :   @Override
    1570             :   protected Predicate<ChangeData> defaultField(String query) throws QueryParseException {
    1571          16 :     if (query.startsWith("refs/")) {
    1572           4 :       return ref(query);
    1573          16 :     } else if (DEF_CHANGE.matcher(query).matches()) {
    1574          14 :       List<Predicate<ChangeData>> predicates = Lists.newArrayListWithCapacity(2);
    1575             :       try {
    1576          14 :         predicates.add(change(query));
    1577           0 :       } catch (QueryParseException e) {
    1578             :         // Skip.
    1579          14 :       }
    1580             : 
    1581             :       // For PAT_LEGACY_ID, it may also be the prefix of some commits.
    1582          14 :       if (query.length() >= 6 && PAT_LEGACY_ID.matcher(query).matches()) {
    1583           0 :         predicates.add(commit(query));
    1584             :       }
    1585             : 
    1586          14 :       return Predicate.or(predicates);
    1587             :     }
    1588             : 
    1589             :     // Adapt the capacity of this list when adding more default predicates.
    1590          10 :     List<Predicate<ChangeData>> predicates = Lists.newArrayListWithCapacity(11);
    1591             :     try {
    1592           5 :       Predicate<ChangeData> p = ownerDefaultField(query);
    1593           5 :       if (!Objects.equals(p, Predicate.<ChangeData>any())) {
    1594           5 :         predicates.add(p);
    1595             :       }
    1596          10 :     } catch (StorageException | IOException | ConfigInvalidException | QueryParseException e) {
    1597             :       // Skip.
    1598           5 :     }
    1599             :     try {
    1600           5 :       Predicate<ChangeData> p = reviewerDefaultField(query);
    1601           5 :       if (!Objects.equals(p, Predicate.<ChangeData>any())) {
    1602           5 :         predicates.add(p);
    1603             :       }
    1604          10 :     } catch (StorageException | IOException | ConfigInvalidException | QueryParseException e) {
    1605             :       // Skip.
    1606           5 :     }
    1607          10 :     predicates.add(file(query));
    1608             :     try {
    1609          10 :       predicates.add(label(query));
    1610           0 :     } catch (StorageException | IOException | ConfigInvalidException | QueryParseException e) {
    1611             :       // Skip.
    1612          10 :     }
    1613          10 :     predicates.add(commit(query));
    1614          10 :     predicates.add(message(query));
    1615          10 :     predicates.add(comment(query));
    1616          10 :     predicates.add(projects(query));
    1617          10 :     predicates.add(ref(query));
    1618          10 :     predicates.add(branch(query));
    1619          10 :     predicates.add(topic(query));
    1620             :     // Adapt the capacity of the "predicates" list when adding more default
    1621             :     // predicates.
    1622          10 :     return Predicate.or(predicates);
    1623             :   }
    1624             : 
    1625             :   protected void checkFieldAvailable(SchemaField<ChangeData, ?> field, String operator)
    1626             :       throws QueryParseException {
    1627           6 :     if (!args.index.getSchema().hasField(field)) {
    1628           0 :       throw new QueryParseException(
    1629           0 :           String.format("'%s' operator is not supported by change index version", operator));
    1630             :     }
    1631           6 :   }
    1632             : 
    1633             :   private Predicate<ChangeData> getAuthorOrCommitterPredicate(
    1634             :       String who,
    1635             :       Function<String, Predicate<ChangeData>> exactPredicateFunc,
    1636             :       Function<String, Predicate<ChangeData>> fullPredicateFunc)
    1637             :       throws QueryParseException {
    1638           4 :     if (Address.tryParse(who) != null) {
    1639           4 :       return exactPredicateFunc.apply(who);
    1640             :     }
    1641           4 :     return getAuthorOrCommitterFullTextPredicate(who, fullPredicateFunc);
    1642             :   }
    1643             : 
    1644             :   private Predicate<ChangeData> getAuthorOrCommitterFullTextPredicate(
    1645             :       String who, Function<String, Predicate<ChangeData>> fullPredicateFunc)
    1646             :       throws QueryParseException {
    1647           4 :     if (isSelf(who)) {
    1648           4 :       IdentifiedUser me = args.getIdentifiedUser();
    1649           4 :       List<Predicate<ChangeData>> predicates =
    1650           4 :           me.getEmailAddresses().stream().map(fullPredicateFunc).collect(toList());
    1651           4 :       return Predicate.or(predicates);
    1652             :     }
    1653           4 :     Set<String> parts = SchemaUtil.getNameParts(who);
    1654           4 :     if (parts.isEmpty()) {
    1655           4 :       throw error("invalid value");
    1656             :     }
    1657             : 
    1658           4 :     List<Predicate<ChangeData>> predicates =
    1659           4 :         parts.stream().map(fullPredicateFunc).collect(toList());
    1660           4 :     return Predicate.and(predicates);
    1661             :   }
    1662             : 
    1663             :   private Set<Account.Id> getMembers(AccountGroup.UUID g) throws IOException {
    1664             :     Set<Account.Id> accounts;
    1665           5 :     Set<Account.Id> allMembers =
    1666           5 :         args.groupMembers.listAccounts(g).stream().map(Account::id).collect(toSet());
    1667           5 :     int maxTerms = args.indexConfig.maxTerms();
    1668           5 :     if (allMembers.size() > maxTerms) {
    1669             :       // limit the number of query terms otherwise Gerrit will barf
    1670           0 :       accounts = allMembers.stream().limit(maxTerms).collect(toSet());
    1671             :     } else {
    1672           5 :       accounts = allMembers;
    1673             :     }
    1674           5 :     return accounts;
    1675             :   }
    1676             : 
    1677             :   private Set<Account.Id> parseAccount(String who)
    1678             :       throws QueryParseException, IOException, ConfigInvalidException {
    1679             :     try {
    1680           6 :       return args.accountResolver.resolve(who).asNonEmptyIdSet();
    1681          10 :     } catch (UnresolvableAccountException e) {
    1682          10 :       if (e.isSelf()) {
    1683           0 :         throw new QueryRequiresAuthException(e.getMessage(), e);
    1684             :       }
    1685          10 :       throw new QueryParseException(e.getMessage(), e);
    1686             :     }
    1687             :   }
    1688             : 
    1689             :   private Set<Account.Id> parseAccount(
    1690             :       String who, java.util.function.Predicate<AccountState> activityFilter)
    1691             :       throws QueryParseException, IOException, ConfigInvalidException {
    1692             :     try {
    1693          10 :       return args.accountResolver.resolve(who, activityFilter).asNonEmptyIdSet();
    1694           4 :     } catch (UnresolvableAccountException e) {
    1695           4 :       if (e.isSelf()) {
    1696           4 :         throw new QueryRequiresAuthException(e.getMessage(), e);
    1697             :       }
    1698           0 :       throw new QueryParseException(e.getMessage(), e);
    1699             :     }
    1700             :   }
    1701             : 
    1702             :   private GroupReference parseGroup(String group) throws QueryParseException {
    1703           4 :     GroupReference g = GroupBackends.findBestSuggestion(args.groupBackend, group);
    1704           4 :     if (g == null) {
    1705           0 :       throw error("Group " + group + " not found");
    1706             :     }
    1707           4 :     return g;
    1708             :   }
    1709             : 
    1710             :   private List<Change> parseChange(String value) throws QueryParseException {
    1711           4 :     return asChanges(parseChangeData(value));
    1712             :   }
    1713             : 
    1714             :   private List<ChangeData> parseChangeData(String value) throws QueryParseException {
    1715           4 :     if (PAT_LEGACY_ID.matcher(value).matches()) {
    1716           4 :       Optional<Change.Id> id = Change.Id.tryParse(value);
    1717           4 :       if (!id.isPresent()) {
    1718           0 :         throw error("Invalid change id " + value);
    1719             :       }
    1720           4 :       return args.queryProvider.get().byLegacyChangeId(id.get());
    1721           4 :     } else if (PAT_CHANGE_ID.matcher(value).matches()) {
    1722           4 :       List<ChangeData> changes = args.queryProvider.get().byKeyPrefix(parseChangeId(value));
    1723           4 :       if (changes.isEmpty()) {
    1724           0 :         throw error("Change " + value + " not found");
    1725             :       }
    1726           4 :       return changes;
    1727             :     }
    1728             : 
    1729           0 :     throw error("Change " + value + " not found");
    1730             :   }
    1731             : 
    1732             :   private static String parseChangeId(String value) {
    1733          16 :     if (value.charAt(0) == 'i') {
    1734           0 :       value = "I" + value.substring(1);
    1735             :     }
    1736          16 :     return value;
    1737             :   }
    1738             : 
    1739             :   private Account.Id self() throws QueryParseException {
    1740          10 :     return args.getIdentifiedUser().getAccountId();
    1741             :   }
    1742             : 
    1743             :   public Predicate<ChangeData> reviewerByState(
    1744             :       String who, ReviewerStateInternal state, boolean forDefaultField)
    1745             :       throws QueryParseException, IOException, ConfigInvalidException {
    1746          10 :     Predicate<ChangeData> reviewerByEmailPredicate = null;
    1747          10 :     Address address = Address.tryParse(who);
    1748          10 :     if (address != null) {
    1749           6 :       reviewerByEmailPredicate = ReviewerByEmailPredicate.forState(address, state);
    1750             :     }
    1751             : 
    1752          10 :     Predicate<ChangeData> reviewerPredicate = null;
    1753             :     try {
    1754           6 :       Set<Account.Id> accounts = parseAccount(who);
    1755           6 :       if (!forDefaultField || accounts.size() <= MAX_ACCOUNTS_PER_DEFAULT_FIELD) {
    1756           6 :         reviewerPredicate =
    1757           6 :             Predicate.or(
    1758           6 :                 accounts.stream()
    1759           6 :                     .map(id -> ReviewerPredicate.forState(id, state))
    1760           6 :                     .collect(toList()));
    1761             :       } else {
    1762           4 :         logger.atFine().log(
    1763             :             "Skipping reviewer predicate for %s in default field query"
    1764             :                 + " because the number of matched accounts (%d) exceeds the limit of %d",
    1765           4 :             who, accounts.size(), MAX_ACCOUNTS_PER_DEFAULT_FIELD);
    1766             :       }
    1767          10 :     } catch (QueryParseException e) {
    1768          10 :       logger.atFine().log("Parsing %s as account failed: %s", who, e.getMessage());
    1769             :       // Propagate this exception only if we can't use 'who' to query by email
    1770          10 :       if (reviewerByEmailPredicate == null) {
    1771          10 :         throw e;
    1772             :       }
    1773           6 :     }
    1774             : 
    1775           6 :     if (reviewerPredicate != null && reviewerByEmailPredicate != null) {
    1776           6 :       return Predicate.or(reviewerPredicate, reviewerByEmailPredicate);
    1777           4 :     } else if (reviewerPredicate != null) {
    1778           4 :       return reviewerPredicate;
    1779           4 :     } else if (reviewerByEmailPredicate != null) {
    1780           4 :       return reviewerByEmailPredicate;
    1781             :     } else {
    1782           4 :       return Predicate.any();
    1783             :     }
    1784             :   }
    1785             : }

Generated by: LCOV version 1.16+git.20220603.dfeb750