Dynamic tag names with JAXB

Also came into this kind of problem recently. After referencing axtavt's answer listed above (and a bunch of other question threads), I made a summary for this kind of problem:

  1. A container class that holds a list (or array) of JAXBElement objects, where this list (or array) is annotated with @XmlAnyElement, thus dynamic element names could be generated.
  2. An XmlAdapter class that handles marshalling/unmarshalling between Map to/from this container class.
  3. Annotate any Map fields of your java bean with @XmlJavaTypeAdapter, with this XmlAdapter class as its value (or you can simply use the container class directly, as you can see below).

Now I'll take Map<String, String> as an example here, where

{"key1": "value1", "key2": "value2"} 

will be marshalled into

<root>
    <key1>value1</key1>
    <key2>value2</key2>
</root>

Below is the full code snippet & comments, as well as examples:

1, The Container (for @XmlAnyElement)

/**
 * <dl>
 * <dt>References:
 * </dt>
 * <dd>
 *  <ul>
 *      <li><a href="http://stackoverflow.com/questions/21382202/use-jaxb-xmlanyelement-type-of-style-to-return-dynamic-element-names">Dynamic element names in JAXB</a></li>
 *      <li><a href="http://stackoverflow.com/questions/3941479/jaxb-how-to-marshall-map-into-keyvalue-key">Marshal Map into key-value pairs</a></li>
 *      <li><a href="http://stackoverflow.com/questions/3293493/dynamic-tag-names-with-jaxb">Dynamic tag names with JAXB</a></li>
 *  </ul>
 * </dd>
 * </dl>
 * @author MEC
 *
 */
@XmlType
public static class MapWrapper{
    private List<JAXBElement<String>> properties = new ArrayList<>();

    public MapWrapper(){

    }
    /**
     * <p>
     * Funny fact: due to type erasure, this method may return 
     * List<Element> instead of List<JAXBElement<String>> in the end;
     * </p>
     * <h4>WARNING: do not use this method in your programme</h4>
     * <p>
     * Thus to retrieve map entries you've stored in this MapWrapper, it's 
     * recommended to use {@link #toMap()} instead.
     * </p>
     * @return
     */
    @XmlAnyElement
    public List<JAXBElement<String>> getProperties() {
        return properties;
    }
    public void setProperties(List<JAXBElement<String>> properties) {
        this.properties = properties;
    }




    /**
     * <p>
     * Only use {@link #addEntry(JAXBElement)} and {{@link #addEntry(String, String)}
     * when this <code>MapWrapper</code> instance is created by yourself 
     * (instead of through unmarshalling).
     * </p>
     * @param key map key
     * @param value map value
     */
    public void addEntry(String key, String value){
        JAXBElement<String> prop = new JAXBElement<String>(new QName(key), String.class, value);
        addEntry(prop);
    }
    public void addEntry(JAXBElement<String> prop){
        properties.add(prop);
    }

    @Override
    public String toString() {
        return "MapWrapper [properties=" + toMap() + "]";
    }

    /**
     * <p>
     * To Read-Only Map
     * </p>
     * 
     * @return
     */
    public Map<String, String> toMap(){
        //Note: Due to type erasure, you cannot use properties.stream() directly when unmashalling is used..
        List<?> props = properties;
        return props.stream().collect(Collectors.toMap(MapWrapper::extractLocalName, MapWrapper::extractTextContent));
    }


    /**
     * <p>
     * Extract local name from <code>obj</code>, whether it's javax.xml.bind.JAXBElement or org.w3c.dom.Element;
     * </p>
     * @param obj
     * @return
     */
    @SuppressWarnings("unchecked")
    private static String extractLocalName(Object obj){

        Map<Class<?>, Function<? super Object, String>> strFuncs = new HashMap<>();
        strFuncs.put(JAXBElement.class, (jaxb) -> ((JAXBElement<String>)jaxb).getName().getLocalPart());
        strFuncs.put(Element.class, ele -> ((Element) ele).getLocalName());
        return extractPart(obj, strFuncs).orElse("");
    }

    /**
     * <p>
     * Extract text content from <code>obj</code>, whether it's javax.xml.bind.JAXBElement or org.w3c.dom.Element;
     * </p>
     * @param obj
     * @return
     */
    @SuppressWarnings("unchecked")
    private static String extractTextContent(Object obj){
        Map<Class<?>, Function<? super Object, String>> strFuncs = new HashMap<>();
        strFuncs.put(JAXBElement.class, (jaxb) -> ((JAXBElement<String>)jaxb).getValue());
        strFuncs.put(Element.class, ele -> ((Element) ele).getTextContent());
        return extractPart(obj, strFuncs).orElse("");
    }

    /**
     * Check class type of <code>obj</code> according to types listed in <code>strFuncs</code> keys,
     * then extract some string part from it according to the extract function specified in <code>strFuncs</code>
     * values.
     * @param obj
     * @param strFuncs
     * @return
     */
    private static <ObjType, T> Optional<T> extractPart(ObjType obj, Map<Class<?>, Function<? super ObjType, T>> strFuncs){
        for(Class<?> clazz : strFuncs.keySet()){
            if(clazz.isInstance(obj)){
                return Optional.of(strFuncs.get(clazz).apply(obj));
            }
        }
        return Optional.empty();
    }
}

Notes:

  1. For the JAXB Binding, all you need to pay attention is this getProperties method, which get annotated by @XmlAnyElement.
  2. Two addEntry methods are introduced here for easy of use. They should be used carefully though, as things may turn out horribly wrong when they are used for a freshly unmarshalled MapWrapper through JAXBContext (instead of created by yourself through a new operator).
  3. toMap is introduced here for info probe, i.e. help to check map entries stored in this MapWrapper instance.

2, The Adapter (XmlAdapter)

XmlAdapter is used in pair with @XmlJavaTypeAdapter, which in this case is only needed when Map<String, String> is used as a bean property.

/**
 * <p>
 * ref: http://stackoverflow.com/questions/21382202/use-jaxb-xmlanyelement-type-of-style-to-return-dynamic-element-names
 * </p>
 * @author MEC
 *
 */
public static class MapAdapter extends XmlAdapter<MapWrapper, Map<String, String>>{

    @Override
    public Map<String, String> unmarshal(MapWrapper v) throws Exception {
        Map<String, String> map = v.toMap();

        return map;
    }

    @Override
    public MapWrapper marshal(Map<String, String> m) throws Exception {
        MapWrapper wrapper = new MapWrapper();

        for(Map.Entry<String, String> entry : m.entrySet()){
             wrapper.addEntry(new JAXBElement<String>(new QName(entry.getKey()), String.class, entry.getValue()));
        }

        return wrapper;
    }

}

3, Examples

Here are two examples showing usage of the container & adapter.

3.1 Example 1

To map this xml:

<root>
    <key1>value1</key1>
    <key2>value2</key2>
<root>

You can use the following class:

@XmlRootElement(name="root")
public class CustomMap extends MapWrapper{
    public CustomMap(){

    }
}

Test Code:

CustomMap map = new CustomMap();
map.addEntry("key1", "value1");
map.addEntry("key1", "value2");

StringWriter sb = new StringWriter();
JAXBContext.newInstance(CustomMap.class).createMarshaller().marshal(map, sb);
out.println(sb.toString());

Note that no @XmlJavaTypeAdapter is used here.

3.2 Example 2

To map this xml:

<root>
    <map>
        <key1>value1</key1>
        <key2>value2</key2>
    </map>
    <other>other content</other>
</root>

You can use the following class:

@XmlRootElement(name="root")
@XmlType(propOrder={"map", "other"})
public class YetAnotherBean{
    private Map<String, String> map = new HashMap<>();
    private String other;
    public YetAnotherBean(){

    }
    public void putEntry(String key, String value){
        map.put(key, value);
    }
    @XmlElement(name="map")
    @XmlJavaTypeAdapter(MapAdapter.class)
    public Map<String, String> getMap(){
        return map;
    }
    public void setMap(Map<String, String> map){
        this.map = map;
    }
    @XmlElement(name="other")
    public String getOther(){
        return other;
    }
    public void setOther(String other){
        this.other = other;
    }
}

Test Code:

YetAnotherBean yab = new YetAnotherBean();
yab.putEntry("key1", "value1");
yab.putEntry("key2", "value2");
yab.setOther("other content");

StringWriter sb = new StringWriter();
JAXBContext.newInstance(YetAnotherBean.class).createMarshaller().marshal(yab, sb);
out.println(sb.toString());

Note that @XmlJavaTypeAdapter is applied onto the Map<String, String> field with MapAdapter as its value.

3.3 Example 3

Now let's add add some attributes to these elements. Due to some mysterious reasons, I have this kind of XML structure to map:

<sys-config>
  <sys-params>
    <ACCESSLOG_FILE_BY attr="C" desc="AccessLog file desc">SYSTEM</ACCESSLOG_FILE_BY>
    <ACCESSLOG_WRITE_MODE attr="D" desc="">DB</ACCESSLOG_WRITE_MODE>
    <CHANEG_BUTTON_IMAGES attr="E" desc="Button Image URL, eh, boolean value. ...Wait, what?">FALSE</CHANEG_BUTTON_IMAGES>
  </sys-params>
</sys-config>

As you can see, system parameter names are all set to be the element's name instead of as its attribute. To resolve this problem we can use a little help from JAXBElement again:

@XmlRootElement(name="sys-config")
public class SysParamConfigXDO{
    private SysParamEntries sysParams = new SysParamEntries();

    public SysParamConfigXDO(){

    }

    public void addSysParam(String name, String value, String attr, String desc){
        sysParams.addEntry(name, value, attr, desc);;
    }

    @XmlElement(name="sys-params")
    @XmlJavaTypeAdapter(SysParamEntriesAdapter.class)
    public SysParamEntries getSysParams() {
        return sysParams;
    }

    public void setSysParams(SysParamEntries sysParams) {
        this.sysParams = sysParams;
    }

    @Override
    public String toString() {
        return "SysParamConfigXDO [sysParams=" + sysParams + "]";
    }
}

@XmlRootElement(name="root")
public class SysParamXDO extends SysParamEntriesWrapper{
    public SysParamXDO(){

    }
}
@SuppressWarnings("unchecked")
@XmlType
public class SysParamEntriesWrapper{
    /**
     * <p>
     * Here is the tricky part:
     * <ul>
     *  <li>When this <code>SysParamEntriesWrapper</code> is created by yourself, objects 
     * stored in this <code>entries</code> list is of type SystemParamEntry</li>
     *  <li>Yet during the unmarshalling process, this <code>SysParamEntriesWrapper</code> is 
     * created by the JAXBContext, thus objects stored in the <code>entries</code> is 
     * of type Element actually.</li>
     * </ul>
     * </p>
     */
    List<JAXBElement<SysParamEntry>> entries = new ArrayList<>();
    public SysParamEntriesWrapper(){
    }


    public void addEntry(String name, String value, String attr, String desc){
        addEntry(new SysParamEntry(name, value, attr, desc));
    }
    public void addEntry(String name, String value){
        addEntry(new SysParamEntry(name, value));
    }

    public void addEntry(SysParamEntry entry){
        JAXBElement<SysParamEntry> bean = new JAXBElement<SysParamEntry>(new QName("", entry.getName()), SysParamEntry.class, entry);
        entries.add(bean);
    }

    @XmlAnyElement
    public List<JAXBElement<SysParamEntry>> getEntries() {
        return entries;
    }
    public void setEntries(List<JAXBElement<SysParamEntry>> entries) {
        this.entries = entries;
    }


    @Override
    public String toString() {
        return "SysParammEntriesWrapper [entries=" + toMap() + "]";
    }


    public Map<String, SysParamEntry> toMap(){
        Map<String, SysParamEntry> retval = new HashMap<>();

        List<?> entries = this.entries;

        entries.stream().map(SysParamEntriesWrapper::convertToParamEntry).
            forEach(entry -> retval.put(entry.getName(), entry));;
        return retval;
    }


    private static SysParamEntry convertToParamEntry(Object entry){
        String name = extractName(entry);
        String attr = extractAttr(entry);
        String desc = extractDesc(entry);
        String value = extractValue(entry);
        return new SysParamEntry(name, value, attr, desc);
    }
    @SuppressWarnings("unchecked")
    private static String extractName(Object entry){
        return extractPart(entry, nameExtractors).orElse("");
    }
    @SuppressWarnings("unchecked")
    private static String extractAttr(Object entry){
        return extractPart(entry, attrExtractors).orElse("");
    }
    @SuppressWarnings("unchecked")
    private static String extractDesc(Object entry){
        return extractPart(entry, descExtractors).orElse("");
    }
    @SuppressWarnings("unchecked")
    private static String extractValue(Object entry){
        return extractPart(entry, valueExtractors).orElse("");
    }
    private static <ObjType, RetType> Optional<RetType> extractPart(ObjType obj, Map<Class<?>,
            Function<? super ObjType, RetType>> extractFuncs ){
        for(Class<?> clazz : extractFuncs.keySet()){
            if(clazz.isInstance(obj)){
                return Optional.ofNullable(extractFuncs.get(clazz).apply(obj));
            }
        }
        return Optional.empty();
    }


    private static Map<Class<?>, Function<? super Object, String>> nameExtractors = new HashMap<>();
    private static Map<Class<?>, Function<? super Object, String>> attrExtractors = new HashMap<>();
    private static Map<Class<?>, Function<? super Object, String>> descExtractors = new HashMap<>();
    private static Map<Class<?>, Function<? super Object, String>> valueExtractors = new HashMap<>();
    static{
        nameExtractors.put(JAXBElement.class, jaxb -> ((JAXBElement<SysParamEntry>)jaxb).getName().getLocalPart());
        nameExtractors.put(Element.class, ele -> ((Element) ele).getLocalName());

        attrExtractors.put(JAXBElement.class, jaxb -> ((JAXBElement<SysParamEntry>)jaxb).getValue().getAttr());
        attrExtractors.put(Element.class, ele -> ((Element) ele).getAttribute("attr"));

        descExtractors.put(JAXBElement.class, jaxb -> ((JAXBElement<SysParamEntry>)jaxb).getValue().getDesc());
        descExtractors.put(Element.class, ele -> ((Element) ele).getAttribute("desc"));

        valueExtractors.put(JAXBElement.class, jaxb -> ((JAXBElement<SysParamEntry>)jaxb).getValue().getValue());
        valueExtractors.put(Element.class, ele -> ((Element) ele).getTextContent());
    }
}

public class SysParamEntriesAdapter extends XmlAdapter<SysParamEntriesWrapper, SysParamEntries>{

    @Override
    public SysParamEntries unmarshal(SysParamEntriesWrapper v) throws Exception {
        SysParamEntries retval = new SysParamEntries();
        v.toMap().values().stream().forEach(retval::addEntry);
        return retval;
    }

    @Override
    public SysParamEntriesWrapper marshal(SysParamEntries v) throws Exception {
        SysParamEntriesWrapper entriesWrapper = new SysParamEntriesWrapper();
        v.getEntries().forEach(entriesWrapper::addEntry);
        return entriesWrapper;
    }
}

public class SysParamEntries{
    List<SysParamEntry> entries = new ArrayList<>();;
    public SysParamEntries(){

    }
    public SysParamEntries(List<SysParamEntry> entries) {
        super();
        this.entries = entries;
    }

    public void addEntry(SysParamEntry entry){
        entries.add(entry);
    }
    public void addEntry(String name, String value){
        addEntry(name, value, "C");
    }

    public void addEntry(String name, String value, String attr){
        addEntry(name, value, attr, "");
    }

    public void addEntry(String name, String value, String attr, String desc){
        entries.add(new SysParamEntry(name, value, attr, desc));
    }
    public List<SysParamEntry> getEntries() {
        return entries;
    }
    public void setEntries(List<SysParamEntry> entries) {
        this.entries = entries;
    }
    @Override
    public String toString() {
        return "SystemParamEntries [entries=" + entries + "]";
    }

}
@XmlType
public class SysParamEntry{
    String name;
    String value = "";
    String attr = "";
    String desc = "";
    public SysParamEntry(){

    }

    public SysParamEntry(String name, String value) {
        super();
        this.name = name;
        this.value = value;
    }

    public SysParamEntry(String name, String value, String attr) {
        super();
        this.name = name;
        this.value = value;
        this.attr = attr;
    }

    public SysParamEntry(String name, String value, String attr, String desc) {
        super();
        this.name = name;
        this.value = value;
        this.attr = attr;
        this.desc = desc;
    }
    @XmlTransient
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    @XmlValue
    public String getValue() {
        return value;
    }
    public void setValue(String value) {
        this.value = value;
    }
    @XmlAttribute(name="attr")
    public String getAttr() {
        return attr;
    }
    public void setAttr(String attr) {
        this.attr = attr;
    }
    @XmlAttribute(name="desc")
    public String getDesc() {
        return desc;
    }
    public void setDesc(String desc) {
        this.desc = desc;
    }
    @Override
    public String toString() {
        return "SystemParamEntry [name=" + name + ", value=" + value + ", attr=" + attr + ", desc=" + desc + "]";
    }
}

And it's time for test:

//Marshal
SysParamConfigXDO xdo = new SysParamConfigXDO();
xdo.addSysParam("ACCESSLOG_FILE_BY", "SYSTEM", "C", "AccessLog file desc");
xdo.addSysParam("ACCESSLOG_WRITE_MODE", "DB", "D", "");
xdo.addSysParam("CHANEG_BUTTON_IMAGES", "FALSE", "E", "Button Image URL, eh, boolean value. ...Wait, what?");

JAXBContext jaxbCtx = JAXBContext.newInstance(SysParamConfigXDO.class, SysParamEntries.class);
jaxbCtx.createMarshaller().marshal(xdo, System.out);


//Unmarshal
Path xmlFile = Paths.get("path_to_the_saved_xml_file.xml");

JAXBContext jaxbCtx = JAXBContext.newInstance(SysParamConfigXDO.class, SysParamEntries.class);
SysParamConfigXDO xdo = (SysParamConfigXDO) jaxbCtx.createUnmarshaller().unmarshal(xmlFile.toFile());
out.println(xdo.toString());

Maybe somebody is interested in an easier solution with marshall and unmarshall example. It is not a map but still a key-value solution because we are using JAXBElement with a key (=localname) and value (=textcontent).

@XmlRootElement(name="map")
@XmlAccessorType(XmlAccessType.FIELD)
public class XmlMap {
    //one caveat (as mec_test_1 pointed out) unmarshalled objects are from type org.w3c.dom.Element and during marshall it is JAXBElement
    @XmlAnyElement
    List<JAXBElement<String>> dates = new ArrayList<>();

To unmarshall lets say this xml file

<map>
   <2019-01-01>Yes</2019-01-01>
   <2019-02-01>No</2019-02-01>
</map>

You have to run:

JAXBContext c = JAXBContext.newInstance(XmlMap.class);
XmlMap map = c.createUnmarshaller().unmarshall(new File("xmlfile.xml"));
//access the objects via
System.out.println("Key: " + ((org.w3c.dom.Element) map.dates.get(0)).getLocalName());
System.out.println("Value: " + ((org.w3c.dom.Element) map.dates.get(0)).getTextContent());

An to marshall a object:

import javax.xml.namespace.QName;
import javax.xml.bind.JAXBElement;

XmlMap xmlMap = new XmlMap();
xmlMap.dates.add(new JAXBElement<String>(new QName("key"), String.class, "value"));
xmlMap.dates.add(new JAXBElement<String>(new QName("2019-01-01"), String.class, "Yes"));

JAXBContext context = JAXBContext.newInstance(XmlMap.class);
Marshaller m = context.createMarshaller();
m.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, Boolean.TRUE);
m.marshal(verzObj, System.out);

Output:

<map>
   <key>val</key>
   <2019-01-01>Yes</2019-01-01>
</map>

You can use an @XmlAnyElement-annotated property and return the elements as JAXBElements:

private Map<String, Integer> months = ...;

@XmlAnyElement
public List<JAXBElement<Integer>> getMonths() {
    List<JAXBElement<Integer>> elements = new ArrayList<JAXBElement<Integer>>();
    for (Map.Entry<String, Integer> month: months.entrySet()) 
        elements.add(new JAXBElement(new QName(month.getKey()), 
                                     Integer.class, month.getValue()));
    return elements;
}

This approach is ugly, but not uglier than the XML it produces.

Tags:

Java

Xml

Jaxb