| // 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 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.InvalidQueryException; |
| import com.google.gerrit.common.errors.NoSuchEntityException; |
| import com.google.gerrit.reviewdb.Account; |
| import com.google.gerrit.reviewdb.Change; |
| import com.google.gerrit.reviewdb.ChangeAccess; |
| import com.google.gerrit.reviewdb.PatchSetApproval; |
| import com.google.gerrit.reviewdb.ReviewDb; |
| import com.google.gerrit.reviewdb.StarredChange; |
| 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.gerrit.server.query.Predicate; |
| import com.google.gerrit.server.query.QueryParseException; |
| import com.google.gerrit.server.query.change.ChangeData; |
| import com.google.gerrit.server.query.change.ChangeDataSource; |
| import com.google.gerrit.server.query.change.ChangeQueryBuilder; |
| import com.google.gerrit.server.query.change.ChangeQueryRewriter; |
| 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; |
| |
| private final ChangeQueryBuilder.Factory queryBuilder; |
| private final Provider<ChangeQueryRewriter> queryRewriter; |
| |
| @Inject |
| ChangeListServiceImpl(final Provider<ReviewDb> schema, |
| final Provider<CurrentUser> currentUser, |
| final ChangeControl.Factory changeControlFactory, |
| final AccountInfoCacheFactory.Factory accountInfoCacheFactory, |
| final ChangeQueryBuilder.Factory queryBuilder, |
| final Provider<ChangeQueryRewriter> queryRewriter) { |
| super(schema, currentUser); |
| this.currentUser = currentUser; |
| this.changeControlFactory = changeControlFactory; |
| this.accountInfoCacheFactory = accountInfoCacheFactory; |
| this.queryBuilder = queryBuilder; |
| this.queryRewriter = queryRewriter; |
| } |
| |
| private boolean canRead(final Change c) { |
| try { |
| return changeControlFactory.controlFor(c).isVisible(); |
| } catch (NoSuchChangeException e) { |
| return false; |
| } |
| } |
| |
| @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, InvalidQueryException { |
| 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, InvalidQueryException { |
| return searchQuery(db, query, lim, key, QUERY_NEXT); |
| } |
| }); |
| } |
| |
| @SuppressWarnings("unchecked") |
| private ResultSet<Change> searchQuery(final ReviewDb db, String query, |
| final int limit, final String key, final Comparator<Change> cmp) |
| throws OrmException, InvalidQueryException { |
| try { |
| final ChangeQueryBuilder builder = queryBuilder.create(currentUser.get()); |
| final Predicate<ChangeData> visibleToMe = builder.is_visible(); |
| Predicate<ChangeData> q = builder.parse(query); |
| q = Predicate.and(q, // |
| cmp == QUERY_PREV // |
| ? builder.sortkey_after(key) // |
| : builder.sortkey_before(key), // |
| builder.limit(limit), // |
| visibleToMe // |
| ); |
| |
| ChangeQueryRewriter rewriter = queryRewriter.get(); |
| Predicate<ChangeData> s = rewriter.rewrite(q); |
| if (!(s instanceof ChangeDataSource)) { |
| s = rewriter.rewrite(Predicate.and(builder.status_open(), q)); |
| } |
| |
| if (s instanceof ChangeDataSource) { |
| ArrayList<Change> r = new ArrayList<Change>(); |
| HashSet<Change.Id> want = new HashSet<Change.Id>(); |
| for (ChangeData d : ((ChangeDataSource) s).read()) { |
| if (d.hasChange()) { |
| // Checking visibleToMe here should be unnecessary, the |
| // query should have already performed it. But we don't |
| // want to trust the query rewriter that much yet. |
| // |
| if (visibleToMe.match(d)) { |
| r.add(d.getChange()); |
| } |
| } else { |
| want.add(d.getId()); |
| } |
| } |
| |
| // Here we have to check canRead. Its impossible to |
| // do that test without the change object, and it being |
| // missing above means we have to compute it ourselves. |
| // |
| if (!want.isEmpty()) { |
| for (Change c : db.changes().get(want)) { |
| if (canRead(c)) { |
| r.add(c); |
| } |
| } |
| } |
| |
| Collections.sort(r, cmp); |
| return new ListResultSet<Change>(r); |
| } else { |
| throw new InvalidQueryException("Not Supported", s.toString()); |
| } |
| } catch (QueryParseException e) { |
| throw new InvalidQueryException(e.getMessage(), query); |
| } |
| } |
| |
| 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 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 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, |
| InvalidQueryException { |
| final AccountInfoCacheFactory ac = accountInfoCacheFactory.create(); |
| final SingleListChangeInfo d = new SingleListChangeInfo(); |
| final Set<Change.Id> starred = currentUser.get().getStarredChanges(); |
| |
| final ArrayList<ChangeInfo> list = new ArrayList<ChangeInfo>(); |
| final ResultSet<Change> rs = query(db, slim, pos); |
| for (final Change c : rs) { |
| 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; |
| } |
| } |
| |
| 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, InvalidQueryException; |
| } |
| |
| 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; |
| } |
| } |
| } |