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><rootTag><body>All systems online.</body></rootTag></body>
</message>
</Report>
As you can see in the above output, for the second instance of "body", the serialization produced
<body><rootTag><body>All systems online.</body></rootTag></body>
instead of
<body><rootTag><body>All systems online.</body></rootTag></body>
How to solve this problem?
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
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With