Skip to content

Instantly share code, notes, and snippets.

@jkschoen
Created February 6, 2013 16:13
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save jkschoen/4723660 to your computer and use it in GitHub Desktop.
Save jkschoen/4723660 to your computer and use it in GitHub Desktop.
Abstract class to use write a custom converter for a class, but for only certain fields.
import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.lang.reflect.ParameterizedType;
import java.util.AbstractList;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import com.thoughtworks.xstream.converters.ConversionException;
import com.thoughtworks.xstream.converters.MarshallingContext;
import com.thoughtworks.xstream.converters.SingleValueConverter;
import com.thoughtworks.xstream.converters.UnmarshallingContext;
import com.thoughtworks.xstream.converters.reflection.ObjectAccessException;
import com.thoughtworks.xstream.converters.reflection.PureJavaReflectionProvider;
import com.thoughtworks.xstream.converters.reflection.ReflectionConverter;
import com.thoughtworks.xstream.converters.reflection.ReflectionProvider;
import com.thoughtworks.xstream.core.ReferencingMarshallingContext;
import com.thoughtworks.xstream.core.util.ArrayIterator;
import com.thoughtworks.xstream.core.util.FastField;
import com.thoughtworks.xstream.core.util.HierarchicalStreams;
import com.thoughtworks.xstream.core.util.Primitives;
import com.thoughtworks.xstream.io.ExtendedHierarchicalStreamWriterHelper;
import com.thoughtworks.xstream.io.HierarchicalStreamReader;
import com.thoughtworks.xstream.io.HierarchicalStreamWriter;
import com.thoughtworks.xstream.mapper.CannotResolveClassException;
import com.thoughtworks.xstream.mapper.Mapper;
public abstract class CustomConverter<T> extends ReflectionConverter {
private Map<String, String[]> customFields;
private transient ReflectionProvider pureJavaReflectionProvider;
public CustomConverter(Mapper mapper,
ReflectionProvider reflectionProvider,
Map<String, String[]> customFields) {
super(mapper, reflectionProvider);
this.customFields = customFields;
if (this.customFields == null) {
this.customFields = new HashMap<String, String[]>();
}
}
public abstract void customMarshal(Object test,
HierarchicalStreamWriter writer, MarshallingContext context);
public abstract Object customUnmarshal(String nodeName,
final Object result, HierarchicalStreamReader reader,
UnmarshallingContext context, Set seenFields);
public boolean canConvert(Class type) {
ParameterizedType parameterizedType = (ParameterizedType) getClass()
.getGenericSuperclass();
Class<?> clazzz = (Class) parameterizedType.getActualTypeArguments()[0];
return clazzz.isAssignableFrom(type);
}
// below is only slightly modified versions from
// com.thoughtworks.xstream.converters.reflection.AbstractReflectionConverter
protected void doMarshal(final Object source,
final HierarchicalStreamWriter writer,
final MarshallingContext context) {
final List fields = new ArrayList();
final Map defaultFieldDefinition = new HashMap();
// Attributes might be preferred to child elements ...
reflectionProvider.visitSerializableFields(source,
new ReflectionProvider.Visitor() {
final Set writtenAttributes = new HashSet();
public void visit(String fieldName, Class type,
Class definedIn, Object value) {
if (!mapper.shouldSerializeMember(definedIn, fieldName)) {
return;
}
if (!defaultFieldDefinition.containsKey(fieldName)) {
Class lookupType = source.getClass();
defaultFieldDefinition.put(fieldName,
reflectionProvider.getField(lookupType,
fieldName));
}
SingleValueConverter converter = mapper
.getConverterFromItemType(fieldName, type,
definedIn);
if (converter != null) {
final String attribute = mapper
.aliasForAttribute(mapper.serializedMember(
definedIn, fieldName));
if (value != null) {
if (writtenAttributes.contains(fieldName)) { // TODO:
// use
// attribute
throw new ConversionException(
"Cannot write field with name '"
+ fieldName
+ "' twice as attribute for object of type "
+ source.getClass()
.getName());
}
final String str = converter.toString(value);
if (str != null) {
writer.addAttribute(attribute, str);
}
}
writtenAttributes.add(fieldName);
}
// changed from else to else if. Basically if it is a
// field we want to handle,
// igonore it
else if (!customFields.keySet().contains(fieldName)) {
fields.add(new FieldInfo(fieldName, type,
definedIn, value));
}
}
});
new Object() {
{
for (Iterator fieldIter = fields.iterator(); fieldIter
.hasNext();) {
FieldInfo info = (FieldInfo) fieldIter.next();
if (info.value != null) {
Mapper.ImplicitCollectionMapping mapping = mapper
.getImplicitCollectionDefForFieldName(
source.getClass(), info.fieldName);
if (mapping != null) {
if (context instanceof ReferencingMarshallingContext) {
if (info.value != Collections.EMPTY_LIST
&& info.value != Collections.EMPTY_SET
&& info.value != Collections.EMPTY_MAP) {
ReferencingMarshallingContext refContext = (ReferencingMarshallingContext) context;
refContext.registerImplicit(info.value);
}
}
final boolean isCollection = info.value instanceof Collection;
final boolean isMap = info.value instanceof Map;
final boolean isEntry = isMap
&& mapping.getKeyFieldName() == null;
final boolean isArray = info.value.getClass()
.isArray();
for (Iterator iter = isArray ? new ArrayIterator(
info.value)
: isCollection ? ((Collection) info.value)
.iterator()
: isEntry ? ((Map) info.value)
.entrySet().iterator()
: ((Map) info.value)
.values()
.iterator(); iter
.hasNext();) {
Object obj = iter.next();
final String itemName;
final Class itemType;
if (obj == null) {
itemType = Object.class;
itemName = mapper.serializedClass(null);
} else if (isEntry) {
final String entryName = mapping
.getItemFieldName() != null ? mapping
.getItemFieldName() : mapper
.serializedClass(Map.Entry.class);
Map.Entry entry = (Map.Entry) obj;
ExtendedHierarchicalStreamWriterHelper
.startNode(writer, entryName,
entry.getClass());
writeItem(entry.getKey(), context, writer);
writeItem(entry.getValue(), context, writer);
writer.endNode();
continue;
} else if (mapping.getItemFieldName() != null) {
itemType = mapping.getItemType();
itemName = mapping.getItemFieldName();
} else {
itemType = obj.getClass();
itemName = mapper.serializedClass(itemType);
}
writeField(info.fieldName, itemName, itemType,
info.definedIn, obj);
}
} else {
writeField(info.fieldName, null, info.type,
info.definedIn, info.value);
}
}
}
}
void writeField(String fieldName, String aliasName,
Class fieldType, Class definedIn, Object newObj) {
Class actualType = newObj != null ? newObj.getClass()
: fieldType;
ExtendedHierarchicalStreamWriterHelper
.startNode(
writer,
aliasName != null ? aliasName : mapper
.serializedMember(source.getClass(),
fieldName), actualType);
if (newObj != null) {
Class defaultType = mapper
.defaultImplementationOf(fieldType);
if (!actualType.equals(defaultType)) {
String serializedClassName = mapper
.serializedClass(actualType);
if (!serializedClassName.equals(mapper
.serializedClass(defaultType))) {
String attributeName = mapper
.aliasForSystemAttribute("class");
if (attributeName != null) {
writer.addAttribute(attributeName,
serializedClassName);
}
}
}
final Field defaultField = (Field) defaultFieldDefinition
.get(fieldName);
if (defaultField.getDeclaringClass() != definedIn) {
String attributeName = mapper
.aliasForSystemAttribute("defined-in");
if (attributeName != null) {
writer.addAttribute(attributeName,
mapper.serializedClass(definedIn));
}
}
Field field = reflectionProvider.getField(definedIn,
fieldName);
marshallField(context, newObj, field);
}
writer.endNode();
}
void writeItem(Object item, MarshallingContext context,
HierarchicalStreamWriter writer) {
if (item == null) {
String name = mapper.serializedClass(null);
ExtendedHierarchicalStreamWriterHelper.startNode(writer,
name, Mapper.Null.class);
writer.endNode();
} else {
String name = mapper.serializedClass(item.getClass());
ExtendedHierarchicalStreamWriterHelper.startNode(writer,
name, item.getClass());
context.convertAnother(item);
writer.endNode();
}
}
};
// Added this to call our custom part.
customMarshal(source, writer, context);
}
public Object doUnmarshal(final Object result,
final HierarchicalStreamReader reader,
final UnmarshallingContext context) {
final Set seenFields = new HashSet() {
public boolean add(Object e) {
if (!super.add(e)) {
throw new DuplicateFieldException(((FastField) e).getName());
}
return true;
}
};
Iterator it = reader.getAttributeNames();
// Process attributes before recursing into child elements.
while (it.hasNext()) {
String attrAlias = (String) it.next();
// TODO: realMember should return FastField
String attrName = mapper.realMember(result.getClass(),
mapper.attributeForAlias(attrAlias));
boolean fieldExistsInClass = reflectionProvider
.fieldDefinedInClass(attrName, result.getClass());
if (fieldExistsInClass) {
Field field = reflectionProvider.getField(result.getClass(),
attrName);
if (Modifier.isTransient(field.getModifiers())
&& !shouldUnmarshalTransientFields()) {
continue;
}
Class classDefiningField = field.getDeclaringClass();
if (!mapper.shouldSerializeMember(classDefiningField, attrName)) {
continue;
}
SingleValueConverter converter = mapper
.getConverterFromAttribute(classDefiningField,
attrName, field.getType());
Class type = field.getType();
if (converter != null) {
Object value = converter.fromString(reader
.getAttribute(attrAlias));
if (type.isPrimitive()) {
type = Primitives.box(type);
}
if (value != null
&& !type.isAssignableFrom(value.getClass())) {
throw new ConversionException("Cannot convert type "
+ value.getClass().getName() + " to type "
+ type.getName());
}
seenFields.add(new FastField(classDefiningField, attrName));
reflectionProvider.writeField(result, attrName, value,
classDefiningField);
}
}
}
Map implicitCollectionsForCurrentObject = null;
while (reader.hasMoreChildren()) {
reader.moveDown();
String originalNodeName = reader.getNodeName();
/*
* Added below code to break out and do our own custom converstion
* when we hit one of out custom fields.
*/
boolean customField = false;
for (String key : this.customFields.keySet()) {
String[] nodeNames = this.customFields.get(key);
for (String node : nodeNames) {
if (node.equals(originalNodeName)) {
customField = true;
break;
}
}
if (customField) {
break;
}
}
if (customField) {
customUnmarshal(originalNodeName, result, reader, context,
seenFields);
} else {
Class classDefiningField = determineWhichClassDefinesField(reader);
Class fieldDeclaringClass = classDefiningField == null ? result
.getClass() : classDefiningField;
String fieldName = mapper.realMember(fieldDeclaringClass,
originalNodeName);
Mapper.ImplicitCollectionMapping implicitCollectionMapping = mapper
.getImplicitCollectionDefForFieldName(
fieldDeclaringClass, fieldName);
boolean fieldExistsInClass = implicitCollectionMapping == null
&& reflectionProvider.fieldDefinedInClass(fieldName,
fieldDeclaringClass);
Class type = implicitCollectionMapping == null
|| implicitCollectionMapping.getItemType() == null ? determineType(
reader, fieldExistsInClass, result, fieldName,
classDefiningField) : implicitCollectionMapping
.getItemType();
final Object value;
if (fieldExistsInClass) {
Field field = reflectionProvider.getField(
classDefiningField != null ? classDefiningField
: result.getClass(), fieldName);
if ((Modifier.isTransient(field.getModifiers()) && !shouldUnmarshalTransientFields())
|| !mapper.shouldSerializeMember(
field.getDeclaringClass(), fieldName)) {
reader.moveUp();
continue;
}
value = unmarshallField(context, result, type, field);
// TODO the reflection provider should have returned the
// proper
// field in first place ....
Class definedType = reflectionProvider.getFieldType(result,
fieldName, classDefiningField);
if (!definedType.isPrimitive()) {
type = definedType;
}
} else {
if (Map.Entry.class.equals(type)) {
reader.moveDown();
final Object key = context.convertAnother(result,
HierarchicalStreams.readClassType(reader,
mapper));
reader.moveUp();
reader.moveDown();
final Object v = context.convertAnother(result,
HierarchicalStreams.readClassType(reader,
mapper));
reader.moveUp();
value = Collections.singletonMap(key, v).entrySet()
.iterator().next();
} else {
value = type != null ? context.convertAnother(result,
type) : null;
}
}
if (value != null && !type.isAssignableFrom(value.getClass())) {
throw new ConversionException("Cannot convert type "
+ value.getClass().getName() + " to type "
+ type.getName());
}
if (fieldExistsInClass) {
reflectionProvider.writeField(result, fieldName, value,
classDefiningField);
seenFields
.add(new FastField(classDefiningField, fieldName));
} else if (type != null) {
implicitCollectionsForCurrentObject = writeValueToImplicitCollection(
context, value,
implicitCollectionsForCurrentObject, result,
originalNodeName);
}
reader.moveUp();
}
}
if (implicitCollectionsForCurrentObject != null) {
for (Iterator iter = implicitCollectionsForCurrentObject.entrySet()
.iterator(); iter.hasNext();) {
Map.Entry entry = (Map.Entry) iter.next();
Object value = entry.getValue();
if (value instanceof ArraysList) {
Object array = ((ArraysList) value).toPhysicalArray();
reflectionProvider.writeField(result,
(String) entry.getKey(), array, null);
}
}
}
return result;
}
private Class determineWhichClassDefinesField(
HierarchicalStreamReader reader) {
String attributeName = mapper.aliasForSystemAttribute("defined-in");
String definedIn = attributeName == null ? null : reader
.getAttribute(attributeName);
return definedIn == null ? null : mapper.realClass(definedIn);
}
private Class determineType(HierarchicalStreamReader reader,
boolean validField, Object result, String fieldName,
Class definedInCls) {
String classAttribute = HierarchicalStreams.readClassAttribute(reader,
mapper);
if (!validField) {
Class itemType = mapper.getItemTypeForItemFieldName(
result.getClass(), fieldName);
if (itemType != null) {
if (classAttribute != null) {
return mapper.realClass(classAttribute);
}
return itemType;
} else {
String originalNodeName = reader.getNodeName();
if (definedInCls == null) {
for (definedInCls = result.getClass(); definedInCls != null; definedInCls = definedInCls
.getSuperclass()) {
if (!mapper.shouldSerializeMember(definedInCls,
originalNodeName)) {
return null;
}
}
}
try {
return mapper.realClass(originalNodeName);
} catch (CannotResolveClassException e) {
throw new UnknownFieldException(
result.getClass().getName(), fieldName);
}
}
} else {
if (classAttribute != null) {
return mapper.realClass(classAttribute);
}
return mapper.defaultImplementationOf(reflectionProvider
.getFieldType(result, fieldName, definedInCls));
}
}
private Map writeValueToImplicitCollection(UnmarshallingContext context,
Object value, Map implicitCollections, Object result,
String itemFieldName) {
String fieldName = mapper.getFieldNameForItemTypeAndName(context
.getRequiredType(), value != null ? value.getClass()
: Mapper.Null.class, itemFieldName);
if (fieldName != null) {
if (implicitCollections == null) {
implicitCollections = new HashMap(); // lazy instantiation
}
Collection collection = (Collection) implicitCollections
.get(fieldName);
if (collection == null) {
Class physicalFieldType = reflectionProvider.getFieldType(
result, fieldName, null);
if (physicalFieldType.isArray()) {
collection = new ArraysList(physicalFieldType);
} else {
Class fieldType = mapper
.defaultImplementationOf(physicalFieldType);
if (!(Collection.class.isAssignableFrom(fieldType) || Map.class
.isAssignableFrom(fieldType))) {
throw new ObjectAccessException(
"Field "
+ fieldName
+ " of "
+ result.getClass().getName()
+ " is configured for an implicit Collection or Map, but field is of type "
+ fieldType.getName());
}
if (pureJavaReflectionProvider == null) {
pureJavaReflectionProvider = new PureJavaReflectionProvider();
}
Object instance = pureJavaReflectionProvider
.newInstance(fieldType);
if (instance instanceof Collection) {
collection = (Collection) instance;
} else {
Mapper.ImplicitCollectionMapping implicitCollectionMapping = mapper
.getImplicitCollectionDefForFieldName(
result.getClass(), fieldName);
collection = new MappingList((Map) instance,
implicitCollectionMapping.getKeyFieldName());
}
reflectionProvider.writeField(result, fieldName, instance,
null);
}
implicitCollections.put(fieldName, collection);
}
collection.add(value);
} else {
throw new ConversionException("Element " + itemFieldName
+ " of type " + value.getClass().getName()
+ " is not defined as field in type "
+ result.getClass().getName());
}
return implicitCollections;
}
protected static class FieldInfo {
final String fieldName;
final Class type;
final Class definedIn;
final Object value;
FieldInfo(String fieldName, Class type, Class definedIn, Object value) {
this.fieldName = fieldName;
this.type = type;
this.definedIn = definedIn;
this.value = value;
}
}
protected static class ArraysList extends ArrayList {
final Class physicalFieldType;
ArraysList(Class physicalFieldType) {
this.physicalFieldType = physicalFieldType;
}
Object toPhysicalArray() {
Object[] objects = toArray();
Object array = Array.newInstance(
physicalFieldType.getComponentType(), objects.length);
if (physicalFieldType.getComponentType().isPrimitive()) {
for (int i = 0; i < objects.length; ++i) {
Array.set(array, i, Array.get(objects, i));
}
} else {
System.arraycopy(objects, 0, array, 0, objects.length);
}
return array;
}
}
protected class MappingList extends AbstractList {
private final Map map;
private final String keyFieldName;
private final Map fieldCache = new HashMap();
public MappingList(Map map, String keyFieldName) {
this.map = map;
this.keyFieldName = keyFieldName;
}
public boolean add(Object object) {
if (object == null) {
boolean containsNull = !map.containsKey(null);
map.put(null, null);
return containsNull;
}
Class itemType = object.getClass();
if (keyFieldName != null) {
Field field = (Field) fieldCache.get(itemType);
if (field == null) {
field = reflectionProvider.getField(itemType, keyFieldName);
fieldCache.put(itemType, field);
}
if (field != null) {
try {
Object key = field.get(object);
return map.put(key, object) == null;
} catch (IllegalArgumentException e) {
throw new ObjectAccessException("Could not get field "
+ field.getClass() + "." + field.getName(), e);
} catch (IllegalAccessException e) {
throw new ObjectAccessException("Could not get field "
+ field.getClass() + "." + field.getName(), e);
}
}
} else if (object instanceof Map.Entry) {
final Map.Entry entry = (Map.Entry) object;
return map.put(entry.getKey(), entry.getValue()) == null;
}
throw new ConversionException("Element of type "
+ object.getClass().getName()
+ " is not defined as entry for map of type "
+ map.getClass().getName());
}
public Object get(int index) {
throw new UnsupportedOperationException();
}
public int size() {
return map.size();
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment