NamedMapConverter.java
/*
* Copyright (C) 2013, 2014, 2016, 2018, 2021 XStream Committers.
* All rights reserved.
*
* The software in this package is published under the terms of the BSD
* style license a copy of which has been included with this distribution in
* the LICENSE.txt file.
*
* Created on 20. September 2013 by Joerg Schaible
*/
package com.thoughtworks.xstream.converters.extended;
import java.util.Map;
import com.thoughtworks.xstream.converters.ConversionException;
import com.thoughtworks.xstream.converters.Converter;
import com.thoughtworks.xstream.converters.ConverterLookup;
import com.thoughtworks.xstream.converters.MarshallingContext;
import com.thoughtworks.xstream.converters.SingleValueConverter;
import com.thoughtworks.xstream.converters.UnmarshallingContext;
import com.thoughtworks.xstream.converters.collections.MapConverter;
import com.thoughtworks.xstream.core.JVM;
import com.thoughtworks.xstream.core.SecurityUtils;
import com.thoughtworks.xstream.core.util.HierarchicalStreams;
import com.thoughtworks.xstream.io.HierarchicalStreamReader;
import com.thoughtworks.xstream.io.HierarchicalStreamWriter;
import com.thoughtworks.xstream.mapper.Mapper;
/**
* A map converter that uses predefined names for its elements.
* <p>
* To be used as local converter. Note, suppress the usage of the implicit type argument, if registered with annotation.
* Depending on the constructor arguments it is possible to support various formats:
* </p>
* <ul>
* <li>new NamedMapConverter(xstream.getMapper(), "entry", "key", String.class, "value", Integer.class);
*
* <pre>
* <map>
* <entry>
* <key>keyValue</key>
* <value>0</value>
* </entry>
* </map>
* </pre>
*
* </li>
* <li>new NamedMapConverter(xstream.getMapper(), null, "key", String.class, "value", Integer.class);
*
* <pre>
* <map>
* <key>keyValue</key>
* <value>0</value>
* </map>
* </pre>
*
* </li>
* <li>new NamedMapConverter(xstream.getMapper(), "entry", "key", String.class, "value", Integer.class, true, true,
* xstream.getConverterLookup());
*
* <pre>
* <map>
* <entry> key="keyValue" value="0"/>
* </map>
* </pre>
*
* </li>
* <li>new NamedMapConverter(xstream.getMapper(), "entry", "key", String.class, "value", Integer.class, true, false,
* xstream.getConverterLookup());
*
* <pre>
* <map>
* <entry key="keyValue">
* <value>0</value>
* </entry>
* </map>
* </pre>
*
* </li>
* <li>new NamedMapConverter(xstream.getMapper(), "entry", "key", String.class, "value", Integer.class, false, true,
* xstream.getConverterLookup());
*
* <pre>
* <map>
* <entry value="0">
* <key>keyValue</key>
* </entry>
* </map>
* </pre>
*
* </li>
* <li>new NamedMapConverter(xstream.getMapper(), "entry", "key", String.class, null, Integer.class, true, false,
* xstream.getConverterLookup());
*
* <pre>
* <map>
* <entry key="keyValue">0</entry>
* </map>
* </pre>
*
* </li>
* </ul>
*
* @author Jörg Schaible
* @since 1.4.5
*/
public class NamedMapConverter extends MapConverter {
private final String entryName;
private final String keyName;
private final Class<?> keyType;
private final String valueName;
private final Class<?> valueType;
private final boolean keyAsAttribute;
private final boolean valueAsAttribute;
private final ConverterLookup lookup;
private final Mapper enumMapper;
/**
* Constructs a NamedMapConverter.
*
* @param mapper the mapper
* @param entryName the name of the entry elements
* @param keyName the name of the key elements
* @param keyType the base type of key elements
* @param valueName the name of the value elements
* @param valueType the base type of value elements
* @since 1.4.5
*/
public NamedMapConverter(
final Mapper mapper, final String entryName, final String keyName, final Class<?> keyType,
final String valueName, final Class<?> valueType) {
this(mapper, entryName, keyName, keyType, valueName, valueType, false, false, null);
}
/**
* Constructs a NamedMapConverter handling an explicit Map type.
*
* @param type the Map type this instance will handle
* @param mapper the mapper
* @param entryName the name of the entry elements
* @param keyName the name of the key elements
* @param keyType the base type of key elements
* @param valueName the name of the value elements
* @param valueType the base type of value elements
* @since 1.4.5
*/
public NamedMapConverter(
@SuppressWarnings("rawtypes") final Class<? extends Map> type, final Mapper mapper, final String entryName,
final String keyName, final Class<?> keyType, final String valueName, final Class<?> valueType) {
this(type, mapper, entryName, keyName, keyType, valueName, valueType, false, false, null);
}
/**
* Constructs a NamedMapConverter with attribute support.
*
* @param mapper the mapper
* @param entryName the name of the entry elements
* @param keyName the name of the key elements
* @param keyType the base type of key elements
* @param valueName the name of the value elements
* @param valueType the base type of value elements
* @param keyAsAttribute flag to write key as attribute of entry element
* @param valueAsAttribute flag to write value as attribute of entry element
* @param lookup used to lookup SingleValueConverter for attributes
* @since 1.4.5
*/
public NamedMapConverter(
final Mapper mapper, final String entryName, final String keyName, final Class<?> keyType,
final String valueName, final Class<?> valueType, final boolean keyAsAttribute,
final boolean valueAsAttribute, final ConverterLookup lookup) {
this(null, mapper, entryName, keyName, keyType, valueName, valueType, keyAsAttribute, valueAsAttribute, lookup);
}
/**
* Constructs a NamedMapConverter with attribute support handling an explicit Map type.
*
* @param type the Map type this instance will handle
* @param mapper the mapper
* @param entryName the name of the entry elements
* @param keyName the name of the key elements
* @param keyType the base type of key elements
* @param valueName the name of the value elements
* @param valueType the base type of value elements
* @param keyAsAttribute flag to write key as attribute of entry element
* @param valueAsAttribute flag to write value as attribute of entry element
* @param lookup used to lookup SingleValueConverter for attributes
* @since 1.4.5
*/
public NamedMapConverter(
@SuppressWarnings("rawtypes") final Class<? extends Map> type, final Mapper mapper, final String entryName,
final String keyName, final Class<?> keyType, final String valueName, final Class<?> valueType,
final boolean keyAsAttribute, final boolean valueAsAttribute, final ConverterLookup lookup) {
super(mapper, type);
this.entryName = entryName != null && entryName.length() == 0 ? null : entryName;
this.keyName = keyName != null && keyName.length() == 0 ? null : keyName;
this.keyType = keyType;
this.valueName = valueName != null && valueName.length() == 0 ? null : valueName;
this.valueType = valueType;
this.keyAsAttribute = keyAsAttribute;
this.valueAsAttribute = valueAsAttribute;
this.lookup = lookup;
enumMapper = UseAttributeForEnumMapper.createEnumMapper(mapper);
if (keyType == null || valueType == null) {
throw new IllegalArgumentException("Class types of key and value are mandatory");
}
if (entryName == null) {
if (keyAsAttribute || valueAsAttribute) {
throw new IllegalArgumentException(
"Cannot write attributes to map entry, if map entry must be omitted");
}
if (valueName == null) {
throw new IllegalArgumentException("Cannot write value as text of entry, if entry must be omitted");
}
}
if (keyName == null) {
throw new IllegalArgumentException("Cannot write key without name");
}
if (valueName == null) {
if (valueAsAttribute) {
throw new IllegalArgumentException("Cannot write value as attribute without name");
} else if (!keyAsAttribute) {
throw new IllegalArgumentException("Cannot write value as text of entry, if key is also child element");
}
}
if (keyAsAttribute && valueAsAttribute && keyName.equals(valueName)) {
throw new IllegalArgumentException("Cannot write key and value with same attribute name");
}
}
@Override
public void marshal(final Object source, final HierarchicalStreamWriter writer, final MarshallingContext context) {
final Map<?, ?> map = (Map<?, ?>)source;
SingleValueConverter keyConverter = null;
SingleValueConverter valueConverter = null;
if (keyAsAttribute) {
final SingleValueConverter singleValueConverter = getSingleValueConverter(keyType, "key");
keyConverter = singleValueConverter;
}
if (valueAsAttribute || valueName == null) {
final SingleValueConverter singleValueConverter = getSingleValueConverter(valueType, "value");
valueConverter = singleValueConverter;
}
for (final Map.Entry<?, ?> entry : map.entrySet()) {
final Object key = entry.getKey();
final Object value = entry.getValue();
if (entryName != null) {
writer.startNode(entryName, entry.getClass());
if (keyConverter != null && key != null) {
writer.addAttribute(keyName, keyConverter.toString(key));
}
if (valueName != null && valueConverter != null && value != null) {
writer.addAttribute(valueName, valueConverter.toString(value));
}
}
if (keyConverter == null) {
writeItem(keyName, keyType, key, context, writer);
}
if (valueConverter == null) {
writeItem(valueName, valueType, value, context, writer);
} else if (valueName == null) {
writer.setValue(valueConverter.toString(value));
}
if (entryName != null) {
writer.endNode();
}
}
}
@Override
protected void populateMap(final HierarchicalStreamReader reader, final UnmarshallingContext context,
final Map<?, ?> map, final Map<?, ?> target) {
SingleValueConverter keyConverter = null;
SingleValueConverter valueConverter = null;
if (keyAsAttribute) {
keyConverter = getSingleValueConverter(keyType, "key");
}
if (valueAsAttribute || valueName == null) {
valueConverter = getSingleValueConverter(valueType, "value");
}
while (reader.hasMoreChildren()) {
Object key = null;
Object value = null;
if (entryName != null) {
reader.moveDown();
if (keyConverter != null) {
final String attribute = reader.getAttribute(keyName);
if (attribute != null) {
key = keyConverter.fromString(attribute);
}
}
if (valueAsAttribute && valueConverter != null) {
final String attribute = reader.getAttribute(valueName);
if (attribute != null) {
value = valueConverter.fromString(attribute);
}
}
}
if (keyConverter == null) {
reader.moveDown();
if (valueConverter == null && !keyName.equals(valueName) && reader.getNodeName().equals(valueName)) {
value = readItem(valueType, reader, context, map);
} else {
key = readItem(keyType, reader, context, map);
}
reader.moveUp();
}
if (valueConverter == null) {
reader.moveDown();
if (keyConverter == null && key == null && value != null) {
key = readItem(keyType, reader, context, map);
} else {
value = readItem(valueType, reader, context, map);
}
reader.moveUp();
} else if (!valueAsAttribute) {
value = valueConverter.fromString(reader.getValue());
}
@SuppressWarnings("unchecked")
final Map<Object, Object> targetMap = (Map<Object, Object>)target;
final long now = System.currentTimeMillis();
targetMap.put(key, value);
SecurityUtils.checkForCollectionDoSAttack(context, now);
if (entryName != null) {
reader.moveUp();
}
}
}
private SingleValueConverter getSingleValueConverter(final Class<?> type, final String part) {
SingleValueConverter conv = Enum.class.isAssignableFrom(type)
? enumMapper.getConverterFromItemType(null, type, null)
: mapper().getConverterFromItemType(null, type, null);
if (conv == null) {
final Converter converter = lookup.lookupConverterForType(type);
if (converter instanceof SingleValueConverter) {
conv = (SingleValueConverter)converter;
} else {
throw new ConversionException("No SingleValueConverter for " + part + " available");
}
}
return conv;
}
protected void writeItem(final String name, final Class<?> type, final Object item,
final MarshallingContext context, final HierarchicalStreamWriter writer) {
final Class<?> itemType = item == null ? Mapper.Null.class : item.getClass();
writer.startNode(name, itemType);
if (!itemType.equals(type)) {
final String attributeName = mapper().aliasForSystemAttribute("class");
if (attributeName != null) {
writer.addAttribute(attributeName, mapper().serializedClass(itemType));
}
}
if (item != null) {
context.convertAnother(item);
}
writer.endNode();
}
protected Object readItem(final Class<?> type, final HierarchicalStreamReader reader,
final UnmarshallingContext context, final Object current) {
final String className = HierarchicalStreams.readClassAttribute(reader, mapper());
final Class<?> itemType = className == null ? type : mapper().realClass(className);
if (Mapper.Null.class.equals(itemType)) {
return null;
} else {
return context.convertAnother(current, itemType);
}
}
}