DZone
Thanks for visiting DZone today,
Edit Profile
  • Manage Email Subscriptions
  • How to Post to DZone
  • Article Submission Guidelines
Sign Out View Profile
  • Post an Article
  • Manage My Drafts
Over 2 million developers have joined DZone.
Log In / Join
Please enter at least three characters to search
Refcards Trend Reports
Events Video Library
Refcards
Trend Reports

Events

View Events Video Library

Zones

Culture and Methodologies Agile Career Development Methodologies Team Management
Data Engineering AI/ML Big Data Data Databases IoT
Software Design and Architecture Cloud Architecture Containers Integration Microservices Performance Security
Coding Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks
Culture and Methodologies
Agile Career Development Methodologies Team Management
Data Engineering
AI/ML Big Data Data Databases IoT
Software Design and Architecture
Cloud Architecture Containers Integration Microservices Performance Security
Coding
Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance
Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks

Modernize your data layer. Learn how to design cloud-native database architectures to meet the evolving demands of AI and GenAI workkloads.

Secure your stack and shape the future! Help dev teams across the globe navigate their software supply chain security challenges.

Releasing software shouldn't be stressful or risky. Learn how to leverage progressive delivery techniques to ensure safer deployments.

Avoid machine learning mistakes and boost model performance! Discover key ML patterns, anti-patterns, data strategies, and more.

Related

  • How to Convert XLS to XLSX in Java
  • Thread-Safety Pitfalls in XML Processing
  • Loading XML into MongoDB
  • SmartXML: An Alternative to XPath for Complex XML Files

Trending

  • Start Coding With Google Cloud Workstations
  • Segmentation Violation and How Rust Helps Overcome It
  • How to Build Scalable Mobile Apps With React Native: A Step-by-Step Guide
  • Scaling Mobile App Performance: How We Cut Screen Load Time From 8s to 2s
  1. DZone
  2. Coding
  3. Languages
  4. Binding Map to XML: Dynamic Tag Names with JAXB

Binding Map to XML: Dynamic Tag Names with JAXB

If for some reason you're forced to generate new XML elements for each key in a Map, see here how JAXB in Java can help you solve the problem.

By 
Wenfei Tang user avatar
Wenfei Tang
·
Jun. 24, 16 · Tutorial
Likes (12)
Comment
Save
Tweet
Share
104.3K Views

Join the DZone community and get the full member experience.

Join For Free

To convert Map to XML , the usual process is to stick with an element name,  while altering its attribute and text content for different map entries. For example, to save this map:

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

into XML content, we usually save it as:

<root>
    <entry key="key1">value1</entry>
    <entry key="key2">value2</entry>
</root>

Yet in some cases (for example, due to some horrible past design choice), we may need to convert the map into the following format:

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

In this case, we can still use JAXB to achieve this conversion.

I recently came into this problem in a project. After some checks on StackOverflow, I wrote a solution and posted it there. And here is a formatted version. In short, to resolve this kind of problem, we need to:

  1. Create a container class that holds a list (or array) of JAXBElement objects, where this list (or array) is annotated with @XmlAnyElement so dynamic element names can be generated.

  2. Create an XmlAdapter class that handles marshaling/unmarshaling 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).

Take the Map data listed above as an example, we need--

1. The Container (for @XmlAnyElement)

A Container to hold the @XmlAnyElement field, where all those elements that got dynamic names will be held as a list (or array).

/**
 * <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 to is this getProperties method, which gets annotated by @XmlAnyElement.
  2. Two addEntry methods are introduced here for ease 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 as an info probe, (to 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;
    }

}

And that's all. Now it's time to play with this container and adapter.

3. Examples

Here are two examples showing usage of the container and 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 in my hand 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());
System.out.println(xdo.toString());

Note that SysParamXDO is not used in the test code, yet it could be your choice when the wrapper element "sys-config" is not needed at all.


4. Summary

As I mentioned in the beginning, it's not recommended to use dynamic element names for keys of Map<String, String>. But in case you do need to, you can still achieve it by some @XmlAnyElement and XmlAdapter/@XmlJavaTypeAdapter.

XML Binding (linguistics)

Published at DZone with permission of Wenfei Tang. See the original article here.

Opinions expressed by DZone contributors are their own.

Related

  • How to Convert XLS to XLSX in Java
  • Thread-Safety Pitfalls in XML Processing
  • Loading XML into MongoDB
  • SmartXML: An Alternative to XPath for Complex XML Files

Partner Resources

×

Comments
Oops! Something Went Wrong

The likes didn't load as expected. Please refresh the page and try again.

ABOUT US

  • About DZone
  • Support and feedback
  • Community research
  • Sitemap

ADVERTISE

  • Advertise with DZone

CONTRIBUTE ON DZONE

  • Article Submission Guidelines
  • Become a Contributor
  • Core Program
  • Visit the Writers' Zone

LEGAL

  • Terms of Service
  • Privacy Policy

CONTACT US

  • 3343 Perimeter Hill Drive
  • Suite 100
  • Nashville, TN 37211
  • support@dzone.com

Let's be friends:

Likes
There are no likes...yet! 👀
Be the first to like this post!
It looks like you're not logged in.
Sign in to see who liked this post!