blob: eccb2e8239d2826d4f332b6e8de7dff60292c580 [file] [log] [blame]
// Copyright (C) 2009 The Android Open Source Project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package com.google.gerrit.server.query.change;
import com.google.gerrit.common.data.ApprovalTypes;
import com.google.gerrit.reviewdb.Account;
import com.google.gerrit.reviewdb.AccountGroup;
import com.google.gerrit.reviewdb.Change;
import com.google.gerrit.reviewdb.Project;
import com.google.gerrit.reviewdb.RevId;
import com.google.gerrit.reviewdb.ReviewDb;
import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.account.AccountResolver;
import com.google.gerrit.server.account.GroupCache;
import com.google.gerrit.server.config.AuthConfig;
import com.google.gerrit.server.config.WildProjectName;
import com.google.gerrit.server.patch.PatchListCache;
import com.google.gerrit.server.project.ChangeControl;
import com.google.gerrit.server.query.IntPredicate;
import com.google.gerrit.server.query.Predicate;
import com.google.gerrit.server.query.QueryBuilder;
import com.google.gerrit.server.query.QueryParseException;
import com.google.gwtorm.client.OrmException;
import com.google.inject.Inject;
import com.google.inject.Provider;
import com.google.inject.assistedinject.Assisted;
import org.eclipse.jgit.lib.AbbreviatedObjectId;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.regex.Pattern;
/**
* Parses a query string meant to be applied to change objects.
*/
public class ChangeQueryBuilder extends QueryBuilder<ChangeData> {
private static final Pattern PAT_LEGACY_ID = Pattern.compile("^[1-9][0-9]*$");
private static final Pattern PAT_CHANGE_ID =
Pattern.compile("^[iI][0-9a-f]{4,}.*$");
private static final Pattern DEF_CHANGE =
Pattern.compile("^([1-9][0-9]*|[iI][0-9a-f]{4,}.*)$");
private static final Pattern PAT_COMMIT =
Pattern.compile("^([0-9a-fA-F]{4," + RevId.LEN + "})$");
private static final Pattern PAT_EMAIL =
Pattern.compile("^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+");
private static final Pattern PAT_LABEL =
Pattern.compile("^[a-zA-Z][a-zA-Z0-9]*((=|>=|<=)[+-]?|[+-])\\d+$");
public static final String FIELD_AGE = "age";
public static final String FIELD_BRANCH = "branch";
public static final String FIELD_CHANGE = "change";
public static final String FIELD_COMMIT = "commit";
public static final String FIELD_DRAFTBY = "draftby";
public static final String FIELD_FILE = "file";
public static final String FIELD_IS = "is";
public static final String FIELD_HAS = "has";
public static final String FIELD_LABEL = "label";
public static final String FIELD_LIMIT = "limit";
public static final String FIELD_OWNER = "owner";
public static final String FIELD_PROJECT = "project";
public static final String FIELD_REF = "ref";
public static final String FIELD_REVIEWER = "reviewer";
public static final String FIELD_STARREDBY = "starredby";
public static final String FIELD_STATUS = "status";
public static final String FIELD_TOPIC = "topic";
public static final String FIELD_TR = "tr";
public static final String FIELD_VISIBLETO = "visibleto";
public static final String FIELD_WATCHEDBY = "watchedby";
private static final QueryBuilder.Definition<ChangeData, ChangeQueryBuilder> mydef =
new QueryBuilder.Definition<ChangeData, ChangeQueryBuilder>(
ChangeQueryBuilder.class);
static class Arguments {
final Provider<ReviewDb> dbProvider;
final Provider<ChangeQueryRewriter> rewriter;
final IdentifiedUser.GenericFactory userFactory;
final ChangeControl.Factory changeControlFactory;
final ChangeControl.GenericFactory changeControlGenericFactory;
final AccountResolver accountResolver;
final GroupCache groupCache;
final AuthConfig authConfig;
final ApprovalTypes approvalTypes;
final Project.NameKey wildProjectName;
final PatchListCache patchListCache;
@Inject
Arguments(Provider<ReviewDb> dbProvider,
Provider<ChangeQueryRewriter> rewriter,
IdentifiedUser.GenericFactory userFactory,
ChangeControl.Factory changeControlFactory,
ChangeControl.GenericFactory changeControlGenericFactory,
AccountResolver accountResolver, GroupCache groupCache,
AuthConfig authConfig, ApprovalTypes approvalTypes,
@WildProjectName Project.NameKey wildProjectName,
PatchListCache patchListCache) {
this.dbProvider = dbProvider;
this.rewriter = rewriter;
this.userFactory = userFactory;
this.changeControlFactory = changeControlFactory;
this.changeControlGenericFactory = changeControlGenericFactory;
this.accountResolver = accountResolver;
this.groupCache = groupCache;
this.authConfig = authConfig;
this.approvalTypes = approvalTypes;
this.wildProjectName = wildProjectName;
this.patchListCache = patchListCache;
}
}
public interface Factory {
ChangeQueryBuilder create(CurrentUser user);
}
private final Arguments args;
private final CurrentUser currentUser;
private boolean allowsFile;
@Inject
ChangeQueryBuilder(Arguments args, @Assisted CurrentUser currentUser) {
super(mydef);
this.args = args;
this.currentUser = currentUser;
}
public void setAllowFile(boolean on) {
allowsFile = on;
}
@Operator
public Predicate<ChangeData> age(String value) {
return new AgePredicate(args.dbProvider, value);
}
@Operator
public Predicate<ChangeData> change(String query) {
if (PAT_LEGACY_ID.matcher(query).matches()) {
return new LegacyChangeIdPredicate(args.dbProvider, Change.Id
.parse(query));
} else if (PAT_CHANGE_ID.matcher(query).matches()) {
if (query.charAt(0) == 'i') {
query = "I" + query.substring(1);
}
return new ChangeIdPredicate(args.dbProvider, query);
}
throw new IllegalArgumentException();
}
@Operator
public Predicate<ChangeData> status(String statusName) {
if ("open".equals(statusName)) {
return status_open();
} else if ("closed".equals(statusName)) {
return ChangeStatusPredicate.closed(args.dbProvider);
} else if ("reviewed".equalsIgnoreCase(statusName)) {
return new IsReviewedPredicate(args.dbProvider);
} else {
return new ChangeStatusPredicate(args.dbProvider, statusName);
}
}
public Predicate<ChangeData> status_open() {
return ChangeStatusPredicate.open(args.dbProvider);
}
@Operator
public Predicate<ChangeData> has(String value) {
if ("star".equalsIgnoreCase(value)) {
return new IsStarredByPredicate(args.dbProvider, currentUser);
}
if ("draft".equalsIgnoreCase(value)) {
if (currentUser instanceof IdentifiedUser) {
return new HasDraftByPredicate(args.dbProvider,
((IdentifiedUser) currentUser).getAccountId());
}
}
throw new IllegalArgumentException();
}
@Operator
public Predicate<ChangeData> is(String value) {
if ("starred".equalsIgnoreCase(value)) {
return new IsStarredByPredicate(args.dbProvider, currentUser);
}
if ("watched".equalsIgnoreCase(value)) {
return new IsWatchedByPredicate(args, currentUser);
}
if ("visible".equalsIgnoreCase(value)) {
return is_visible();
}
if ("reviewed".equalsIgnoreCase(value)) {
return new IsReviewedPredicate(args.dbProvider);
}
try {
return status(value);
} catch (IllegalArgumentException e) {
// not status: alias?
}
throw new IllegalArgumentException();
}
@Operator
public Predicate<ChangeData> commit(String id) {
return new CommitPredicate(args.dbProvider, AbbreviatedObjectId
.fromString(id));
}
@Operator
public Predicate<ChangeData> project(String name) {
return new ProjectPredicate(args.dbProvider, name);
}
@Operator
public Predicate<ChangeData> branch(String name) {
return new BranchPredicate(args.dbProvider, name);
}
@Operator
public Predicate<ChangeData> topic(String name) {
return new TopicPredicate(args.dbProvider, name);
}
@Operator
public Predicate<ChangeData> ref(String ref) {
return new RefPredicate(args.dbProvider, ref);
}
@Operator
public Predicate<ChangeData> file(String file) throws QueryParseException {
if (!allowsFile) {
throw error("operator not permitted here: file:" + file);
}
if (file.startsWith("^")) {
return new RegexFilePredicate(args.dbProvider, args.patchListCache, file);
}
throw new IllegalArgumentException();
}
@Operator
public Predicate<ChangeData> label(String name) {
return new LabelPredicate(args.changeControlGenericFactory,
args.userFactory, args.dbProvider, args.approvalTypes, name);
}
@Operator
public Predicate<ChangeData> starredby(String who)
throws QueryParseException, OrmException {
Account account = args.accountResolver.find(who);
if (account == null) {
throw error("User " + who + " not found");
}
return new IsStarredByPredicate(args.dbProvider, //
args.userFactory.create(args.dbProvider, account.getId()));
}
@Operator
public Predicate<ChangeData> watchedby(String who)
throws QueryParseException, OrmException {
Account account = args.accountResolver.find(who);
if (account == null) {
throw error("User " + who + " not found");
}
return new IsWatchedByPredicate(args, args.userFactory.create(
args.dbProvider, account.getId()));
}
@Operator
public Predicate<ChangeData> draftby(String who) throws QueryParseException,
OrmException {
Account account = args.accountResolver.find(who);
if (account == null) {
throw error("User " + who + " not found");
}
return new HasDraftByPredicate(args.dbProvider, account.getId());
}
@Operator
public Predicate<ChangeData> visibleto(String who)
throws QueryParseException, OrmException {
Account account = args.accountResolver.find(who);
if (account != null) {
return visibleto(args.userFactory
.create(args.dbProvider, account.getId()));
}
// If its not an account, maybe its a group?
//
AccountGroup g = args.groupCache.get(new AccountGroup.NameKey(who));
if (g != null) {
return visibleto(new SingleGroupUser(args.authConfig, g.getId()));
}
Collection<AccountGroup> matches =
args.groupCache.get(new AccountGroup.ExternalNameKey(who));
if (matches != null && !matches.isEmpty()) {
HashSet<AccountGroup.Id> ids = new HashSet<AccountGroup.Id>();
for (AccountGroup group : matches) {
ids.add(group.getId());
}
return visibleto(new SingleGroupUser(args.authConfig, ids));
}
throw error("No user or group matches \"" + who + "\".");
}
public Predicate<ChangeData> visibleto(CurrentUser user) {
return new IsVisibleToPredicate(args.dbProvider, //
args.changeControlGenericFactory, //
user);
}
public Predicate<ChangeData> is_visible() {
return visibleto(currentUser);
}
@Operator
public Predicate<ChangeData> owner(String who) throws QueryParseException,
OrmException {
Set<Account.Id> m = args.accountResolver.findAll(who);
if (m.isEmpty()) {
throw error("User " + who + " not found");
} else if (m.size() == 1) {
Account.Id id = m.iterator().next();
return new OwnerPredicate(args.dbProvider, id);
} else {
List<OwnerPredicate> p = new ArrayList<OwnerPredicate>(m.size());
for (Account.Id id : m) {
p.add(new OwnerPredicate(args.dbProvider, id));
}
return Predicate.or(p);
}
}
@Operator
public Predicate<ChangeData> reviewer(String who)
throws QueryParseException, OrmException {
Set<Account.Id> m = args.accountResolver.findAll(who);
if (m.isEmpty()) {
throw error("User " + who + " not found");
} else if (m.size() == 1) {
Account.Id id = m.iterator().next();
return new ReviewerPredicate(args.dbProvider, id);
} else {
List<ReviewerPredicate> p = new ArrayList<ReviewerPredicate>(m.size());
for (Account.Id id : m) {
p.add(new ReviewerPredicate(args.dbProvider, id));
}
return Predicate.or(p);
}
}
@Operator
public Predicate<ChangeData> tr(String trackingId) {
return new TrackingIdPredicate(args.dbProvider, trackingId);
}
@Operator
public Predicate<ChangeData> bug(String trackingId) {
return tr(trackingId);
}
@Operator
public Predicate<ChangeData> limit(String limit) {
return limit(Integer.parseInt(limit));
}
public Predicate<ChangeData> limit(int limit) {
return new IntPredicate<ChangeData>(FIELD_LIMIT, limit) {
@Override
public boolean match(ChangeData object) {
return true;
}
@Override
public int getCost() {
return 0;
}
};
}
@Operator
public Predicate<ChangeData> sortkey_after(String sortKey) {
return new SortKeyPredicate.After(args.dbProvider, sortKey);
}
@Operator
public Predicate<ChangeData> sortkey_before(String sortKey) {
return new SortKeyPredicate.Before(args.dbProvider, sortKey);
}
@Operator
public Predicate<ChangeData> resume_sortkey(String sortKey) {
return sortkey_before(sortKey);
}
@SuppressWarnings("unchecked")
public boolean hasLimit(Predicate<ChangeData> p) {
return find(p, IntPredicate.class, FIELD_LIMIT) != null;
}
@SuppressWarnings("unchecked")
public int getLimit(Predicate<ChangeData> p) {
return ((IntPredicate) find(p, IntPredicate.class, FIELD_LIMIT)).intValue();
}
@SuppressWarnings("unchecked")
public boolean hasSortKey(Predicate<ChangeData> p) {
return find(p, SortKeyPredicate.class, "sortkey_after") != null
|| find(p, SortKeyPredicate.class, "sortkey_before") != null;
}
@SuppressWarnings("unchecked")
@Override
protected Predicate<ChangeData> defaultField(String query)
throws QueryParseException {
if (query.startsWith("refs/")) {
return ref(query);
} else if (DEF_CHANGE.matcher(query).matches()) {
return change(query);
} else if (PAT_COMMIT.matcher(query).matches()) {
return commit(query);
} else if (PAT_EMAIL.matcher(query).find()) {
try {
return Predicate.or(owner(query), reviewer(query));
} catch (OrmException err) {
throw error("Cannot lookup user", err);
}
} else if (PAT_LABEL.matcher(query).find()) {
return label(query);
} else {
throw error("Unsupported query:" + query);
}
}
}