JAXB: how to marshall map into <key>value</key>
Solution 1
the code provided didn't work for me. I found another way to Map :
MapElements :
package com.cellfish.mediadb.rest.lucene;
import javax.xml.bind.annotation.XmlElement;
class MapElements
{
@XmlElement public String key;
@XmlElement public Integer value;
private MapElements() {} //Required by JAXB
public MapElements(String key, Integer value)
{
this.key = key;
this.value = value;
}
}
MapAdapter :
import java.util.HashMap;
import java.util.Map;
import javax.xml.bind.annotation.adapters.XmlAdapter;
class MapAdapter extends XmlAdapter<MapElements[], Map<String, Integer>> {
public MapElements[] marshal(Map<String, Integer> arg0) throws Exception {
MapElements[] mapElements = new MapElements[arg0.size()];
int i = 0;
for (Map.Entry<String, Integer> entry : arg0.entrySet())
mapElements[i++] = new MapElements(entry.getKey(), entry.getValue());
return mapElements;
}
public Map<String, Integer> unmarshal(MapElements[] arg0) throws Exception {
Map<String, Integer> r = new HashMap<String, Integer>();
for (MapElements mapelement : arg0)
r.put(mapelement.key, mapelement.value);
return r;
}
}
The rootElement :
import java.util.HashMap;
import java.util.Map;
import javax.xml.bind.annotation.XmlRootElement;
import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
@XmlRootElement
public class Root {
private Map<String, Integer> mapProperty;
public Root() {
mapProperty = new HashMap<String, Integer>();
}
@XmlJavaTypeAdapter(MapAdapter.class)
public Map<String, Integer> getMapProperty() {
return mapProperty;
}
public void setMapProperty(Map<String, Integer> map) {
this.mapProperty = map;
}
}
I found the code in this website : http://www.developpez.net/forums/d972324/java/general-java/xml/hashmap-jaxb/
Solution 2
There may be a valid reason why you want to do this, but generating this kind of XML is generally best avoided. Why? Because it means that the XML elements of your map are dependent on the runtime contents of your map. And since XML is usually used as an external interface or interface layer this is not desirable. Let me explain.
The Xml Schema (xsd) defines the interface contract of your XML documents. In addition to being able to generate code from the XSD, JAXB can also generate the XML schema for you from the code. This allows you to restrict the data exchanged over the interface to the pre-agreed structures defined in the XSD.
In the default case for a Map<String, String>
, the generated XSD will restrict the map element to contain multiple entry elements each of which must contain one xs:string
key and one xs:string
value. That's a pretty clear interface contract.
What you describe is that you want the xml map to contain elements whose name will be determined by the content of the map at runtime. Then the generated XSD can only specify that the map must contain a list of elements whose type is unknown at compile time. This is something that you should generally avoid when defining an interface contract.
To achieve a strict contract in this case, you should use an enumerated type as the key of the map instead of a String. E.g.
public enum KeyType {
KEY, KEY2;
}
@XmlJavaTypeAdapter(MapAdapter.class)
Map<KeyType , String> mapProperty;
That way the keys which you want to become elements in XML are known at compile time so JAXB should be able to generate a schema that would restrict the elements of map to elements using one of the predefined keys KEY or KEY2.
On the other hand, if you wish to simplify the default generated structure
<map>
<entry>
<key>KEY</key>
<value>VALUE</value>
</entry>
<entry>
<key>KEY2</key>
<value>VALUE2</value>
</entry>
</map>
To something simpler like this
<map>
<item key="KEY" value="VALUE"/>
<item key="KEY2" value="VALUE2"/>
</map>
You can use a MapAdapter that converts the Map to an array of MapElements as follows:
class MapElements {
@XmlAttribute
public String key;
@XmlAttribute
public String value;
private MapElements() {
} //Required by JAXB
public MapElements(String key, String value) {
this.key = key;
this.value = value;
}
}
public class MapAdapter extends XmlAdapter<MapElements[], Map<String, String>> {
public MapAdapter() {
}
public MapElements[] marshal(Map<String, String> arg0) throws Exception {
MapElements[] mapElements = new MapElements[arg0.size()];
int i = 0;
for (Map.Entry<String, String> entry : arg0.entrySet())
mapElements[i++] = new MapElements(entry.getKey(), entry.getValue());
return mapElements;
}
public Map<String, String> unmarshal(MapElements[] arg0) throws Exception {
Map<String, String> r = new TreeMap<String, String>();
for (MapElements mapelement : arg0)
r.put(mapelement.key, mapelement.value);
return r;
}
}
Solution 3
I'm still working on a better solution but using MOXy JAXB, I've been able to handle the following XML:
<?xml version="1.0" encoding="UTF-8"?>
<root>
<mapProperty>
<map>
<key>value</key>
<key2>value2</key2>
</map>
</mapProperty>
</root>
You need to use an @XmlJavaTypeAdapter on your Map property:
import java.util.HashMap;
import java.util.Map;
import javax.xml.bind.annotation.XmlRootElement;
import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
@XmlRootElement
public class Root {
private Map<String, String> mapProperty;
public Root() {
mapProperty = new HashMap<String, String>();
}
@XmlJavaTypeAdapter(MapAdapter.class)
public Map<String, String> getMapProperty() {
return mapProperty;
}
public void setMapProperty(Map<String, String> map) {
this.mapProperty = map;
}
}
The implementation of the XmlAdapter is as follows:
import java.util.HashMap;
import java.util.Map;
import java.util.Map.Entry;
import javax.xml.bind.annotation.adapters.XmlAdapter;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
public class MapAdapter extends XmlAdapter<AdaptedMap, Map<String, String>> {
@Override
public AdaptedMap marshal(Map<String, String> map) throws Exception {
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
DocumentBuilder db = dbf.newDocumentBuilder();
Document document = db.newDocument();
Element rootElement = document.createElement("map");
document.appendChild(rootElement);
for(Entry<String,String> entry : map.entrySet()) {
Element mapElement = document.createElement(entry.getKey());
mapElement.setTextContent(entry.getValue());
rootElement.appendChild(mapElement);
}
AdaptedMap adaptedMap = new AdaptedMap();
adaptedMap.setValue(document);
return adaptedMap;
}
@Override
public Map<String, String> unmarshal(AdaptedMap adaptedMap) throws Exception {
Map<String, String> map = new HashMap<String, String>();
Element rootElement = (Element) adaptedMap.getValue();
NodeList childNodes = rootElement.getChildNodes();
for(int x=0,size=childNodes.getLength(); x<size; x++) {
Node childNode = childNodes.item(x);
if(childNode.getNodeType() == Node.ELEMENT_NODE) {
map.put(childNode.getLocalName(), childNode.getTextContent());
}
}
return map;
}
}
The AdpatedMap class is where all the magic happens, we will use a DOM to represent the content. We will trick JAXB intro dealing with a DOM through the combination of @XmlAnyElement and a property of type Object:
import javax.xml.bind.annotation.XmlAnyElement;
public class AdaptedMap {
private Object value;
@XmlAnyElement
public Object getValue() {
return value;
}
public void setValue(Object value) {
this.value = value;
}
}
This solution requires the MOXy JAXB implementation. You can configure the JAXB runtime to use the MOXy implementation by adding a file named jaxb.properties in with your model classes with the following entry:
javax.xml.bind.context.factory=org.eclipse.persistence.jaxb.JAXBContextFactory
The following demo code can be used to verify the code:
import java.io.File;
import javax.xml.bind.JAXBContext;
import javax.xml.bind.Marshaller;
import javax.xml.bind.Unmarshaller;
public class Demo {
public static void main(String[] args) throws Exception {
JAXBContext jc = JAXBContext.newInstance(Root.class);
Unmarshaller unmarshaller = jc.createUnmarshaller();
Root root = (Root) unmarshaller.unmarshal(new File("src/forum74/input.xml"));
Marshaller marshaller = jc.createMarshaller();
marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true);
marshaller.marshal(root, System.out);
}
}
Solution 4
I didn't see anything that really answered this very well. I found something that worked pretty well here:
Use JAXB XMLAnyElement type of style to return dynamic element names
I modified it a bit to support hashmap trees. You could add other collections.
public class MapAdapter extends XmlAdapter<MapWrapper, Map<String, Object>> {
@Override
public MapWrapper marshal(Map<String, Object> m) throws Exception {
MapWrapper wrapper = new MapWrapper();
List elements = new ArrayList();
for (Map.Entry<String, Object> property : m.entrySet()) {
if (property.getValue() instanceof Map)
elements.add(new JAXBElement<MapWrapper>(new QName(getCleanLabel(property.getKey())),
MapWrapper.class, marshal((Map) property.getValue())));
else
elements.add(new JAXBElement<String>(new QName(getCleanLabel(property.getKey())),
String.class, property.getValue().toString()));
}
wrapper.elements = elements;
return wrapper;
}
@Override
public Map<String, Object> unmarshal(MapWrapper v) throws Exception {
HashMap<String, Object> returnval = new HashMap();
for (Object o : v.elements) {
Element e = (Element) o;
if (e.getChildNodes().getLength() > 1) {
MapWrapper mw = new MapWrapper();
mw.elements = new ArrayList();
for (int count = 0; count < e.getChildNodes().getLength(); count++) {
if (e.getChildNodes().item(count) instanceof Element) {
mw.elements.add(e.getChildNodes().item(count));
}
}
returnval.put(e.getTagName(), unmarshal(mw));
} else {
returnval.put(e.getTagName(), e.getTextContent());
}
}
return returnval;
}
// Return a XML-safe attribute. Might want to add camel case support
private String getCleanLabel(String attributeLabel) {
attributeLabel = attributeLabel.replaceAll("[()]", "").replaceAll("[^\\w\\s]", "_").replaceAll(" ", "_");
return attributeLabel;
}
}
class MapWrapper {
@XmlAnyElement
List elements;
}
Then to implement it:
static class myxml {
String name = "Full Name";
String address = "1234 Main St";
// I assign values to the map elsewhere, but it's just a simple
// hashmap with a hashmap child as an example.
@XmlJavaTypeAdapter(MapAdapter.class)
public Map<String, Object> childMap;
}
Feeding this through a simple Marshaller gives output that looks like this:
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<myxml>
<name>Full Name</name>
<address>1234 Main St</address>
<childMap>
<key2>value2</key2>
<key1>value1</key1>
<childTree>
<childkey1>childvalue1</childkey1>
</childTree>
</childMap>
</myxml>
Solution 5
I have solution without adapter. Transient map converted to xml-elements and vise versa:
@XmlAccessorType(XmlAccessType.FIELD)
@XmlRootElement(name = "SchemaBasedProperties")
public class SchemaBasedProperties
{
@XmlTransient
Map<String, Map<String, String>> properties;
@XmlAnyElement(lax = true)
List<Object> xmlmap;
public Map<String, Map<String, String>> getProperties()
{
if (properties == null)
properties = new LinkedHashMap<String, Map<String, String>>(); // I want same order
return properties;
}
boolean beforeMarshal(Marshaller m)
{
try
{
if (properties != null && !properties.isEmpty())
{
if (xmlmap == null)
xmlmap = new ArrayList<Object>();
else
xmlmap.clear();
javax.xml.parsers.DocumentBuilderFactory dbf = javax.xml.parsers.DocumentBuilderFactory.newInstance();
javax.xml.parsers.DocumentBuilder db = dbf.newDocumentBuilder();
org.w3c.dom.Document doc = db.newDocument();
org.w3c.dom.Element element;
Map<String, String> attrs;
for (Map.Entry<String, Map<String, String>> it: properties.entrySet())
{
element = doc.createElement(it.getKey());
attrs = it.getValue();
if (attrs != null)
for (Map.Entry<String, String> at: attrs.entrySet())
element.setAttribute(at.getKey(), at.getValue());
xmlmap.add(element);
}
}
else
xmlmap = null;
}
catch (Exception e)
{
e.printStackTrace();
return false;
}
return true;
}
void afterUnmarshal(Unmarshaller u, Object p)
{
org.w3c.dom.Node node;
org.w3c.dom.NamedNodeMap nodeMap;
String name;
Map<String, String> attrs;
getProperties().clear();
if (xmlmap != null)
for (Object xmlNode: xmlmap)
if (xmlNode instanceof org.w3c.dom.Node)
{
node = (org.w3c.dom.Node) xmlNode;
nodeMap = node.getAttributes();
name = node.getLocalName();
attrs = new HashMap<String, String>();
for (int i = 0, l = nodeMap.getLength(); i < l; i++)
{
node = nodeMap.item(i);
attrs.put(node.getNodeName(), node.getNodeValue());
}
getProperties().put(name, attrs);
}
xmlmap = null;
}
public static void main(String[] args)
throws Exception
{
SchemaBasedProperties props = new SchemaBasedProperties();
Map<String, String> attrs;
attrs = new HashMap<String, String>();
attrs.put("ResId", "A_LABEL");
props.getProperties().put("LABEL", attrs);
attrs = new HashMap<String, String>();
attrs.put("ResId", "A_TOOLTIP");
props.getProperties().put("TOOLTIP", attrs);
attrs = new HashMap<String, String>();
attrs.put("Value", "hide");
props.getProperties().put("DISPLAYHINT", attrs);
javax.xml.bind.JAXBContext jc = javax.xml.bind.JAXBContext.newInstance(SchemaBasedProperties.class);
Marshaller marshaller = jc.createMarshaller();
marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true);
marshaller.marshal(props, new java.io.File("test.xml"));
Unmarshaller unmarshaller = jc.createUnmarshaller();
props = (SchemaBasedProperties) unmarshaller.unmarshal(new java.io.File("test.xml"));
System.out.println(props.getProperties());
}
}
My output as espected:
<SchemaBasedProperties>
<LABEL ResId="A_LABEL"/>
<TOOLTIP ResId="A_TOOLTIP"/>
<DISPLAYHINT Value="hide"/>
</SchemaBasedProperties>
{LABEL={ResId=A_LABEL}, TOOLTIP={ResId=A_TOOLTIP}, DISPLAYHINT={Value=hide}}
You can use element name/value pair. I need attributes... Have fun!
Tim
Software Developer and Agile Project Manager with 15 years of experience in Software Engineering.
Updated on August 28, 2021Comments
-
Tim over 2 years
The question is about JAXB Map marshalling - there is plenty of examples on how to marhsall a Map into a structure like follows:
<map> <entry> <key> KEY </key> <value> VALUE </value> </entry> <entry> <key> KEY2 </key> <value> VALUE2 </value> </entry> <entry> ... </map>
In fact, this is natively supported by JAXB. What I need, however, is the XML where key is the element name, and value is its content:
<map> <key> VALUE </key> <key2> VALUE2 </key2> ... </map>
I didn't succeed implementing my Map adapter the way it is recommended by JAXB developers (https://jaxb.dev.java.net/guide/Mapping_your_favorite_class.html), as I need, he - dynamic attribute name :)
Is there any solution for that?
P.S. Currently I have to create a dedicated container class for each typical set of key-value pairs I want to marshall to XML - it works, but I have to create way too many of these helper containers.
-
blo0p3r almost 11 yearsI'm not sure how this got so many votes since it doesn't do what was asked. This still returns
<key>key</key> <value>value</value>
. NOT<key>value</key>
. I've pretty much copied and paced this code. Has this been the "best solution"? -
NSPKUWCExi2pr8wVoGNk almost 11 yearsThis is not a solution to a stated problem.
-
Marcel Piquet about 10 yearsI agree with @blo0p3r this just produces a series of items: <item> <key>delete</key> <value>1</value> </item> <item> <key>edit</key> <value>1</value> </item> <item> <key>create</key> <value>1</value> </item>
-
Jonah over 9 yearsHow can this be modified to not include the "map" element?
-
Nassim MOUALEK over 9 yearsHi @Blaise Doughan, is there a better solution without Moxy JAXB?
-
Tarik over 3 yearsIt would be great if you added the unmarshall function
-
BATMAN_2008 about 3 yearsYou are awesome. I was trying something like this from Yesterday. You helped me. Thanks a lot :)
-
BATMAN_2008 almost 3 years@JavaJeff The
marshalling
part really helped me. I am really stuck in theunmarshalling
part as of now. Can you please provide an example of theunmarshalling
method. If you can provide then that would be really great. -
BATMAN_2008 almost 3 yearsHi, Thanks for this. I followed the similar approach in my application but running to the issue and unable to get the desired output. If possible can you please have a look at this question where I have explained in-detail what issue I am facing with code sample. If you have some suggestion or workaround it would be really helpful for me: stackoverflow.com/questions/67648941/…
-
BATMAN_2008 almost 3 yearsThanks for your answer helped me in fixing one issue which was bothering me for 2 weeks. Thanks again. Keep sharing and happy coding :)
-
JavaJeff over 2 years@BATMAN_2008 - added unmarshal