Index: src/java/com/thoughtworks/xstream/XStream.java =================================================================== --- src/java/com/thoughtworks/xstream/XStream.java (revision 671) +++ src/java/com/thoughtworks/xstream/XStream.java (working copy) @@ -4,6 +4,7 @@ import com.thoughtworks.xstream.converters.Converter; import com.thoughtworks.xstream.converters.ConverterLookup; import com.thoughtworks.xstream.converters.DataHolder; +import com.thoughtworks.xstream.converters.AttributeConverter; import com.thoughtworks.xstream.converters.basic.*; import com.thoughtworks.xstream.converters.collections.*; import com.thoughtworks.xstream.converters.extended.*; @@ -131,6 +132,7 @@ private ClassAliasingMapper classAliasingMapper; private FieldAliasingMapper fieldAliasingMapper; + private AttributeAliasingMapper attributeAliasingMapper; private DefaultImplementationsMapper defaultImplementationsMapper; private ImmutableTypesMapper immutableTypesMapper; private ImplicitCollectionMapper implicitCollectionMapper; @@ -214,6 +216,10 @@ classAliasingMapper = (ClassAliasingMapper) mapper; // need a reference to that one mapper = new FieldAliasingMapper(mapper); fieldAliasingMapper = (FieldAliasingMapper) mapper; // need a reference to that one + + mapper = new AttributeAliasingMapper(mapper); + attributeAliasingMapper = (AttributeAliasingMapper) mapper; // need a reference to that one + mapper = new ImplicitCollectionMapper(mapper); implicitCollectionMapper = (ImplicitCollectionMapper)mapper; // need a reference to this one mapper = new DynamicProxyMapper(mapper); @@ -578,6 +584,8 @@ fieldAliasingMapper.addFieldAlias(alias, type, fieldName); } + + /** * Associate a default implementation of a class with an object. Whenever XStream encounters an instance of this * type, it will use the default implementation instead. @@ -852,6 +860,10 @@ fieldAliasingMapper.omitField(type, fieldName); } + public void attributeAlias(String alias, Class type, AttributeConverter converter) { + attributeAliasingMapper.addAttributeAlias(alias, type, converter); + } + public static class InitializationException extends BaseException { public InitializationException(String message, Throwable cause) { super(message, cause); Index: src/java/com/thoughtworks/xstream/mapper/Mapper.java =================================================================== --- src/java/com/thoughtworks/xstream/mapper/Mapper.java (revision 671) +++ src/java/com/thoughtworks/xstream/mapper/Mapper.java (working copy) @@ -1,5 +1,8 @@ package com.thoughtworks.xstream.mapper; +import com.thoughtworks.xstream.converters.Converter; +import com.thoughtworks.xstream.converters.AttributeConverter; + public interface Mapper { /** @@ -58,10 +61,16 @@ */ boolean shouldSerializeMember(Class definedIn, String fieldName); + AttributeConverter getAttributeConverterForItemType(Class clazz); + + AttributeConverter getAttributeConverterForAttributeName(String attrName); + interface ImplicitCollectionMapping { String getFieldName(); String getItemFieldName(); Class getItemType(); } + + } Index: src/java/com/thoughtworks/xstream/mapper/MapperWrapper.java =================================================================== --- src/java/com/thoughtworks/xstream/mapper/MapperWrapper.java (revision 671) +++ src/java/com/thoughtworks/xstream/mapper/MapperWrapper.java (working copy) @@ -1,6 +1,7 @@ package com.thoughtworks.xstream.mapper; import com.thoughtworks.xstream.alias.ClassMapper; +import com.thoughtworks.xstream.converters.AttributeConverter; public abstract class MapperWrapper implements ClassMapper { @@ -124,4 +125,18 @@ } } } + + public AttributeConverter getAttributeConverterForItemType(Class clazz) { + // null check needed to get ReflectionConverterTest to pass. Weird. + if (wrapped != null) { + return wrapped.getAttributeConverterForItemType(clazz); + } else { + return null; + } + } + + public AttributeConverter getAttributeConverterForAttributeName(String attrName) { + return wrapped.getAttributeConverterForAttributeName(attrName); + } + } Index: src/java/com/thoughtworks/xstream/converters/reflection/ReflectionConverter.java =================================================================== --- src/java/com/thoughtworks/xstream/converters/reflection/ReflectionConverter.java (revision 671) +++ src/java/com/thoughtworks/xstream/converters/reflection/ReflectionConverter.java (working copy) @@ -4,6 +4,7 @@ import com.thoughtworks.xstream.converters.Converter; import com.thoughtworks.xstream.converters.MarshallingContext; import com.thoughtworks.xstream.converters.UnmarshallingContext; +import com.thoughtworks.xstream.converters.AttributeConverter; import com.thoughtworks.xstream.io.HierarchicalStreamReader; import com.thoughtworks.xstream.io.HierarchicalStreamWriter; import com.thoughtworks.xstream.mapper.Mapper; @@ -32,7 +33,7 @@ return true; } - public void marshal(Object original, final HierarchicalStreamWriter writer, final MarshallingContext context) { + public void marshal(final Object original, final HierarchicalStreamWriter writer, final MarshallingContext context) { final Object source = serializationMethodInvoker.callWriteReplace(original); if (source.getClass() != original.getClass()) { @@ -40,24 +41,42 @@ } final Set seenFields = new HashSet(); + final Set seenAsAttributes = new HashSet(); + // Attributes might be preferred to child elements ... + reflectionProvider.visitSerializableFields(source, new ReflectionProvider.Visitor() { + public void visit(String fieldName, Class type, Class definedIn, Object value) { + AttributeConverter attrConverter = mapper.getAttributeConverterForItemType(type); + if (attrConverter != null) { + attrConverter.marshal(value, writer, context); + seenAsAttributes.add(fieldName); + } + + } + }); + + // Child Elements not covered already processed as Attributes ... + + reflectionProvider.visitSerializableFields(source, new ReflectionProvider.Visitor() { public void visit(String fieldName, Class fieldType, Class definedIn, Object newObj) { - if (newObj != null) { - Mapper.ImplicitCollectionMapping mapping = mapper.getImplicitCollectionDefForFieldName(source.getClass(), fieldName); - if (mapping != null) { - if (mapping.getItemFieldName() != null) { - ArrayList list = (ArrayList) newObj; - for (Iterator iter = list.iterator(); iter.hasNext();) { - Object obj = iter.next(); - writeField(mapping.getItemFieldName(), mapping.getItemType(), definedIn, obj); + if (!seenAsAttributes.contains(fieldName)) { + if (newObj != null) { + Mapper.ImplicitCollectionMapping mapping = mapper.getImplicitCollectionDefForFieldName(source.getClass(), fieldName); + if (mapping != null) { + if (mapping.getItemFieldName() != null) { + ArrayList list = (ArrayList) newObj; + for (Iterator iter = list.iterator(); iter.hasNext();) { + Object obj = iter.next(); + writeField(mapping.getItemFieldName(), mapping.getItemType(), definedIn, obj); + } + } else { + context.convertAnother(newObj); } } else { - context.convertAnother(newObj); + writeField(fieldName, fieldType, definedIn, newObj); + seenFields.add(fieldName); } - } else { - writeField(fieldName, fieldType, definedIn, newObj); - seenFields.add(fieldName); } } } @@ -69,21 +88,23 @@ writer.startNode(mapper.serializedMember(definedIn, fieldName)); Class actualType = newObj.getClass(); - + Class defaultType = mapper.defaultImplementationOf(fieldType); if (!actualType.equals(defaultType)) { + writer.addAttribute(mapper.attributeForImplementationClass(), mapper.serializedClass(actualType)); } if (seenFields.contains(fieldName)) { writer.addAttribute(mapper.attributeForClassDefiningField(), mapper.serializedClass(definedIn)); } - + if (source != newObj) { context.convertAnother(newObj); } else { writer.addAttribute("self", ""); } + writer.endNode(); } @@ -94,12 +115,24 @@ final Object result = instantiateNewInstance(context, reader.getAttribute(mapper.attributeForReadResolveField())); final SeenFields seenFields = new SeenFields(); + Iterator it = reader.getAttributeNames(); + + // Process attributes before recursing into child elements. + + while (it.hasNext()) { + String attrName = (String) it.next(); + AttributeConverter converter = mapper.getAttributeConverterForAttributeName(attrName); + if (converter != null) { + converter.unmarshal(result, reader); + } + } + Map implicitCollectionsForCurrentObject = null; while (reader.hasMoreChildren()) { reader.moveDown(); String fieldName = mapper.realMember(result.getClass(), reader.getNodeName()); - + Class classDefiningField = determineWhichClassDefinesField(reader); boolean fieldExistsInClass = reflectionProvider.fieldDefinedInClass(fieldName, result.getClass()); @@ -110,8 +143,8 @@ value = context.convertAnother(result, type); } else { value = result; - } - + } + if (fieldExistsInClass) { reflectionProvider.writeField(result, fieldName, value, classDefiningField); seenFields.add(classDefiningField, fieldName);