Fading Coder

One Final Commit for the Last Sprint

Home > Tech > Content

Bidirectional Conversion Between Java Objects and XML Using XStream

Tech May 8 4

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);

Related Articles

Understanding Strong and Weak References in Java

Strong References Strong reference are the most prevalent type of object referencing in Java. When an object has a strong reference pointing to it, the garbage collector will not reclaim its memory. F...

Comprehensive Guide to SSTI Explained with Payload Bypass Techniques

Introduction Server-Side Template Injection (SSTI) is a vulnerability in web applications where user input is improper handled within the template engine and executed on the server. This exploit can r...

Implement Image Upload Functionality for Django Integrated TinyMCE Editor

Django’s Admin panel is highly user-friendly, and pairing it with TinyMCE, an effective rich text editor, simplifies content management significantly. Combining the two is particular useful for bloggi...

Leave a Comment

Anonymous

◎Feel free to join the discussion and share your thoughts.