Created
February 6, 2013 16:13
-
-
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.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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