package com.googlecode.objectify.impl;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import com.google.appengine.api.datastore.Entity;
import com.googlecode.objectify.ObjectifyFactory;
import com.googlecode.objectify.impl.TypeUtils.FieldMetadata;
import com.googlecode.objectify.impl.TypeUtils.MethodMetadata;
import com.googlecode.objectify.impl.load.EmbeddedArraySetter;
import com.googlecode.objectify.impl.load.EmbeddedClassSetter;
import com.googlecode.objectify.impl.load.EmbeddedCollectionSetter;
import com.googlecode.objectify.impl.load.EmbeddedMultivalueSetter;
import com.googlecode.objectify.impl.load.EmbeddedNullIndexSetter;
import com.googlecode.objectify.impl.load.LeafSetter;
import com.googlecode.objectify.impl.load.RootSetter;
import com.googlecode.objectify.impl.load.Setter;
import com.googlecode.objectify.impl.save.ClassSaver;
/**
* <p>Class which knows how to load data from Entity to POJO and save data from POJO to Entity.</p>
*
* <p>Note that this class completely ignores @Id and @Parent fields.</p>
*
* <p>To understand this code, you must first understand that a "leaf" value is anything that
* can be put into the datastore in a single property. Simple types like String, and Enum,
* and Key are leaf nodes, but so are Collections and arrays of these basic types. @Embedded
* values are nonleaf - they branch the persistance graph, producing multiple properties in a
* datastore Entity.</p>
*
* <p>Also realize that there are two separate dimensions to understand. Misunderstanding
* the two related graphs will make this code very confusing:</p>
* <ul>
* <li>There is a class graph, which branches at @Embedded classes (either simple fields
* or array/collection fields). The static analysis code that builds Setters and Savers
* must traverse this graph.</li>
* <li>There is an object graph, which branches at @Embedded arrays. The runtime execution
* code must traverse this graph when setting and saving entities.</li>
* </ul>
*
* <p>The core structures that operate at runtime are Setters (for loading datastore Entities into
* typed pojos) and Savers (for saving the fields of typed pojos into datastore Entities). They are
* NOT parallel hierarchies, and they work very differently:</p>
* <ul>
* <li>When loading, Transmog <em>iterates</em> through the properties of an Entity and for each one calls a Setter
* that knows how to set this property somewhere deep in the object graph of a typed pojo. In the case
* of @Embedded arrays and collections, this single collection datastore value will set multipel
* values in the pojo. The core data structure is {@code rootSetters}, a map of entity property
* name to a Setter which knows what to do with that data.</li>
* <li>When saving, Transmog <em>recurses</em> through the class structure of a pojo (and any embedded objects), calling
* all relevant Savers to populate the datastore Entity. The core data structure is {@code rootSaver}, which
* understands the whole pojo object graph and knows how to translate it into a number of properties
* on the Entity.</li>
* </ul>
*
* @author Jeff Schnitzer <jeff@infohazard.org>
*/
public class Transmog<T>
{
/** Needed to convert Key types */
ObjectifyFactory factory;
/** Useful to have around for error logging purposes */
Class<T> clazz;
/** Maps full "blah.blah.blah" property name to a particular Setter implementation */
Map<String, Setter> rootSetters = new HashMap<String, Setter>();
/** The root saver that knows how to persist an object of type T */
ClassSaver rootSaver;
/**
* <p>Object which visits various levels of the pojo class graph does two things:
* <ol>
* <li>Builds up the rootSetters map in {@code rootSetters}.</li>
* <li>Validates the structure of the pojo.</li>
* </ol>
*
* <p>This visitor does not have *anything* to do with Savers, which are built up
* separately in a different pass. This is the inherent nature of the beast.</p>
*/
class Visitor
{
Setter setterChain;
String prefix; // starts null for root
boolean embedded;
Set<String> fieldPathsUsed;
Set<String> methodPathsUsed;
/** Constructs a visitor for a top-level entity */
public Visitor()
{
this.setterChain = new RootSetter();
this.fieldPathsUsed = new HashSet<String>();
this.methodPathsUsed = new HashSet<String>();
}
/**
* Constructs a visitor for an embedded object.
* @param setterChain is the root of the setter chain
*/
public Visitor(Setter setterChain, String prefix, Set<String> fieldPathsUsed, Set<String> methodPathsUsed)
{
this.setterChain = setterChain;
this.prefix = prefix;
this.embedded = true;
this.fieldPathsUsed = fieldPathsUsed;
this.methodPathsUsed = methodPathsUsed;
}
/**
* Creates a set of Setters for the class (and parent/embedded classes) in the
* Transmog.rootSetters collection
*
* @param clazz is the class to inspect
*/
public void visitClass(Class<?> clazz)
{
// Only good fields come back from this method call
List<FieldMetadata> fields = TypeUtils.getPesistentFields(clazz);
for (FieldMetadata meta: fields)
this.visitField(meta.field, meta.names);
// Only good methods come back from this method call
List<MethodMetadata> methods = TypeUtils.getAlsoLoadMethods(clazz);
for (MethodMetadata meta: methods)
this.visitMethod(meta.method, meta.names);
}
/**
* @param method must be a proper @AlsoLoad method.
* @param names are all the property names which should be used to call the method
*/
void visitMethod(Method method, Collection<String> names)
{
List<String> paths = this.namesToPaths(names);
for (String path: paths)
{
List<String> collisions = makeCollisions(paths, path);
LeafSetter setter = new LeafSetter(factory, new MethodWrapper(method), collisions);
this.addRootSetter(path, setter, true);
}
}
/**
* Check out a field. Note that only leaf fields (ie, non-embedded) complete a Setter
* chain and thus result in one getting added to the rootSetters. Until then all Setter
* chains are in limbo.
*
* @param field must be a proper persistable field.
*/
void visitField(Field field, Collection<String> names)
{
List<String> paths = this.namesToPaths(names);
if (TypeUtils.isEmbedded(field))
{
if (field.getType().isArray())
{
Class<?> visitType = field.getType().getComponentType();
for (String path: paths)
{
List<String> collisions = makeCollisions(paths, path);
EmbeddedMultivalueSetter setter = new EmbeddedArraySetter(field, path, collisions);
this.addNullIndexSetter(setter, path, collisions);
Visitor visitor = new Visitor(this.setterChain.extend(setter), path, this.fieldPathsUsed, this.methodPathsUsed);
visitor.visitClass(visitType);
}
}
else if (Collection.class.isAssignableFrom(field.getType()))
{
Class<?> visitType = TypeUtils.getComponentType(field.getType(), field.getGenericType());
for (String path: paths)
{
List<String> collisions = makeCollisions(paths, path);
EmbeddedMultivalueSetter setter = new EmbeddedCollectionSetter(field, path, collisions);
this.addNullIndexSetter(setter, path, collisions);
Visitor visitor = new Visitor(this.setterChain.extend(setter), path, this.fieldPathsUsed, this.methodPathsUsed);
visitor.visitClass(visitType);
}
}
else // basic class
{
Class<?> visitType = field.getType();
for (String path: paths)
{
List<String> collisions = makeCollisions(paths, path);
Setter setter = new EmbeddedClassSetter(field, collisions);
Visitor visitor = new Visitor(this.setterChain.extend(setter), path, this.fieldPathsUsed, this.methodPathsUsed);
visitor.visitClass(visitType);
}
}
}
else // not embedded, so we're at a leaf object (including arrays and collections of basic types)
{
for (String path: paths)
{
List<String> collisions = makeCollisions(paths, path);
LeafSetter setter = new LeafSetter(factory, new FieldWrapper(field), collisions);
this.addRootSetter(path, setter, false);
}
}
}
/**
* Translates names to paths based on the current path prefix.
*/
private List<String> namesToPaths(Collection<String> names)
{
List<String> paths = new ArrayList<String>();
for (String name: names)
paths.add(TypeUtils.extendPropertyPath(this.prefix, name));
return paths;
}
/**
* Make the list of collisions given the paths without the path; return null
* if there is only one path (no collisions). Note that the return type
* is always a list because at runtime we want fast iteration.
*/
private List<String> makeCollisions(Collection<String> paths, String forPath)
{
if (paths.size() > 1)
{
List<String> collisions = new ArrayList<String>(paths.size()-1);
for (String path: paths)
if (!path.equals(forPath))
collisions.add(path);
return collisions;
}
else
{
return null;
}
}
/**
* Embedded collections need a null index setter to handle the case of an all-null
* collection.
*/
void addNullIndexSetter(EmbeddedMultivalueSetter setter, String path, Collection<String> collisionPaths)
{
EmbeddedNullIndexSetter nes = new EmbeddedNullIndexSetter(setter, path, collisionPaths);
this.addRootSetter(TypeUtils.getNullIndexPath(path), nes, false);
}
/**
* Takes a final leaf setter, extends the setter chain so far, and
* Adds a final leaf setter to the setters collection.
* @param fullPath is the whole "blah.blah.blah" path for this property
* @param method is true if this is setting a method, false if setting a field
*/
void addRootSetter(String fullPath, Setter setter, boolean method)
{
if (method)
{
if (this.methodPathsUsed.contains(fullPath))
throw new IllegalStateException("Attempting to create multiple associations on " + clazz + " for " + fullPath);
else
this.methodPathsUsed.add(fullPath);
}
else
{
if (this.fieldPathsUsed.contains(fullPath) || this.methodPathsUsed.contains(fullPath))
throw new IllegalStateException("Attempting to create multiple associations on " + clazz + " for " + fullPath);
else
this.fieldPathsUsed.add(fullPath);
}
// Extend and strip off the unnecessary (at runtime) SetterRoot
Setter chain = this.setterChain.extend(setter).getNext();
rootSetters.put(fullPath, chain);
}
}
/**
* Creats a transmog for the specified class, introspecting it and discovering
* how to load/save its properties.
*/
public Transmog(ObjectifyFactory fact, Class<T> clazz)
{
this.factory = fact;
this.clazz = clazz;
// This creates the setters in the rootSetters collection and validates the pojo
new Visitor().visitClass(clazz);
// Construction of the savers is relatively straighforward
this.rootSaver = new ClassSaver(fact, clazz);
}
/**
* Loads the property data in an Entity into a POJO. Does not affect id/parent
* (ie key) fields; those are assumed to already have been set.
*
* @param fromEntity is a raw datastore entity
* @param toPojo is your typed entity
*/
public void load(Entity fromEntity, T toPojo)
{
LoadContext context = new LoadContext(toPojo, fromEntity);
for (Map.Entry<String, Object> property: fromEntity.getProperties().entrySet())
{
Setter setter = this.rootSetters.get(property.getKey());
if (setter != null)
setter.set(toPojo, property.getValue(), context);
}
context.done();
}
/**
* Saves the fields of a POJO into the properties of an Entity. Does not affect id/parent
* (ie key) fields; those are assumed to already have been set.
*
* @param fromPojo is your typed entity
* @param toEntity is a raw datastore entity
*/
public void save(T fromPojo, Entity toEntity)
{
// The default is to index all fields
this.rootSaver.save(fromPojo, toEntity, true);
}
}