// Copyright (C) 2013 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.index; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Function; import com.google.common.base.Objects; import com.google.common.collect.ImmutableList; import com.google.common.collect.Iterables; import com.google.common.collect.Lists; 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.Paginated; import com.google.gerrit.server.query.change.SortKeyPredicate; import com.google.gwtorm.server.OrmException; import com.google.gwtorm.server.ResultSet; import java.util.Collection; import java.util.Iterator; import java.util.List; /** * Wrapper combining an {@link IndexPredicate} together with a * {@link ChangeDataSource} that returns matching results from the index. * <p> * Appropriate to return as the rootmost predicate that can be processed using * the secondary index; such predicates must also implement * {@link ChangeDataSource} to be chosen by the query processor. */ public class IndexedChangeQuery extends Predicate<ChangeData> implements ChangeDataSource, Paginated { /** * Replace all {@link SortKeyPredicate}s in a tree. * <p> * Strictly speaking this should replace only the {@link SortKeyPredicate} at * the top-level AND node, but this implementation is simpler, and the * behavior of having multiple sortkey operators is undefined anyway. * * @param p predicate to replace in. * @param newValue new cut value to replace all sortkey operators with. * @return a copy of {@code p} with all sortkey predicates replaced; or p * itself. */ @VisibleForTesting static Predicate<ChangeData> replaceSortKeyPredicates( Predicate<ChangeData> p, String newValue) { if (p instanceof SortKeyPredicate) { return ((SortKeyPredicate) p).copy(newValue); } else if (p.getChildCount() > 0) { List<Predicate<ChangeData>> newChildren = Lists.newArrayListWithCapacity(p.getChildCount()); boolean replaced = false; for (Predicate<ChangeData> c : p.getChildren()) { Predicate<ChangeData> nc = replaceSortKeyPredicates(c, newValue); newChildren.add(nc); if (nc != c) { replaced = true; } } return replaced ? p.copy(newChildren) : p; } else { return p; } } private final ChangeIndex index; private final int limit; private Predicate<ChangeData> pred; private ChangeDataSource source; public IndexedChangeQuery(ChangeIndex index, Predicate<ChangeData> pred, int limit) throws QueryParseException { this.index = index; this.limit = limit; this.pred = pred; this.source = index.getSource(pred, 0, limit); } @Override public int getChildCount() { return 1; } @Override public Predicate<ChangeData> getChild(int i) { if (i == 0) { return pred; } throw new ArrayIndexOutOfBoundsException(i); } @Override public List<Predicate<ChangeData>> getChildren() { return ImmutableList.of(pred); } @Override public int limit() { return limit; } @Override public int getCardinality() { return source != null ? source.getCardinality() : limit(); } @Override public boolean hasChange() { return index.getSchema().getFields() .containsKey(ChangeField.CHANGE.getName()); } @Override public ResultSet<ChangeData> read() throws OrmException { final ChangeDataSource currSource = source; final ResultSet<ChangeData> rs = currSource.read(); return new ResultSet<ChangeData>() { @Override public Iterator<ChangeData> iterator() { return Iterables.transform( rs, new Function<ChangeData, ChangeData>() { @Override public ChangeData apply(ChangeData input) { input.cacheFromSource(currSource); return input; } }).iterator(); } @Override public List<ChangeData> toList() { List<ChangeData> r = rs.toList(); for (ChangeData cd : r) { cd.cacheFromSource(currSource); } return r; } @Override public void close() { rs.close(); } }; } @Override public ResultSet<ChangeData> restart(ChangeData last) throws OrmException { pred = replaceSortKeyPredicates(pred, last.change().getSortKey()); try { source = index.getSource(pred, 0, limit); } catch (QueryParseException e) { // Don't need to show this exception to the user; the only thing that // changed about pred was its SortKeyPredicates, and any other QPEs // that might happen should have already thrown from the constructor. throw new OrmException(e); } return read(); } @Override public ResultSet<ChangeData> restart(int start) throws OrmException { try { source = index.getSource(pred, start, limit); } catch (QueryParseException e) { // Don't need to show this exception to the user; the only thing that // changed about pred was its SortKeyPredicates, and any other QPEs // that might happen should have already thrown from the constructor. throw new OrmException(e); } return read(); } @Override public Predicate<ChangeData> copy( Collection<? extends Predicate<ChangeData>> children) { return this; } @Override public boolean match(ChangeData cd) throws OrmException { return (source != null && cd.isFromSource(source)) || pred.match(cd); } @Override public int getCost() { // Index queries are assumed to be cheaper than any other type of query, so // so try to make sure they get picked. Note that pred's cost may be higher // because it doesn't know whether it's being used in an index query or not. return 1; } @Override public int hashCode() { return pred.hashCode(); } @Override public boolean equals(Object other) { if (other == null || getClass() != other.getClass()) { return false; } IndexedChangeQuery o = (IndexedChangeQuery) other; return pred.equals(o.pred) && limit == o.limit; } @Override public String toString() { return Objects.toStringHelper("index") .add("p", pred) .add("limit", limit) .toString(); } }