All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.googlecode.objectify.impl.Transmog Maven / Gradle / Ivy

Go to download

*** THIS VERSION UPLOADED FOR USE WITH CEDAR-COMMON, TO AVOID DEPENDENCIES ON GOOGLE CODE-BASED MAVEN REPOSITORIES. *** The simplest convenient interface to the Google App Engine datastore

The newest version!
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.impl.TypeUtils.FieldMetadata;
import com.googlecode.objectify.impl.TypeUtils.MethodMetadata;
import com.googlecode.objectify.impl.conv.Conversions;
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.EmbeddedMapSetter;
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;
import com.googlecode.objectify.impl.save.Path;

/**
 * 

Class which knows how to load data from Entity to POJO and save data from POJO to Entity.

* *

Note that this class completely ignores @Id and @Parent fields.

* *

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.

* *

Also realize that there are two separate dimensions to understand. Misunderstanding * the two related graphs will make this code very confusing:

*
    *
  • 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.
  • *
  • There is an object graph, which branches at @Embedded arrays. The runtime execution * code must traverse this graph when setting and saving entities.
  • *
* *

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:

*
    *
  • When loading, Transmog iterates 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.
  • *
  • When saving, Transmog recurses 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.
  • *
* * @author Jeff Schnitzer */ public class Transmog { /** */ Conversions conversions; /** Useful to have around for error logging purposes */ Class clazz; /** Maps full "blah.blah.blah" property name to a particular Setter implementation */ Map rootSetters = new HashMap(); /** The root saver that knows how to persist an object of type T */ ClassSaver rootSaver; /** *

Object which visits various levels of the pojo class graph does two things: *

    *
  1. Builds up the rootSetters map in {@code rootSetters}.
  2. *
  3. Validates the structure of the pojo.
  4. *
* *

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.

*/ class Visitor { Setter setterChain; String prefix; // starts null for root boolean embedded; Set fieldPathsUsed; Set methodPathsUsed; /** Constructs a visitor for a top-level entity */ public Visitor() { this.setterChain = new RootSetter(); this.fieldPathsUsed = new HashSet(); this.methodPathsUsed = new HashSet(); } /** * Constructs a visitor for an embedded object. * @param setterChain is the root of the setter chain */ public Visitor(Setter setterChain, String prefix, Set fieldPathsUsed, Set 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 currentClazz is the class to inspect */ public void visitClass(Class currentClazz) { // Only good fields come back from this method call List fields = TypeUtils.getPesistentFields(currentClazz, this.embedded); for (FieldMetadata meta: fields) this.visitField(meta.field, meta.names); // Only good methods come back from this method call List methods = TypeUtils.getAlsoLoadMethods(currentClazz); 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 names) { List paths = this.namesToPaths(names); for (String path: paths) { List collisions = makeCollisions(paths, path); LeafSetter setter = new LeafSetter(conversions, 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 names) { List paths = this.namesToPaths(names); if (TypeUtils.isEmbedded(field)) { if (field.getType().isArray()) { Class visitType = field.getType().getComponentType(); for (String path: paths) { List 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 (Map.class.isAssignableFrom(field.getType())) { Class visitType = TypeUtils.getMapValueType(field.getGenericType()); for (String path : paths) { List collisions = makeCollisions(paths, path); EmbeddedMapSetter setter = new EmbeddedMapSetter(field, visitType, conversions, collisions); addRootSetter(path, setter, false); } } else if (Collection.class.isAssignableFrom(field.getType())) { Class visitType = TypeUtils.getComponentType(field.getType(), field.getGenericType()); for (String path: paths) { List 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 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 collisions = makeCollisions(paths, path); LeafSetter setter = new LeafSetter(conversions, new FieldWrapper(field), collisions); this.addRootSetter(path, setter, false); } } } /** * Translates names to paths based on the current path prefix. */ private List namesToPaths(Collection names) { List paths = new ArrayList(); 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 makeCollisions(Collection paths, String forPath) { if (paths.size() > 1) { List collisions = new ArrayList(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 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(Conversions conversions, Class clazz) { this.conversions = conversions; 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(conversions, 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 property: fromEntity.getProperties().entrySet()) { String key = property.getKey(); Object value = property.getValue(); loadSingleValue(key, value, toPojo, context); } context.done(); } /** * Loads the single value {@code value} into {@code toPojo} using {@code key} as the key within * the entity. Will use any of {@link #rootSetters} or might delegate through a map setter. * * @param key the key for this value * @param value the value from the datastore * @param toPojo the target pojo to load into */ public void loadSingleValue(String key, Object value, Object toPojo, LoadContext context) { Setter setter = this.rootSetters.get(key); if (setter != null) { setter.set(toPojo, value, context); } else { String mapPrefix = key; int lastDotIndex = mapPrefix.lastIndexOf('.'); while (setter == null && lastDotIndex != -1) { mapPrefix = mapPrefix.substring(0, lastDotIndex); setter = this.rootSetters.get(mapPrefix); if (setter != null) { int mapKeyEnd = mapPrefix.length() + 1; int followingDot = key.indexOf('.', mapKeyEnd); if (followingDot == -1) { context.currentMapEntry = key.substring(mapKeyEnd); context.currentMapSuffix = ""; } else { context.currentMapEntry = key.substring(mapKeyEnd, followingDot); context.currentMapSuffix = key.substring(followingDot + 1); } break; } lastDotIndex = mapPrefix.lastIndexOf('.'); } if (setter != null) { setter.set(toPojo, value, context); } } } /** * 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, Path.root(), true); } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy