package org.karmaexchange.util;
import static org.karmaexchange.util.OfyService.ofy;
import java.util.Collection;
import java.util.List;
import javax.annotation.Nullable;
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.core.UriInfo;
import org.karmaexchange.dao.BaseDao;
import org.karmaexchange.resources.msg.ErrorResponseMsg;
import org.karmaexchange.resources.msg.ErrorResponseMsg.ErrorInfo;
import org.karmaexchange.resources.msg.ListResponseMsg.PagingInfo;
import com.google.appengine.api.datastore.Cursor;
import com.google.appengine.api.datastore.QueryResultIterator;
import com.google.common.collect.Iterators;
import com.google.common.collect.Lists;
import com.googlecode.objectify.Key;
import com.googlecode.objectify.cmd.Query;
import lombok.AccessLevel;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.RequiredArgsConstructor;
import lombok.ToString;
@Data
@RequiredArgsConstructor(access = AccessLevel.PRIVATE)
public class PaginatedQuery<T extends BaseDao<T>> {
private final Class<T> resourceClass;
@Nullable
private final Cursor afterCursor;
private final Collection<QueryClause> queryClauses;
@Nullable
private final UriInfo uriInfo;
private final int limit;
public Result<T> execute() {
QueryResultIterator<T> queryIter = getOfyQuery().iterator();
List<T> searchResults = Lists.newArrayList(Iterators.limit(queryIter, limit));
Cursor nextCursor = queryIter.getCursor();
PagingInfo pagingInfo =
(uriInfo == null) ?
null :
PagingInfo.create(nextCursor, limit, queryIter.hasNext(), uriInfo);
return new Result<T>(searchResults, nextCursor, pagingInfo, this);
}
private Query<T> getOfyQuery() {
Query<T> query = ofy().load().type(resourceClass);
for (QueryClause queryClause : queryClauses) {
query = queryClause.apply(query);
}
if (afterCursor != null) {
query = query.startAt(afterCursor);
}
return query;
}
@Data
public static class Result<T extends BaseDao<T>> {
private final List<T> searchResults;
@Nullable
private final Cursor nextCursor;
@Nullable
private final PagingInfo pagingInfo;
private final PaginatedQuery<T> query;
public boolean hasMoreResults() {
return (pagingInfo != null) && (pagingInfo.getNext() != null);
}
public Result<T> fetchNextBatch() {
return new PaginatedQuery<T>(query, nextCursor).execute();
}
}
private PaginatedQuery(PaginatedQuery<T> prevQuery, Cursor afterCursor) {
resourceClass = prevQuery.resourceClass;
this.afterCursor = afterCursor;
queryClauses = prevQuery.queryClauses;
uriInfo = prevQuery.uriInfo;
limit = prevQuery.limit;
}
@Data
public static class Builder<T extends BaseDao<T>> {
private final Class<T> resourceClass;
private List<FilterQueryClause> queryFilters = Lists.newArrayList();
@Nullable
private Key<?> ancestor;
@Nullable
private String order;
private final int limit;
@Nullable
private final Cursor afterCursor;
@Nullable
private final UriInfo uriInfo;
public static <T extends BaseDao<T>> Builder<T> create(Class<T> resourceClass,
UriInfo uriInfo, int defaultLimit) {
return new Builder<T>(resourceClass, uriInfo, null, defaultLimit);
}
public static <T extends BaseDao<T>> Builder<T> create(Class<T> resourceClass,
@Nullable UriInfo uriInfo, @Nullable MultivaluedMap<String, String> queryParams,
int defaultLimit) {
return new Builder<T>(resourceClass, uriInfo, queryParams, defaultLimit);
}
private Builder(Class<T> resourceClass, @Nullable UriInfo uriInfo,
@Nullable MultivaluedMap<String, String> queryParams, int defaultLimit) {
if (uriInfo != null) {
queryParams = uriInfo.getQueryParameters();
}
this.resourceClass = resourceClass;
this.uriInfo = uriInfo;
String afterCursorStr = queryParams.getFirst(PagingInfo.AFTER_CURSOR_PARAM);
if (afterCursorStr != null) {
afterCursor = Cursor.fromWebSafeString(afterCursorStr);
} else {
afterCursor = null;
}
limit = queryParams.containsKey(PagingInfo.LIMIT_PARAM) ?
Integer.valueOf(queryParams.getFirst(PagingInfo.LIMIT_PARAM)) : defaultLimit;
if (limit <= 0) {
throw ErrorResponseMsg.createException("limit must be greater than zero",
ErrorInfo.Type.BAD_REQUEST);
}
}
public Builder<T> addFilter(FilterQueryClause filter) {
queryFilters.add(filter);
return this;
}
public Builder<T> addFilters(Collection<? extends FilterQueryClause> filters) {
queryFilters.addAll(filters);
return this;
}
public Builder<T> setAncestor(Key<?> ancestor) {
this.ancestor = ancestor;
return this;
}
public Builder<T> setOrder(String order) {
this.order = order;
return this;
}
public PaginatedQuery<T> build() {
List<QueryClause> queryClauses = Lists.newArrayList();
queryClauses.addAll(queryFilters);
if (ancestor != null) {
queryClauses.add(new AncestorQueryClause(ancestor));
}
if (order != null) {
queryClauses.add(new OrderQueryClause(order));
}
// Request for one more than the limit to determine if there are results after the limit.
queryClauses.add(new LimitQueryClause(limit + 1));
return new PaginatedQuery<T>(resourceClass, afterCursor, queryClauses, uriInfo, limit);
}
}
@Data
public static abstract class QueryClause {
public abstract <T> Query<T> apply(Query<T> query);
}
public static abstract class FilterQueryClause extends QueryClause {
}
@Data
@EqualsAndHashCode(callSuper=true)
@ToString(callSuper=true)
public static class ConditionFilter extends FilterQueryClause {
private final String condition;
private final Object[] values;
public ConditionFilter(String condition, Object... values) {
this.condition = condition;
this.values = values;
}
@Override
public <T> Query<T> apply(Query<T> query) {
for (Object value : values) {
query = query.filter(condition, value);
}
return query;
}
}
@Data
@EqualsAndHashCode(callSuper=true)
@ToString(callSuper=true)
public static class StartsWithFilter extends FilterQueryClause {
private final String fieldName;
private final String prefix;
@Override
public <T> Query<T> apply(Query<T> query) {
return query.filter(fieldName + " >=", prefix)
.filter(fieldName + " <", prefix + "\uFFFD");
}
}
@Data
@EqualsAndHashCode(callSuper=true)
@ToString(callSuper=true)
private static class AncestorQueryClause extends QueryClause {
private final Key<?> ancestor;
@Override
public <T> Query<T> apply(Query<T> query) {
return query.ancestor(ancestor);
}
}
@Data
@EqualsAndHashCode(callSuper=true)
@ToString(callSuper=true)
private static class OrderQueryClause extends QueryClause {
private final String order;
@Override
public <T> Query<T> apply(Query<T> query) {
return query.order(order);
}
}
@Data
@EqualsAndHashCode(callSuper=true)
@ToString(callSuper=true)
private static class LimitQueryClause extends QueryClause {
private final int limit;
@Override
public <T> Query<T> apply(Query<T> query) {
return query.limit(limit);
}
}
}