MRX
MRX

Reputation: 1651

JAXB Marshalling Unmarshalling with CDATA

I am trying to do marshaling with JAXB.

My output is like this:

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<root>
    <name>&lt;![CDATA[&lt;h1&gt;kshitij&lt;/h1&gt;]]&gt;</name>
    <surname>&lt;h1&gt;solanki&lt;/h1&gt;</surname>
    <id>&lt;h1&gt;1&lt;/h1&gt;</id>
</root>

...but I need output like this:

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
    <root>
        <name><![CDATA[<h1>kshitij</h1>]]></name>
        <surname><![CDATA[<h1>solanki</h1>]]></surname>
        <id><![CDATA[0]]></id>
    </root>

I am using following code to do this.

If I uncomment code I get PropertyBindingException. Without it I can compile but I am not getting the exact required output.

  package com.ksh.templates;

import java.io.IOException;
import java.io.StringWriter;
import java.io.Writer;
import javax.xml.bind.JAXBContext;
import javax.xml.bind.Marshaller;
import com.sun.xml.bind.marshaller.CharacterEscapeHandler;

public class MainCDATA {
    public static void main(String args[])
    {
        try
        {
            String name = "<h1>kshitij</h1>";
            String surname = "<h1>solanki</h1>";
            String id = "<h1>1</h1>";
            
            TestingCDATA cdata = new TestingCDATA();
            cdata.setId(id);
            cdata.setName(name);
            cdata.setSurname(surname);
            
            JAXBContext jaxbContext = JAXBContext.newInstance(TestingCDATA.class);
            Marshaller marshaller = jaxbContext.createMarshaller();
            marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true);
            
            marshaller.setProperty(CharacterEscapeHandler.class.getName(), new CharacterEscapeHandler() { 
                public void escape(char[] ac, int i, int j, boolean flag,
                Writer writer) throws IOException {
                writer.write( ac, i, j ); }
                });
            StringWriter stringWriter = new StringWriter(); 
            marshaller.marshal(cdata, stringWriter);
            System.out.println(stringWriter.toString());
        } catch (Exception e) {
            System.out.println(e);
        }       
    }
}

My bean looks like this:

package com.ksh.templates;

import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlRootElement;
import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
import com.sun.xml.txw2.annotation.XmlCDATA;

@XmlRootElement(name = "root")
@XmlAccessorType(XmlAccessType.FIELD)
public class TestingCDATA {

    @XmlElement
    @XmlJavaTypeAdapter(value = AdaptorCDATA.class)
    private String name;
    @XmlElement
    @XmlJavaTypeAdapter(value = AdaptorCDATA.class)
    private String surname;
    
    @XmlCDATA
    public String getName() {
        return name;
    }
    @XmlCDATA
    public void setName(String name) {
        this.name = name;
    }
    @XmlCDATA
    public String getSurname() {
        return surname;
    }
    @XmlCDATA
    public void setSurname(String surname) {
        this.surname = surname;
    }
}

Adaptor Class

public class AdaptorCDATA extends XmlAdapter<String, String> {

    @Override
    public String marshal(String arg0) throws Exception {
        return "<![CDATA[" + arg0 + "]]>";
    }
    @Override
    public String unmarshal(String arg0) throws Exception {
        return arg0;
    }
}

Upvotes: 33

Views: 92496

Answers (7)

Bogdan Samondros
Bogdan Samondros

Reputation: 158

In addition to @bdoughan answer. Character escape handler with CDATA support

import com.sun.xml.bind.marshaller.CharacterEscapeHandler;

import java.io.IOException;
import java.io.Writer;

/**
 * This class is a modern version of JAXB MinimumEscapeHandler with CDATA support
 *
 * @author me
 * @see com.sun.xml.bind.marshaller.MinimumEscapeHandler
 */
public class CdataEscapeHandler implements CharacterEscapeHandler {

    private CdataEscapeHandler() {
    }  // no instanciation please

    public static final CharacterEscapeHandler theInstance = new CdataEscapeHandler();

    @Override
    public void escape(char[] ch, int start, int length, boolean isAttVal, Writer out) throws IOException {
        // avoid calling the Writerwrite method too much by assuming
        // that the escaping occurs rarely.
        // profiling revealed that this is faster than the naive code.
        int limit = start + length;
        for (int i = start; i < limit; i++) {
            if (!isAttVal
                    && i <= limit - 12
                    && ch[i] == '<'
                    && ch[i + 1] == '!'
                    && ch[i + 2] == '['
                    && ch[i + 3] == 'C'
                    && ch[i + 4] == 'D'
                    && ch[i + 5] == 'A'
                    && ch[i + 6] == 'T'
                    && ch[i + 7] == 'A'
                    && ch[i + 8] == '[') {
                int cdataEnd = i + 8;
                for (int k = i + 9; k < limit - 2; k++) {
                    if (ch[k] == ']'
                            && ch[k + 1] == ']'
                            && ch[k + 2] == '>') {
                        cdataEnd = k + 2;
                        break;
                    }
                }
                out.write(ch, start, cdataEnd + 1);
                if (cdataEnd == limit - 1) return;
                start = i = cdataEnd + 1;
            }
            char c = ch[i];
            if (c == '&' || c == '<' || c == '>' || c == '\r' || (c == '\"' && isAttVal)) {
                if (i != start)
                    out.write(ch, start, i - start);
                start = i + 1;
                switch (ch[i]) {
                    case '&':
                        out.write("&amp;");
                        break;
                    case '<':
                        out.write("&lt;");
                        break;
                    case '>':
                        out.write("&gt;");
                        break;
                    case '\"':
                        out.write("&quot;");
                        break;
                }
            }
        }

        if (start != limit)
            out.write(ch, start, limit - start);
    }
}

Upvotes: 0

bdoughan
bdoughan

Reputation: 149047

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

If you use MOXy as your JAXB (JSR-222) provider then you can leverage the @XmlCDATA extension for your use case.

Root

The @XmlCDATA annotation is used to indicate that you want the contents of a field/property wrapped in a CDATA section. The @XmlCDATA annotation can be used in combination with @XmlElement.

package forum14193944;

import javax.xml.bind.annotation.*;
import org.eclipse.persistence.oxm.annotations.XmlCDATA;

@XmlRootElement
@XmlAccessorType(XmlAccessType.FIELD)
public class Root {

    @XmlCDATA
    private String name;

    @XmlCDATA
    private String surname;

    @XmlCDATA
    private String id;

}

jaxb.properties

To use MOXy as your JAXB provider you need to add file named jaxb.properties with the following entry.

javax.xml.bind.context.factory=org.eclipse.persistence.jaxb.JAXBContextFactory

Demo

Below is some demo code to prove that everything works.

package forum14193944;

import java.io.File;
import javax.xml.bind.*;

public class Demo {

    public static void main(String[] args) throws Exception {
        JAXBContext jc = JAXBContext.newInstance(Root.class);

        Unmarshaller unmarshaller = jc.createUnmarshaller();
        File xml = new File("src/forum14193944/input.xml");
        Root root = (Root) unmarshaller.unmarshal(xml);

        Marshaller marshaller = jc.createMarshaller();
        marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true);
        marshaller.marshal(root, System.out);
    }

}

input.xml/Output

Below is the input to and output from running the demo code.

<?xml version="1.0" encoding="UTF-8"?>
<root>
   <name><![CDATA[<h1>kshitij</h1>]]></name>
   <surname><![CDATA[<h1>solanki</h1>]]></surname>
   <id><![CDATA[0]]></id>
</root>

For More Information

Upvotes: 15

pkoli
pkoli

Reputation: 676

I landed on this page trying to find solution to a similar issue, I found another approach to solve this. One way to solve this problem is to send XML as SAX2 events to a handler, then write the logic in the handler to add the CDATA tags to the XML. This approach doesn't require any annotation to be added. Useful in scenarios where classes to be marshaled are generated from XSD's.

Suppose you have a String field in a class generated from XSD which is to be marshaled and the String field contains special characters which are to be put inside a CDATA tag.

@XmlRootElement
public class TestingCDATA{
    public String xmlContent;

}

We'll start by searching a suitable class whose method can be overridden in our content handler. One such class is XMLWriter found in package com.sun.xml.txw2.output, It's available in jdk 1.7 and 1.8

import com.sun.xml.txw2.output.XMLWriter;
import org.xml.sax.SAXException;

import java.io.IOException;
import java.io.Writer;
import java.util.regex.Pattern;

public class CDATAContentHandler extends XMLWriter {
    public CDATAContentHandler(Writer writer, String encoding) throws IOException {
        super(writer, encoding);
    }

    // see http://www.w3.org/TR/xml/#syntax
    private static final Pattern XML_CHARS = Pattern.compile("[<>&]");

    public void characters(char[] ch, int start, int length) throws SAXException {
        boolean useCData = XML_CHARS.matcher(new String(ch, start, length)).find();
        if (useCData) {
            super.startCDATA();
        }
        super.characters(ch, start, length);
        if (useCData) {
            super.endCDATA();
        }
    }
}

We are overriding the characters method, using regex to check if any special characters are contained. If they are found then we put CDATA tags around them. In this case XMLWriter takes care of adding CDATA tag.

We'll use the following code for marshaling:

public String addCDATAToXML(TestingCDATA request) throws FormatException {
    try {
        JAXBContext jaxbContext = JAXBContext.newInstance(TestingCDATA.class);
        Marshaller jaxbMarshaller = jaxbContext.createMarshaller();
        StringWriter sw = new StringWriter();
        CDATAContentHandler cDataContentHandler = new CDATAContentHandler(sw, "UTF-8");
        jaxbMarshaller.marshal(request, cDataContentHandler);
        return sw.toString();
    } catch (JAXBException | IOException e) {
        throw new FormatException("Unable to add CDATA for request", e);
    }
}

This would marshal the object and return XML, if we pass a request to be marshaled as mentioned below.

TestingCDATA request=new TestingCDATA();
request.xmlContent="<?xml>";

System.out.println(addCDATAToXML(request)); // Would return the following String

Output- 

<?xml version="1.0" encoding="UTF-8"?>
<testingCDATA>
<xmlContent><![CDATA[<?xml>]]></xmlContent>
</testingCDATA>

Upvotes: 1

Marc P.
Marc P.

Reputation: 101

Sorry for digging out this question, and posting a new answer (my rep isn't high enough yet to comment...). I ran into the same issue, I tried Blaise Doughan's answer, but from my tests, either it doesn't cover all cases, either I'm doing something wrong somewhere.



    marshaller.setProperty(CharacterEscapeHandler.class.getName(),
                    new CharacterEscapeHandler() {
                        @Override
                        public void escape(char[] ac, int i, int j, boolean flag,
                                Writer writer) throws IOException {
                            writer.write(ac, i, j);
                        }
                    });


From my tests, this code removes all escaping, no matter if you are using the @XmlJavaTypeAdapter(AdapterCDATA.class) annotation on your attribute...

To fix that issue, I implemented the following CharacterEscapeHandler :


    public class CDataAwareUtfEncodedXmlCharacterEscapeHandler implements CharacterEscapeHandler {

        private static final char[] cDataPrefix = "<![CDATA[".toCharArray();
        private static final char[] cDataSuffix = "]]>".toCharArray();

        public static final CDataAwareUtfEncodedXmlCharacterEscapeHandler instance = new CDataAwareUtfEncodedXmlCharacterEscapeHandler();

        private CDataAwareUtfEncodedXmlCharacterEscapeHandler() {
        }

        @Override
        public void escape(char[] ch, int start, int length, boolean isAttVal, Writer out) throws IOException {
            boolean isCData = length > cDataPrefix.length + cDataSuffix.length;
            if (isCData) {
                for (int i = 0, j = start; i < cDataPrefix.length; ++i, ++j) {
                    if (cDataPrefix[i] != ch[j]) {
                        isCData = false;
                        break;
                    }
                }
                if (isCData) {
                    for (int i = cDataSuffix.length - 1, j = start + length - 1; i >= 0; --i, --j) {
                        if (cDataSuffix[i] != ch[j]) {
                            isCData = false;
                            break;
                        }
                    }
                }
            }
            if (isCData) {
                out.write(ch, start, length);
            } else {
                MinimumEscapeHandler.theInstance.escape(ch, start, length, isAttVal, out);
            }
        }
    }

If your encoding is not UTF*, you may not want to call MinimumEscapeHandler but rather NioEscapeHandler or even DumbEscapeHandler.

Upvotes: 10

Bruno Lee
Bruno Lee

Reputation: 1977

com.sun.internal dont works with play2,but this works

private static String marshal(YOurCLass xml){
    try{
        StringWriter stringWritter = new StringWriter();
        Marshaller marshaller = JAXBContext.newInstance(YourCLass.class).createMarshaller();
        marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true);
        marshaller.setProperty(Marshaller.JAXB_ENCODING, "ISO-8859-1");
        marshaller.marshal(xml, stringWritter);
        return stringWritter.toString().replaceAll("&lt;", "<").replaceAll("&gt;", ">");
    }
    catch(JAXBException e){
        throw new RuntimeException(e);
    }
}

Upvotes: -1

user1346730
user1346730

Reputation: 165

    @Test
    public void t() throws Exception {
        JAXBContext jc = JAXBContext.newInstance(Root.class);
        Marshaller marshaller = jc.createMarshaller();
        marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true);
        Root root = new Root();
        root.name = "<p>Jorge & Mary</p>";
        marshaller.marshal(root, System.out);
    }
    @XmlRootElement
    @XmlAccessorType(XmlAccessType.FIELD)
    public static class Root {
        @XmlCDATA
        private String name;
    }
    /* WHAT I SEE IN THE CONSOLE
     * 
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<root>
    <name>&lt;p&gt;Jorge &amp; Mary&lt;/p&gt;</name>
</root>
     */

Upvotes: 0

bdoughan
bdoughan

Reputation: 149047

You could do the following:

AdapterCDATA

package forum14193944;

import javax.xml.bind.annotation.adapters.XmlAdapter;

public class AdapterCDATA extends XmlAdapter<String, String> {

    @Override
    public String marshal(String arg0) throws Exception {
        return "<![CDATA[" + arg0 + "]]>";
    }
    @Override
    public String unmarshal(String arg0) throws Exception {
        return arg0;
    }

}

Root

The @XmlJavaTypeAdapter annotation is used to specify that the XmlAdapter should be used.

package forum14193944;

import javax.xml.bind.annotation.*;
import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;

@XmlRootElement
@XmlAccessorType(XmlAccessType.FIELD)
public class Root {

    @XmlJavaTypeAdapter(AdapterCDATA.class)
    private String name;

    @XmlJavaTypeAdapter(AdapterCDATA.class)
    private String surname;

    @XmlJavaTypeAdapter(AdapterCDATA.class)
    private String id;

}

Demo

I had to wrap System.out in an OutputStreamWriter to get the desired effect. Also note that setting a CharacterEscapeHandler means that it is responsible for all escape handling for that Marshaller.

package forum14193944;

import java.io.*;
import javax.xml.bind.*;
import com.sun.xml.bind.marshaller.*;

public class Demo {

    public static void main(String[] args) throws Exception {
        JAXBContext jc = JAXBContext.newInstance(Root.class);

        Unmarshaller unmarshaller = jc.createUnmarshaller();
        File xml = new File("src/forum14193944/input.xml");
        Root root = (Root) unmarshaller.unmarshal(xml);

        Marshaller marshaller = jc.createMarshaller();
        marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true);
        marshaller.setProperty(CharacterEscapeHandler.class.getName(),
                new CharacterEscapeHandler() {
                    @Override
                    public void escape(char[] ac, int i, int j, boolean flag,
                            Writer writer) throws IOException {
                        writer.write(ac, i, j);
                    }
                });
        marshaller.marshal(root, new OutputStreamWriter(System.out));
    }

}

input.xml/Output

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<root>
    <name><![CDATA[<h1>kshitij</h1>]]></name>
    <surname><![CDATA[<h1>solanki</h1>]]></surname>
    <id><![CDATA[0]]></id>
</root>

Upvotes: 47

Related Questions