/**
*
*/
package xapi.model.api;
import xapi.collect.X_Collect;
import xapi.collect.api.ClassTo;
import xapi.collect.api.IntTo;
import xapi.model.service.ModelService;
import xapi.source.api.CharIterator;
import static xapi.collect.X_Collect.newList;
import java.lang.reflect.Array;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.List;
/**
* A ModelQuery is a bean describing a query for a given model.
* <p>
* It contains a set of query parameters that describe filters and sorting
* that is requested for a given field.
* <p>
* In the future, subclasses of queries can be generated from indexing annotations
* on the model fields.
*
* @author James X. Nelson (james@wetheinter.net, @james)
*
*/
public class ModelQuery <M extends Model> {
public enum QueryParameterType {
EQUALS, GREATER_THAN, LESS_THAN, CONTAINS
}
public enum SortOrder {
ASCENDING, DESCENDING
}
public static final class SortOption {
public SortOption(final String propertyName, final SortOrder order) {
this.propertyName = propertyName;
this.order = order;
}
/**
* @return -> propertyName
*/
public String getPropertyName() {
return propertyName;
}
/**
* @return -> order
*/
public SortOrder getOrder() {
return order;
}
private final String propertyName;
private final SortOrder order;
}
public static final class QueryParameter {
private QueryParameterType filterType;
private Object filterValue;
private String parameterName;
/**
* @param parameterName
* @param filterType
* @param filterValue
*/
public QueryParameter(final String parameterName, final QueryParameterType filterType, final Object filterValue) {
setParameterName(parameterName);
setFilterType(filterType);
setFilterValue(filterValue);
}
/**
* @return -> filterType
*/
public QueryParameterType getFilterType() {
return filterType;
}
/**
* @param filterType -> set filterType
* @return
*/
public QueryParameter setFilterType(final QueryParameterType filterType) {
this.filterType = filterType;
return this;
}
/**
* @return -> filterValue
*/
public Object getFilterValue() {
return filterValue;
}
/**
* @param filterValue -> set filterValue
* @return
*/
public QueryParameter setFilterValue(final Object filterValue) {
this.filterValue = filterValue;
return this;
}
/**
* @return -> parameterName
*/
public String getParameterName() {
return parameterName;
}
/**
* @param parameterName -> set parameterName
* @return
*/
public QueryParameter setParameterName(final String parameterName) {
this.parameterName = parameterName;
return this;
}
}
private ModelKey ancestor;
private final IntTo<QueryParameter> parameters;
private final IntTo<SortOption> sortOptions;
private int pageSize;
private int limit;
private String cursor;
private String namespace;
@SuppressWarnings("unchecked")
public ModelQuery() {
parameters = newList(QueryParameter.class);
sortOptions = newList(SortOption.class);
pageSize = 50;
limit = 500;
namespace = "";
}
public ModelQuery<M> addSort(final String propertyName, final SortOrder order) {
sortOptions.add(new SortOption(propertyName, order));
return this;
}
public ModelQuery<M> addSortAscending(final String propertyName) {
return addSort(propertyName, SortOrder.ASCENDING);
}
public ModelQuery<M> addSortDescending(final String propertyName) {
return addSort(propertyName, SortOrder.DESCENDING);
}
public ModelQuery<M> addFilter(final String parameterName, final QueryParameterType filterType, final Object filterValue) {
parameters.add(new QueryParameter(parameterName, filterType, filterValue));
return this;
}
public ModelQuery<M> addEqualsFilter(final String parameterName, final Object filterValue) {
return addFilter(parameterName, QueryParameterType.EQUALS, filterValue);
}
public ModelQuery<M> addGreaterThanFilter(final String parameterName, final Object filterValue) {
return addFilter(parameterName, QueryParameterType.GREATER_THAN, filterValue);
}
public ModelQuery<M> addLessThanFilter(final String parameterName, final Object filterValue) {
return addFilter(parameterName, QueryParameterType.LESS_THAN, filterValue);
}
public ModelQuery<M> addContainsFilter(final String parameterName, final Object filterValue) {
return addFilter(parameterName, QueryParameterType.CONTAINS, filterValue);
}
public Iterable<QueryParameter> getParameters() {
return parameters.forEach();
}
public Iterable<SortOption> getSortOptions() {
return sortOptions.forEach();
}
/**
* @return -> pageSize
*/
public int getPageSize() {
return pageSize;
}
public ModelQuery<M> setPageSize(final int pageSize) {
this.pageSize = pageSize;
return this;
}
/**
* @return -> cursor
*/
public String getCursor() {
return cursor;
}
/**
* @param cursor -> set cursor
*/
public void setCursor(final String cursor) {
this.cursor = cursor;
}
/**
* @return -> namespace
*/
public String getNamespace() {
return namespace;
}
/**
* @param namespace -> set namespace
* @return
*/
public ModelQuery<M> setNamespace( final String namespace) {
assert namespace != null : "Namespace cannot be null!";
this.namespace = namespace;
return this;
}
/**
* @return -> limit
*/
public int getLimit() {
return limit;
}
/**
* @param limit -> set limit
* @return
*/
public ModelQuery<M> setLimit(final int limit) {
this.limit = limit;
return this;
}
/**
* @return -> ancestor
*/
public ModelKey getAncestor() {
return ancestor;
}
/**
* @param ancestor -> set ancestor
* @return
*/
public ModelQuery<M> setAncestor(final ModelKey ancestor) {
this.ancestor = ancestor;
return this;
}
/**
* @return
*/
@SuppressWarnings("rawtypes")
public String serialize(final ModelService service, final PrimitiveSerializer primitives) {
final StringBuilder b = new StringBuilder();
if (ancestor == null) {
b.append(primitives.serializeBoolean(false));
} else {
b.append(primitives.serializeBoolean(true));
b.append(primitives.serializeString(service.keyToString(ancestor)));
}
b.append(primitives.serializeInt(pageSize));
b.append(primitives.serializeInt(limit));
b.append(primitives.serializeString(cursor));
b.append(primitives.serializeString(namespace));
b.append(primitives.serializeInt(parameters.size()));
for (final QueryParameter param : parameters.forEach()) {
b.append(primitives.serializeString(param.getParameterName()));
b.append(primitives.serializeInt(param.getFilterType().ordinal()));
final Object value = param.getFilterValue();
if (value == null) {
b.append(primitives.serializeInt(-1));
} else {
if (value.getClass().isArray()) {
final int length = Array.getLength(value);
b.append(primitives.serializeInt(0));
b.append(primitives.serializeInt(typeOf(value.getClass().getComponentType())));
b.append(primitives.serializeInt(Array.getLength(value)));
for (int i = 0; i < length; i++) {
writeFilterValue(b, primitives, Array.get(value, i));
}
} else if (value instanceof Collection) {
final Collection all = (Collection)value;
b.append(primitives.serializeInt(1));
b.append(primitives.serializeInt(all.size()));
for (final Object item : all) {
writeFilterValue(b, primitives, item);
}
} else {
b.append(primitives.serializeInt(2));
writeFilterValue(b, primitives, value);
}
}
}
b.append(primitives.serializeInt(sortOptions.size()));
for (final SortOption sort : sortOptions.forEach()) {
b.append(primitives.serializeString(sort.getPropertyName()));
b.append(primitives.serializeInt(sort.getOrder().ordinal()));
}
return b.toString();
}
private static final int
TYPE_String = 0,
TYPE_Date = 1,
TYPE_int = 2,
TYPE_Integer = 3,
TYPE_long = 4,
TYPE_Long = 5,
TYPE_float = 6,
TYPE_Float = 7,
TYPE_double = 8,
TYPE_Double = 9,
TYPE_boolean = 10,
TYPE_Boolean = 11,
TYPE_byte = 12,
TYPE_Byte = 13,
TYPE_char = 14,
TYPE_Character = 15,
TYPE_short = 16,
TYPE_Short = 17;
private static final ClassTo<Integer> componentsTypes = X_Collect.newClassMap(Integer.class);
static {
componentsTypes.put(String.class, TYPE_String);
componentsTypes.put(Date.class, TYPE_Date);
componentsTypes.put(int.class, TYPE_int);
componentsTypes.put(Integer.class, TYPE_Integer);
componentsTypes.put(long.class, TYPE_long);
componentsTypes.put(Long.class, TYPE_Long);
componentsTypes.put(float.class, TYPE_float);
componentsTypes.put(Float.class, TYPE_Float);
componentsTypes.put(double.class, TYPE_double);
componentsTypes.put(Double.class, TYPE_Double);
componentsTypes.put(boolean.class, TYPE_boolean);
componentsTypes.put(Boolean.class, TYPE_Boolean);
componentsTypes.put(byte.class, TYPE_byte);
componentsTypes.put(Byte.class, TYPE_Byte);
componentsTypes.put(char.class, TYPE_char);
componentsTypes.put(Character.class, TYPE_Character);
componentsTypes.put(short.class, TYPE_short);
componentsTypes.put(Short.class, TYPE_Short);
}
private int typeOf(final Class<?> componentType) {
return componentsTypes.get(componentType);
}
private void writeFilterValue(final StringBuilder b, final PrimitiveSerializer primitives, final Object value) {
b.append(primitives.serializeInt(typeOf(value.getClass())));
if (value instanceof String) {
b.append(primitives.serializeString((String)value));
} else if (value instanceof Long) {
b.append(primitives.serializeLong((Long)value));
} else if (value instanceof Integer) {
b.append(primitives.serializeInt((Integer)value));
} else if (value instanceof Float) {
b.append(primitives.serializeFloat((Float)value));
} else if (value instanceof Double) {
b.append(primitives.serializeDouble((Double)value));
} else if (value instanceof Short) {
b.append(primitives.serializeShort((Short)value));
} else if (value instanceof Character) {
b.append(primitives.serializeChar((Character)value));
} else if (value instanceof Byte) {
b.append(primitives.serializeByte((Byte)value));
} else if (value instanceof Boolean) {
b.append(primitives.serializeBoolean((Boolean)value));
} else if (value instanceof Date) {
b.append(primitives.serializeLong(((Date)value).getTime()));
} else {
throw new UnsupportedOperationException("Unable to filter on objects of type "+value.getClass()+"; bad filter value: "+value);
}
}
public static <M extends Model> ModelQuery<M> deserialize(final ModelService service, final PrimitiveSerializer primitives,
final CharIterator queryString) {
final ModelQuery query = new ModelQuery<>();
final boolean hasAncestor = primitives.deserializeBoolean(queryString);
if (hasAncestor) {
query.setAncestor(service.keyFromString(primitives.deserializeString(queryString)));
}
query.setPageSize(primitives.deserializeInt(queryString));
query.setLimit(primitives.deserializeInt(queryString));
query.setCursor(primitives.deserializeString(queryString));
query.setNamespace(primitives.deserializeString(queryString));
int params = primitives.deserializeInt(queryString);
while (params --> 0) {
final String propertyName = primitives.deserializeString(queryString);
final QueryParameterType filterType = QueryParameterType.values()[primitives.deserializeInt(queryString)];
final Object filterValue = deserializeFilterValue(primitives, queryString);
query.addFilter(propertyName, filterType, filterValue);
}
int sorts = primitives.deserializeInt(queryString);
while (sorts --> 0) {
final String propertyName = primitives.deserializeString(queryString);
final SortOrder sortOrder = SortOrder.values()[primitives.deserializeInt(queryString)];
query.addSort(propertyName, sortOrder);
}
return query;
}
private static Object deserializeFilterValue(final PrimitiveSerializer primitives, final CharIterator queryString) {
final int type = primitives.deserializeInt(queryString);
int componentType = 0;
switch (type) {
case -1:
return null;
case 0:
componentType = primitives.deserializeInt(queryString);
case 1:
final List filter = new ArrayList();
int size = primitives.deserializeInt(queryString);
while (size --> 0) {
filter.add(readFilterValue(primitives, queryString));
}
if (type == 0) {
// Must convert to an array of the correct type!
return toArray(componentType, filter);
}
return filter;
case 2:
return readFilterValue(primitives, queryString);
default:
throw new IllegalStateException("Failure to deserialize ModelQuery; leftover text: "+queryString);
}
}
/**
* @param componentType
* @param filter
* @return
*/
private static Object toArray(final int componentType, final List filter) {
int size = filter.size();
switch (componentType) {
case TYPE_boolean:
final boolean[] booleans = new boolean[size];
while (size --> 0) {
booleans[size] = (Boolean)filter.get(size);
}
return booleans;
case TYPE_byte:
final byte[] bytes = new byte[size];
while (size --> 0) {
bytes[size] = (Byte)filter.get(size);
}
return bytes;
case TYPE_char:
final char[] chars = new char[size];
while (size --> 0) {
chars[size] = (Character)filter.get(size);
}
return chars;
case TYPE_short:
final short[] shorts = new short[size];
while (size --> 0) {
shorts[size] = (Short)filter.get(size);
}
return shorts;
case TYPE_int:
final int[] ints = new int[size];
while (size --> 0) {
ints[size] = (Integer)filter.get(size);
}
return ints;
case TYPE_long:
final long[] longs = new long[size];
while (size --> 0) {
longs[size] = (Long)filter.get(size);
}
return longs;
case TYPE_float:
final float[] floats = new float[size];
while (size --> 0) {
floats[size] = (Float)filter.get(size);
}
return floats;
case TYPE_double:
final double[] doubles= new double[size];
while (size --> 0) {
doubles[size] = (Double)filter.get(size);
}
return doubles;
default:
return filter.toArray(newObjectArray(componentType, size));
}
}
private static Object[] newObjectArray(final int componentType, final int size) {
switch (componentType) {
case TYPE_Boolean:
return new Boolean[size];
case TYPE_Byte:
return new Byte[size];
case TYPE_Character:
return new Character[size];
case TYPE_Short:
return new Short[size];
case TYPE_Integer:
return new Integer[size];
case TYPE_Long:
return new Long[size];
case TYPE_Float:
return new Float[size];
case TYPE_Double:
return new Double[size];
case TYPE_String:
return new String[size];
case TYPE_Date:
return new Date[size];
default:
throw new UnsupportedOperationException("Unable to create array for type "+componentType);
}
}
/**
* @param primitives
* @param queryString
* @return
*/
private static Object readFilterValue(final PrimitiveSerializer primitives, final CharIterator queryString) {
return null;
}
}