JAXB Marshalling Unmarshalling with CDATA
Asked Answered
M

7

32

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;
    }
}
Masquer answered 7/1, 2013 at 10:22 Comment(0)
B
45

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>
Bordure answered 7/1, 2013 at 14:25 Comment(23)
I just tried to run this code and i am getting same error that i was getting yesterday. means still there is some problem. and also i made new project and try to run this code. and getting same error. there are no external jars. still not working.Masquer
i am getting following error. javax.xml.bind.PropertyException: name: CharacterEscapeHandler value: MainClass$1@66e815 javax.xml.bind.PropertyException: name: CharacterEscapeHandler value: MainClass$1@66e815 at javax.xml.bind.helpers.AbstractMarshallerImpl.setProperty(Unknown Source) at com.sun.xml.internal.bind.v2.runtime.MarshallerImpl.setProperty(Unknown Source) at MainClass.main(MainClass.java:35)Masquer
i can do unmarshal from file but i can not do marshalling.Masquer
@kshitij - The code as is requires that you download the JAXB reference implementation from jaxb.java.net, or EclipseLink JAXB (MOXy) from eclipse.org/eclipselink/moxy.php. You could also try changing the CharacterEscapeHandler import statement to be import com.sun.xml.internal.bind.marshaller.*Bordure
can i do it without this moxy.? because i can not change jar files right now. because i have to test all things with this moxy. is this thing is in jersey framework. ?Masquer
@kshitij - As I said you could try changing the CharacterEscapeHandler import to com.sun.xml.internal.bind.marshaller.* which may or may not be allowed in your environment.Bordure
i already tried it. it is not working. my jdk version is 1.7 so i can not find com.sun.xml.internal.bind.marshaller.CharacterEscapeHandler. and in jdk 1.6 i can find that class but it is still giving error.Masquer
@kshitij - Then for this approach you will need to use the JAXB reference implementation which can be downloaded from jaxb.java.net or EclipseLink JAXB (MOXy) eclipse.org/eclipselink/moxy.phpBordure
ok. that means i have to download jaxb-api.jar and jaxb-impl.jar. then what to do extra in above code?Masquer
let us continue this discussion in chatBordure
@kshitij - All you would need to do is add the jaxb-impl.jar to your class path. If you are using JDK 1.6 download a 2.1 implementation and if you are using JDK 1.7 get the 2.2 impl.Bordure
is there any configuration like in moxy we have to provide one jaxb.properties. like that?Masquer
@blaise-doughan , your "package forum14193944;" smells so sweet!!Panne
@BlaiseDoughan, your AdapterCDATA gives me following xml: <subject>&lt;![CDATA[sometext]]&gt;</subject>, so because of &lt; and &gt; this code is missinterpreted.Dismay
If you already declared your CDATA adapter. Then why an additional escape handler ? Are you showing 2 separate solutions ? Or am I missing the link between the adapter and the escape handler?Corbin
This will disable character escaping on all fields, not just cdata fields. Quite dangerous to use.Moralist
How to achieve these annotations by using binding.xml or modifying XSD ?Frye
This solution uses internal Java code (not part of the API), which is subject for removal in future versions or in other JVM implementations, and isn't guaranteed consistent behavior.Electromagnet
There are 2 problems with this code - it uses internal package and ... what seems to be "bit" more serious - it does not escape the strings.. Imagine you have <>@ in another field, it will not produce the valid XML!!Afeard
Excellent this works for me and saved a lot of time.Harriott
It worked. thanks !! Can someone explain in simple terms what this is doing ? "CharacterEscapeHandler"Spandau
Dirty method that disable escaping, to me it's a bad practice.Newmark
Will disable escaping for the entire marshaller. Not good - the same thing as new NoEscapeHandler()Millard
B
15

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

Bordure answered 7/1, 2013 at 11:7 Comment(13)
is there any way to do it without adding moxy. ? like i am following below link but i am not getting Success. odedpeer.blogspot.in/2010/07/…Masquer
@kshitij - Can you post the stack trace that you are seeing with your current approach?Bordure
when i am un-commenting code i am getting error. javax.xml.bind.PropertyException: name: com.sun.xml.bind.marshaller.CharacterEscapeHandler value: com.ksh.templates.MainCDATA$1@8b819fMasquer
Are using the property name that contains internal as in your sample code or without internal as per the stack trace?Bordure
@kshitij - instead could you post the entire class that contains the try block (complete with ports)?Bordure
@kshitij - Are you using the JAXB RI or the version of JAXB in the JDK? This would be easy for me to tell if you updated your question to contain the stack trace.Bordure
in my library i have added jaxb library. how to know it is using from JDK or from library.?Masquer
@kshitij - Do you see internal as part of the package name?Bordure
I updated Bean class. would you like to tell me that what effect can occur if i am using from internal package?Masquer
@kshitij - No I meant do you see internal as part of com.sun.xml.bind in the stack trace?Bordure
let us continue this discussion in chatMasquer
In a spring-boot app I had to use the java services locator approach, by adding /scr/main/resoures/META-INF/services/javax.xml.bind.JAXBContext with the content of org.eclipse.persistence.jaxb.JAXBContextFactory - afterwards the @XmlCDATA annotation was working nicely.Millard
If someone is having issues following this approach, check the location of the jabx.properties file. It has to be on the same package as the entity definition ( package com.foo.bar.model for example). I have tried in src/main/resources but it just doesn't work. It has to be located in the same package of the class that needs the CDATA.Chungchungking
A
9

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.

Ammonal answered 28/12, 2016 at 1:18 Comment(1)
This should be the accepted answer, because it only removes escaping from the CDATA portion of the XMLGoody
R
1

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>
Remonstrance answered 28/2, 2017 at 9:52 Comment(0)
B
0
    @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>
     */
Bushhammer answered 15/4, 2014 at 19:49 Comment(1)
What implementation of JAXB are you using here ? Can you explicit more the configuration required for obtaining this result ?Myeshamyhre
R
0

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);
    }
}
Rato answered 27/1, 2020 at 14:41 Comment(0)
R
-1

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);
    }
}
Retake answered 1/12, 2014 at 17:54 Comment(2)
That's all fun and games, until you really have a String containing a '>' character. First it will be converted correctly to "&gt;" by the marshaller. Next you will replaced it by a '>', breaking the xml.Corbin
@bvdb, then only he needs this: return stringWritter.toString().replaceAll("&lt;![CDATA[", "<![CDATA[").replaceAll("]]&gt;", "]]>");Penultimate

© 2022 - 2024 — McMap. All rights reserved.