using Eclipselink MOXy, I have the following class:
#XmlAccessorType(XmlAccessType.PUBLIC_MEMBER)
#XmlType(name = "")
public class MyObject {
private Map<String, String> meta;
#XmlPath(".")
#XmlJavaTypeAdapter(MetaMapAdapter.class)
public Map<String, String> getMeta() {
return meta;
}
public setMeta(Map<String, String> m) {
meta = m;
}
}
My AdaptedMap looks like this (credits to JAXB: how to marshall map into <key>value</key>):
import javax.xml.bind.annotation.XmlAnyElement;
public class AdaptedMap {
private Object value;
public AdaptedMap() {}
#XmlAnyElement
public Object getValue() { return value; }
public void setValue(final Object value) { this.value = value; }
}
And the MapAdapter looks like this (credits to JAXB: how to marshall map into <key>value</key>):
import java.util.*;
import javax.xml.bind.annotation.adapters.XmlAdapter;
import javax.xml.parsers.*;
import org.eclipse.persistence.oxm.XMLRoot;
import org.w3c.dom.*;
public class MetaMapAdapter extends XmlAdapter<AdaptedMap, Map<String, String>> {
public MapAdapter() {}
#Override public AdaptedMap marshal(final Map<String, String> map) throws Exception {
if (map == null) { return null; }
final DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
final DocumentBuilder db = dbf.newDocumentBuilder();
final Document document = db.newDocument();
final Element rootElement = document.createElement(getTagName());
document.appendChild(rootElement);
for (final Entry<String, String> entry : map.entrySet()) {
final Element mapElement = document.createElement(entry.getKey());
mapElement.setTextContent(entry.getValue());
rootElement.appendChild(mapElement);
}
final AdaptedMap adaptedMap = new AdaptedMap();
adaptedMap.setValue(document);
return adaptedMap;
}
#Override public Map<String, String> unmarshal(final AdaptedMap adaptedMap) {
if (adaptedMap == null) { return null; }
final Map<String, String> map = new HashMap<String, String>();
final Element rootElement = (Element) adaptedMap.getValue();
final NodeList childNodes = rootElement.getChildNodes();
for (int x = 0, size = childNodes.getLength(); x < size; x++) {
final Node childNode = childNodes.item(x);
if (childNode.getNodeType() == Node.ELEMENT_NODE) {
map.put(childNode.getLocalName(), childNode.getTextContent());
}
}
return map;
}
}
By using Eclipselink MOXy, I'm able to get this JSON in return with the help of XmlPath:
{
"meta": {
"akey":"avalue",
"bkey":"bvalue"
}
}
Unfortunately, I'm unable to unmarshal to MyObject in reverse due to the usage of XmlPath to collapse the outer meta element.
On a side note, I'm also not able to use the new XmlVariableNode in Eclipselink 2.6 as I'm only allowed to use stable releases of the API :(
Anyone knows how I can resolve this?
On a side note, I'm also not able to use the new XmlVariableNode in
Eclipselink 2.6 as I'm only allowed to use stable releases of the API
:(
#XmlVariableNode has also been included in EclipseLink 2.5.1 which is now released:
http://www.eclipse.org/eclipselink/downloads/
This annotation is well suited for mapping your use case:
http://blog.bdoughan.com/2013/06/moxys-xmlvariablenode-using-maps-key-as.html
Related
I am using SaxParser to read the large complex XML file. I do not wish to create the model class as I do not know the exact data which will be coming in the XML so I am trying to find if there is a generic way of reading the XML data using some sort of Context.
I have used a similar approach for JSON using the Jackson, which worked very well for me. Since I am new to Sax Parser, I cannot completely understand how to achieve the same. for complex inner values, I am unable to establish a parent-child relationship and I am unable to build relationships between tags and attributes.
Following is the code I have so far:
ContextNode my generic class to store all XML information using the parent-child relationships.
#Getter
#Setter
#ToString
#NoArgsConstructor
public class ContextNode {
protected String name;
protected String value;
protected ArrayList<ContextNode> children = new ArrayList<>();
protected ContextNode parent;
//Constructor 1: To store the simple field information.
public ContextNode(final String name, final String value) {
this.name = name;
this.value = value;
}
//Constructor 2: To store the complex field which has inner elements.
public ContextNode(final ContextNode parent, final String name, final String value) {
this(name, value);
this.parent = parent;
}
Following is my method to parse XML using SAX within EventReader.class
public class EventReader{
//Method to read XML events and create pre-hash string from it.
public static void xmlParser(final InputStream xmlStream) {
final SAXParserFactory factory = SAXParserFactory.newInstance();
try {
final SAXParser saxParser = factory.newSAXParser();
final SaxHandler handler = new SaxHandler();
saxParser.parse(xmlStream, handler);
} catch (ParserConfigurationException | SAXException | IOException e) {
e.printStackTrace();
}
}
}
Following is my SaxHandler:
import org.xml.sax.Attributes;
import org.xml.sax.helpers.DefaultHandler;
import java.util.HashMap;
public class SaxHandler extends DefaultHandler {
private final List<String> XML_IGNORE_FIELDS = Arrays.asList("person:personDocument","DocumentBody","DocumentList");
private final List<String> EVENT_TYPES = Arrays.asList("person");
private Map<String, String> XML_NAMESPACES = null;
private ContextNode contextNode = null;
private StringBuilder currentValue = new StringBuilder();
#Override
public void startDocument() {
ConstantEventInfo.XML_NAMESPACES = new HashMap<>();
}
#Override
public void startElement(final String uri, final String localName, final String qName, final Attributes attributes) {
//For every new element in XML reset the StringBuilder.
currentValue.setLength(0);
if (qName.equalsIgnoreCase("person:personDocument")) {
// Add the attributes and name-spaces to Map
for (int att = 0; att < attributes.getLength(); att++) {
if (attributes.getQName(att).contains(":")) {
//Find all Namespaces within the XML Header information and save it to the Map for future use.
XML_NAMESPACES.put(attributes.getQName(att).substring(attributes.getQName(att).indexOf(":") + 1), attributes.getValue(att));
} else {
//Find all other attributes within XML and store this information within Map.
XML_NAMESPACES.put(attributes.getQName(att), attributes.getValue(att));
}
}
} else if (EVENT_TYPES.contains(qName)) {
contextNode = new ContextNode("type", qName);
}
}
#Override
public void characters(char ch[], int start, int length) {
currentValue.append(ch, start, length);
}
#Override
public void endElement(final String uri, final String localName, final String qName) {
if (!XML_IGNORE_FIELDS.contains(qName)) {
if (!EVENT_TYPES.contains(qName)) {
System.out.println("QName : " + qName + " Value : " + currentValue);
contextNode.children.add(new ContextNode(qName, currentValue.toString()));
}
}
}
#Override
public void endDocument() {
System.out.println(contextNode.getChildren().toString());
System.out.println("End of Document");
}
}
Following is my TestCase which will call the method xmlParser
#Test
public void xmlReader() throws Exception {
final InputStream xmlStream = getClass().getResourceAsStream("/xmlFileContents.xml");
EventReader.xmlParser(xmlStream);
}
Following is the XML I need to read using a generic approach:
<?xml version="1.0" ?>
<person:personDocument xmlns:person="https://example.com" schemaVersion="1.2" creationDate="2020-03-03T13:07:51.709Z">
<DocumentBody>
<DocumentList>
<Person>
<bithTime>2020-03-04T11:00:30.000+01:00</bithTime>
<name>Batman</name>
<Place>London</Place>
<hobbies>
<hobby>painting</hobby>
<hobby>football</hobby>
</hobbies>
<jogging distance="10.3">daily</jogging>
<purpose2>
<id>1</id>
<purpose>Dont know</purpose>
</purpose2>
</Person>
</DocumentList>
</DocumentBody>
</person:personDocument>
Providing the answer as it can be helpful to someone in the future:
First we need to create a class ContextNode which can hold the information:
#Getter
#Setter
public class ContextNode {
protected String name;
protected String value;
protected ArrayList<ContextNode> attributes = new ArrayList<>();
protected ArrayList<ContextNode> children = new ArrayList<>();
protected ContextNode parent;
protected Map<String, String> namespaces;
public ContextNode(final ContextNode parent, final String name, final String value) {
this.parent = parent;
this.name = name;
this.value = value;
this.namespaces = parent.namespaces;
}
public ContextNode(final Map<String, String> namespaces) {
this.namespaces = namespaces;
}
public ContextNode(final Map<String, String> namespaces) {
this.namespaces = namespaces;
}
}
Then we can read the XML and store the information in the context node:
import lombok.Getter;
import org.xml.sax.Attributes;
import org.xml.sax.helpers.DefaultHandler;
import java.security.NoSuchAlgorithmException;
import java.util.*;
public class SaxHandler extends DefaultHandler {
//Variables needed to store the required information during the parsing of the XML document.
private final Deque<String> path = new ArrayDeque<>();
private final StringBuilder currentValue = new StringBuilder();
private ContextNode currentNode = null;
private ContextNode rootNode = null;
private Map<String, String> currentAttributes;
private final HashMap<String, String> contextHeader = new HashMap<>();
#Override
public void startElement(final String uri, final String localName, final String qName, final Attributes attributes) {
//Put every XML tag within the stack at the beginning of the XML tag.
path.push(qName);
//Reset attributes for every element
currentAttributes = new HashMap<>();
//Get the path from Deque as / separated values.
final String p = path();
//If the XML tag contains the Namespaces or attributes then add to respective Namespaces Map or Attributes Map.
if (attributes.getLength() > 0) {
//Loop over every attribute and add them to respective Map.
for (int att = 0; att < attributes.getLength(); att++) {
//If the attributes contain the : then consider them as namespaces.
if (attributes.getQName(att).contains(":") && attributes.getQName(att).startsWith("xmlns:")) {
contextHeader.put(attributes.getQName(att).substring(attributes.getQName(att).indexOf(":") + 1), attributes.getValue(att));
} else {
currentAttributes.put(attributes.getQName(att), attributes.getValue(att).trim());
}
}
}
if (rootNode == null) {
rootNode = new ContextNode(contextHeader);
currentNode = rootNode;
rootNode.children.add(new ContextNode(rootNode, "type", qName));
} else if (currentNode != null) {
ContextNode n = new ContextNode(currentNode, qName, (String) null);
currentNode.children.add(n);
currentNode = n;
}
}
#Override
public void characters(char[] ch, int start, int length) {
currentValue.append(ch, start, length);
}
#Override
public void endElement(final String uri, final String localName, final String qName) {
try {
System.out.println("completed reading");
System.out.println(rootNode);
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
}
rootNode = null;
//At the end of the XML element tag reset the value for next element.
currentValue.setLength(0);
//After completing the particular element reading, remove that element from the stack.
path.pop();
}
private String path() {
return String.join("/", this.path);
}
}
You may need to make some additional changes based on your particular requirement. This is just a sample that gives some idea.
So, I have JSON file and all data I put to LinkedTreeMap<String, Object>. If one of JSON field is complex:
{
"val1": "1",
"val2": "2",
"val3": {
"embVal1": "emb1",
"embVal2": emb2
},
"val4": "4"
}
like val3, map value with Object type will transform to other LinkedTreeMap<String, Object>, and my structure will look like LinkedTreeMap<String, LinkedTreeMap<Sting, Object>>.
If val3 in it body has other complex object, my value of Object type will also transform to new LinkedTreeMap<String, Object> and so on into the depths of Json tree.
How can I traverse all embedded nodes in structure like this?
I did the code below, I didn't think in performance so much but it works well
AppTest.java
public class AppTest {
#Test
public void testApp() {
LinkedTreeMap<String, Object> node = new LinkedTreeMap<>();
LinkedTreeMap<String, Object> node2 = new LinkedTreeMap<>();
LinkedTreeMap<String, Object> node3 = new LinkedTreeMap<>();
node2.put("embembVal1", "embemb1");
node2.put("embembVal2", "embemb2");
node3.put("embVal1", "emb1");
node3.put("embVal2", node2);
node.put("val1", "1");
node.put("val2", "2");
node.put("val3", node3);
node.put("val4", "4");
MyJson json = new MyJson();
json.read(node);
System.out.println(MyJsonBuilder.build());
}
}
MyJson.java
public class MyJson {
public void read(LinkedTreeMap<String, Object> node) {
MyJsonBuilder.append("{");
for(Entry<String, Object> set : node.entrySet()) {
if(!getInstanceType(set.getValue())) {
jsonFormat(set.getKey(), set.getValue());
} else {
new MyJson().read( (LinkedTreeMap<String, Object>) set.getValue() );
}
}
MyJsonBuilder.append("}");
}
private void jsonFormat(String k, Object v) {
MyJsonBuilder.append( String.format("\"%s\":\"%s\"", k, v) );
}
private boolean getInstanceType(Object obj) {
if(obj instanceof LinkedTreeMap) return true;
else return false;
}
}
MyJsonBuilder.java
public class MyJsonBuilder {
private static StringBuilder jsonBuilder = new StringBuilder();
public static void append(String node) {
jsonBuilder.append(node);
}
private static String format(String json) {
String adjustedjson = json;
if (adjustedjson.contains("\"\"")) adjustedjson = adjustedjson.replaceAll("\"\"", "\",\"");
if (adjustedjson.contains("}\"")) adjustedjson = adjustedjson.replaceAll("}\"", "},\"");
return adjustedjson;
}
public static String build() {
return format(jsonBuilder.toString());
}
}
I'm using xStream to deserialize XML.
My xml contains a tag:
<Element Name="Test" Value="TestValue" Tag="tag" Text.Color="Red"/>
and class
public class Element {
#XStreamAsAttribute
public String Name;
#XStreamAsAttribute
public String Value;
public Map<String, String> AnyAttr = new HashMap<String, String>();
}
fields Name and Value deserialize correct,
How can I deserialize undeclared fields (Tag, Text.Color) to my map ( Map AnyAttr )?
You have to create custom Converter class. Like this one:
public class ElementConverter implements Converter
{
public boolean canConvert(Class clazz) {
return Element.class == clazz;
}
public void marshal(Object object, HierarchicalStreamWriter hsw, MarshallingContext mc) {
Element e = (Element) object;
hsw.addAttribute("Name", e.Name);
hsw.addAttribute("Value", e.Value);
for (Map.Entry<String, String> entry : e.AnyAttr.entrySet())
{
hsw.addAttribute(entry.getKey(), entry.getValue());
}
}
public Object unmarshal(HierarchicalStreamReader hsr, UnmarshallingContext uc) {
Element e = new Element();
String key;
int count = hsr.getAttributeCount();
for (int i = 0; i < count; i++)
{
key = hsr.getAttributeName(i);
if (key.equals("Name")) e.Name = hsr.getAttribute(i);
else
if (key.equals("Value")) e.Value = hsr.getAttribute(i);
else e.AnyAttr.put(key, hsr.getAttribute(i));
}
return e;
}
}
and then you have to register converter in XStream before you use it:
XStream xstream = new XStream();
xstream.aliasType("Element", Element.class);
xstream.registerConverter(new ElementConverter());
You can write your own Converter. That's the only way, you can't achieve this with simple configuration.
Ok, so we all know that Maps are somewhat of a pain in JAXB.
I here present an alternative to the current solutions. My main objective is to get feedback on any and all potential problems with this solution. Maybe it is not even a good solution for some reasons.
When I played around with the standard Generic Map Adapter it seemed like the adapters for the classes were not used. The classes are instead scanned, forcing me to mark my data model with JAXB annotations and adding default constructors where I don't want them (I'm talking about complex classes that I want to store in Maps, not simple data types). Above all, this makes my internal data model public thereby breaking encapsulation since the generated XML is a direct representation of the internal structures.
The "workaround" I did was to combine the adapter with the Marshall.Listener and Unmarshall.Listner thereby being able to extract additional annotation information. A field would then be
#XmlElement(name = "testMap")
#XmlJavaTypeAdapter(MapAdapter.class)
#MapKeyValueAdapters(key=SomeComplexClassAdapter.class)
private final HashMap<SomeComplexClass, String> testMap2 = new HashMap<SomeComplexClass, String>();
This additional annotation accepts both key and value as arguments. If omitted the functionality falls back on standard qualification for the omitted. The example above will use the given adapter for the key and standard handling for the value.
Here the annotation.
import static java.lang.annotation.ElementType.FIELD;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import javax.xml.bind.annotation.adapters.XmlAdapter;
/**
* This annotation holds the adapters for the key and value used in the MapAdapter.
*/
#Retention(RUNTIME)
#Target({ FIELD })
public #interface MapKeyValueAdapters {
/**
* Points to the class that converts the value type to a bound type or vice versa. See {#link XmlAdapter} for more
* details.
*/
Class<? extends XmlAdapter<?, ?>> key() default UNDEFINED.class;
/**
* Points to the class that converts the value type to a bound type or vice versa. See {#link XmlAdapter} for more
* details.
*/
Class<? extends XmlAdapter<?, ?>> value() default UNDEFINED.class;
static final class UNDEFINED extends XmlAdapter<String, String> {
#Override
public String unmarshal(String v) throws Exception {
return null;
}
#Override
public String marshal(String v) throws Exception {
return null;
}
}
}
Here so the adapter
import java.io.StringReader;
import java.io.StringWriter;
import java.lang.annotation.Annotation;
import java.lang.annotation.IncompleteAnnotationException;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBElement;
import javax.xml.bind.JAXBIntrospector;
import javax.xml.bind.Marshaller;
import javax.xml.bind.Unmarshaller;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlElementWrapper;
import javax.xml.bind.annotation.XmlType;
import javax.xml.bind.annotation.adapters.XmlAdapter;
import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
import javax.xml.namespace.QName;
/**
* This class represents a general purpose Map adapter. It is capable of handling any type of class implementing the Map
* interface and has a no-args constructor.
*/
public class MapAdapter extends XmlAdapter<MapAdapter.Wrapper, Map<Object, Object>> {
private static final String XSI_NS = "xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"";
private static final String XSI_TYPE = "xsi:type";
private static final String CDATA_START = "<![CDATA[";
private static final String CDATA_END = "]]>";
private final MarshallerListener marshallerListener = new MarshallerListener();
private final UnmarshallerListener unmarshallerListener = new UnmarshallerListener();
private final JAXBContext context;
public MapAdapter(JAXBContext inContext) {
context = inContext;
}
#SuppressWarnings({ "unchecked", "rawtypes" })
#Override
public Map<Object, Object> unmarshal(Wrapper inWrapper) throws Exception {
if (inWrapper == null) {
return null;
}
Info info = null;
for (Info element : unmarshallerListener.infoList) {
if (element.field.equals(inWrapper.field)) {
info = element;
}
}
if (info != null) {
Class<Map<Object, Object>> clazz = (Class<Map<Object, Object>>) Class.forName(inWrapper.mapClass);
Map<Object, Object> outMap = clazz.newInstance();
XmlAdapter<Object, Object> keyAdapter = null;
XmlAdapter<Object, Object> valueAdapter = null;
if (info.adapters.key() != MapKeyValueAdapters.UNDEFINED.class) {
keyAdapter = (XmlAdapter<Object, Object>) info.adapters.key().getConstructor().newInstance();
}
if (info.adapters.value() != MapKeyValueAdapters.UNDEFINED.class) {
valueAdapter = (XmlAdapter<Object, Object>) info.adapters.value().getConstructor().newInstance();
}
Unmarshaller um = context.createUnmarshaller();
for (MapEntry entry : inWrapper.mapList) {
Object key = ((JAXBElement) um.unmarshal(new StringReader(entry.key))).getValue();
if (keyAdapter != null) {
key = keyAdapter.unmarshal(key);
}
Object value = ((JAXBElement) um.unmarshal(new StringReader(entry.value))).getValue();
if (valueAdapter != null) {
value = valueAdapter.unmarshal(value);
}
outMap.put(key, value);
}
return outMap;
} else {
throw new IllegalStateException("Adapter info could not be found.");
}
}
#SuppressWarnings("unchecked")
#Override
public Wrapper marshal(Map<Object, Object> inMap) throws Exception {
if (inMap == null) {
return null;
}
Info info = null;
for (Info element : marshallerListener.infoList) {
if (element.map == inMap) {
info = element;
}
}
if (info != null) {
Wrapper outWrapper = new Wrapper();
outWrapper.mapClass = inMap.getClass().getName();
outWrapper.field = info.field;
Marshaller m = context.createMarshaller();
m.setProperty(Marshaller.JAXB_FRAGMENT, true);
JAXBIntrospector introspector = context.createJAXBIntrospector();
XmlAdapter<Object, Object> keyAdapter = null;
XmlAdapter<Object, Object> valueAdapter = null;
if (info.adapters.key() != MapKeyValueAdapters.UNDEFINED.class) {
keyAdapter = (XmlAdapter<Object, Object>) info.adapters.key().getConstructor().newInstance();
}
if (info.adapters.value() != MapKeyValueAdapters.UNDEFINED.class) {
valueAdapter = (XmlAdapter<Object, Object>) info.adapters.value().getConstructor().newInstance();
}
for (Map.Entry<?, ?> entry : inMap.entrySet()) {
MapEntry jaxbEntry = new MapEntry();
outWrapper.mapList.add(jaxbEntry);
Object key = entry.getKey();
if (key != null) {
Class<Object> clazz = Object.class;
if (keyAdapter != null) {
key = keyAdapter.marshal(key);
clazz = (Class<Object>) key.getClass();
}
if (introspector.getElementName(key) == null) {
// The value of clazz determines if the qualification is written or not; Object.class generates the
// qualification.
key = new JAXBElement<Object>(new QName("key"), clazz, key);
}
StringWriter writer = new StringWriter();
m.marshal(key, writer);
jaxbEntry.key = format("key", writer.toString());
}
Object value = entry.getValue();
if (value != null) {
Class<Object> clazz = Object.class;
if (valueAdapter != null) {
value = valueAdapter.marshal(value);
clazz = (Class<Object>) value.getClass();
}
if (introspector.getElementName(value) == null) {
// The value of clazz determines if the qualification is written or not; Object.class generates the
// qualification.
value = new JAXBElement<Object>(new QName("value"), clazz, value);
}
StringWriter writer = new StringWriter();
m.marshal(value, writer);
jaxbEntry.value = format("value", writer.toString());
}
}
return outWrapper;
} else {
throw new IllegalStateException("Adapter info could not be found.");
}
}
private String format(String inTagName, String inXML) {
String element = "<" + inTagName;
// Remove unneeded namespaces, they are already declared in the top node.
int beginIndex = inXML.indexOf(XSI_TYPE);
if (beginIndex != -1) {
int endIndex = inXML.indexOf(" ", beginIndex);
element += " " + inXML.substring(beginIndex, endIndex) + " " + XSI_NS;
}
beginIndex = inXML.indexOf('>');
element += inXML.substring(beginIndex);
return CDATA_START + element + CDATA_END;
}
#XmlType(name = "map")
static class Wrapper {
#XmlElement(name = "mapClass")
private String mapClass;
#XmlElement(name = "field")
private String field;
#XmlElementWrapper(name = "map")
#XmlElement(name = "entry")
private final List<MapEntry> mapList = new ArrayList<MapEntry>();
}
#XmlType(name = "mapEntry")
static class MapEntry {
#XmlElement(name = "key")
private String key;
#XmlElement(name = "value")
private String value;
}
public Marshaller.Listener getMarshallerListener() {
return marshallerListener;
}
public Unmarshaller.Listener getUnmarshallerListener() {
return unmarshallerListener;
}
private static class MarshallerListener extends Marshaller.Listener {
private final List<Info> infoList = new ArrayList<Info>();
#Override
public void beforeMarshal(Object inSource) {
extractInfo(infoList, inSource);
}
}
private class UnmarshallerListener extends Unmarshaller.Listener {
private final List<Info> infoList = new ArrayList<Info>();
#Override
public void beforeUnmarshal(Object inTarget, Object inParent) {
extractInfo(infoList, inTarget);
}
}
private static void extractInfo(List<Info> inList, Object inObject) {
for (Field field : inObject.getClass().getDeclaredFields()) {
for (Annotation a : field.getAnnotations()) {
if (a.annotationType() == XmlJavaTypeAdapter.class) {
if (((XmlJavaTypeAdapter) a).value() == MapAdapter.class) {
MapKeyValueAdapters adapters = field.getAnnotation(MapKeyValueAdapters.class);
if (adapters == null) {
throw new IncompleteAnnotationException(XmlJavaTypeAdapter.class, "; XmlJavaTypeAdapter specifies "
+ MapAdapter.class.getName() + " for field " + field.getName() + " in "
+ inObject.getClass().getName() + ". This must be used in combination with annotation "
+ MapKeyValueAdapters.class.getName());
}
try {
field.setAccessible(true);
Map<?, ?> value = (Map<?, ?>) field.get(inObject);
if (value != null) {
Info info = new Info();
info.field = field.getName();
info.map = value;
info.adapters = adapters;
inList.add(info);
}
} catch (Exception e) {
throw new RuntimeException("Failed extracting annotation information from " + field.getName() + " in "
+ inObject.getClass().getName(), e);
}
}
}
}
}
}
private static class Info {
private String field;
private Map<?, ?> map;
private MapKeyValueAdapters adapters;
}
}
Note that the adapter is capable of handling all types of Maps as long as it has a default constructor.
Finally the code to set up the usage of the adapter.
import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBException;
import javax.xml.bind.Marshaller;
import javax.xml.bind.Unmarshaller;
/**
* Singleton that manages the JAXB functionality.
*/
public enum JAXBManager {
INSTANCE;
private JAXBContext context;
private JAXBManager() {
try {
context = JAXBContext.newInstance(SomeComplexClass.class.getPackage().getName());
} catch (JAXBException e) {
throw new RuntimeException(e);
}
}
public Marshaller createMarshaller() throws JAXBException {
Marshaller m = context.createMarshaller();
MapAdapter adapter = new MapAdapter(context);
m.setAdapter(adapter);
m.setListener(adapter.getMarshallerListener());
m.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, Boolean.TRUE);
return m;
}
public Unmarshaller createUnmarshaller() throws JAXBException {
Unmarshaller um = context.createUnmarshaller();
MapAdapter adapter = new MapAdapter(context);
um.setAdapter(adapter);
um.setListener(adapter.getUnmarshallerListener());
return um;
}
}
This could generate an output of something like
<testMap2>
<mapClass>java.util.HashMap</mapClass>
<field>testMap2</field>
<map>
<entry>
<key><![CDATA[<key><number>1357</number><type>Unspecified</type></key>]]></key>
<value><![CDATA[<value xsi:type="xs:string" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">gn</value>]]></value>
</entry>
</map>
</testMap2>
As can be seen the qualification info is not needed for the key since we already know the adapter to use.
Also note that I add CDATA to the output. I have implemented a simple character escape handler that respects this (not included in this code example).
Due to our release cycles I have a bit of time before the opportunity opens for implementing this functionality in our code so I therefore thought it would be wise to check with the community if there are any problems with this solution or if there are better ways already in the JAXB specification that I have overlooked. I also assume that there are sections of the code that can be done in better ways.
Thanks for comments.
Here is my proposal for a workaround:
Make the map XmlTransient
Use a wrapped List for the marshalling
reinit the map from the list whenever it is needed
if you need to keep the list and the map in sync use an add(order) function
Example Customer with a Map of Orders
#XmlRootElement
#XmlAccessorType(XmlAccessType.FIELD)
public static class Order {
#XmlID
String orderId;
String item;
int count;
}
#XmlRootElement(name = "customer")
#XmlAccessorType(XmlAccessType.FIELD)
public static class Customer {
String name;
String firstname;
#XmlElementWrapper(name = "orders")
#XmlElement(name = "order")
List<Order> orders = new ArrayList<Order>();
#XmlTransient
private Map<String, Order> ordermap = new LinkedHashMap<String, Order>();
/**
* reinitialize the order list
*/
public void reinit() {
for (Order order : orders) {
ordermap.put(order.orderId, order);
}
}
/**
* add the given order to the internal list and map
* #param order - the order to add
*/
public void addOrder(Order order) {
orders.add(order);
ordermap.put(order.orderId,order);
}
}
Example XML
<customer>
<name>Doe</name>
<firstname>John</firstname>
<orders>
<order>
<orderId>Id1</orderId>
<item>Item 1</item>
<count>1</count>
</order>
<order>
<orderId>Id2</orderId>
<item>Item 2</item>
<count>2</count>
</order>
</orders>
</customer>
Mininimal complete and verifiable example
An example according to
https://stackoverflow.com/help/mcve
can be found at
https://github.com/BITPlan/com.bitplan.simplerest/blob/master/src/test/java/com/bitplan/jaxb/TestJaxbFactory.java#L390
I want to convert a HashMap in a POJO class to XML. I tried using the XmlAdapter but it results in only the key and value pairs of the HashMap being the attributes of the XML Elements. I need the Key to be the Element itself and the value of the HashMap to be the value of the element. For instance, I need the following XML:
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<cart>
<supervisor_id>555</supervisor_id>
<payments>
<payment sequence="1">
<amount>123.45</amount>
<billing_method>12345</billing_method>
<form>card</form>
<delivery_mode>Q</delivery_mode>
</payment>
<payment sequence="2">
<amount>123.45</amount>
<person_id>2333</person_id>
<form>cash</form>
<delivery_mode>Q</delivery_mode>
</payment>
</payments>
</cart>
I created the following classes: MyMapType holds a list of MyMapEntryType class which has two fields namely Key and Value. How do I change the Key element to be #XmlElement and assign the value field to the Key field?
Here are my source files.
MyMapType.java
import java.util.ArrayList;
import java.util.List;
public class MyMapType {
private List<MyMapEntryType> entry = new ArrayList<MyMapEntryType>();
public List<MyMapEntryType> getEntry() {
return entry;
}
public void setEntry(List<MyMapEntryType> entry) {
this.entry = entry;
}
}
MyMapEntryType.java
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlAttribute;
import javax.xml.bind.annotation.XmlValue;
#XmlAccessorType(XmlAccessType.FIELD)
public class MyMapEntryType {
#XmlAttribute
private String key;
#XmlValue
private String value;
public String getKey() {
return key;
}
public void setKey(String key) {
this.key = key;
}
public String getValue() {
return value;
}
public void setValue(String value) {
this.value = value;
}
}
Please also find the adapter class:
MyMapAdapter.java
import java.util.HashMap;
import java.util.Map;
import java.util.Map.Entry;
import javax.xml.bind.annotation.adapters.XmlAdapter;
public class MyMapAdapter extends XmlAdapter<MyMapType, Map<String, String>> {
#Override
public MyMapType marshal(Map<String, String> map) throws Exception {
MyMapType myMapType = new MyMapType();
for(Entry<String, String> entry : map.entrySet()) {
MyMapEntryType myMapEntryType = new MyMapEntryType();
myMapEntryType.setKey(entry.getKey());
myMapEntryType.setValue(entry.getValue());
myMapType.getEntry().add(myMapEntryType);
}
return myMapType;
}
#Override
public Map<String, String> unmarshal(MyMapType map) throws Exception {
HashMap<String, String> hashMap = new HashMap<String, String>();
for(MyMapEntryType myEntryType : map.getEntry()) {
hashMap.put(myEntryType.getKey(), myEntryType.getValue());
}
return hashMap;
}
}
This is the class which has the HashMap field:
XmlElementMap.java
#XmlAccessorType(XmlAccessType.FIELD)
public class XmlElementMap {
#XmlAttribute(name="sequence")
private int sequence;
#XmlJavaTypeAdapter(MyMapAdapter.class)
private Map<String, String> map = new HashMap<String, String>();
public int getSequence() {
return sequence;
}
public void setSequence(int sequence) {
this.sequence = sequence;
}
public Map<String, String> getMap() {
return map;
}
public void setMap(Map<String, String> map) {
this.map = map;
}
}
Please advise on how to achieve this.
Regards,
-Anand
Currently it produces the following output:
I have the same requirement "I need the Key to be the Element itself and the value of the HashMap to be the value of the element".
I didn't use customized adapter, but implemented it by converting the HashMap entries dynamically to a list of JAXBElement objects, and then annotated the list with #XmlAnyElement.
#XmlRootElement(name="root")
public class MyMapType {
#XmlAnyElement
public List<JAXBElement> entries = new ArrayList<JAXBElement>();
public MyMapType() { // JAXB required
}
public MyMapType(Map<String, String> map) {
for (Map.Entry<String, String> entry : map.entrySet()) {
entries.add(new JAXBElement(new QName(entry.getKey()),
String.class, entry.getValue()));
}
}
public static void main(String[] args) throws Exception {
JAXBContext context = JAXBContext.newInstance(MyMapType.class);
Marshaller marshaller = context.createMarshaller();
marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true);
Map<String, String> map = new LinkedHashMap<String, String>();
map.put("key1", "value1");
map.put("key2", "value2");
MyMapType mt = new MyMapType(map);
marshaller.marshal(mt, System.out);
}
}
The output is,
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<root>
<key1>value1</key1>
<key2>value2</key2>
</root>
Note: a marshal/unmarshal example for Map instance can be found here: Dynamic tag names with JAXB.