Tuesday, July 6, 2010

JAXB, Sun and how to marshal a CDATA element

According to https://jaxb.dev.java.net/faq/index.html#marshalling_cdata there is no direct support in marshalling CDATA blocks, this is vendor specific.
I'll describe how this is done when using the built-in Sun implementation by an example.


Suppose this is my JAXB annotated class:
package org.oded;

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

@XmlRootElement
@XmlAccessorType(XmlAccessType.FIELD)
public class Item
{
 @XmlAttribute public int id;
 
 public String text;
}

Next I run the Main class:
package org.oded;

import java.io.*;

import javax.xml.bind.*;

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

public class Main
{
 public static void main( String[] args ) throws Exception
 {
  Item i1 = new Item();
  i1.text = "hello";
  i1.id = 1;
  
  Item i2 = new Item();
  i2.text = "";
  i2.id = 2;
    
  Marshaller m = JAXBContext.newInstance( Item.class ).createMarshaller();
  
  m.marshal( i1, new OutputStreamWriter( System.out ) );
  System.out.println();
  m.marshal( i2, new OutputStreamWriter( System.out ) );
 }
}

The output is:
hello
<code><helloWorld/></code>

Now suppose I want to wrap the XML value of text in a CDATA element and avoid the escaping, I need to add specify an Adapter for text to surround the value in a CDATA element in the following way:

package org.oded;

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

@XmlRootElement
@XmlAccessorType(XmlAccessType.FIELD)
public class Item
{
 @XmlAttribute public int id;
 
 @XmlJavaTypeAdapter(value=Adapter.class) 
 public String text;
 
 private static class Adapter extends XmlAdapter
 {

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

  @Override
  public String unmarshal( String v ) throws Exception
  {
   return v;
  }
  
 }
}

Now tell the Marshaller, in a Sun-specific way, not to escape the value of text:
package org.oded;

import java.io.*;

import javax.xml.bind.*;

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

public class Main
{
 public static void main( String[] args ) throws Exception
 {
  Item i1 = new Item();
  i1.text = "hello";
  i1.id = 1;
  
  Item i2 = new Item();
  i2.text = "";
  i2.id = 2;
    
  Marshaller m = JAXBContext.newInstance( Item.class ).createMarshaller();
  m.setProperty( "com.sun.xml.internal.bind.characterEscapeHandler", new CharacterEscapeHandler() {
   @Override
   public void escape( char[] ac, int i, int j, boolean flag, Writer writer ) throws IOException
   {
    // do not escape
    writer.write( ac, i, j );
   }
  });
  
  m.marshal( i1, new OutputStreamWriter( System.out ) );
  System.out.println();
  m.marshal( i2, new OutputStreamWriter( System.out ) );
 }
}

Now the output is:
<![CDATA[hello]]>
<![CDATA[]]>

9 comments:

  1. This post helped me a lot.

    ReplyDelete
  2. Very nice. Clean.

    ReplyDelete
  3. thanks... easy to understand... i implemented the above and got the below exception:

    javax.xml.bind.PropertyException: name: com.sun.xml.internal.bind.marshaller.characterEscapeHandler value: test.ObjectToXML$1@44118bb

    please can you advise what could be the problem?
    found this post that with the same problem...:
    http://metro.1045641.n5.nabble.com/Implementing-CharacterEscapeHandler-using-JDK6-td1063492.html

    ReplyDelete
  4. I looked at the source of the exception (http://grepcode.com/file/repository.grepcode.com/java/root/jdk/openjdk/6-b14/com/sun/xml/internal/bind/v2/runtime/MarshallerImpl.java) and it seems that your class "test.ObjectToXML" does not implement "com.sun.xml.internal.bind.marshaller.CharacterEscapeHandler"

    ReplyDelete
  5. try using

    m.setProperty( CharacterEscapeHandler.class.getName(), new CharacterEscapeHandler() {
    public void escape(char[] ac, int i, int j, boolean flag,
    Writer writer) throws IOException {
    // TODO Auto-generated method stub
    writer.write( ac, i, j );

    }
    });

    ReplyDelete
  6. Nice post. But could not find out jar contains class you used:
    com.sun.xml.internal.bind.marshaller.CharacterEscapeHandler

    Also, the compiler complains about:
    private static class Adapter extends XmlAdapter

    Should it be like below?
    private static class Adapter extends XmlAdapter

    ReplyDelete
  7. Very useful thank you very much.
    About the errors above with the CharacterEscapeHandler, plase have a look here: http://stackoverflow.com/questions/860187/access-restriction-on-class-due-to-restriction-on-required-library-rt-jar

    ReplyDelete
  8. Unfortunately, your example is broken: Your adapter does not handle CDATA end tags ]]> properly. This breaks the generated XML if that string is contained in the input.

    More severely, the CharacterEscapeHandler instance turns off escaping completely for all tags. As a consequence you _must_ use CDATA tags for all members that might contain special charactes like (greater than).

    Unfortunately, there there seems to be no feasible way to conveniently use escaping for some tags and CDATA for others with your approach.

    ReplyDelete
  9. Thanks, great post. Really usefull!

    ReplyDelete