6

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?

CodeBlue
  • 14,631
  • 33
  • 94
  • 132

3 Answers3

7

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
bdoughan
  • 147,609
  • 23
  • 300
  • 400
  • After doing this, I get an exception saying - "unable to marshal type "java.lang.String" as an element because it is missing an @XmlRootElement annotation" – CodeBlue Sep 14 '12 at 19:19
  • @CodeBlue - Yes there appears to be a bug in the JAXB (JSR-222) implementation wrt this example. I used EclipseLink JAXB (MOXy) to put the example together. – bdoughan Sep 14 '12 at 19:26
  • 1
    Wow. Why haven't they fixed it yet? – CodeBlue Sep 14 '12 at 19:32
  • 1
    @CodeBlue and BDoughan : Is this bug fixed? I am too getting this error. – Arun Mar 14 '13 at 06:04
  • The bug does not seem to be fixed with JDK7 1.7.0\_76 yet. – wilx Jun 24 '15 at 08:54
2

If its only for Marshalling, and to ignore the < and >, We can use the following:

marshaller.setProperty("com.sun.xml.bind.marshaller.CharacterEscapeHandler",
                new CharacterEscapeHandler() {
                    @Override
                    public void escape(char[] ac, int i, int j, boolean flag,
                            Writer writer) throws IOException {
                        writer.write(ac, i, j);
                    }
                });
Lokesh
  • 21
  • 1
  • Has no such property: javax.xml.bind.PropertyException: name: com.sun.xml.bind.marshaller.CharacterEscapeHandler... – Enginer Nov 19 '14 at 16:40
2

3 different solutions 1), 2) 3), here below :

1) Following post is a the description of your solution Loresh :

http://anna-safronova.livejournal.com/2524.html?thread=9180

This is still missing limitations details.

  • With embeeded html, we need a <![CDATA block

  • JAXB's dependancy

com.sun.xml.bind.marshaller.CharacterEscapeHandler

Needs import jaxb-impl for compilation / and may be required for excution, e.g.

<groupId>com.sun.xml.bind</groupId>
<artifactId>jaxb-impl</artifactId>  
 <version>2.2.4</version>
  • Limitation : this solution is Container-specific and may not run because of class-loading policy.

2) Another similar approach is JDK's rt.jar dependancy

com.sun.xml.internal.bind.CharacterEscapeHandler

http://theopentutorials.com/tutorials/java/jaxb/jaxb-marshalling-and-unmarshalling-cdata-block/

Same limitation / dependends on target JDK, and some tweaks on Eclipse/Maven are necessary (bad alternative / My opinion)

3) Finally, the best solution was found on another post of Reg Whitton :

https://stackoverflow.com/a/12637295/560410

and this is the detailed reciepe :

http://javacoalface.blogspot.co.uk/2012/09/outputting-cdata-sections-with-jaxb.html

Worked perfect for me !

Community
  • 1
  • 1
skay
  • 1,681
  • 1
  • 14
  • 13