package org.hivedb.serialization;
import com.thoughtworks.xstream.XStream;
import com.thoughtworks.xstream.converters.Converter;
import com.thoughtworks.xstream.converters.MarshallingContext;
import com.thoughtworks.xstream.converters.SingleValueConverterWrapper;
import com.thoughtworks.xstream.converters.UnmarshallingContext;
import com.thoughtworks.xstream.io.HierarchicalStreamReader;
import com.thoughtworks.xstream.io.HierarchicalStreamWriter;
import org.hivedb.annotations.AnnotationHelper;
import org.hivedb.util.PrimitiveUtils;
import org.hivedb.util.classgen.GeneratedClassFactory;
import org.hivedb.util.classgen.GeneratedInstanceInterceptor;
import org.hivedb.util.classgen.ReflectionTools;
import org.hivedb.util.functional.Filter;
import org.hivedb.util.functional.Predicate;
import org.hivedb.util.functional.Transform;
import org.hivedb.util.functional.Transform.IdentityFunction;
import org.hivedb.util.functional.Unary;
import org.hivedb.versioning.Modernizer;
import org.hivedb.versioning.XmlModernizationPaver;
import org.hivedb.versioning.XmlModernizationPaverImpl;
import java.io.InputStream;
import java.lang.reflect.Method;
import java.util.*;
/**
* An XStream implementation of Serializer. Serializes and deserializes from the given class
* and a compressed InputStream of XML.
* @author alikuski@cafepress.com
*
* @param <RAW>
*/
public class XmlXStreamSerializer<RAW> implements Serializer<RAW, InputStream> {
private Class<RAW> clazz;
private XStream xStream;
private Map<Class<?>, ClassXmlTransformer<?>> classXmlTransformerMap;
/**
* Constructs a serializer for the given interface. The implementation of the interface
* is generated by CGLib and named based on the @GeneratedClass attribute on the interface.
* If an implementation is provided instead of an interface then no generation occurs.
* @param clazz
*/
public XmlXStreamSerializer(final Class<?> clazz)
{
this(clazz, new Hashtable<Class<?>, XmlModernizationPaver<?>>());
}
/**
* Constructs a serializer like the other constructor, but additionally accepts an XmlModernizationPaver
* that modernizes old versions of XML to the current version. An XmlModernizationPaver is only
* neccessary when explicit modifications are needed to make an old blob compatible with the
* current version of the class (e.g. deletion of a property.)
* @param clazz
* @param xmlModernizationPaverMap
*/
@SuppressWarnings("unchecked")
public XmlXStreamSerializer(final Class<?> clazz, final Map<Class<?>, XmlModernizationPaver<?>> xmlModernizationPaverMap)
{
this.clazz = (Class<RAW>) clazz;
Collection<Class<?>> propertyTypes = ReflectionTools.getUniqueComplexPropertyTypes(Collections.singletonList(clazz));
classXmlTransformerMap = Transform.toMap(
new IdentityFunction<Class<?>>(),
new Unary<Class<?>, ClassXmlTransformer<?>>() {
public ClassXmlTransformer<?> f(Class propertyType ) {
Collection classes = xmlModernizationPaverMap.keySet();
final Class whichIsImplemented = ReflectionTools.whichIsImplemented(
propertyType,
classes);
return new ClassXmlTransformerImpl(
propertyType,
whichIsImplemented != null
? xmlModernizationPaverMap.get(whichIsImplemented)
: XmlModernizationPaverImpl.getDefaultXmlModernizationPaver());
}},
propertyTypes);
this.xStream = new XStream();
for (ClassXmlTransformer<?> classXmlTransformer : classXmlTransformerMap.values()) {
xStream.registerConverter(new ClassConverter(classXmlTransformer));
Class generatedClass = GeneratedClassFactory.getGeneratedClass(classXmlTransformer.getRespresentedInterface());
xStream.alias(classXmlTransformer.getClassAbbreviation(), generatedClass);
}
}
/**
* Serializes the given instance to a compressed XML InputStream. Compression of the XML is a two
* step process. First the element names are abbreviated using the @Abbreviate element on each
* class name or getter, or using the default deterministic abbreviation algorithm. Second the
* XML is streamed and compressed using GZIP.
*/
@SuppressWarnings("unchecked")
public InputStream serialize(final RAW raw) {
RAW objectToSerialize = (RAW) resolveClassXmlTransformer(raw).wrapInSerializingImplementation(raw);
final String xml = xStream.toXML(objectToSerialize);
return Compression.compress(xml);
}
/**
* Deserializes the given InputStream by decompressing the GZIP to the underlying XML and then
* elongating the abbreviated XML element names to their full class and property names. During
* deserialization, the given XMLModernizationPaver may modify various element names or their
* values to bring the XML data up to date with the current version of the class.
*
* Additionally, if properties exist in the class that are not present in the deserialized XML,
* the properties will be initialized to empty lists if they are collections and left null otherwise.
* It is ideal to have no null properties, so in the future all fields may require initialization
* to default values.
*
*/
@SuppressWarnings("unchecked")
public RAW deserialize(InputStream serial) {
return (RAW)xStream.fromXML(Compression.decompress(serial));
}
private ClassXmlTransformer resolveClassXmlTransformer(final Object instance) {
return classXmlTransformerMap.get(
Filter.grepSingle(new Predicate<Class>() {
public boolean f(Class classXmlClass) {
return ReflectionTools.doesImplementOrExtend(instance.getClass(), classXmlClass);
}},
classXmlTransformerMap.keySet()));
}
public class ClassConverter implements Converter {
private ClassXmlTransformer classXmlTransformer;
public ClassConverter(ClassXmlTransformer classXmlTransformer) {
this.classXmlTransformer = classXmlTransformer;
}
@SuppressWarnings("unchecked")
public void marshal(Object source, HierarchicalStreamWriter writer, MarshallingContext context) {
marshalAttributes(source, writer);
marshalNodes(source, writer, context);
}
@SuppressWarnings("unchecked")
private void marshalAttributes(Object source, HierarchicalStreamWriter writer) {
final Class<?> respresentedInterface = classXmlTransformer.getRespresentedInterface();
for (String propertyName : grepNotIgnored(respresentedInterface, Filter.grepUnique(ReflectionTools.getPropertiesOfPrimitiveGetters(respresentedInterface))))
{
Object value = ReflectionTools.invokeGetter(source, propertyName);
Class<?> fieldClass = ReflectionTools.getPropertyType(respresentedInterface, propertyName);
if (value != null)
try {
String attributeValue = (propertyName.equals(BLOB_VERSION_ATTRIBUTE_ABREVIATION))
? classXmlTransformer.getCurrentXmlVersion().toString()
: ((SingleValueConverterWrapper)xStream.getConverterLookup().lookupConverterForType(fieldClass)).toString(value);
writer.addAttribute(classXmlTransformer.abbreviate(propertyName), attributeValue);
}
catch (Exception e) {
throw new RuntimeException(e);
}
}
}
@SuppressWarnings("unchecked")
private void marshalNodes(Object source, HierarchicalStreamWriter writer, MarshallingContext context) {
final Class<?> respresentedInterface = classXmlTransformer.getRespresentedInterface();
for (String propertyName : grepNotIgnored(respresentedInterface, Filter.grepUnique(ReflectionTools.getPropertiesOfComplexGetters(respresentedInterface))))
{
Object value = ReflectionTools.invokeGetter(source, propertyName);
if (new NullOrEmptyRejector().filter(value)) {
writer.startNode(classXmlTransformer.abbreviate(propertyName));
context.convertAnother(value);
writer.endNode();
}
}
}
private Collection<String> grepNotIgnored(final Class<?> respresentedInterface, Collection<String> collection) {
return Filter.grep(new Predicate<String>() {
public boolean f(String propertyName) {
return ReflectionTools.getPropertyType(respresentedInterface, propertyName) != Class.class &&
AnnotationHelper.getAnnotationDeeply(respresentedInterface, propertyName, SerializerIgnore.class) == null;
}
}, collection);
}
public Object unmarshal(HierarchicalStreamReader reader, UnmarshallingContext context) {
Object instance = classXmlTransformer.createInstance();
Integer xmlVersion = seekXmlVersion(reader, context, (ClassXmlTransformer<Object>) classXmlTransformer);
UnmarshallInterceptor<Object> unmarshallInterceptor = new UnmarshalModernizer<Object>(
(Modernizer<Object>) classXmlTransformer.getModernizer(xmlVersion, classXmlTransformer.getCurrentXmlVersion()));
unmarshalAttributes(reader, instance, unmarshallInterceptor);
unmarshalNodes(reader, context, instance, unmarshallInterceptor);
return unmarshallInterceptor.postUnmarshalInstanceModifier(instance);
}
@SuppressWarnings("unchecked")
private void unmarshalAttributes(HierarchicalStreamReader reader, Object instance, UnmarshallInterceptor<Object> unmarshalInterceptor) {
Iterator iterator = reader.getAttributeNames();
final Class<?> respresentedInterface = classXmlTransformer.getRespresentedInterface();
Map<String,String> abbreviatedNameToPropertyName = Transform.toMap(
new Unary<String,String>() {
public String f(String propertyName) {
return classXmlTransformer.abbreviate(propertyName);
}},
new IdentityFunction<String>(),
ReflectionTools.getPropertiesOfPrimitiveGetters(respresentedInterface));
while (iterator.hasNext())
{
String abreviatedAttributeName = (String)iterator.next();
if (!unmarshalInterceptor.isElementedDeleted(abreviatedAttributeName)) {
String updatedAbreviatedAttributeName = unmarshalInterceptor.preUnmarshalAbreviatedElementNameTransformer(abreviatedAttributeName);
if (!abbreviatedNameToPropertyName.containsKey(updatedAbreviatedAttributeName))
throw new RuntimeException(String.format("The abreviated attribute name %s is not recognized by the ClassXmlTransformer for %s", updatedAbreviatedAttributeName, respresentedInterface.getName()));
String fullAttributeName = abbreviatedNameToPropertyName.get(updatedAbreviatedAttributeName);
// propertyName will match fullAttributeName unless the interceptor changes it
String propertyName = unmarshalInterceptor.preUnmarshalElementNameTransformer(
fullAttributeName);
// Assume attributes are simple converters
SingleValueConverterWrapper converter = getAttributeConverter(
ReflectionTools.getPropertyType(respresentedInterface, propertyName));
try {
ReflectionTools.invokeSetter(
instance,
propertyName,
unmarshalInterceptor.postUnmarshalElementValueTransformer(
propertyName,
converter.fromString(
reader.getAttribute(abreviatedAttributeName))));
}
catch (Exception e) {
throw new RuntimeException(String.format("Error unmarshalling attribute %s of class %s", abreviatedAttributeName, instance.getClass().getSimpleName()), e);
}
}
}
}
@SuppressWarnings("unchecked")
private void unmarshalNodes(HierarchicalStreamReader reader, UnmarshallingContext context, Object instance, UnmarshallInterceptor<Object> unmarshalInterceptor) {
final Class<?> respresentedInterface = classXmlTransformer.getRespresentedInterface();
Map<String,String> abbreviatedNameToPropertyName = Transform.toMap(
new Unary<String,String>() {
public String f(String propertyName) {
return classXmlTransformer.abbreviate(propertyName);
}},
new IdentityFunction<String>(),
ReflectionTools.getPropertiesOfComplexGetters(respresentedInterface));
while (reader.hasMoreChildren())
{
reader.moveDown();
String abreviatedNodeName = reader.getNodeName();
if (!unmarshalInterceptor.isElementedDeleted(abreviatedNodeName)) {
String updatedAbreviatedNodeName = unmarshalInterceptor.preUnmarshalAbreviatedElementNameTransformer(abreviatedNodeName);
if (!abbreviatedNameToPropertyName.containsKey(updatedAbreviatedNodeName))
throw new RuntimeException(String.format("The abreviated node name %s is not recognized by the ClassXmlTransformer of %s", updatedAbreviatedNodeName, classXmlTransformer.getRespresentedInterface().getName()));
String fullNodeName = abbreviatedNameToPropertyName.get(updatedAbreviatedNodeName);
// propertyName will match fullNodeName unless the interceptor changes it
String propertyName = unmarshalInterceptor.preUnmarshalElementNameTransformer(fullNodeName);
Class fieldClass = ReflectionTools.isCollectionProperty(
respresentedInterface,
propertyName)
? ArrayList.class
: ReflectionTools.getPropertyType(clazz, propertyName);
ReflectionTools.invokeSetter(
instance,
propertyName,
unmarshalInterceptor.postUnmarshalElementValueTransformer(
propertyName,
context.convertAnother(instance,fieldClass)));
}
reader.moveUp();
}
// Initialize missing collections to empty, null is inappropriate
for (Method getter: ReflectionTools.getCollectionGetters(respresentedInterface))
try {
if (getter.invoke(instance, new Object[] {}) == null)
GeneratedInstanceInterceptor.setProperty(instance, ReflectionTools.getPropertyNameOfAccessor(getter), new ArrayList());
} catch (Exception e) {
throw new RuntimeException(e);
}
}
final String BLOB_VERSION_ATTRIBUTE_ABREVIATION = Blobbable.BLOB_VERSION_ABBREVIATION;
private Integer seekXmlVersion(HierarchicalStreamReader reader, UnmarshallingContext context, ClassXmlTransformer<Object> classXmlTransformer) {
String version = reader.getAttribute(BLOB_VERSION_ATTRIBUTE_ABREVIATION);
if (version != null)
context.put(BLOB_VERSION_ATTRIBUTE_ABREVIATION, version);
else
version = (String) context.get(BLOB_VERSION_ATTRIBUTE_ABREVIATION);
if (version == null)
throw new RuntimeException("No XML blob version found in XML or MarshallingContext");
SingleValueConverterWrapper converter = getAttributeConverter(Integer.class);
return (Integer) converter.fromString(version);
}
private SingleValueConverterWrapper getAttributeConverter(Class fieldClass) {
return (SingleValueConverterWrapper)xStream.getConverterLookup().lookupConverterForType(fieldClass);
}
public boolean canConvert(Class type) {
return !PrimitiveUtils.isPrimitiveClass(type)
&& ReflectionTools.doesImplementOrExtend(
type,
this.classXmlTransformer.getRespresentedInterface());
}
}
// This interface maps one-to-one to Modernizer, but is named with more general methods
// for other possible uses.
protected interface UnmarshallInterceptor<T>
{
String preUnmarshalAbreviatedElementNameTransformer(String abreviatedElementName);
String preUnmarshalElementNameTransformer(String elementName);
Boolean isElementedDeleted(String abreviatedElementName);
Object postUnmarshalElementValueTransformer(String elementName, Object elementValue);
T postUnmarshalInstanceModifier(T instance);
}
protected static class UnmarshalModernizer<T> implements UnmarshallInterceptor<T> {
Modernizer<T> modernizer;
public UnmarshalModernizer(Modernizer<T> modernizer)
{
this.modernizer = modernizer;
}
public String preUnmarshalAbreviatedElementNameTransformer(String abreviatedElementName) {
return modernizer.getNewAbreviatedElementName(abreviatedElementName);
}
/**
* Preprocesses an XML element name, either an attribute or node name, and possibly modifies the name
* @param elementName the name of the XML attribute or node
* @return The optionally modified name
*/
public String preUnmarshalElementNameTransformer(String elementName) {
return modernizer.getNewElementName(elementName);
}
/**
* Postproccess an object deserialized from XML, either an attribute value or a node.
* @param object
* @return
*/
public Object postUnmarshalElementValueTransformer(String elementName, Object elementValue) {
return modernizer.getUpdatedElementValue(elementName, elementValue);
}
/**
* Reports whether or not an element represented by the given name, either of an attribute or node,
* has been deleted and is no longer represented in the instance.
*/
public Boolean isElementedDeleted(String elementName) {
return modernizer.isDeletedElement(elementName);
}
public T postUnmarshalInstanceModifier(T instance) {
return modernizer.modifyInstance(instance);
}
}
public static class NullOrEmptyRejector
{
public boolean filter(Object value) {
return value != null
&& !(ReflectionTools.doesImplementOrExtend(value.getClass(), Collection.class) && ((Collection)value).size()==0);
}
}
public Integer getCurrentClassVersion() {
return classXmlTransformerMap.get(clazz).getCurrentXmlVersion();
}
}