package com.googlecode.objectify.impl;
import com.google.appengine.api.datastore.Entity;
import com.google.appengine.api.datastore.PropertyContainer;
import com.googlecode.objectify.Key;
import com.googlecode.objectify.Ref;
import com.googlecode.objectify.annotation.Id;
import com.googlecode.objectify.annotation.Parent;
import com.googlecode.objectify.impl.translate.CreateContext;
import com.googlecode.objectify.impl.translate.LoadContext;
import com.googlecode.objectify.impl.translate.SaveContext;
import com.googlecode.objectify.impl.translate.Translator;
import com.googlecode.objectify.impl.translate.TypeKey;
import com.googlecode.objectify.repackaged.gentyref.GenericTypeReflector;
import com.googlecode.objectify.util.DatastoreUtils;
import java.lang.reflect.Field;
import java.lang.reflect.Type;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* Figures out what to do with key fields on POJO entities.
*
* @author Jeff Schnitzer <jeff@infohazard.org>
*/
public class KeyMetadata<P>
{
/** */
private static final Logger log = Logger.getLogger(KeyMetadata.class.getName());
/** The @Id field on the pojo - it will be Long, long, or String */
private PropertyPopulator<Object, Object> idMeta;
/** The @Parent field on the pojo, or null if there is no parent */
private PropertyPopulator<Object, Object> parentMeta;
/** */
private Class<P> clazz;
/** The kind that is associated with the class, ala ObjectifyFactory.getKind(Class<?>) */
private String kind;
/** */
public KeyMetadata(Class<P> clazz, CreateContext ctx, Path path) {
this.clazz = clazz;
findKeyFields(clazz, ctx, path);
// There must be some field marked with @Id
if (this.idMeta == null)
throw new IllegalStateException("There must be an @Id field (String, Long, or long) for " + clazz.getName());
this.kind = Key.getKind(clazz);
}
/**
* Recursively go through the class hierarchy looking for the idMeta and parentMeta fields.
* @throws IllegalStateException if @Id or @Parent shows up twice.
*/
private void findKeyFields(Class<?> inspect, CreateContext ctx, Path path) {
if (inspect == Object.class)
return;
findKeyFields(inspect.getSuperclass(), ctx, path);
for (Field field: inspect.getDeclaredFields()) {
if (field.getAnnotation(Id.class) != null) {
if (this.idMeta != null)
throw new IllegalStateException("Multiple @Id fields in the class hierarchy of " + clazz.getName());
if ((field.getType() != Long.class) && (field.getType() != long.class) && (field.getType() != String.class))
throw new IllegalStateException("@Id field '" + field.getName() + "' in " + inspect.getName() + " must be of type Long, long, or String");
Property prop = new FieldProperty(ctx.getFactory(), clazz, field);
Translator<Object, Object> translator = ctx.getTranslator(new TypeKey<>(prop), ctx, path.extend(prop.getName()));
this.idMeta = new PropertyPopulator<>(prop, translator);
} else if (field.getAnnotation(Parent.class) != null) {
if (this.parentMeta != null)
throw new IllegalStateException("Multiple @Parent fields in the class hierarchy of " + clazz.getName());
if (!isAllowedParentFieldType(field.getType()))
throw new IllegalStateException("@Parent fields must be Ref<?>, Key<?>, or datastore Key. Illegal parent: " + field);
Property prop = new FieldProperty(ctx.getFactory(), clazz, field);
Translator<Object, Object> translator = ctx.getTranslator(new TypeKey<>(prop), ctx, path.extend(prop.getName()));
this.parentMeta = new PropertyPopulator<>(prop, translator);
}
}
}
/** @return true if the type is an allowed parent type */
private boolean isAllowedParentFieldType(Type type) {
Class<?> erased = GenericTypeReflector.erase(type);
return com.google.appengine.api.datastore.Key.class.isAssignableFrom(erased)
|| Key.class.isAssignableFrom(erased)
|| Ref.class.isAssignableFrom(erased);
}
/**
* Sets the key (from the container) onto the POJO id/parent fields. Also doublechecks to make sure
* that the key fields aren't present in the container, which means we're in a very bad state.
*/
public void setKey(P pojo, PropertyContainer container, LoadContext ctx, Path containerPath) {
if (container.hasProperty(idMeta.getProperty().getName()))
throw new IllegalStateException("Datastore has a property present for the id field " + idMeta.getProperty() + " which would conflict with the key: " + container);
if (parentMeta != null && container.hasProperty(parentMeta.getProperty().getName()))
throw new IllegalStateException("Datastore has a property present for the parent field " + parentMeta.getProperty() + " which would conflict with the key: " + container);
this.setKey(pojo, DatastoreUtils.getKey(container), ctx, containerPath);
}
/**
* Sets the key onto the POJO id/parent fields
*/
private void setKey(P pojo, com.google.appengine.api.datastore.Key key, LoadContext ctx, Path containerPath) {
if (!clazz.isAssignableFrom(pojo.getClass()))
throw new IllegalArgumentException("Trying to use metadata for " + clazz.getName() + " to set key of " + pojo.getClass().getName());
// If no key, don't need to do anything
if (key == null)
return;
idMeta.setValue(pojo, DatastoreUtils.getId(key), ctx, containerPath);
com.google.appengine.api.datastore.Key parentKey = key.getParent();
if (parentKey != null) {
if (this.parentMeta == null)
throw new IllegalStateException("Loaded Entity has parent but " + clazz.getName() + " has no @Parent");
parentMeta.setValue(pojo, parentKey, ctx, containerPath);
}
}
/** @return the datastore kind associated with this metadata */
public String getKind() {
return kind;
}
/**
* <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.
*/
public Entity initEntity(P pojo) {
Object id = getId(pojo);
if (id == null)
if (isIdNumeric()) {
if (log.isLoggable(Level.FINEST))
log.finest("Getting parent key from " + pojo);
return new com.google.appengine.api.datastore.Entity(this.kind, getParentRaw(pojo));
} else
throw new IllegalStateException("Cannot save an entity with a null String @Id: " + pojo);
else
return new com.google.appengine.api.datastore.Entity(getRawKey(pojo));
}
/**
* Gets a key composed of the relevant id and parent fields in the object.
*
* @param pojo must be of the entityClass type for this metadata.
* @return null if the id is null (ie, null Long)
*/
public com.google.appengine.api.datastore.Key getRawKeyOrNull(P pojo) {
if (log.isLoggable(Level.FINEST))
log.finest("Getting key from " + pojo);
if (!clazz.isAssignableFrom(pojo.getClass()))
throw new IllegalArgumentException("Trying to use metadata for " + clazz.getName() + " to get key of " + pojo.getClass().getName());
final Object id = getId(pojo);
if (id == null)
return null;
final com.google.appengine.api.datastore.Key parent = getParentRaw(pojo);
return DatastoreUtils.createKey(parent, kind, id);
}
/**
* Gets a key composed of the relevant id and parent fields in the object.
*
* @param pojo must be of the entityClass type for this metadata.
* @throws IllegalArgumentException if pojo has a null id
*/
public com.google.appengine.api.datastore.Key getRawKey(P pojo) {
final com.google.appengine.api.datastore.Key key = getRawKeyOrNull(pojo);
if (key == null)
throw new IllegalArgumentException("You cannot create a Key for an object with a null @Id. Object was " + pojo);
else
return key;
}
/** @return the name of the parent field, or null if there wasn't one */
public String getParentFieldName() {
return parentMeta == null ? null : parentMeta.getProperty().getName();
}
/** @return the name of the id field */
public String getIdFieldName() {
return idMeta.getProperty().getName();
}
/** @return the java type of the id field; it will be either Long.class, Long.TYPE, or String.class */
public Class<?> getIdFieldType() {
// The id must be Long, long, or String, therefore the type is always a Class
return (Class<?>)idMeta.getProperty().getType();
}
/**
* @return true if the id field is numeric, false if it is String
*/
private boolean isIdNumeric() {
return !(this.idMeta.getProperty().getType() == String.class);
}
/**
* @return true if the entity has a parent field
*/
public boolean hasParentField() {
return this.parentMeta != null;
}
/**
* @return true if the parent should be loaded given the enabled fetch groups
*/
public boolean shouldLoadParent(Set<Class<?>> enabledGroups) {
if (this.parentMeta == null)
return false;
return parentMeta.getLoadConditions().shouldLoad(enabledGroups, false);
}
/**
* @return true if the id field is uppercase-Long, which can be genearted.
*/
public boolean isIdGeneratable() {
return this.idMeta.getProperty().getType() == Long.class;
}
/**
* Sets the numeric id field
*/
public void setLongId(P pojo, Long id) {
if (!clazz.isAssignableFrom(pojo.getClass()))
throw new IllegalArgumentException("Trying to use metadata for " + clazz.getName() + " to set key of " + pojo.getClass().getName());
this.idMeta.getProperty().set(pojo, id);
}
/**
* Get the contents of the @Parent field as a datastore key.
* @return null if there was no @Parent field, or the field is null.
*/
private com.google.appengine.api.datastore.Key getParentRaw(P pojo) {
if (parentMeta == null)
return null;
return (com.google.appengine.api.datastore.Key)parentMeta.getValue(pojo, new SaveContext(), Path.root());
}
/**
* Get whatever is in the @Id field of the pojo doing no type checking or conversion
* @return Long or String or null
*/
private Object getId(P pojo) {
return idMeta.getProperty().get(pojo);
}
/**
* Change this slightly to just a null check on the id so that null String will be allowed.
* The advantage of this is that we'll get a consistent error message when you actually
* try to save a null String id, instead of the weird things that will happen when you try
* to generate a Key for the object.
*
* @return true if this entity object has a null id
*/
public boolean requiresAutogeneratedId(P entity) {
return /*isIdGeneratable() &&*/ getId(entity) == null;
}
}