package siena.gae;
import java.io.IOException;
import java.lang.reflect.Field;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.UUID;
import siena.ClassInfo;
import siena.Id;
import siena.Json;
import siena.Query;
import siena.QueryAggregated;
import siena.QueryOwned;
import siena.SienaException;
import siena.SienaRestrictedApiException;
import siena.Util;
import siena.core.DecimalPrecision;
import siena.core.Relation;
import siena.core.RelationMode;
import siena.embed.Embedded;
import siena.embed.JavaSerializer;
import siena.embed.JsonSerializer;
import com.google.appengine.api.datastore.Blob;
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.Text;
public class GaeMappingUtils {
public static Entity createEntityInstance(Field idField, ClassInfo info, Object obj){
Entity entity = null;
Id id = idField.getAnnotation(Id.class);
Class<?> type = idField.getType();
if(id != null){
switch(id.value()) {
case NONE:
Object idVal = null;
idVal = Util.readField(obj, idField);
if(idVal == null)
throw new SienaException("Id Field " + idField.getName() + " value null");
String keyVal = Util.toString(idField, idVal);
entity = new Entity(info.tableName, keyVal);
break;
case AUTO_INCREMENT:
// manages String ID as not long!!!
if(Long.TYPE == type || Long.class.isAssignableFrom(type)){
entity = new Entity(info.tableName);
}else {
Object idStringVal = null;
idStringVal = Util.readField(obj, idField);
if(idStringVal == null)
throw new SienaException("Id Field " + idField.getName() + " value null");
String keyStringVal = Util.toString(idField, idStringVal);
entity = new Entity(info.tableName, keyStringVal);
}
break;
case UUID:
entity = new Entity(info.tableName, UUID.randomUUID().toString());
break;
default:
throw new SienaRestrictedApiException("DB", "createEntityInstance", "Id Generator "+id.value()+ " not supported");
}
}
else throw new SienaException("Field " + idField.getName() + " is not an @Id field");
return entity;
}
public static Entity createEntityInstanceForUpdate(ClassInfo info, Object obj){
Key key = makeKey(info, obj);
Entity entity = new Entity(key);
return entity;
}
public static Entity createEntityInstanceForUpdateFromParent(ClassInfo info, Object obj, Key parentKey, ClassInfo parentInfo, Field parentField){
Key key = makeKeyFromParent(info, obj, parentKey, parentInfo, parentField);
Entity entity = new Entity(key);
return entity;
}
public static String getKindWithAncestorField(ClassInfo childInfo, ClassInfo parentInfo, Field field){
return childInfo.tableName + ":" + parentInfo.tableName + ":" + ClassInfo.getSingleColumnName(field);
}
public static Entity createEntityInstanceFromParent(
Field idField, ClassInfo info, Object obj,
Key parentKey, ClassInfo parentInfo, Field parentField){
Entity entity = null;
Id id = idField.getAnnotation(Id.class);
Class<?> type = idField.getType();
if(id != null){
switch(id.value()) {
case NONE:
Object idVal = null;
idVal = Util.readField(obj, idField);
if(idVal == null)
throw new SienaException("Id Field " + idField.getName() + " value null");
String keyVal = Util.toString(idField, idVal);
entity = new Entity(getKindWithAncestorField(info, parentInfo, parentField), keyVal, parentKey);
break;
case AUTO_INCREMENT:
// manages String ID as not long!!!
if(Long.TYPE == type || Long.class.isAssignableFrom(type)){
entity = new Entity(getKindWithAncestorField(info, parentInfo, parentField), parentKey);
}else {
Object idStringVal = null;
idStringVal = Util.readField(obj, idField);
if(idStringVal == null)
throw new SienaException("Id Field " + idField.getName() + " value null");
String keyStringVal = Util.toString(idField, idStringVal);
entity = new Entity(getKindWithAncestorField(info, parentInfo, parentField), keyStringVal, parentKey);
}
break;
case UUID:
entity = new Entity(getKindWithAncestorField(info, parentInfo, parentField), UUID.randomUUID().toString(), parentKey);
break;
default:
throw new SienaRestrictedApiException("DB", "createEntityInstance", "Id Generator "+id.value()+ " not supported");
}
}
else throw new SienaException("Field " + idField.getName() + " is not an @Id field");
return entity;
}
public static void setIdFromKey(Field idField, Object obj, Key key) {
Id id = idField.getAnnotation(Id.class);
Class<?> type = idField.getType();
if(id != null){
switch(id.value()) {
case NONE:
//idField.setAccessible(true);
Object val = null;
if (Long.TYPE==type || Long.class.isAssignableFrom(type)){
val = Long.parseLong((String) key.getName());
}
else if (String.class.isAssignableFrom(type)){
val = key.getName();
}
else{
throw new SienaRestrictedApiException("DB", "setKey", "Id Type "+idField.getType()+ " not supported");
}
Util.setField(obj, idField, val);
break;
case AUTO_INCREMENT:
// Long value means key.getId()
if (Long.TYPE==type || Long.class.isAssignableFrom(idField.getType())){
Util.setField(obj, idField, key.getId());
}else {
idField.setAccessible(true);
Object val2 = null;
if (Long.TYPE==type || Long.class.isAssignableFrom(idField.getType())){
val = Long.parseLong((String) key.getName());
}
else if (String.class.isAssignableFrom(idField.getType())){
val = key.getName();
}
else{
throw new SienaRestrictedApiException("DB", "setKey", "Id Type "+idField.getType()+ " not supported");
}
Util.setField(obj, idField, val2);
}
break;
case UUID:
Util.setField(obj, idField, key.getName());
break;
default:
throw new SienaException("Id Generator "+id.value()+ " not supported");
}
}
else throw new SienaException("Field " + idField.getName() + " is not an @Id field");
}
public static Key getKey(Object obj) {
Class<?> clazz = obj.getClass();
ClassInfo info = ClassInfo.getClassInfo(clazz);
try {
Field idField = info.getIdField();
Object value = Util.readField(obj, idField);
// TODO verify that returning NULL is not a bad thing
if(value == null) return null;
Class<?> type = idField.getType();
if(idField.isAnnotationPresent(Id.class)){
Id id = idField.getAnnotation(Id.class);
switch(id.value()) {
case NONE:
// long or string goes toString
return KeyFactory.createKey(
ClassInfo.getClassInfo(clazz).tableName,
value.toString());
case AUTO_INCREMENT:
// as a string with auto_increment can't exist, it is not cast into long
if (Long.TYPE == type || Long.class.isAssignableFrom(type)){
return KeyFactory.createKey(
ClassInfo.getClassInfo(clazz).tableName,
(Long)value);
}
return KeyFactory.createKey(
ClassInfo.getClassInfo(clazz).tableName,
value.toString());
case UUID:
return KeyFactory.createKey(
ClassInfo.getClassInfo(clazz).tableName,
value.toString());
default:
throw new SienaException("Id Generator "+id.value()+ " not supported");
}
}
else throw new SienaException("Field " + idField.getName() + " is not an @Id field");
} catch (Exception e) {
throw new SienaException(e);
}
}
public static Key getKeyFromParent(Object obj, Key parentKey, ClassInfo parentInfo, Field parentField) {
Class<?> clazz = obj.getClass();
ClassInfo info = ClassInfo.getClassInfo(clazz);
try {
Field idField = info.getIdField();
Object value = Util.readField(obj, idField);
// TODO verify that returning NULL is not a bad thing
if(value == null) return null;
Class<?> type = idField.getType();
if(idField.isAnnotationPresent(Id.class)){
Id id = idField.getAnnotation(Id.class);
switch(id.value()) {
case NONE:
// long or string goes toString
return KeyFactory.createKey(
parentKey,
getKindWithAncestorField(info, parentInfo, parentField),
value.toString());
case AUTO_INCREMENT:
// as a string with auto_increment can't exist, it is not cast into long
if (Long.TYPE == type || Long.class.isAssignableFrom(type)){
return KeyFactory.createKey(
parentKey,
getKindWithAncestorField(info, parentInfo, parentField),
(Long)value);
}
return KeyFactory.createKey(
parentKey,
getKindWithAncestorField(info, parentInfo, parentField),
value.toString());
case UUID:
return KeyFactory.createKey(
parentKey,
getKindWithAncestorField(info, parentInfo, parentField),
value.toString());
default:
throw new SienaException("Id Generator "+id.value()+ " not supported");
}
}
else throw new SienaException("Field " + idField.getName() + " is not an @Id field");
} catch (Exception e) {
throw new SienaException(e);
}
}
public static Key makeKeyFromId(Class<?> clazz, Object idVal) {
if(idVal == null)
throw new SienaException("makeKeyFromId with Id null");
ClassInfo info = ClassInfo.getClassInfo(clazz);
try {
Field idField = info.getIdField();
if(idField.isAnnotationPresent(Id.class)){
Id id = idField.getAnnotation(Id.class);
switch(id.value()) {
case NONE:
// long or string goes toString
return KeyFactory.createKey(
ClassInfo.getClassInfo(clazz).tableName,
idVal.toString());
case AUTO_INCREMENT:
Class<?> type = idField.getType();
// as a string with auto_increment can't exist, it is not cast into long
if (Long.TYPE==type || Long.class.isAssignableFrom(type)){
return KeyFactory.createKey(
ClassInfo.getClassInfo(clazz).tableName,
(Long)idVal);
}
return KeyFactory.createKey(
ClassInfo.getClassInfo(clazz).tableName,
idVal.toString());
case UUID:
return KeyFactory.createKey(
ClassInfo.getClassInfo(clazz).tableName,
idVal.toString());
default:
throw new SienaException("Id Generator "+id.value()+ " not supported");
}
}
else throw new SienaException("Field " + idField.getName() + " is not an @Id field");
} catch (Exception e) {
throw new SienaException(e);
}
}
public static Key makeKey(Object object) {
return makeKey(ClassInfo.getClassInfo(object.getClass()), object);
}
public static Key makeKey(ClassInfo info, Object object) {
Field idField = info.getIdField();
Object idVal = Util.readField(object, idField);
if(idVal == null)
throw new SienaException("Id Field " + idField.getName() + " value null");
return makeKeyFromId(object.getClass(), idVal);
}
public static Key makeKeyFromParent(ClassInfo info, Object object, Key parentKey, ClassInfo parentInfo, Field parentField) {
try {
Field idField = info.getIdField();
Object idVal = Util.readField(object, idField);
if(idVal == null)
throw new SienaException("Id Field " + idField.getName() + " value null");
if(idField.isAnnotationPresent(Id.class)){
Id id = idField.getAnnotation(Id.class);
switch(id.value()) {
case NONE:
// long or string goes toString
return KeyFactory.createKey(
parentKey,
getKindWithAncestorField(info, parentInfo, parentField),
idVal.toString());
case AUTO_INCREMENT:
Class<?> type = idField.getType();
// as a string with auto_increment can't exist, it is not cast into long
if (Long.TYPE==type || Long.class.isAssignableFrom(type)){
return KeyFactory.createKey(
parentKey,
getKindWithAncestorField(info, parentInfo, parentField),
(Long)idVal);
}
return KeyFactory.createKey(
parentKey,
getKindWithAncestorField(info, parentInfo, parentField),
idVal.toString());
case UUID:
return KeyFactory.createKey(
parentKey,
getKindWithAncestorField(info, parentInfo, parentField),
idVal.toString());
default:
throw new SienaException("Id Generator "+id.value()+ " not supported");
}
}
else throw new SienaException("Field " + idField.getName() + " is not an @Id field");
} catch (Exception e) {
throw new SienaException(e);
}
}
public static void fillEntity(Object obj, Entity entity) {
Class<?> clazz = obj.getClass();
for (Field field : ClassInfo.getClassInfo(clazz).updateFields) {
String property = ClassInfo.getColumnNames(field)[0];
Object value = Util.readField(obj, field);
Class<?> fieldClass = field.getType();
if (ClassInfo.isModel(fieldClass)
&& !ClassInfo.isEmbedded(field)
/*&& !ClassInfo.isAggregated(field)
&& !ClassInfo.isOwned(field)*/) {
if (value == null) {
entity.setProperty(property, null);
} else {
Key key = getKey(value);
entity.setProperty(property, key);
}
} else {
if (value != null) {
if (fieldClass == Json.class) {
value = value.toString();
} else if (value instanceof String) {
String s = (String) value;
if (s.length() > 500)
value = new Text(s);
} else if (value instanceof byte[]) {
byte[] arr = (byte[]) value;
// GAE Blob doesn't accept more than 1MB
if (arr.length < 1000000)
value = new Blob(arr);
else
value = new Blob(Arrays.copyOf(arr, 1000000));
}
else if (ClassInfo.isEmbedded(field)) {
Embedded embed = field.getAnnotation(Embedded.class);
switch(embed.mode()){
case SERIALIZE_JSON:
value = JsonSerializer.serialize(value).toString();
String s = (String) value;
if (s.length() > 500)
value = new Text(s);
break;
case SERIALIZE_JAVA:
// this embedding mode doesn't manage @EmbedIgnores
try {
byte[] b = JavaSerializer.serialize(value);
// if length is less than 1Mb, can store in a blob else???
if(b.length <= 1000000){
value = new Blob(b);
}else{
throw new SienaException("object can be java serialized because it's too large >1mb");
}
}
catch(IOException ex) {
throw new SienaException(ex);
}
break;
case NATIVE:
GaeNativeSerializer.embed(entity, ClassInfo.getSingleColumnName(field), value, 0);
// has set several new properties in entity so go to next field
continue;
}
}
/*else if (ClassInfo.isAggregated(field)){
// can't save it now as it requires its parent key to be mapped
// so don't do anything for the time being
continue;
}
else if (ClassInfo.isOwned(field)){
// can't save it now as it requires its parent key to be mapped
// so don't do anything for the time being
continue;
}*/
else if (fieldClass == BigDecimal.class){
DecimalPrecision ann = field.getAnnotation(DecimalPrecision.class);
if(ann == null) {
value = ((BigDecimal)value).toPlainString();
}else {
switch(ann.storageType()){
case DOUBLE:
value = ((BigDecimal)value).doubleValue();
break;
case STRING:
case NATIVE:
value = ((BigDecimal)value).toPlainString();
break;
}
}
}
// enum is after embedded because an enum can be embedded
// don't know if anyone will use it but it will work :)
else if (Enum.class.isAssignableFrom(field.getType())) {
value = value.toString();
}
}
Unindexed ui = field.getAnnotation(Unindexed.class);
if (ui == null) {
entity.setProperty(property, value);
} else {
entity.setUnindexedProperty(property, value);
}
}
}
}
public static void fillModel(Object obj, Entity entity) {
Class<?> clazz = obj.getClass();
for (Field field : ClassInfo.getClassInfo(clazz).updateFields) {
String property = ClassInfo.getColumnNames(field)[0];
try {
Class<?> fieldClass = field.getType();
if (ClassInfo.isModel(fieldClass) && !ClassInfo.isEmbedded(field)) {
/*if(!ClassInfo.isAggregated(field)){*/
Key key = (Key) entity.getProperty(property);
if (key != null) {
Object value = Util.createObjectInstance(fieldClass);
Field id = ClassInfo.getIdField(fieldClass);
setIdFromKey(id, value, key);
Util.setField(obj, field, value);
}
/*}*/
}
/*else if(ClassInfo.isAggregated(field)){
// does nothing for the time being
}
else if (ClassInfo.isOwned(field)){
// does nothing for the time being
}*/
else if(ClassInfo.isEmbedded(field) && field.getAnnotation(Embedded.class).mode() == Embedded.Mode.NATIVE){
Object value = GaeNativeSerializer.unembed(
field.getType(), ClassInfo.getSingleColumnName(field), entity, 0);
Util.setField(obj, field, value);
}
else {
setFromObject(obj, field, entity.getProperty(property));
}
} catch (Exception e) {
throw new SienaException(e);
}
}
}
public static void fillModelAndKey(Object obj, Entity entity) {
Class<?> clazz = obj.getClass();
ClassInfo info = ClassInfo.getClassInfo(clazz);
Field id = info.getIdField();
Class<?> fieldClass = id.getType();
Key key = entity.getKey();
if (key != null) {
setIdFromKey(id, obj, key);
}
for (Field field : info.updateFields) {
String property = ClassInfo.getColumnNames(field)[0];
try {
fieldClass = field.getType();
if (ClassInfo.isModel(fieldClass)
&& !ClassInfo.isEmbedded(field)) {
/*if(!ClassInfo.isAggregated(field)){*/
key = (Key) entity.getProperty(property);
if (key != null) {
Object value = Util.createObjectInstance(fieldClass);
id = ClassInfo.getIdField(fieldClass);
setIdFromKey(id, value, key);
Util.setField(obj, field, value);
}
/*}*/
}
else if(ClassInfo.isEmbedded(field) && field.getAnnotation(Embedded.class).mode() == Embedded.Mode.NATIVE){
Object value = GaeNativeSerializer.unembed(
field.getType(), ClassInfo.getSingleColumnName(field), entity, 0);
Util.setField(obj, field, value);
}
/*else if(ClassInfo.isAggregated(field)){
// does nothing for the time being
}
else if (ClassInfo.isOwned(field)){
// does nothing for the time being
}*/
else {
setFromObject(obj, field, entity.getProperty(property));
}
} catch (Exception e) {
throw new SienaException(e);
}
}
}
public static void setFromObject(Object object, Field f, Object value)
throws IllegalArgumentException, IllegalAccessException {
if(value != null){
if(Text.class.isAssignableFrom(value.getClass()))
value = ((Text) value).getValue();
else if(Blob.class.isAssignableFrom(value.getClass())) {
if(f.getType() == byte[].class) {
value = ((Blob) value).getBytes();
}
else {
Embedded embed = f.getAnnotation(Embedded.class);
if(embed != null) {
switch(embed.mode()){
case SERIALIZE_JSON:
break;
case SERIALIZE_JAVA:
value = ((Blob) value).getBytes();
break;
case NATIVE:
// shouldn't happen
break;
}
}
}
}
else if(f.getType() == BigDecimal.class){
DecimalPrecision ann = f.getAnnotation(DecimalPrecision.class);
if(ann == null) {
value = new BigDecimal((String)value);
}else {
switch(ann.storageType()){
case DOUBLE:
value = BigDecimal.valueOf((Double)value);
break;
case STRING:
case NATIVE:
value = new BigDecimal((String)value);
break;
}
}
}
}
Util.setFromObject(object, f, value);
}
public static <T> T mapEntityKeysOnly(Entity entity, Class<T> clazz) {
Field id = ClassInfo.getIdField(clazz);
T obj;
try {
obj = Util.createObjectInstance(clazz);
setIdFromKey(id, obj, entity.getKey());
} catch (SienaException e) {
throw e;
} catch (Exception e) {
throw new SienaException(e);
}
return obj;
}
public static <T> List<T> mapEntitiesKeysOnly(List<Entity> entities,
Class<T> clazz) {
Field id = ClassInfo.getIdField(clazz);
List<T> list = new ArrayList<T>(entities.size());
for (Entity entity : entities) {
T obj;
try {
obj = Util.createObjectInstance(clazz);
list.add(obj);
setIdFromKey(id, obj, entity.getKey());
} catch (SienaException e) {
throw e;
} catch (Exception e) {
throw new SienaException(e);
}
}
return list;
}
public static <T> List<T> mapEntitiesKeysOnly(Iterable<Entity> entities,
Class<T> clazz) {
Field id = ClassInfo.getIdField(clazz);
List<T> list = new ArrayList<T>();
for (Entity entity : entities) {
T obj;
try {
obj = Util.createObjectInstance(clazz);
list.add(obj);
setIdFromKey(id, obj, entity.getKey());
} catch (SienaException e) {
throw e;
} catch (Exception e) {
throw new SienaException(e);
}
}
return list;
}
public static <T> T mapEntity(Entity entity, Class<T> clazz) {
Field id = ClassInfo.getIdField(clazz);
T obj = null;
// try to find a constructor
try {
if(entity != null){
obj = Util.createObjectInstance(clazz);
fillModel(obj, entity);
setIdFromKey(id, obj, entity.getKey());
}
} catch (SienaException e) {
throw e;
} catch (Exception e) {
throw new SienaException(e);
}
return obj;
}
public static <T> List<T> mapEntities(List<Entity> entities,
Class<T> clazz) {
Field id = ClassInfo.getIdField(clazz);
List<T> list = new ArrayList<T>(entities.size());
for (Entity entity : entities) {
T obj;
try {
obj = Util.createObjectInstance(clazz);
fillModel(obj, entity);
list.add(obj);
setIdFromKey(id, obj, entity.getKey());
} catch (SienaException e) {
throw e;
} catch (Exception e) {
throw new SienaException(e);
}
}
return list;
}
public static <T> List<T> mapEntities(Iterable<Entity> entities,
Class<T> clazz) {
Field id = ClassInfo.getIdField(clazz);
List<T> list = new ArrayList<T>();
for (Entity entity : entities) {
T obj;
try {
obj = Util.createObjectInstance(clazz);
fillModel(obj, entity);
list.add(obj);
setIdFromKey(id, obj, entity.getKey());
} catch (SienaException e) {
throw e;
} catch (Exception e) {
throw new SienaException(e);
}
}
return list;
}
public static <T> T mapRelation(Query<T> query, T obj, ClassInfo info) {
// aggregators/owners
List<QueryAggregated> aggregs = query.getAggregatees();
List<QueryOwned> ownees = query.getOwnees();
if(aggregs.isEmpty() && ownees.isEmpty()){
return obj;
}
if(aggregs.size() == 1){
// aggregators
QueryAggregated aggreg = aggregs.get(0);
Relation rel =
new Relation(RelationMode.AGGREGATION, aggreg.aggregator, aggreg.field);
Util.setField(obj, info.aggregator, rel);
}
else if(aggregs.size() > 1){
throw new SienaException("Only one aggregation per query allowed");
}
if(ownees.size() == 1){
// owners
QueryOwned ownee = ownees.get(0);
Util.setField(obj, ownee.field, ownee.owner);
}
else if(ownees.size() > 1){
throw new SienaException("Only one owner per query allowed");
}
return obj;
}
public static <T> List<T> mapRelations(Query<T> query, List<T> objs, ClassInfo info) {
List<QueryAggregated> aggregs = query.getAggregatees();
List<QueryOwned> ownees = query.getOwnees();
if(aggregs.isEmpty() && ownees.isEmpty()){
return objs;
}
if(aggregs.size() == 1){
QueryAggregated aggreg = aggregs.get(0);
Relation rel =
new Relation(RelationMode.AGGREGATION, aggreg.aggregator, aggreg.field);
for(T obj: objs){
Util.setField(obj, info.aggregator, rel);
}
}
else if(aggregs.size() > 1){
throw new SienaException("Only one aggregation per query allowed");
}
if(ownees.size() == 1){
// owners
QueryOwned ownee = ownees.get(0);
for(T obj: objs){
Util.setField(obj, ownee.field, ownee.owner);
}
}
else if(ownees.size() > 1){
throw new SienaException("Only one owner per query allowed");
}
return objs;
}
}