package com.googlecode.objectify.impl.load;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.lang.reflect.Array;
import java.util.Collection;
import com.google.appengine.api.datastore.Blob;
import com.google.appengine.api.datastore.Text;
import com.googlecode.objectify.Key;
import com.googlecode.objectify.ObjectifyFactory;
import com.googlecode.objectify.impl.LoadContext;
import com.googlecode.objectify.impl.TypeUtils;
import com.googlecode.objectify.impl.Wrapper;
/**
* <p>Setter which knows how to set any kind of leaf value. This could be any basic type
* or a collection of basic types; basically anything except an @Embedded.</p>
*
* <p>This class is a little weird. It is a termination setter which itself cannot be
* extended. However, when constructed, it actually sets its next value to be a more-specific
* type of setter defined by the three inner classes: ForBasic, ForArray, and ForCollection.
* This allows us to break up the logic a bit better.</p>
*
* <p>This is always the termination of a setter chain; the {@code next} value is ignored.</p>
*/
public class LeafSetter extends CollisionDetectingSetter
{
/** Knows how to set basic noncollection and nonarray values */
class ForBasic extends Setter
{
@Override
public void set(Object toPojo, Object value, LoadContext context)
{
field.set(toPojo, importBasic(value, field.getType()));
}
}
/** Knows how to set arrays of basic types */
class ForArray extends Setter
{
Class<?> componentType = field.getType().getComponentType();
@Override
public void set(Object toPojo, Object value, LoadContext context)
{
if (value == null)
{
field.set(toPojo, null);
}
else
{
if (!(value instanceof Collection<?>))
throw new IllegalStateException("Cannot load non-collection value '" + value + "' into " + field);
Collection<?> datastoreCollection = (Collection<?>)value;
Object array = Array.newInstance(this.componentType, datastoreCollection.size());
int index = 0;
for (Object componentValue: datastoreCollection)
{
componentValue = importBasic(componentValue, this.componentType);
Array.set(array, index++, componentValue);
}
field.set(toPojo, array);
}
}
}
/**
* <p>Deals with collections of basic types.</p>
*
* <p>The special considerations for collections follows
* {@link TypeUtils#prepareCollection(Object, Wrapper)}</p>
*/
class ForCollection extends Setter
{
Class<?> componentType = TypeUtils.getComponentType(field.getType(), field.getGenericType());
@Override
public void set(Object toPojo, Object value, LoadContext context)
{
if (value == null)
{
field.set(toPojo, null);
}
else
{
if (!(value instanceof Collection<?>))
throw new IllegalStateException("Cannot load non-collection value '" + value + "' into " + field);
Collection<?> datastoreCollection = (Collection<?>)value;
Collection<Object> target = TypeUtils.prepareCollection(toPojo, field, datastoreCollection.size());
for (Object datastoreValue: datastoreCollection)
target.add(importBasic(datastoreValue, componentType));
}
}
}
/** Need one of these to convert keys */
ObjectifyFactory factory;
/** The field or method we set */
Wrapper field;
/** If true, we expect a Blob and need to de-serialize it */
boolean serialized;
/** */
public LeafSetter(ObjectifyFactory fact, Wrapper field, Collection<String> collisionPaths)
{
super(collisionPaths);
this.factory = fact;
this.field = field;
this.serialized = field.isSerialized();
if (!this.serialized && field.getType().isArray())
{
// byte[] is a special case because it gets converted to and from blob
if (field.getType().getComponentType() == Byte.TYPE)
this.next = new ForBasic();
else
this.next = new ForArray();
}
else if (!this.serialized && Collection.class.isAssignableFrom(field.getType()))
{
this.next = new ForCollection();
}
else
{
this.next = new ForBasic();
}
}
/* (non-Javadoc)
* @see com.googlecode.objectify.impl.load.CollisionDetectingSetter#safeSet(java.lang.Object, java.lang.Object, com.googlecode.objectify.impl.LoadContext)
*/
@Override
protected void safeSet(Object obj, Object value, LoadContext context)
{
this.next.set(obj, value, context);
}
/**
* Converts a value obtained from the datastore into what gets sent on the field.
* The datastore translates values in ways that are not always convenient; for
* example, all numbers become Long and booleans become Boolean. This method translates
* just the basic types - not collection types.
*
* @param fromValue is the property value that came out of the datastore Entity
* @param toType is the type to convert it to.
*/
@SuppressWarnings("unchecked")
Object importBasic(Object fromValue, Class<?> toType)
{
if (fromValue == null)
{
return null;
}
else if (this.serialized)
{
// Above all others, if we're serialized, take the Blob and deserialize it.
if (!(fromValue instanceof Blob))
throw new IllegalStateException("Tried to deserialize non-Blob " + fromValue + " for field " + this.field);
try
{
ByteArrayInputStream bais = new ByteArrayInputStream(((Blob)fromValue).getBytes());
ObjectInputStream ois = new ObjectInputStream(bais);
return ois.readObject();
}
catch (IOException ex) { throw new RuntimeException(ex); }
catch (ClassNotFoundException ex) { throw new IllegalStateException("Unable to deserialize " + fromValue + " on field " + this.field + ": " + ex); }
}
else if (toType.isAssignableFrom(fromValue.getClass()))
{
return fromValue;
}
else if (toType == String.class)
{
if (fromValue instanceof Text)
return ((Text) fromValue).getValue();
else
return fromValue.toString();
}
else if (Enum.class.isAssignableFrom(toType))
{
// Anyone have any idea how to avoid this generics warning?
return Enum.valueOf((Class<Enum>) toType, fromValue.toString());
}
else if ((toType == Boolean.TYPE) && (fromValue instanceof Boolean))
{
return fromValue;
}
else if (fromValue instanceof Number)
{
return coerceNumber((Number) fromValue, toType);
}
else if (Key.class.isAssignableFrom(toType) && fromValue instanceof com.google.appengine.api.datastore.Key)
{
return this.factory.rawKeyToTypedKey((com.google.appengine.api.datastore.Key) fromValue);
}
else if (fromValue instanceof Blob && toType.isArray() && toType.getComponentType() == Byte.TYPE)
{
return ((Blob)fromValue).getBytes();
}
throw new IllegalArgumentException("Don't know how to convert " + fromValue.getClass() + " to " + toType);
}
/**
* Coerces the value to be a number of the specified type; needed because
* all numbers come back from the datastore as Long and this screws up
* any type that expects something smaller. Also does toString just for the
* hell of it.
*/
Object coerceNumber(Number value, Class<?> type)
{
if ((type == Byte.class) || (type == Byte.TYPE)) return value.byteValue();
else if ((type == Short.class) || (type == Short.TYPE)) return value.shortValue();
else if ((type == Integer.class) || (type == Integer.TYPE)) return value.intValue();
else if ((type == Long.class) || (type == Long.TYPE)) return value.longValue();
else if ((type == Float.class) || (type == Float.TYPE)) return value.floatValue();
else if ((type == Double.class) || (type == Double.TYPE)) return value.doubleValue();
else if (type == String.class) return value.toString();
else throw new IllegalArgumentException("Don't know how to convert " + value.getClass() + " to " + type);
}
/** Ensure that nobody tries to extend the leaf nodes. */
@Override
final public Setter extend(Setter tail)
{
throw new UnsupportedOperationException("Objectify bug - cannot extend Leaf setters");
}
}