package com.googlecode.objectify.impl;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Comparator;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import com.google.appengine.api.datastore.Cursor;
import com.google.appengine.api.datastore.Entity;
import com.google.appengine.api.datastore.FetchOptions;
import com.google.appengine.api.datastore.KeyFactory;
import com.google.appengine.api.datastore.PreparedQuery;
import com.google.appengine.api.datastore.QueryResultIterable;
import com.google.appengine.api.datastore.QueryResultIterator;
import com.google.appengine.api.datastore.Query.FilterOperator;
import com.google.appengine.api.datastore.Query.FilterPredicate;
import com.google.appengine.api.datastore.Query.SortDirection;
import com.google.appengine.api.datastore.Query.SortPredicate;
import com.googlecode.objectify.Key;
import com.googlecode.objectify.Objectify;
import com.googlecode.objectify.ObjectifyFactory;
import com.googlecode.objectify.Query;
import com.googlecode.objectify.helper.TranslatingQueryResultIterator;
/**
* Implementation of Query.
*
* @author Jeff Schnitzer <jeff@infohazard.org>
*/
public class QueryImpl<T> implements Query<T>
{
/** */
ObjectifyFactory factory;
Objectify ofy;
/** We need to track this because it enables the ability to filter/sort by id */
Class<T> classRestriction;
/** The actual datastore query constructed by this object */
com.google.appengine.api.datastore.Query actual;
/** */
int limit;
int offset;
Cursor cursor;
/** */
public QueryImpl(ObjectifyFactory fact, Objectify objectify)
{
this.factory = fact;
this.ofy = objectify;
this.actual = new com.google.appengine.api.datastore.Query();
}
/** */
public QueryImpl(ObjectifyFactory fact, Objectify objectify, Class<T> clazz)
{
this.factory = fact;
this.ofy = objectify;
this.actual = new com.google.appengine.api.datastore.Query(this.factory.getKind(clazz));
this.classRestriction = clazz;
}
/** @return the underlying datastore query object */
protected com.google.appengine.api.datastore.Query getActual()
{
return this.actual;
}
/* (non-Javadoc)
* @see com.googlecode.objectify.Query#filter(java.lang.String, java.lang.Object)
*/
@Override
public Query<T> filter(String condition, Object value)
{
String[] parts = condition.trim().split(" ");
if (parts.length < 1 || parts.length > 2)
throw new IllegalArgumentException("'" + condition + "' is not a legal filter condition");
String prop = parts[0].trim();
FilterOperator op = (parts.length == 2) ? this.translate(parts[1]) : FilterOperator.EQUAL;
// If we have a class restriction, check to see if the property is the @Id
if (this.classRestriction != null)
{
EntityMetadata<?> meta = this.factory.getMetadata(this.classRestriction);
if (meta.isIdField(prop) || meta.isNameField(prop))
{
if (meta.hasParentField())
throw new IllegalStateException("Cannot (yet) filter by @Id fields on entities which have @Parent fields. Tried '" + prop + "' on " + this.classRestriction.getName() + ".");
boolean isNumericId = meta.isIdField(prop);
if (op == FilterOperator.IN)
{
if (!(value instanceof Iterable<?> || value instanceof Object[]))
throw new IllegalStateException("IN operator requires a collection value. Value was " + value);
if (value instanceof Object[])
value = Arrays.asList(((Object[])value));
// This is a bit complicated - we need to make a list of vanilla datastore Key objects.
List<Object> keys = (value instanceof Collection<?>)
? new ArrayList<Object>(((Collection<?>)value).size())
: new ArrayList<Object>();
for (Object obj: (Iterable<?>)value)
{
if (isNumericId)
keys.add(KeyFactory.createKey(meta.getKind(), ((Number)obj).longValue()));
else
keys.add(KeyFactory.createKey(meta.getKind(), obj.toString()));
}
value = keys;
}
else
{
if (isNumericId)
value = KeyFactory.createKey(meta.getKind(), ((Number)value).longValue());
else
value = KeyFactory.createKey(meta.getKind(), value.toString());
}
prop = "__key__";
}
}
// Convert to something filterable, possibly extracting/converting keys
value = this.factory.makeFilterable(value);
this.actual.addFilter(prop, op, value);
return this;
}
/**
* Converts the textual operator (">", "<=", etc) into a FilterOperator.
* Forgiving about the syntax; != and <> are NOT_EQUAL, = and == are EQUAL.
*/
protected FilterOperator translate(String operator)
{
operator = operator.trim();
if (operator.equals("=") || operator.equals("=="))
return FilterOperator.EQUAL;
else if (operator.equals(">"))
return FilterOperator.GREATER_THAN;
else if (operator.equals(">="))
return FilterOperator.GREATER_THAN_OR_EQUAL;
else if (operator.equals("<"))
return FilterOperator.LESS_THAN;
else if (operator.equals("<="))
return FilterOperator.LESS_THAN_OR_EQUAL;
else if (operator.equals("!=") || operator.equals("<>"))
return FilterOperator.NOT_EQUAL;
else if (operator.toLowerCase().equals("in"))
return FilterOperator.IN;
else
throw new IllegalArgumentException("Unknown operator '" + operator + "'");
}
/* (non-Javadoc)
* @see com.googlecode.objectify.Query#order(java.lang.String)
*/
@Override
public Query<T> order(String condition)
{
condition = condition.trim();
SortDirection dir = SortDirection.ASCENDING;
if (condition.startsWith("-"))
{
dir = SortDirection.DESCENDING;
condition = condition.substring(1).trim();
}
// Check for @Id field
if (this.classRestriction != null)
{
EntityMetadata<?> meta = this.factory.getMetadata(this.classRestriction);
if (meta.isIdField(condition) || meta.isNameField(condition))
condition = "__key__";
}
this.actual.addSort(condition, dir);
return this;
}
/* (non-Javadoc)
* @see com.googlecode.objectify.Query#ancestor(java.lang.Object)
*/
@Override
public Query<T> ancestor(Object keyOrEntity)
{
this.actual.setAncestor(this.factory.getRawKey(keyOrEntity));
return this;
}
/* (non-Javadoc)
* @see com.googlecode.objectify.Query#limit(int)
*/
@Override
public Query<T> limit(int value)
{
this.limit = value;
return this;
}
/* (non-Javadoc)
* @see com.googlecode.objectify.Query#offset(int)
*/
@Override
public Query<T> offset(int value)
{
this.offset = value;
return this;
}
/* (non-Javadoc)
* @see com.googlecode.objectify.Query#cursor(com.google.appengine.api.datastore.Cursor)
*/
@Override
public Query<T> cursor(Cursor value)
{
this.cursor = value;
return this;
}
/* (non-Javadoc)
* @see java.lang.Object#toString()
*/
@Override
public String toString()
{
StringBuilder bld = new StringBuilder(this.getClass().getName());
bld.append("{kind=");
bld.append(this.actual.getKind());
bld.append(",ancestor=");
if (this.actual.getAncestor() != null)
bld.append(KeyFactory.keyToString(this.actual.getAncestor()));
// We need to sort filters to make a stable string value
FilterPredicate[] filters = this.actual.getFilterPredicates().toArray(new FilterPredicate[this.actual.getFilterPredicates().size()]);
Arrays.sort(filters, new Comparator<FilterPredicate>() {
@Override
public int compare(FilterPredicate o1, FilterPredicate o2)
{
int result = o1.getPropertyName().compareTo(o2.getPropertyName());
if (result != 0)
return result;
result = o1.getOperator().compareTo(o2.getOperator());
if (result != 0)
return result;
if (o1.getValue() == null)
return o2.getValue() == null ? 0 : -1;
else if (o2.getValue() == null)
return 1;
else
return o1.getValue().toString().compareTo(o2.getValue().toString()); // not perfect, but probably as good as we can do
}
});
for (FilterPredicate filter: filters)
{
bld.append(",filter=");
bld.append(filter.getPropertyName());
bld.append(filter.getOperator().name());
bld.append(filter.getValue());
}
// We need to sort sorts to make a stable string value
SortPredicate[] sorts = this.actual.getSortPredicates().toArray(new SortPredicate[this.actual.getSortPredicates().size()]);
Arrays.sort(sorts, new Comparator<SortPredicate>() {
@Override
public int compare(SortPredicate o1, SortPredicate o2)
{
int result = o1.getPropertyName().compareTo(o2.getPropertyName());
if (result != 0)
return result;
// Actually, it should be impossible to have the same prop with multiple directions
return o1.getDirection().compareTo(o2.getDirection());
}
});
for (SortPredicate sort: this.actual.getSortPredicates())
{
bld.append(",sort=");
bld.append(sort.getPropertyName());
bld.append(sort.getDirection().name());
}
if (this.limit > 0)
bld.append(",limit=").append(this.limit);
if (this.offset > 0)
bld.append(",offset=").append(this.offset);
if (this.cursor != null)
bld.append(",cursor=").append(this.cursor.toWebSafeString());
bld.append('}');
return bld.toString();
}
/* (non-Javadoc)
* @see java.lang.Iterable#iterator()
*/
@Override
public QueryResultIterator<T> iterator()
{
FetchOptions opts = this.fetchOptions();
if (opts == null)
return new ToObjectIterator(this.prepare().asQueryResultIterator());
else
return new ToObjectIterator(this.prepare().asQueryResultIterator(opts));
}
/* (non-Javadoc)
* @see com.googlecode.objectify.Query#get()
*/
@Override
public T get()
{
// The underlying datastore is basically doing this for PreparedQuery.asSingleEntity(),
// so we can do it by faking the limit
int oldLimit = this.limit;
Iterator<T> it = this.iterator();
T result = null;
if (it.hasNext())
result = it.next();
this.limit = oldLimit;
return result;
}
/* (non-Javadoc)
* @see com.googlecode.objectify.Query#getKey()
*/
@Override
public Key<T> getKey()
{
int oldLimit = this.limit;
Iterator<Key<T>> it = this.fetchKeys().iterator();
Key<T> result = null;
if (it.hasNext())
result = it.next();
this.limit = oldLimit;
return result;
}
/* (non-Javadoc)
* @see com.googlecode.objectify.Query#countAll()
*/
@Override
public int countAll()
{
return this.prepare().countEntities();
}
/* (non-Javadoc)
* @see com.googlecode.objectify.Query#fetch()
*/
@Override
public QueryResultIterable<T> fetch()
{
return this;
}
/* (non-Javadoc)
* @see com.googlecode.objectify.Query#fetchKeys()
*/
@Override
public QueryResultIterable<Key<T>> fetchKeys()
{
FetchOptions opts = this.fetchOptions();
if (opts == null)
return new ToKeyIterable(this.prepareKeysOnly().asQueryResultIterable());
else
return new ToKeyIterable(this.prepareKeysOnly().asQueryResultIterable(opts));
}
/* (non-Javadoc)
* @see com.googlecode.objectify.Query#fetchParentKeys()
*/
@Override
public <V> Set<Key<V>> fetchParentKeys()
{
Set<Key<V>> parentKeys = new LinkedHashSet<Key<V>>();
for (Key<T> key: this.fetchKeys())
{
if (key.getParent() == null)
throw new IllegalStateException("Tried to fetch parent from a key that has no parent: " + key);
parentKeys.add(key.<V>getParent());
}
return parentKeys;
}
/* (non-Javadoc)
* @see com.googlecode.objectify.Query#fetchParents()
*/
@Override
public <V> Map<Key<V>, V> fetchParents()
{
Set<Key<V>> parentKeys = this.fetchParentKeys();
return this.ofy.get(parentKeys);
}
/* (non-Javadoc)
* @see com.googlecode.objectify.Query#list()
*/
@Override
public List<T> list()
{
List<T> result = new ArrayList<T>();
for (T obj: this)
result.add(obj);
return result;
}
/* (non-Javadoc)
* @see com.googlecode.objectify.Query#listKeys()
*/
@Override
public List<Key<T>> listKeys()
{
List<Key<T>> result = new ArrayList<Key<T>>();
for (Key<T> key: this.fetchKeys())
result.add(key);
return result;
}
/**
* Create a PreparedQuery relevant to our current state.
*/
private PreparedQuery prepare()
{
return this.ofy.getDatastore().prepare(this.ofy.getTxn(), this.actual);
}
/**
* Create a PreparedQuery that fetches keys only, relevant to our current state.
*/
private PreparedQuery prepareKeysOnly()
{
// Can't modify the query, we might need to use it again
com.google.appengine.api.datastore.Query cloned = this.cloneRawQuery(this.actual);
cloned.setKeysOnly();
return this.ofy.getDatastore().prepare(this.ofy.getTxn(), cloned);
}
/**
* @return a set of fetch options for the current limit and offset, or null if
* there is no limit or offset.
*/
private FetchOptions fetchOptions()
{
FetchOptions opts = null;
if (this.cursor != null)
{
opts = FetchOptions.Builder.withCursor(this.cursor);
}
if (this.limit != 0)
{
if (opts == null)
opts = FetchOptions.Builder.withLimit(this.limit);
else
opts = opts.limit(this.limit);
}
if (this.offset != 0)
{
if (opts == null)
opts = FetchOptions.Builder.withOffset(this.offset);
else
opts = opts.offset(this.offset);
}
return opts;
}
/**
* Make a new Query object that is exactly like the old. Too bad Query isn't Cloneable.
*/
protected com.google.appengine.api.datastore.Query cloneRawQuery(com.google.appengine.api.datastore.Query orig)
{
com.google.appengine.api.datastore.Query copy = new com.google.appengine.api.datastore.Query(orig.getKind(), orig.getAncestor());
for (FilterPredicate filter: orig.getFilterPredicates())
copy.addFilter(filter.getPropertyName(), filter.getOperator(), filter.getValue());
for (SortPredicate sort: orig.getSortPredicates())
copy.addSort(sort.getPropertyName(), sort.getDirection());
// This should be impossible but who knows what might happen in the future
if (orig.isKeysOnly())
copy.setKeysOnly();
return copy;
}
/**
* Iterable that translates from datastore Entity to Keys
*/
protected class ToKeyIterable implements QueryResultIterable<Key<T>>
{
QueryResultIterable<Entity> source;
public ToKeyIterable(QueryResultIterable<Entity> source)
{
this.source = source;
}
@Override
public QueryResultIterator<Key<T>> iterator()
{
return new ToKeyIterator(this.source.iterator());
}
}
/**
* Iterator that translates from datastore Entity to Keys
*/
protected class ToKeyIterator extends TranslatingQueryResultIterator<Entity, Key<T>>
{
public ToKeyIterator(QueryResultIterator<Entity> source)
{
super(source);
}
@Override
@SuppressWarnings("unchecked")
protected Key<T> translate(Entity from)
{
return (Key<T>)factory.rawKeyToTypedKey(from.getKey());
}
}
/**
* Iterator that translates from datastore Entity to typed Objects
*/
protected class ToObjectIterator extends TranslatingQueryResultIterator<Entity, T>
{
public ToObjectIterator(QueryResultIterator<Entity> source)
{
super(source);
}
@Override
protected T translate(Entity from)
{
EntityMetadata<T> meta = factory.getMetadata(from.getKey());
return meta.toObject(from, ofy);
}
}
}