blob: d71a7811c0ebb413031b5226e82728f870f07309 [file] [log] [blame]
// Copyright (C) 2008 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.httpd.rpc;
import static com.google.gerrit.reviewdb.AccountExternalId.SCHEME_USERNAME;
import com.google.gerrit.common.data.AccountDashboardInfo;
import com.google.gerrit.common.data.ChangeInfo;
import com.google.gerrit.common.data.ChangeListService;
import com.google.gerrit.common.data.SingleListChangeInfo;
import com.google.gerrit.common.data.ToggleStarRequest;
import com.google.gerrit.common.errors.NoSuchEntityException;
import com.google.gerrit.reviewdb.Account;
import com.google.gerrit.reviewdb.AccountExternalId;
import com.google.gerrit.reviewdb.Change;
import com.google.gerrit.reviewdb.ChangeAccess;
import com.google.gerrit.reviewdb.PatchLineComment;
import com.google.gerrit.reviewdb.PatchSet;
import com.google.gerrit.reviewdb.PatchSetApproval;
import com.google.gerrit.reviewdb.Project;
import com.google.gerrit.reviewdb.RevId;
import com.google.gerrit.reviewdb.ReviewDb;
import com.google.gerrit.reviewdb.StarredChange;
import com.google.gerrit.reviewdb.TrackingId;
import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.account.AccountInfoCacheFactory;
import com.google.gerrit.server.project.ChangeControl;
import com.google.gerrit.server.project.NoSuchChangeException;
import com.google.gwt.user.client.rpc.AsyncCallback;
import com.google.gwtjsonrpc.client.VoidResult;
import com.google.gwtorm.client.OrmException;
import com.google.gwtorm.client.ResultSet;
import com.google.gwtorm.client.impl.ListResultSet;
import com.google.inject.Inject;
import com.google.inject.Provider;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
public class ChangeListServiceImpl extends BaseServiceImplementation implements
ChangeListService {
private static final Comparator<ChangeInfo> ID_COMP =
new Comparator<ChangeInfo>() {
public int compare(final ChangeInfo o1, final ChangeInfo o2) {
return o1.getId().get() - o2.getId().get();
}
};
private static final Comparator<ChangeInfo> SORT_KEY_COMP =
new Comparator<ChangeInfo>() {
public int compare(final ChangeInfo o1, final ChangeInfo o2) {
return o2.getSortKey().compareTo(o1.getSortKey());
}
};
private static final Comparator<Change> QUERY_PREV =
new Comparator<Change>() {
public int compare(final Change a, final Change b) {
return a.getSortKey().compareTo(b.getSortKey());
}
};
private static final Comparator<Change> QUERY_NEXT =
new Comparator<Change>() {
public int compare(final Change a, final Change b) {
return b.getSortKey().compareTo(a.getSortKey());
}
};
private static final int MAX_PER_PAGE = 100;
private static int safePageSize(final int pageSize) {
return 0 < pageSize && pageSize <= MAX_PER_PAGE ? pageSize : MAX_PER_PAGE;
}
private final Provider<CurrentUser> currentUser;
private final ChangeControl.Factory changeControlFactory;
private final AccountInfoCacheFactory.Factory accountInfoCacheFactory;
@Inject
ChangeListServiceImpl(final Provider<ReviewDb> schema,
final Provider<CurrentUser> currentUser,
final ChangeControl.Factory changeControlFactory,
final AccountInfoCacheFactory.Factory accountInfoCacheFactory) {
super(schema, currentUser);
this.currentUser = currentUser;
this.changeControlFactory = changeControlFactory;
this.accountInfoCacheFactory = accountInfoCacheFactory;
}
private boolean canRead(final Change c) {
try {
return changeControlFactory.controlFor(c).isVisible();
} catch (NoSuchChangeException e) {
return false;
}
}
public void allOpenPrev(final String pos, final int pageSize,
final AsyncCallback<SingleListChangeInfo> callback) {
run(callback, new QueryPrev(pageSize, pos) {
@Override
ResultSet<Change> query(ReviewDb db, int slim, String sortKey)
throws OrmException {
return db.changes().allOpenPrev(sortKey, slim);
}
});
}
public void allOpenNext(final String pos, final int pageSize,
final AsyncCallback<SingleListChangeInfo> callback) {
run(callback, new QueryNext(pageSize, pos) {
@Override
ResultSet<Change> query(ReviewDb db, int slim, String sortKey)
throws OrmException {
return db.changes().allOpenNext(sortKey, slim);
}
});
}
public void byProjectOpenPrev(final Project.NameKey project,
final String pos, final int pageSize,
final AsyncCallback<SingleListChangeInfo> callback) {
run(callback, new QueryPrev(pageSize, pos) {
@Override
ResultSet<Change> query(ReviewDb db, int slim, String sortKey)
throws OrmException {
return db.changes().byProjectOpenPrev(project, sortKey, slim);
}
});
}
public void byProjectOpenNext(final Project.NameKey project,
final String pos, final int pageSize,
final AsyncCallback<SingleListChangeInfo> callback) {
run(callback, new QueryNext(pageSize, pos) {
@Override
ResultSet<Change> query(ReviewDb db, int slim, String sortKey)
throws OrmException {
return db.changes().byProjectOpenNext(project, sortKey, slim);
}
});
}
public void byProjectClosedPrev(final Project.NameKey project,
final Change.Status s, final String pos, final int pageSize,
final AsyncCallback<SingleListChangeInfo> callback) {
run(callback, new QueryPrev(pageSize, pos) {
@Override
ResultSet<Change> query(ReviewDb db, int slim, String sortKey)
throws OrmException {
return db.changes().byProjectClosedPrev(s.getCode(), project, sortKey,
slim);
}
});
}
public void byProjectClosedNext(final Project.NameKey project,
final Change.Status s, final String pos, final int pageSize,
final AsyncCallback<SingleListChangeInfo> callback) {
run(callback, new QueryNext(pageSize, pos) {
@Override
ResultSet<Change> query(ReviewDb db, int slim, String sortKey)
throws OrmException {
return db.changes().byProjectClosedNext(s.getCode(), project, sortKey,
slim);
}
});
}
public void allClosedPrev(final Change.Status s, final String pos,
final int pageSize, final AsyncCallback<SingleListChangeInfo> callback) {
run(callback, new QueryPrev(pageSize, pos) {
@Override
ResultSet<Change> query(ReviewDb db, int lim, String key)
throws OrmException {
return db.changes().allClosedPrev(s.getCode(), key, lim);
}
});
}
public void allClosedNext(final Change.Status s, final String pos,
final int pageSize, final AsyncCallback<SingleListChangeInfo> callback) {
run(callback, new QueryNext(pageSize, pos) {
@Override
ResultSet<Change> query(ReviewDb db, int lim, String key)
throws OrmException {
return db.changes().allClosedNext(s.getCode(), key, lim);
}
});
}
@Override
public void allQueryPrev(final String query, final String pos,
final int pageSize, final AsyncCallback<SingleListChangeInfo> callback) {
run(callback, new QueryPrev(pageSize, pos) {
@Override
ResultSet<Change> query(ReviewDb db, int lim, String key)
throws OrmException {
return searchQuery(db, query, lim, key, QUERY_PREV);
}
});
}
@Override
public void allQueryNext(final String query, final String pos,
final int pageSize, final AsyncCallback<SingleListChangeInfo> callback) {
run(callback, new QueryNext(pageSize, pos) {
@Override
ResultSet<Change> query(ReviewDb db, int lim, String key)
throws OrmException {
return searchQuery(db, query, lim, key, QUERY_NEXT);
}
});
}
private ResultSet<Change> searchQuery(final ReviewDb db, String query,
final int limit, final String key, final Comparator<Change> cmp)
throws OrmException {
List<Change> result = new ArrayList<Change>();
final HashSet<Change.Id> want = new HashSet<Change.Id>();
query = query.trim();
if (query.matches("^[1-9][0-9]*$")) {
want.add(Change.Id.parse(query));
} else if (query.matches("^[iI][0-9a-f]{4,}.*$")) {
if (query.startsWith("i")) {
query = "I" + query.substring(1);
}
final Change.Key a = new Change.Key(query);
final Change.Key b = a.max();
filterBySortKey(result, db.changes().byKeyRange(a, b), cmp, key);
Collections.sort(result, cmp);
if (limit < result.size()) {
result = result.subList(0, limit);
}
} else if (query.matches("^([0-9a-fA-F]{4," + RevId.LEN + "})$")) {
final RevId id = new RevId(query);
final ResultSet<PatchSet> patches;
if (id.isComplete()) {
patches = db.patchSets().byRevision(id);
} else {
patches = db.patchSets().byRevisionRange(id, id.max());
}
for (PatchSet p : patches) {
want.add(p.getId().getParentKey());
}
} else if (query.contains("owner:")) {
String[] parsedQuery = query.split(":");
if (parsedQuery.length > 1) {
filterBySortKey(result, changesCreatedBy(db, parsedQuery[1]), cmp, key);
}
} else if (query.contains("reviewer:")) {
String[] parsedQuery = query.split(":");
if (parsedQuery.length > 1) {
want.addAll(changesReviewedBy(db, parsedQuery[1]));
}
} else if (query.contains("bug:") || query.contains("tr:")) {
String[] parsedQuery = query.split(":");
if (parsedQuery.length > 1) {
want.addAll(changesReferencingTr(db, parsedQuery[1]));
}
}
if (result.isEmpty() && want.isEmpty()) {
return new ListResultSet<Change>(Collections.<Change> emptyList());
}
filterBySortKey(result, db.changes().get(want), cmp, key);
Collections.sort(result, cmp);
if (limit < result.size()) {
result = result.subList(0, limit);
}
return new ListResultSet<Change>(result);
}
private static void filterBySortKey(final List<Change> dst,
final Iterable<Change> src, final Comparator<Change> cmp, final String key) {
if (cmp == QUERY_PREV) {
for (Change c : src) {
if (c.getSortKey().compareTo(key) > 0) {
dst.add(c);
}
}
} else /* cmp == QUERY_NEXT */{
for (Change c : src) {
if (c.getSortKey().compareTo(key) < 0) {
dst.add(c);
}
}
}
}
public void forAccount(final Account.Id id,
final AsyncCallback<AccountDashboardInfo> callback) {
final Account.Id me = getAccountId();
final Account.Id target = id != null ? id : me;
if (target == null) {
callback.onFailure(new NoSuchEntityException());
return;
}
run(callback, new Action<AccountDashboardInfo>() {
public AccountDashboardInfo run(final ReviewDb db) throws OrmException,
Failure {
final AccountInfoCacheFactory ac = accountInfoCacheFactory.create();
final Account user = ac.get(target);
if (user == null) {
throw new Failure(new NoSuchEntityException());
}
final Set<Change.Id> stars = currentUser.get().getStarredChanges();
final ChangeAccess changes = db.changes();
final AccountDashboardInfo d;
final Set<Change.Id> openReviews = new HashSet<Change.Id>();
final Set<Change.Id> closedReviews = new HashSet<Change.Id>();
for (final PatchSetApproval ca : db.patchSetApprovals().openByUser(id)) {
openReviews.add(ca.getPatchSetId().getParentKey());
}
for (final PatchSetApproval ca : db.patchSetApprovals()
.closedByUser(id)) {
closedReviews.add(ca.getPatchSetId().getParentKey());
}
d = new AccountDashboardInfo(target);
d.setByOwner(filter(changes.byOwnerOpen(target), stars, ac));
d.setClosed(filter(changes.byOwnerClosed(target), stars, ac));
for (final ChangeInfo c : d.getByOwner()) {
openReviews.remove(c.getId());
}
d.setForReview(filter(changes.get(openReviews), stars, ac));
Collections.sort(d.getForReview(), ID_COMP);
for (final ChangeInfo c : d.getClosed()) {
closedReviews.remove(c.getId());
}
if (!closedReviews.isEmpty()) {
d.getClosed().addAll(filter(changes.get(closedReviews), stars, ac));
Collections.sort(d.getClosed(), SORT_KEY_COMP);
}
d.setAccounts(ac.create());
return d;
}
});
}
public void myStarredChanges(
final AsyncCallback<SingleListChangeInfo> callback) {
run(callback, new Action<SingleListChangeInfo>() {
public SingleListChangeInfo run(final ReviewDb db) throws OrmException {
final AccountInfoCacheFactory ac = accountInfoCacheFactory.create();
final SingleListChangeInfo d = new SingleListChangeInfo();
final Set<Change.Id> starred = currentUser.get().getStarredChanges();
d.setChanges(filter(db.changes().get(starred), starred, ac));
Collections.sort(d.getChanges(), new Comparator<ChangeInfo>() {
public int compare(final ChangeInfo o1, final ChangeInfo o2) {
return o1.getLastUpdatedOn().compareTo(o2.getLastUpdatedOn());
}
});
d.setAccounts(ac.create());
return d;
}
});
}
public void myDraftChanges(final AsyncCallback<SingleListChangeInfo> callback) {
run(callback, new Action<SingleListChangeInfo>() {
public SingleListChangeInfo run(final ReviewDb db) throws OrmException {
final Account.Id me = getAccountId();
final AccountInfoCacheFactory ac = accountInfoCacheFactory.create();
final SingleListChangeInfo d = new SingleListChangeInfo();
final Set<Change.Id> starred = currentUser.get().getStarredChanges();
final Set<Change.Id> drafted = draftedBy(db, me);
d.setChanges(filter(db.changes().get(drafted), starred, ac));
Collections.sort(d.getChanges(), new Comparator<ChangeInfo>() {
public int compare(final ChangeInfo o1, final ChangeInfo o2) {
return o1.getLastUpdatedOn().compareTo(o2.getLastUpdatedOn());
}
});
d.setAccounts(ac.create());
return d;
}
});
}
public void toggleStars(final ToggleStarRequest req,
final AsyncCallback<VoidResult> callback) {
run(callback, new Action<VoidResult>() {
public VoidResult run(final ReviewDb db) throws OrmException {
final Account.Id me = getAccountId();
final Set<Change.Id> existing = currentUser.get().getStarredChanges();
List<StarredChange> add = new ArrayList<StarredChange>();
List<StarredChange.Key> remove = new ArrayList<StarredChange.Key>();
if (req.getAddSet() != null) {
for (final Change.Id id : req.getAddSet()) {
if (!existing.contains(id)) {
add.add(new StarredChange(new StarredChange.Key(me, id)));
}
}
}
if (req.getRemoveSet() != null) {
for (final Change.Id id : req.getRemoveSet()) {
remove.add(new StarredChange.Key(me, id));
}
}
db.starredChanges().insert(add);
db.starredChanges().deleteKeys(remove);
return VoidResult.INSTANCE;
}
});
}
public void myStarredChangeIds(final AsyncCallback<Set<Change.Id>> callback) {
callback.onSuccess(currentUser.get().getStarredChanges());
}
private List<ChangeInfo> filter(final ResultSet<Change> rs,
final Set<Change.Id> starred, final AccountInfoCacheFactory accts) {
final ArrayList<ChangeInfo> r = new ArrayList<ChangeInfo>();
for (final Change c : rs) {
if (canRead(c)) {
final ChangeInfo ci = new ChangeInfo(c);
accts.want(ci.getOwner());
ci.setStarred(starred.contains(ci.getId()));
r.add(ci);
}
}
return r;
}
private static Set<Change.Id> draftedBy(final ReviewDb db, final Account.Id me)
throws OrmException {
final Set<Change.Id> existing = new HashSet<Change.Id>();
if (me != null) {
for (final PatchLineComment sc : db.patchComments().draftByAuthor(me)) {
final Change.Id c =
sc.getKey().getParentKey().getParentKey().getParentKey();
existing.add(c);
}
}
return existing;
}
/**
* @return a set of all the account ID's matching the given user name in
* either of the following columns: ssh name, email address, full name
*/
private static Set<Account.Id> getAccountSources(final ReviewDb db,
final String userName) throws OrmException {
Set<Account.Id> result = new HashSet<Account.Id>();
String a = userName;
String b = userName + "\u9fa5";
addAll(result, db.accounts().suggestByFullName(a, b, 10));
for (AccountExternalId extId : db.accountExternalIds().suggestByKey(
new AccountExternalId.Key(SCHEME_USERNAME, a),
new AccountExternalId.Key(SCHEME_USERNAME, b), 10)) {
result.add(extId.getAccountId());
}
for (AccountExternalId extId : db.accountExternalIds()
.suggestByEmailAddress(a, b, 10)) {
result.add(extId.getAccountId());
}
return result;
}
private static void addAll(Set<Account.Id> result, ResultSet<Account> rs) {
for (Account account : rs) {
result.add(account.getId());
}
}
/**
* @return a set of all the changes created by userName. This method tries to
* find userName in 1) the ssh user names, 2) the full names and 3)
* the email addresses. The returned changes are unique and sorted by
* time stamp, newer first.
*/
private List<Change> changesCreatedBy(final ReviewDb db, final String userName)
throws OrmException {
final List<Change> resultChanges = new ArrayList<Change>();
for (Account.Id account : getAccountSources(db, userName)) {
for (Change change : db.changes().byOwnerOpen(account)) {
resultChanges.add(change);
}
for (Change change : db.changes().byOwnerClosedAll(account)) {
resultChanges.add(change);
}
}
return resultChanges;
}
/**
* @return a set of all the changes reviewed by userName. This method tries to
* find userName in 1) the ssh user names, 2) the full names and the
* email addresses. The returned changes are unique and sorted by time
* stamp, newer first.
*/
private Set<Change.Id> changesReviewedBy(final ReviewDb db,
final String userName) throws OrmException {
final Set<Change.Id> resultChanges = new HashSet<Change.Id>();
for (Account.Id account : getAccountSources(db, userName)) {
for (PatchSetApproval a : db.patchSetApprovals().openByUser(account)) {
resultChanges.add(a.getPatchSetId().getParentKey());
}
for (PatchSetApproval a : db.patchSetApprovals().closedByUserAll(account)) {
resultChanges.add(a.getPatchSetId().getParentKey());
}
}
return resultChanges;
}
/**
* @return a set of all the changes referencing tracking id. This method find
* all changes with a reference to the given external tracking id.
* The returned changes are unique and sorted by time stamp, newer first.
*/
private Set<Change.Id> changesReferencingTr(final ReviewDb db,
final String trackingId) throws OrmException {
final Set<Change.Id> resultChanges = new HashSet<Change.Id>();
for (final TrackingId tr : db.trackingIds().byTrackingId(
new TrackingId.Id(trackingId))) {
resultChanges.add(tr.getChangeId());
}
return resultChanges;
}
private abstract class QueryNext implements Action<SingleListChangeInfo> {
protected final String pos;
protected final int limit;
protected final int slim;
QueryNext(final int pageSize, final String pos) {
this.pos = pos;
this.limit = safePageSize(pageSize);
this.slim = limit + 1;
}
public SingleListChangeInfo run(final ReviewDb db) throws OrmException {
final AccountInfoCacheFactory ac = accountInfoCacheFactory.create();
final SingleListChangeInfo d = new SingleListChangeInfo();
final Set<Change.Id> starred = currentUser.get().getStarredChanges();
boolean results = true;
String sortKey = pos;
final ArrayList<ChangeInfo> list = new ArrayList<ChangeInfo>();
while (results && list.size() < slim) {
results = false;
final ResultSet<Change> rs = query(db, slim, sortKey);
for (final Change c : rs) {
results = true;
if (canRead(c)) {
final ChangeInfo ci = new ChangeInfo(c);
ac.want(ci.getOwner());
ci.setStarred(starred.contains(ci.getId()));
list.add(ci);
if (list.size() == slim) {
rs.close();
break;
}
}
sortKey = c.getSortKey();
}
}
final boolean atEnd = finish(list);
d.setChanges(list, atEnd);
d.setAccounts(ac.create());
return d;
}
boolean finish(final ArrayList<ChangeInfo> list) {
final boolean atEnd = list.size() <= limit;
if (list.size() == slim) {
list.remove(limit);
}
return atEnd;
}
abstract ResultSet<Change> query(final ReviewDb db, final int slim,
String sortKey) throws OrmException;
}
private abstract class QueryPrev extends QueryNext {
QueryPrev(int pageSize, String pos) {
super(pageSize, pos);
}
@Override
boolean finish(final ArrayList<ChangeInfo> list) {
final boolean atEnd = super.finish(list);
Collections.reverse(list);
return atEnd;
}
}
}