// Copyright (C) 2012 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.common.collect.Lists; import com.google.gerrit.extensions.common.ListChangesOption; import com.google.gerrit.extensions.restapi.AuthException; import com.google.gerrit.extensions.restapi.BadRequestException; import com.google.gerrit.extensions.restapi.RestReadView; import com.google.gerrit.extensions.restapi.TopLevelResource; import com.google.gerrit.server.CurrentUser; import com.google.gerrit.server.IdentifiedUser; import com.google.gerrit.server.change.ChangeJson; import com.google.gerrit.server.change.ChangeJson.ChangeInfo; import com.google.gerrit.server.query.QueryParseException; import com.google.gwtorm.server.OrmException; import com.google.inject.Inject; import com.google.inject.Provider; import org.kohsuke.args4j.Option; import java.util.BitSet; import java.util.Collections; import java.util.EnumSet; import java.util.List; import java.util.regex.Matcher; import java.util.regex.Pattern; public class QueryChanges implements RestReadView<TopLevelResource> { private final ChangeJson json; private final QueryProcessor imp; private final Provider<CurrentUser> user; private boolean reverse; private EnumSet<ListChangesOption> options; @Option(name = "--query", aliases = {"-q"}, metaVar = "QUERY", usage = "Query string") private List<String> queries; @Option(name = "--limit", aliases = {"-n"}, metaVar = "CNT", usage = "Maximum number of results to return") public void setLimit(int limit) { imp.setLimit(limit); } @Option(name = "-o", usage = "Output options per change") public void addOption(ListChangesOption o) { options.add(o); } @Option(name = "-O", usage = "Output option flags, in hex") void setOptionFlagsHex(String hex) { options.addAll(ListChangesOption.fromBits(Integer.parseInt(hex, 16))); } @Option(name = "-P", metaVar = "SORTKEY", usage = "Previous changes before SORTKEY") public void setSortKeyAfter(String key) { // Querying for the prior page of changes requires sortkey_after predicate. // Changes are shown most recent->least recent. The previous page of // results contains changes that were updated after the given key. imp.setSortkeyAfter(key); reverse = true; } @Option(name = "-N", metaVar = "SORTKEY", usage = "Next changes after SORTKEY") public void setSortKeyBefore(String key) { // Querying for the next page of changes requires sortkey_before predicate. // Changes are shown most recent->least recent. The next page contains // changes that were updated before the given key. imp.setSortkeyBefore(key); } @Option(name = "-S", metaVar = "CNT", usage = "Number of changes to skip") public void setStart(int start) { imp.setStart(start); } @Inject QueryChanges(ChangeJson json, QueryProcessor qp, Provider<CurrentUser> user) { this.json = json; this.imp = qp; this.user = user; options = EnumSet.noneOf(ListChangesOption.class); } public void addQuery(String query) { if (queries == null) { queries = Lists.newArrayList(); } queries.add(query); } public String getQuery(int i) { return queries.get(i); } @Override public List<?> apply(TopLevelResource rsrc) throws BadRequestException, AuthException, OrmException { List<List<ChangeInfo>> out; try { out = query(); } catch (QueryParseException e) { // This is a hack to detect an operator that requires authentication. Pattern p = Pattern.compile("^Error in operator (.*:self)$"); Matcher m = p.matcher(e.getMessage()); if (m.matches()) { String op = m.group(1); throw new AuthException("Must be signed-in to use " + op); } throw new BadRequestException(e.getMessage()); } return out.size() == 1 ? out.get(0) : out; } private List<List<ChangeInfo>> query() throws OrmException, QueryParseException { if (imp.isDisabled()) { throw new QueryParseException("query disabled"); } if (queries == null || queries.isEmpty()) { queries = Collections.singletonList("status:open"); } else if (queries.size() > 10) { // Hard-code a default maximum number of queries to prevent // users from submitting too much to the server in a single call. throw new QueryParseException("limit of 10 queries"); } IdentifiedUser self = null; try { if (user.get().isIdentifiedUser()) { self = (IdentifiedUser) user.get(); self.asyncStarredChanges(); } return query0(); } finally { if (self != null) { self.abortStarredChanges(); } } } private List<List<ChangeInfo>> query0() throws OrmException, QueryParseException { int cnt = queries.size(); BitSet more = new BitSet(cnt); List<List<ChangeData>> data = imp.queryChanges(queries); for (int n = 0; n < cnt; n++) { List<ChangeData> changes = data.get(n); if (imp.getLimit() > 0 && changes.size() > imp.getLimit()) { if (reverse) { changes = changes.subList(1, changes.size()); } else { changes = changes.subList(0, imp.getLimit()); } data.set(n, changes); more.set(n, true); } } List<List<ChangeInfo>> res = json.addOptions(options).formatList2(data); for (int n = 0; n < cnt; n++) { List<ChangeInfo> info = res.get(n); if (more.get(n) && !info.isEmpty()) { if (reverse) { info.get(0)._moreChanges = true; } else { info.get(info.size() - 1)._moreChanges = true; } } } return res; } }