package com.googlecode.objectify.impl.translate; import com.google.appengine.api.datastore.PropertyContainer; import com.google.common.base.Predicate; import com.googlecode.objectify.ObjectifyFactory; import com.googlecode.objectify.annotation.AlsoLoad; import com.googlecode.objectify.annotation.Id; import com.googlecode.objectify.annotation.Ignore; import com.googlecode.objectify.annotation.Index; import com.googlecode.objectify.annotation.OnLoad; import com.googlecode.objectify.annotation.OnSave; import com.googlecode.objectify.annotation.Parent; import com.googlecode.objectify.annotation.Unindex; import com.googlecode.objectify.impl.FieldProperty; import com.googlecode.objectify.impl.MethodProperty; import com.googlecode.objectify.impl.Path; import com.googlecode.objectify.impl.Property; import com.googlecode.objectify.impl.PropertyPopulator; import com.googlecode.objectify.impl.TypeUtils; import java.lang.annotation.Annotation; import java.lang.reflect.Field; import java.lang.reflect.Method; import java.lang.reflect.Modifier; import java.util.ArrayList; import java.util.List; import java.util.logging.Level; import java.util.logging.Logger; /** * <p>Used by translators to populate properties between POJO and PropertiesContainer. Unlike * translators, this does not create the POJO or container, it just copies translated properties * between them.</p> * * <p>Always excludes the key fields, @Id and @Parent.</p> * * @author Jeff Schnitzer <jeff@infohazard.org> */ public class ClassPopulator<P> implements Populator<P> { private static final Logger log = Logger.getLogger(ClassPopulator.class.getName()); /** We do not persist fields with any of these modifiers */ private static final int NOT_SAVEABLE_MODIFIERS = Modifier.FINAL | Modifier.STATIC; /** We don't want to include the key fields in population */ private static final Predicate<Property> INCLUDED_FIELDS = new Predicate<Property>() { @Override public boolean apply(Property prop) { return prop.getAnnotation(Id.class) == null && prop.getAnnotation(Parent.class) == null; } }; /** */ private final Class<P> clazz; /** Populator for the superclass */ private final Populator<? super P> superPopulator; /** Only includes fields declared on this class */ private final List<PropertyPopulator<Object, Object>> props = new ArrayList<>(); /** Three-state index instruction for the whole class. Null means "leave it as-is". */ private final Boolean indexInstruction; /** */ private final List<LifecycleMethod> onSaveMethods = new ArrayList<>(); private final List<LifecycleMethod> onLoadMethods = new ArrayList<>(); /** */ public ClassPopulator(Class<P> clazz, CreateContext ctx, Path path) { this.clazz = clazz; // Recursively climb the superclass chain this.superPopulator = ctx.getPopulator(clazz.getSuperclass(), path); if (log.isLoggable(Level.FINEST)) log.finest("Creating class translator for " + clazz.getName() + " at path '"+ path + "'"); indexInstruction = getIndexInstruction(clazz); // Find all the basic properties for (Property prop: getDeclaredProperties(ctx.getFactory(), clazz)) { if (INCLUDED_FIELDS.apply(prop)) { Path propPath = path.extend(prop.getName()); try { Translator<Object, Object> translator = ctx.getTranslator(new TypeKey<>(prop), ctx, propPath); PropertyPopulator<Object, Object> tprop = new PropertyPopulator<>(prop, translator); props.add(tprop); } catch (Exception ex) { // Catch any errors during this process and wrap them in an exception that exposes more useful information. propPath.throwIllegalState("Error registering " + clazz.getName(), ex); } } } // Find the @OnSave methods for (Method method: clazz.getDeclaredMethods()) { if (method.isAnnotationPresent(OnSave.class)) onSaveMethods.add(new LifecycleMethod(method)); if (method.isAnnotationPresent(OnLoad.class)) onLoadMethods.add(new LifecycleMethod(method)); } } /* */ @Override public void load(PropertyContainer node, LoadContext ctx, Path path, final P into) { superPopulator.load(node, ctx, path, into); ctx.enterContainerContext(into); try { for (PropertyPopulator<Object, Object> prop: props) { prop.load(node, ctx, path, into); } } finally { ctx.exitContainerContext(into); } // If there are any @OnLoad methods, call them after everything else if (!onLoadMethods.isEmpty()) { ctx.defer(new Runnable() { @Override public void run() { for (LifecycleMethod method: onLoadMethods) method.execute(into); } @Override public String toString() { return "(deferred invoke " + clazz + " @OnLoad callbacks on " + into + ")"; } }); } } /* */ @Override public void save(P pojo, boolean index, SaveContext ctx, Path path, PropertyContainer into) { superPopulator.save(pojo, index, ctx, path, into); // Must do @OnSave methods after superclass but before actual population if (!ctx.skipLifecycle() && !onSaveMethods.isEmpty()) for (LifecycleMethod method: onSaveMethods) method.execute(pojo); if (indexInstruction != null) index = indexInstruction; for (PropertyPopulator<Object, Object> prop: props) { prop.save(pojo, index, ctx, path, into); } } /** * Figure out if there is an index instruction for the whole class. * @return true, false, or null (which means no info) */ private Boolean getIndexInstruction(Class<P> clazz) { Index ind = clazz.getAnnotation(Index.class); Unindex unind = clazz.getAnnotation(Unindex.class); if (ind != null && unind != null) throw new IllegalStateException("You cannot have @Index and @Unindex on the same class: " + clazz); if (ind != null) return true; else if (unind != null) return false; else return null; } /** * Determine if we should create a Property for the field. Things we ignore: static, final, @Ignore, synthetic */ private boolean isOfInterest(Field field) { return !field.isAnnotationPresent(Ignore.class) && ((field.getModifiers() & NOT_SAVEABLE_MODIFIERS) == 0) && !field.isSynthetic() && !field.getName().startsWith("bitmap$init"); // Scala adds a field bitmap$init$0 and bitmap$init$1 etc } /** * Determine if we should create a Property for the method (ie, @AlsoLoad) */ private boolean isOfInterest(Method method) { for (Annotation[] annos: method.getParameterAnnotations()) if (TypeUtils.getAnnotation(annos, AlsoLoad.class) != null) return true; return false; } /** * Get all the persistable fields and methods declared on a class. Ignores superclasses. * * @return the fields we load and save, including @Id and @Parent fields. All fields will be set accessable * and returned in order of declaration. */ private List<Property> getDeclaredProperties(ObjectifyFactory fact, Class<?> clazz) { List<Property> good = new ArrayList<>(); for (Field field: clazz.getDeclaredFields()) if (isOfInterest(field)) good.add(new FieldProperty(fact, clazz, field)); for (Method method: clazz.getDeclaredMethods()) if (isOfInterest(method)) good.add(new MethodProperty(method)); return good; } }