How to marshall a string using JAXB that sometimes contains XML content and sometimes does not?

Consider this example –

I have a class called Report that has a field of type Message. The Message class has a field called “body” which is a string. “body” can be any string, but sometimes it contains properly formatted XML content. How can I ensure that when the “body” contains XML content, the serialization takes the form of an XML structure rather than what it gives at present?

Here is the code with the output –

Report class

import javax.xml.bind.annotation.XmlRootElement;
import javax.xml.bind.annotation.XmlType;

@XmlRootElement(name = "Report")
@XmlType(propOrder = { "message"})
public class Report
{
    private Message message;
    public Message getMessage() { return message; }
    public void setMessage(Message m) { message = m; }
}

Message class

import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlType;

@XmlType(propOrder = { "body" })
public class Message
{
    private String body;
    public String getBody() { return body; }
    @XmlElement
    public void setBody(String body) { this.body = body; }
}

Main

import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBException;
import javax.xml.bind.Marshaller;



public class SerializationTest
{
    public static void main(String args[]) throws Exception
    {
       JAXBContext jaxbContext = JAXBContext.newInstance(Report.class);
       Marshaller jaxbMarshaller = jaxbContext.createMarshaller();
       jaxbMarshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true);

       Report report = new Report();
       Message message = new Message();

       message.setBody("Sample report message.");
       report.setMessage(message);
       jaxbMarshaller.marshal(report, System.out);

       message.setBody("<rootTag><body>All systems online.</body></rootTag>");
       report.setMessage(message);
       jaxbMarshaller.marshal(report, System.out);
    }
}

The output is as follows –

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<Report>
    <message>
        <body>Sample report message.</body>
    </message>
</Report>
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<Report>
    <message>
        <body>&lt;rootTag&gt;&lt;body&gt;All systems online.&lt;/body&gt;&lt;/rootTag&gt;</body>
    </message>
</Report>

As you can see in the above output, for the second instance of “body”, the serialization produced

 <body>&lt;rootTag&gt;&lt;body&gt;All systems online.&lt;/body&gt;&lt;/rootTag&gt;</body>

instead of

<body><rootTag><body>All systems online.</body></rootTag></body>

How to solve this problem?

Answer

Note: I’m the EclipseLink JAXB (MOXy) lead and a member of the JAXB (JSR-222) expert group.

This use case is mapped using the @XmlAnyElement annotation and specifying a DOMHandler. There appears to be bug when doing this with the JAXB RI, but the following use case works with EclipseLink JAXB (MOXy).

BodyDomHandler

By default a JAXB impleemntation will represent unmapped content as a DOM node. You can leverage a DomHandler to an alternate representation of the DOM, In this case we will represent the DOM as a String.

import java.io.*;
import javax.xml.bind.ValidationEventHandler;
import javax.xml.bind.annotation.DomHandler;
import javax.xml.transform.Source;
import javax.xml.transform.stream.*;

public class BodyDomHandler implements DomHandler<String, StreamResult> {

    private static final String BODY_START_TAG = "<body>";
    private static final String BODY_END_TAG = "</body>";

    private StringWriter xmlWriter = new StringWriter();

    public StreamResult createUnmarshaller(ValidationEventHandler errorHandler) {
        return new StreamResult(xmlWriter);
    }

    public String getElement(StreamResult rt) {
        String xml = rt.getWriter().toString();
        int beginIndex = xml.indexOf(BODY_START_TAG) + BODY_START_TAG.length();
        int endIndex = xml.indexOf(BODY_END_TAG);
        return xml.substring(beginIndex, endIndex);
    }

    public Source marshal(String n, ValidationEventHandler errorHandler) {
        try {
            String xml = BODY_START_TAG + n.trim() + BODY_END_TAG;
            StringReader xmlReader = new StringReader(xml);
            return new StreamSource(xmlReader);
        } catch(Exception e) {
            throw new RuntimeException(e);
        }
    }

}

Message

Below is how you would specify the @XmlAnyElement annotation on your Message class.

import javax.xml.bind.annotation.XmlAnyElement;
import javax.xml.bind.annotation.XmlType;

@XmlType(propOrder = { "body" })
public class Message
{
    private String body;
    public String getBody() { return body; }
    @XmlAnyElement(BodyDomHandler.class)
    public void setBody(String body) { this.body = body; }
}

Output

Below is the output from running your SerialziationTest:

<?xml version="1.0" encoding="UTF-8"?>
<Report>
   <message>
      <body>Sample report message.</body>
   </message>
</Report>
<?xml version="1.0" encoding="UTF-8"?>
<Report>
   <message>
      <body>
         <rootTag>
            <body>All systems online.</body>
         </rootTag>
      </body>
   </message>
</Report>

For More Information

NOTE – Bug in JAXB RI

There appears to be a bug in the JAXB reference implementation, and the example code will result in a stack trace like the following:

Exception in thread "main" javax.xml.bind.MarshalException
 - with linked exception:
[com.sun.istack.internal.SAXException2: unable to marshal type "java.lang.String" as an element because it is missing an @XmlRootElement annotation]
    at com.sun.xml.internal.bind.v2.runtime.MarshallerImpl.write(MarshallerImpl.java:317)
    at com.sun.xml.internal.bind.v2.runtime.MarshallerImpl.marshal(MarshallerImpl.java:243)
    at javax.xml.bind.helpers.AbstractMarshallerImpl.marshal(AbstractMarshallerImpl.java:75)
    at forum12428727.SerializationTest.main(SerializationTest.java:20)
Caused by: com.sun.istack.internal.SAXException2: unable to marshal type "java.lang.String" as an element because it is missing an @XmlRootElement annotation
    at com.sun.xml.internal.bind.v2.runtime.XMLSerializer.reportError(XMLSerializer.java:216)
    at com.sun.xml.internal.bind.v2.runtime.LeafBeanInfoImpl.serializeRoot(LeafBeanInfoImpl.java:126)
    at com.sun.xml.internal.bind.v2.runtime.property.SingleReferenceNodeProperty.serializeBody(SingleReferenceNodeProperty.java:100)
    at com.sun.xml.internal.bind.v2.runtime.ClassBeanInfoImpl.serializeBody(ClassBeanInfoImpl.java:306)
    at com.sun.xml.internal.bind.v2.runtime.XMLSerializer.childAsXsiType(XMLSerializer.java:664)
    at com.sun.xml.internal.bind.v2.runtime.property.SingleElementNodeProperty.serializeBody(SingleElementNodeProperty.java:141)
    at com.sun.xml.internal.bind.v2.runtime.ClassBeanInfoImpl.serializeBody(ClassBeanInfoImpl.java:306)
    at com.sun.xml.internal.bind.v2.runtime.XMLSerializer.childAsSoleContent(XMLSerializer.java:561)
    at com.sun.xml.internal.bind.v2.runtime.ClassBeanInfoImpl.serializeRoot(ClassBeanInfoImpl.java:290)
    at com.sun.xml.internal.bind.v2.runtime.XMLSerializer.childAsRoot(XMLSerializer.java:462)
    at com.sun.xml.internal.bind.v2.runtime.MarshallerImpl.write(MarshallerImpl.java:314)
    ... 3 more

Leave a Reply

Your email address will not be published. Required fields are marked *