Bidirectional Conversion Between Java Objects and XML Using XStream
Maven Dependency
<dependency>
<groupId>com.thoughtworks.xstream</groupId>
<artifactId>xstream</artifactId>
<version>1.4.4</version>
</dependency>
Utility Classes
Custom Converter: Handling Null Fields with Nillable Attribute
The default XStream reflection converter does not respect the nillable attribute from JAXB annotations. This custom implementation extends the standard reflection converter to write empty elemetns when the nillable attribute is set to true.
import com.thoughtworks.xstream.converters.ConversionException;
import com.thoughtworks.xstream.converters.MarshallingContext;
import com.thoughtworks.xstream.converters.SingleValueConverter;
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.io.ExtendedHierarchicalStreamWriterHelper;
import com.thoughtworks.xstream.io.HierarchicalStreamWriter;
import com.thoughtworks.xstream.mapper.Mapper;
import lombok.SneakyThrows;
import javax.xml.bind.annotation.XmlElement;
import java.lang.reflect.Field;
import java.util.*;
public class NillableReflectionConverter extends ReflectionConverter {
private transient ReflectionProvider pureJavaReflectionProvider;
public NillableReflectionConverter(Mapper mapper, ReflectionProvider reflectionProvider) {
super(mapper, reflectionProvider);
}
@Override
protected void doMarshal(final Object source, final HierarchicalStreamWriter writer, final MarshallingContext context) {
final List fields = new ArrayList();
final Map defaultFieldDefinition = new HashMap();
reflectionProvider.visitSerializableFields(source, new ReflectionProvider.Visitor() {
final Set writtenAttributes = new HashSet();
@SneakyThrows
@Override
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)) {
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);
} else {
Field field = source.getClass().getDeclaredField(fieldName);
XmlElement xmlElement = field.getAnnotation(XmlElement.class);
boolean nillable = false;
if (xmlElement != null) {
nillable = xmlElement.nillable();
}
fields.add(new FieldInfo(fieldName, type, definedIn, value, nillable));
}
}
});
new Object() {
{
for (Iterator fieldIter = fields.iterator(); fieldIter.hasNext(); ) {
FieldInfo info = (FieldInfo) fieldIter.next();
if (info.value == null) {
if (info.nillable) {
writeField(
info.fieldName, null, info.type, info.definedIn, "");
}
} else {
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();
}
}
};
}
private static class FieldInfo {
final String fieldName;
final Class type;
final Class definedIn;
final Object value;
final boolean nillable;
FieldInfo(String fieldName, Class type, Class definedIn, Object value, boolean nillable) {
this.fieldName = fieldName;
this.type = type;
this.definedIn = definedIn;
this.value = value;
this.nillable = nillable;
}
}
}
XmlUtil
import com.thoughtworks.xstream.XStream;
import com.thoughtworks.xstream.converters.reflection.Sun14ReflectionProvider;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public final class XmlUtil {
/**
* Serializes a Java object to XML string.
*
* @param obj the object to serialize
* @return XML string representation
*/
public static String toXml(Object obj) {
XStream xStream = new XStream();
xStream.autodetectAnnotations(true);
xStream.registerConverter(new NillableReflectionConverter(xStream.getMapper(), new Sun14ReflectionProvider()), XStream.PRIORITY_VERY_LOW);
return xStream.toXML(obj);
}
/**
* Deserializes an XML string to a Java object.
*
* @param xml XML string
* @param targetClazz target class for deserialization
* @param <T> type parameter
* @return deserialized object
*/
public static <T> T fromXml(String xml, Class<T> targetClazz) {
XStream xStream = new XStream();
xStream.autodetectAnnotations(true);
xStream.processAnnotations(targetClazz);
return (T) xStream.fromXML(xml);
}
}
Usage Examples
Scehdule Entity
import com.thoughtworks.xstream.annotations.XStreamAlias;
import lombok.Data;
import javax.xml.bind.annotation.XmlElement;
import java.util.List;
@Data
@XStreamAlias("Schedule")
public class Schedule {
private String id;
private String eventType;
@XStreamAlias("videoInputChannelID")
private String videoInputChannelId;
@XmlElement(nillable = true)
@XStreamAlias("TimeBlockList")
private List<TimeBlock> timeBlockList;
@XmlElement(nillable = true)
@XStreamAlias("HolidayBlockList")
private List<TimeBlock> holidayBlockList;
}
TimeBlock Entity
import com.thoughtworks.xstream.annotations.XStreamAlias;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
@AllArgsConstructor
@XStreamAlias("TimeBlock")
public class TimeBlock {
private String dayOfWeek;
@XStreamAlias("TimeRange")
private TimeRange timeRange;
}
TimeRange Entity
import com.thoughtworks.xstream.annotations.XStreamAlias;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
@AllArgsConstructor
@XStreamAlias("TimeRange")
public class TimeRange {
private String beginTime;
private String endTime;
}
Conversion Examples
Object to XML:
Schedule schedule = new Schedule();
schedule.setId("001");
schedule.setEventType("motion");
schedule.setVideoInputChannelId("CH1");
schedule.setTimeBlockList(null);
schedule.setHolidayBlockList(null);
String xml = XmlUtil.toXml(schedule);
Output:
<Schedule>
<id>001</id>
<eventType>motion</eventType>
<videoInputChannelID>CH1</videoInputChannelID>
<TimeBlockList xsi:nil="true" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"/>
<HolidayBlockList xsi:nil="true" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"/>
</Schedule>
XML to Object:
String xml = "<Schedule>...</Schedule>";
Schedule schedule = XmlUtil.fromXml(xml, Schedule.class);