package com.googlecode.objectify.impl;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;
import javax.persistence.Id;
import javax.persistence.PostLoad;
import javax.persistence.PrePersist;
import com.google.appengine.api.datastore.Entity;
import com.google.appengine.api.datastore.KeyFactory;
import com.googlecode.objectify.Key;
import com.googlecode.objectify.Objectify;
import com.googlecode.objectify.ObjectifyFactory;
import com.googlecode.objectify.annotation.Cached;
import com.googlecode.objectify.annotation.Parent;
/**
* Everything you need to know about mapping between Datastore Entity objects
* and typed entity objects.
*
* @author Jeff Schnitzer <jeff@infohazard.org>
*/
public class EntityMetadata<T>
{
/** Needed for key translation */
protected ObjectifyFactory factory;
/** */
protected Class<T> entityClass;
protected Constructor<T> entityClassConstructor;
/** The kind that is associated with the class, ala ObjectifyFactory.getKind(Class<?>) */
protected String kind;
/** We treat the @Id key field specially - it will be either Long id or String name */
protected Field idField;
protected Field nameField;
/** If the entity has a @Parent field, treat it specially */
protected Field parentField;
/** Any methods in the hierarchy annotated with @PrePersist, could be null */
protected List<Method> prePersistMethods;
/** Any methods in the hierarchy annotated with @PostLoad, could be null */
protected List<Method> postLoadMethods;
/** For translating between pojos and entities */
protected Transmog<T> transmog;
/** The cached annotation, or null if entity should not be cached */
protected Cached cached;
/**
* Inspects and stores the metadata for a particular entity class.
* @param clazz must be a properly-annotated Objectify entity class.
*/
public EntityMetadata(ObjectifyFactory fact, Class<T> clazz)
{
this.factory = fact;
this.entityClass = clazz;
this.entityClassConstructor = TypeUtils.getNoArgConstructor(clazz);
this.kind = this.factory.getKind(clazz);
this.cached = clazz.getAnnotation(Cached.class);
// Recursively walk up the inheritance chain looking for @Id and @Parent fields
this.processKeyFields(clazz);
// Walk up the inheritance chain looking for @PrePersist and @PostLoad
this.processLifecycleCallbacks(clazz);
// Now figure out how to handle normal properties
this.transmog = new Transmog<T>(fact, clazz);
// There must be some field marked with @Id
if ((this.idField == null) && (this.nameField == null))
throw new IllegalStateException("There must be an @Id field (String, Long, or long) for " + this.entityClass.getName());
}
/**
*/
public Class<T> getEntityClass()
{
return this.entityClass;
}
/** @return the datastore kind associated with this metadata */
public String getKind()
{
return this.kind;
}
/**
* @return the Cached instruction for this entity, or null if it should not be cached.
*/
public Cached getCached()
{
return this.cached;
}
/**
* Recursive function which walks up the superclass hierarchy looking
* for key-related fields (@Id and @Parent). Ignores all other fields;
* those are the responsibility of the Transmog.
*/
private void processKeyFields(Class<?> clazz)
{
if ((clazz == null) || (clazz == Object.class))
return;
// Start at the top of the chain
this.processKeyFields(clazz.getSuperclass());
// Check all the fields
for (Field field: clazz.getDeclaredFields())
{
if (!TypeUtils.isSaveable(field))
continue;
field.setAccessible(true);
if (field.isAnnotationPresent(Id.class))
{
if ((this.idField != null) || (this.nameField != null))
throw new IllegalStateException("Multiple @Id fields in the class hierarchy of " + this.entityClass.getName());
if ((field.getType() == Long.class) || (field.getType() == Long.TYPE))
this.idField = field;
else if (field.getType() == String.class)
this.nameField = field;
else
throw new IllegalStateException("Only fields of type Long, long, or String are allowed as @Id. Invalid on field "
+ field + " in " + clazz.getName());
}
else if (field.isAnnotationPresent(Parent.class))
{
if (this.parentField != null)
throw new IllegalStateException("Multiple @Parent fields in the class hierarchy of " + this.entityClass.getName());
if (field.getType() != com.google.appengine.api.datastore.Key.class && field.getType() != Key.class)
throw new IllegalStateException("Only fields of type Key<?> or Key are allowed as @Parent. Illegal parent '" + field + "' in " + clazz.getName());
this.parentField = field;
}
}
}
/**
* Recursive function which walks up the superclass hierarchy looking
* for lifecycle-related methods (@PrePersist and @PostLoad).
*/
private void processLifecycleCallbacks(Class<?> clazz)
{
if ((clazz == null) || (clazz == Object.class))
return;
// Start at the top of the chain
this.processLifecycleCallbacks(clazz.getSuperclass());
// Check all the methods
for (Method method: clazz.getDeclaredMethods())
{
if (method.isAnnotationPresent(PrePersist.class) || method.isAnnotationPresent(PostLoad.class))
{
method.setAccessible(true);
Class<?>[] ptypes = method.getParameterTypes();
for (int i=0; i<ptypes.length; i++)
if (ptypes[i] != Objectify.class && ptypes[i] != Entity.class)
throw new IllegalStateException("@PrePersist and @PostLoad methods can only have parameters of type Objectify or Entity");
if (method.isAnnotationPresent(PrePersist.class))
{
if (this.prePersistMethods == null)
this.prePersistMethods = new ArrayList<Method>();
this.prePersistMethods.add(method);
}
if (method.isAnnotationPresent(PostLoad.class))
{
if (this.postLoadMethods == null)
this.postLoadMethods = new ArrayList<Method>();
this.postLoadMethods.add(method);
}
}
}
}
/**
* Converts an entity to an object of the appropriate type for this metadata structure.
* Does not check that the entity is appropriate; that should be done when choosing
* which EntityMetadata to call.
*/
public T toObject(Entity ent, Objectify ofy)
{
T pojo = TypeUtils.newInstance(this.entityClassConstructor);
// This will set the id and parent fields as appropriate.
this.setKey(pojo, ent.getKey());
this.transmog.load(ent, pojo);
// If there are any @PostLoad methods, call them
this.invokeLifecycleCallbacks(this.postLoadMethods, pojo, ent, ofy);
return pojo;
}
/**
* Converts an object to a datastore Entity with the appropriate Key type.
*/
public Entity toEntity(T pojo, Objectify ofy)
{
Entity ent = this.initEntity(pojo);
// If there are any @PrePersist methods, call them
this.invokeLifecycleCallbacks(this.prePersistMethods, pojo, ent, ofy);
this.transmog.save(pojo, ent);
return ent;
}
/**
* Invoke a set of lifecycle callbacks on the pojo.
*
* @param callbacks can be null if there are no callbacks
*/
private void invokeLifecycleCallbacks(List<Method> callbacks, Object pojo, Entity ent, Objectify ofy)
{
try
{
if (callbacks != null)
for (Method method: callbacks)
if (method.getParameterTypes().length == 0)
method.invoke(pojo);
else
{
Object[] params = new Object[method.getParameterTypes().length];
for (int i=0; i<method.getParameterTypes().length; i++)
{
Class<?> ptype = method.getParameterTypes()[i];
if (ptype == Objectify.class)
params[i] = ofy;
else if (ptype == Entity.class)
params[i] = ent;
else
throw new IllegalStateException("Lifecycle callback cannot have parameter type " + ptype);
}
method.invoke(pojo, params);
}
}
catch (IllegalAccessException e) { throw new RuntimeException(e); }
catch (InvocationTargetException e) { throw new RuntimeException(e); }
}
/**
* <p>This hides all the messiness of trying to create an Entity from an object that:</p>
* <ul>
* <li>Might have a long id, might have a String name</li>
* <li>If it's a Long id, might be null and require autogeneration</li>
* <li>Might have a parent key</li>
* </ul>
*
* @return an empty Entity object whose key has been set but no other properties.
*/
Entity initEntity(T obj)
{
try
{
com.google.appengine.api.datastore.Key parentKey = null;
// First thing, get the parentKey (if appropriate)
if (this.parentField != null)
{
parentKey = this.getRawKey(this.parentField, obj);
if (parentKey == null)
throw new IllegalStateException("Missing parent of " + obj);
}
if (this.idField != null)
{
Long id = (Long)this.idField.get(obj); // possibly null
if (id != null)
{
if (parentKey != null)
return new Entity(KeyFactory.createKey(parentKey, this.kind, id));
else
return new Entity(KeyFactory.createKey(this.kind, id));
}
else // id is null, must autogenerate
{
if (parentKey != null)
return new Entity(this.kind, parentKey);
else
return new Entity(this.kind);
}
}
else // this.nameField contains id
{
String name = (String)this.nameField.get(obj);
if (name == null)
throw new IllegalStateException("Tried to persist null String @Id for " + obj);
if (parentKey != null)
return new Entity(this.kind, name, parentKey);
else
return new Entity(this.kind, name);
}
}
catch (IllegalAccessException ex) { throw new RuntimeException(ex); }
}
/**
* Sets the relevant id and parent fields of the object to the values stored in the key.
* @param obj must be of the entityClass type for this metadata.
*/
public void setKey(T obj, com.google.appengine.api.datastore.Key key)
{
if (!this.entityClass.isAssignableFrom(obj.getClass()))
throw new IllegalArgumentException("Trying to use metadata for " + this.entityClass.getName() + " to set key of " + obj.getClass().getName());
try
{
if (key.getName() != null)
{
if (this.nameField == null)
throw new IllegalStateException("Loaded Entity has name but " + this.entityClass.getName() + " has no String @Id");
this.nameField.set(obj, key.getName());
}
else
{
if (this.idField == null)
throw new IllegalStateException("Loaded Entity has numeric id but " + this.entityClass.getName() + " has no Long (or long) @Id");
this.idField.set(obj, key.getId());
}
com.google.appengine.api.datastore.Key parentKey = key.getParent();
if (parentKey != null)
{
if (this.parentField == null)
throw new IllegalStateException("Loaded Entity has parent but " + this.entityClass.getName() + " has no @Parent");
if (this.parentField.getType() == com.google.appengine.api.datastore.Key.class)
this.parentField.set(obj, parentKey);
else
this.parentField.set(obj, this.factory.rawKeyToTypedKey(parentKey));
}
}
catch (IllegalAccessException e) { throw new RuntimeException(e); }
}
/**
* Gets a key composed of the relevant id and parent fields in the object.
*
* @param obj must be of the entityClass type for this metadata.
* @throws IllegalArgumentException if obj has a null id
*/
public com.google.appengine.api.datastore.Key getKey(Object obj)
{
if (!this.entityClass.isAssignableFrom(obj.getClass()))
throw new IllegalArgumentException("Trying to use metadata for " + this.entityClass.getName() + " to get key of " + obj.getClass().getName());
try
{
if (this.nameField != null)
{
String name = (String)this.nameField.get(obj);
if (this.parentField != null)
{
com.google.appengine.api.datastore.Key parent = this.getRawKey(this.parentField, obj);
return KeyFactory.createKey(parent, this.kind, name);
}
else // name yes parent no
{
return KeyFactory.createKey(this.kind, name);
}
}
else // has id not name
{
Long id = (Long) this.idField.get(obj);
if (id == null)
throw new IllegalArgumentException("You cannot create a Key for an object with a null @Id. Object was " + obj);
if (this.parentField != null)
{
com.google.appengine.api.datastore.Key parent = this.getRawKey(this.parentField, obj);
return KeyFactory.createKey(parent, this.kind, id);
}
else // id yes parent no
{
return KeyFactory.createKey(this.kind, id);
}
}
}
catch (IllegalAccessException e) { throw new RuntimeException(e); }
}
/** @return the raw key even if the field is an Key */
private com.google.appengine.api.datastore.Key getRawKey(Field keyField, Object obj) throws IllegalAccessException
{
if (keyField.getType() == com.google.appengine.api.datastore.Key.class)
return (com.google.appengine.api.datastore.Key)keyField.get(obj);
else
return this.factory.typedKeyToRawKey((Key<?>)keyField.get(obj));
}
/**
* @return true if the property name corresponds to a Long/long @Id
* field. If the entity has a String name @Id, this will return false.
*/
public boolean isIdField(String propertyName)
{
return this.idField != null && this.idField.getName().equals(propertyName);
}
/**
* @return true if the property name corresponds to a String @Id
* field. If the entity has a Long/long @Id, this will return false.
*/
public boolean isNameField(String propertyName)
{
return this.nameField != null && this.nameField.getName().equals(propertyName);
}
/**
* @return true if the entity has a parent field
*/
public boolean hasParentField()
{
return this.parentField != null;
}
}