package siena.gae;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import siena.ClassInfo;
import siena.Id;
import siena.QueryData;
import siena.QueryFilter;
import siena.QueryFilterSearch;
import siena.QueryFilterSimple;
import siena.QueryJoin;
import siena.QueryOrder;
import siena.QueryOwned;
import siena.SienaException;
import siena.SienaRestrictedApiException;
import siena.Util;
import siena.core.QueryFilterEmbedded;
import siena.core.options.QueryOptionOffset;
import siena.core.options.QueryOptionPage;
import siena.core.options.QueryOptionState;
import com.google.appengine.api.datastore.Entity;
import com.google.appengine.api.datastore.Key;
import com.google.appengine.api.datastore.KeyFactory;
import com.google.appengine.api.datastore.Query.FilterOperator;
import com.google.appengine.api.datastore.Query.SortDirection;
public class GaeQueryUtils {
public static final Map<String, FilterOperator> operators = new HashMap<String, FilterOperator>() {
private static final long serialVersionUID = 1L;
{
put("=", FilterOperator.EQUAL);
put("!=", FilterOperator.NOT_EQUAL);
put("<", FilterOperator.LESS_THAN);
put(">", FilterOperator.GREATER_THAN);
put("<=", FilterOperator.LESS_THAN_OR_EQUAL);
put(">=", FilterOperator.GREATER_THAN_OR_EQUAL);
put(" IN", FilterOperator.IN);
}
};
public static <T> com.google.appengine.api.datastore.Query
addFiltersOrders(
QueryData<T> query,
com.google.appengine.api.datastore.Query q)
{
return addFiltersOrders(query, q, null);
}
public static <T> com.google.appengine.api.datastore.Query
addFiltersOrders(
QueryData<T> query,
com.google.appengine.api.datastore.Query q, Key parentKey)
{
List<QueryFilter> filters = query.getFilters();
for (QueryFilter filter : filters) {
if(QueryFilterSimple.class.isAssignableFrom(filter.getClass())){
QueryFilterSimple qf = (QueryFilterSimple)filter;
Field f = qf.field;
String propertyName = ClassInfo.getColumnNames(f)[0];
Object value = qf.value;
FilterOperator op = operators.get(qf.operator);
// IN and NOT_EQUAL doesn't allow to use cursors
if(op == FilterOperator.IN || op == FilterOperator.NOT_EQUAL){
QueryOptionGaeContext gaeCtx = (QueryOptionGaeContext)query.option(QueryOptionGaeContext.ID);
if(gaeCtx==null){
gaeCtx = new QueryOptionGaeContext();
query.options().put(gaeCtx.type, gaeCtx);
}
gaeCtx.useCursor = false;
query.option(QueryOptionOffset.ID).activate();
}
if (value != null && ClassInfo.isModel(value.getClass())) {
Key key = GaeMappingUtils.getKey(value);
q.addFilter(propertyName, op, key);
} else {
if (ClassInfo.isId(f)) {
Id id = f.getAnnotation(Id.class);
switch(id.value()) {
case NONE:
if(value != null){
if(!Collection.class.isAssignableFrom(value.getClass())){
// long or string goes toString
Key key;
if(parentKey == null){
key = KeyFactory.createKey(
q.getKind(),
value.toString());
}else {
key = KeyFactory.createKey(
parentKey,
q.getKind(),
value.toString());
}
q.addFilter(Entity.KEY_RESERVED_PROPERTY, op, key);
}else {
List<Key> keys = new ArrayList<Key>();
for(Object val: (Collection<?>)value) {
if(parentKey == null){
keys.add(KeyFactory.createKey(q.getKind(), val.toString()));
}else {
keys.add(KeyFactory.createKey(parentKey, q.getKind(), val.toString()));
}
}
q.addFilter(Entity.KEY_RESERVED_PROPERTY, op, keys);
}
}
break;
case AUTO_INCREMENT:
if(value != null){
if(!Collection.class.isAssignableFrom(value.getClass())){
Key key;
Class<?> type = f.getType();
if(Long.TYPE == type || Long.class.isAssignableFrom(type)){
if(parentKey == null){
key = KeyFactory.createKey(
q.getKind(),
(Long)value);
}else {
key = KeyFactory.createKey(
parentKey,
q.getKind(),
(Long)value);
}
} else {
if(parentKey == null){
key = KeyFactory.createKey(
q.getKind(),
value.toString());
}else {
key = KeyFactory.createKey(
parentKey,
q.getKind(),
value.toString());
}
}
q.addFilter(Entity.KEY_RESERVED_PROPERTY, op, key);
}else {
List<Key> keys = new ArrayList<Key>();
for(Object val: (Collection<?>)value) {
if (value instanceof String)
val = Long.parseLong((String) val);
if(parentKey == null){
keys.add(KeyFactory.createKey(q.getKind(), (Long)val));
}else {
keys.add(KeyFactory.createKey(parentKey, q.getKind(), (Long)val));
}
}
q.addFilter(Entity.KEY_RESERVED_PROPERTY, op, keys);
}
}
break;
case UUID:
if(value != null) {
if(!Collection.class.isAssignableFrom(value.getClass())){
// long or string goes toString
Key key;
if(parentKey == null){
key = KeyFactory.createKey(
q.getKind(),
value.toString());
}else {
key = KeyFactory.createKey(
parentKey,
q.getKind(),
value.toString());
}
q.addFilter(Entity.KEY_RESERVED_PROPERTY, op, key);
}else {
List<Key> keys = new ArrayList<Key>();
for(Object val: (Collection<?>)value) {
keys.add(KeyFactory.createKey(q.getKind(), val.toString()));
}
q.addFilter(Entity.KEY_RESERVED_PROPERTY, op, keys);
}
}
break;
default:
throw new SienaException("Id Generator "+id.value()+ " not supported");
}
} else if (Enum.class.isAssignableFrom(f.getType())) {
value = value.toString();
q.addFilter(propertyName, op, value);
} else {
q.addFilter(propertyName, op, value);
}
}
}else if(QueryFilterSearch.class.isAssignableFrom(filter.getClass())){
Class<T> clazz = query.getQueriedClass();
QueryFilterSearch qf = (QueryFilterSearch)filter;
if(qf.fields.length>1)
throw new SienaException("Search not possible for several fields in GAE: only one field");
try {
Field field = Util.getField(clazz, qf.fields[0]);
if(field.isAnnotationPresent(Unindexed.class)){
throw new SienaException("Cannot search the @Unindexed field "+field.getName());
}
// cuts match into words
String[] words = qf.match.split("\\s");
// if several words, then only OR operator represented by IN GAE
Pattern pNormal = Pattern.compile("[^\\*](\\w+)[^\\*]");
if(words.length>1){
for(String word:words){
if(!pNormal.matcher(word).matches()){
throw new SienaException("Cannot do a multiwords search with the * operator");
}
}
List<String> wordList = new ArrayList<String>();
Collections.addAll(wordList, words);
addSearchFilterIn(q, field, wordList);
}else {
// searches for pattern such as "alpha*" or "*alpha" or "alpha"
Pattern pStart = Pattern.compile("(\\w+)\\*");
String word = words[0];
Matcher matcher = pStart.matcher(word);
if(matcher.matches()){
String realWord = matcher.group(1);
addSearchFilterBeginsWith(q, field, realWord);
continue;
}
matcher = pNormal.matcher(word);
if(matcher.matches()){
addSearchFilterEquals(q, field, word);
continue;
}
Pattern pEnd = Pattern.compile("\\*(\\w+)");
matcher = pEnd.matcher(word);
if(matcher.matches()){
throw new SienaException("Cannot do a \"*word\" search in GAE");
}
}
}catch(Exception e){
throw new SienaException(e);
}
break;
}else if(QueryFilterEmbedded.class.isAssignableFrom(filter.getClass())){
QueryFilterEmbedded qf = (QueryFilterEmbedded)filter;
String propName = "";
int sz = qf.fields.size();
for(int i=0; i<sz; i++){
propName += ClassInfo.getSingleColumnName(qf.fields.get(i));
if(i < sz-1){
propName += qf.fieldSeparator;
}
}
Object value = qf.value;
FilterOperator op = operators.get(qf.operator);
// IN and NOT_EQUAL doesn't allow to use cursors
if(op == FilterOperator.IN || op == FilterOperator.NOT_EQUAL){
QueryOptionGaeContext gaeCtx = (QueryOptionGaeContext)query.option(QueryOptionGaeContext.ID);
if(gaeCtx==null){
gaeCtx = new QueryOptionGaeContext();
query.options().put(gaeCtx.type, gaeCtx);
}
gaeCtx.useCursor = false;
query.option(QueryOptionOffset.ID).activate();
}
q.addFilter(propName, op, value);
}
}
// adds filter on owners
List<QueryOwned> ownees = query.getOwnees();
for (QueryOwned ownee : ownees) {
String propertyName = ClassInfo.getSimplestColumnName(ownee.field);
FilterOperator op = operators.get("=");
Key key = GaeMappingUtils.getKey(ownee.owner);
q.addFilter(propertyName, op, key);
}
List<QueryOrder> orders = query.getOrders();
for (QueryOrder order : orders) {
Field f = order.field;
if (ClassInfo.isId(f)) {
q.addSort(Entity.KEY_RESERVED_PROPERTY,
order.ascending ? SortDirection.ASCENDING
: SortDirection.DESCENDING);
} else {
q.addSort(ClassInfo.getColumnNames(f)[0],
order.ascending ? SortDirection.ASCENDING
: SortDirection.DESCENDING);
}
}
return q;
}
public static void addSearchFilterBeginsWith(com.google.appengine.api.datastore.Query q, Field field, String match)
{
String[] columns = ClassInfo.getColumnNames(field);
if(columns.length>1)
throw new SienaException("Search not possible for multi-column fields in GAE: only one field with one column");
q.addFilter(columns[0], FilterOperator.GREATER_THAN_OR_EQUAL, match);
q.addFilter(columns[0], FilterOperator.LESS_THAN, match + "\ufffd");
}
public static void addSearchFilterEquals(com.google.appengine.api.datastore.Query q, Field field, String match)
{
String[] columns = ClassInfo.getColumnNames(field);
if(columns.length>1)
throw new SienaException("Search not possible for multi-column fields in GAE: only one field with one column");
q.addFilter(columns[0], FilterOperator.EQUAL, match);
}
public static void addSearchFilterIn(com.google.appengine.api.datastore.Query q, Field field, List<String> matches)
{
String[] columns = ClassInfo.getColumnNames(field);
if(columns.length>1)
throw new SienaException("Search not possible for multi-column fields in GAE: only one field with one column");
q.addFilter(columns[0], FilterOperator.IN, matches);
}
public static <T> Map<Field, ArrayList<Key>> buildJoinFieldKeysMap(QueryData<T> query){
List<QueryJoin> joins = query.getJoins();
// join queries
Map<Field, ArrayList<Key>> fieldMap = new HashMap<Field, ArrayList<Key>>();
for (QueryJoin join : joins) {
Field field = join.field;
if (!ClassInfo.isModel(field.getType())){
throw new SienaRestrictedApiException(GaePersistenceManager.DB, "join", "Join not possible: Field "+field.getName()+" is not a relation field");
}
else if(join.sortFields!=null && join.sortFields.length!=0)
throw new SienaRestrictedApiException(GaePersistenceManager.DB, "join", "Join not allowed with sort fields");
fieldMap.put(field, new ArrayList<Key>());
}
// join annotations
for(Field field:
ClassInfo.getClassInfo(query.getQueriedClass()).joinFields)
{
fieldMap.put(field, new ArrayList<Key>());
}
return fieldMap;
}
public static <T> Map<Field, ArrayList<Key>> buildJoinFieldKeysMap(T model){
// join queries
Map<Field, ArrayList<Key>> fieldMap = new HashMap<Field, ArrayList<Key>>();
// join annotations
for(Field field:
ClassInfo.getClassInfo(model.getClass()).joinFields)
{
fieldMap.put(field, new ArrayList<Key>());
}
return fieldMap;
}
public static <T> void paginate(QueryData<T> query) {
QueryOptionGaeContext gaeCtx = (QueryOptionGaeContext)query.option(QueryOptionGaeContext.ID);
QueryOptionState state = (QueryOptionState)query.option(QueryOptionState.ID);
if(gaeCtx==null){
gaeCtx = new QueryOptionGaeContext();
query.options().put(gaeCtx.type, gaeCtx);
}
// resets the realoffset to 0 if stateless
if(state.isStateless()){
gaeCtx.realOffset = 0;
}
}
public static <T> void nextPage(QueryData<T> query) {
QueryOptionPage pag = (QueryOptionPage)query.option(QueryOptionPage.ID);
QueryOptionState state = (QueryOptionState)query.option(QueryOptionState.ID);
QueryOptionGaeContext gaeCtx = (QueryOptionGaeContext)query.option(QueryOptionGaeContext.ID);
if(gaeCtx==null){
gaeCtx = new QueryOptionGaeContext();
query.options().put(gaeCtx.type, gaeCtx);
}
// if no more data after, doesn't try to go after
if(gaeCtx.noMoreDataAfter){
return;
}
// if no more data before, removes flag to be able and stay there
if(gaeCtx.noMoreDataBefore){
gaeCtx.noMoreDataBefore = false;
return;
}
if(pag.isPaginating()){
gaeCtx.realPageSize = pag.pageSize;
if(state.isStateless()) {
//QueryOptionOffset offset = (QueryOptionOffset)query.option(QueryOptionOffset.ID);
//if(offset.isActive()){
gaeCtx.realOffset+=pag.pageSize;
//}
}
else {
if(!gaeCtx.isActive()){
QueryOptionOffset offset = (QueryOptionOffset)query.option(QueryOptionOffset.ID);
if(!gaeCtx.useCursor){
// then uses offset (in case of IN or != operators)
//if(offset.isActive()){
gaeCtx.realOffset+=pag.pageSize;
//}
}
// if the cursor is used, just passivates the offset
else {
offset.passivate();
// keeps track of the offset anyway
gaeCtx.realOffset+=pag.pageSize;
}
}else {
QueryOptionOffset offset = (QueryOptionOffset)query.option(QueryOptionOffset.ID);
// if there is a next cursor, we prefer using it because it can mean it was a
// cursor added by a previousPage call which tries to go backward the first page
if(!gaeCtx.useCursor && !gaeCtx.hasNextCursor()){
// then uses offset (in case of IN or != operators)
//if(offset.isActive()){
gaeCtx.realOffset+=pag.pageSize;
//}
}else{
// forces cursor to be sure it is used
gaeCtx.useCursor = true;
String cursor = gaeCtx.nextCursor();
// if the cursor is null, it means we are back to the first page so we reactivate the offset
gaeCtx.realOffset+=pag.pageSize;
if(cursor==null){
offset.activate();
}else {
offset.passivate();
}
}
}
}
}else {
// throws exception because it's impossible to reuse nextPage when paginating has been interrupted, the cases are too many
throw new SienaException("Can't use nextPage after pagination has been interrupted...");
}
}
public static <T> void previousPage(QueryData<T> query) {
QueryOptionPage pag = (QueryOptionPage)query.option(QueryOptionPage.ID);
QueryOptionState state = (QueryOptionState)query.option(QueryOptionState.ID);
QueryOptionGaeContext gaeCtx = (QueryOptionGaeContext)query.option(QueryOptionGaeContext.ID);
if(gaeCtx==null){
gaeCtx = new QueryOptionGaeContext();
query.options().put(gaeCtx.type, gaeCtx);
}
// if no more data before, doesn't try to go before
if(gaeCtx.noMoreDataBefore){
return;
}
// if no more data after, removes flag to be able to go before
if(gaeCtx.noMoreDataAfter){
gaeCtx.noMoreDataAfter = false;
}
if(pag.isPaginating()){
gaeCtx.realPageSize = pag.pageSize;
if(state.isStateless()) {
//QueryOptionOffset offset = (QueryOptionOffset)query.option(QueryOptionOffset.ID);
//if(offset.isActive()){
if(gaeCtx.realOffset>=pag.pageSize) {
gaeCtx.realOffset-=pag.pageSize;
}
else {
gaeCtx.realOffset = 0;
gaeCtx.noMoreDataBefore = true;
}
//}
}
else {
if(!gaeCtx.isActive()){
if(!gaeCtx.useCursor){
// then uses offset (in case of IN or != operators)
//QueryOptionOffset offset = (QueryOptionOffset)query.option(QueryOptionOffset.ID);
//if(offset.isActive()){
if(gaeCtx.realOffset>=pag.pageSize) {
gaeCtx.realOffset-=pag.pageSize;
}
else {
gaeCtx.realOffset = 0;
gaeCtx.noMoreDataBefore = true;
}
//}
}
// if the cursor is active, verifies this is not the first page
// with the offset (active or passive) and sets noMoreData in this case
else {
//QueryOptionOffset offset = (QueryOptionOffset)query.option(QueryOptionOffset.ID);
if(gaeCtx.realOffset==0) {
gaeCtx.noMoreDataBefore = true;
}
}
}else {
QueryOptionOffset offset = (QueryOptionOffset)query.option(QueryOptionOffset.ID);
if(!gaeCtx.useCursor){
// then uses offset (in case of IN or != operators)
//if(offset.isActive()){
if(gaeCtx.realOffset>=pag.pageSize) {
gaeCtx.realOffset-=pag.pageSize;
}
// passivates offset and computes the page before because we are at the first page
else{
offset.passivate();
gaeCtx.noMoreDataBefore = true;
previousPage(query);
}
//}
}else{
String cursor = gaeCtx.previousCursor();
// if the cursor is null, it means we are back to the first page
// so we reactivate the offset and deactivate the useCursor
// and recall the previousPage with the offset mechanism
if(cursor==null){
offset.activate();
gaeCtx.useCursor = false;
previousPage(query);
}else {
offset.passivate();
gaeCtx.useCursor = true;
if(gaeCtx.realOffset>=pag.pageSize) {
gaeCtx.realOffset-=pag.pageSize;
}
// passivates offset and computes the page before because we are at the first page
else{
gaeCtx.noMoreDataBefore = true;
previousPage(query);
}
//previousPage(query);
}
}
}
}
} else {
// throws exception because it's impossible to reuse nextPage when paginating has been interrupted, the cases are too many
throw new SienaException("Can't use nextPage after pagination has been interrupted...");
}
}
public static <T> void release(QueryData<T> query) {
QueryOptionGaeContext gaeCtx = (QueryOptionGaeContext)query.option(QueryOptionGaeContext.ID);
if(gaeCtx != null){
gaeCtx.cursors.clear();
gaeCtx.passivate();
}
}
/*
public static <T> List<T> mapKeysOnly(QueryData<T> query, Iterable<Entity> entities) {
Class<?> clazz = query.getQueriedClass();
@SuppressWarnings("unchecked")
List<T> result = (List<T>) GaeMappingUtils.mapEntitiesKeysOnly(entities, clazz);
return result;
}
public static <T> List<T> mapKeysOnly(QueryData<T> query, QueryResultList<Entity> entities) {
Class<?> clazz = query.getQueriedClass();
@SuppressWarnings("unchecked")
List<T> result = (List<T>) GaeMappingUtils.mapEntitiesKeysOnly(entities, clazz);
return result;
}
*/
}