package com.googlecode.objectify.impl.save;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;
import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Collection;
import com.google.appengine.api.datastore.Blob;
import com.google.appengine.api.datastore.Entity;
import com.google.appengine.api.datastore.Text;
import com.googlecode.objectify.Key;
import com.googlecode.objectify.ObjectifyFactory;
import com.googlecode.objectify.annotation.Serialized;
import com.googlecode.objectify.impl.TypeUtils;
/**
* <p>Saver which knows how to save basic leaf values. Leaf values are things that
* go into the datastore: basic types or collections of basic types. Basically
* anything except an @Embedded.</p>
*/
public class LeafFieldSaver extends FieldSaver
{
/** We need this to translate keys */
ObjectifyFactory factory;
/** If true, we add values to a collection inside the entity */
boolean collectionize;
/** If true, we serialize the value into a Blob */
boolean serialize;
/** If true, null values are not saved. Leaf collection types are treated this way. */
boolean ignoreIfNull;
/**
* @param field must be a noncollection, nonarray type if collectionize is true
* @param collectionize when true will cause this leaf saver to persist simple basic
* types in a collection inside the entity property. If set is called multiple times,
* the collection will be appended to.
*/
public LeafFieldSaver(ObjectifyFactory fact, String pathPrefix, Class<?> examinedClass, Field field, boolean collectionize)
{
super(pathPrefix, examinedClass, field, collectionize);
this.factory = fact;
this.collectionize = collectionize;
this.serialize = field.isAnnotationPresent(Serialized.class);
if (this.collectionize)
if (!this.serialize && TypeUtils.isArrayOrCollection(field.getType()))
throw new IllegalStateException("Cannot place array or collection properties inside @Embedded arrays or collections. The offending field is " + field);
// Don't save null arrays or collections
if (!this.serialize && TypeUtils.isArrayOrCollection(field.getType()))
this.ignoreIfNull = true;
}
/* (non-Javadoc)
* @see com.googlecode.objectify.impl.save.FieldSaver#saveValue(java.lang.Object, com.google.appengine.api.datastore.Entity, boolean)
*/
@Override
@SuppressWarnings("unchecked")
public void saveValue(Object value, Entity entity, boolean index)
{
value = this.prepareForSave(value);
// Maybe we are supposed to ignore this
if (value == null && this.ignoreIfNull)
return;
if (this.collectionize)
{
Collection<Object> savedCollection = (Collection<Object>)entity.getProperty(this.path);
if (savedCollection == null)
{
savedCollection = new ArrayList<Object>();
this.setEntityProperty(entity, savedCollection, index);
}
savedCollection.add(value);
}
else
{
if (value instanceof Collection && ((Collection)value).isEmpty())
{
// We do not save empty collections
// COLLECTION STATE TRACKING: we could store this (and null) as an out-of-band property
}
else
{
this.setEntityProperty(entity, value, index);
}
}
}
/**
* Converts the value into an object suitable for storing in the datastore. This is
* the "parallel" method of LeafSetter; not that because outbound processing of arrays and
* collections is so simple, we don't have an object hierarchy to deal with that case.
*
* @param value can be any basic type or an array or collection of basic types
* @return something that can be saved in the datastore; note that arrays are always
* converted to collections.
*/
protected Object prepareForSave(Object value)
{
if (value == null)
{
return null;
}
else if (this.serialize)
{
// If it's @Serialized, we serialize it no matter what it looks like
try
{
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos);
oos.writeObject(value);
return new Blob(baos.toByteArray());
}
catch (IOException ex) { throw new RuntimeException(ex); }
}
else if (value instanceof String)
{
// Check to see if it's too long and needs to be Text instead
if (((String)value).length() > 500)
{
if (this.collectionize)
throw new IllegalStateException("Objectify cannot autoconvert Strings greater than 500 characters to Text within @Embedded collections." +
" You must use Text for the field type instead." +
" This is what you tried to save into " + this.field + ": " + value);
return new Text((String)value);
}
}
else if (value instanceof Enum<?>)
{
return ((Enum<?>)value).name();
}
else if (value.getClass().isArray())
{
if (value.getClass().getComponentType() == Byte.TYPE)
{
// Special case! byte[] gets turned into Blob.
return new Blob((byte[])value);
}
else
{
// The datastore cannot persist arrays, but it can persist ArrayList
int length = Array.getLength(value);
ArrayList<Object> list = new ArrayList<Object>(length);
for (int i=0; i<length; i++)
list.add(this.prepareForSave(Array.get(value, i)));
return list;
}
}
else if (value instanceof Collection<?>)
{
// All collections get turned into a List that preserves the order. We must
// also be sure to convert anything contained in the collection
ArrayList<Object> list = new ArrayList<Object>(((Collection<?>)value).size());
for (Object obj: (Collection<?>)value)
list.add(this.prepareForSave(obj));
return list;
}
else if (value instanceof Key<?>)
{
return this.factory.typedKeyToRawKey((Key<?>)value);
}
// Usually we just want to return the value
return value;
}
}