March 23, 2011

JAXB, Web Services, and Binary Data

When an instance of a class is used with a Web Service, the JAX-WS implementation can choose to handle fields/properties that hold binary data as SOAP attachment.  An attachment is a means to send the data outside of the XML message, this is done as an optimization since binary data encoded as a xs:base64Binary string could be quite large.  JAXB offers a couple of annotations to control this behaviour:

  • @XmlInlineBinaryData
    This specifies that the binary data for this field/property must be written to the XML document as xs:base64Binary and not sent as an attachment.
  • @XmlMimeType
    For properties of type java.awt.Image or javax.xml.transform.Source, this annotation allows the mime type to be specified that will be used for encoding the data as bytes.


Java Model

The following class will be used as the domain model for this example.  It has various properties for representing binary data.  Property "c" has been annotated with @XmlInlineBinaryData to prevent that data from being treated as an attachment, and property "d" has been annotated with @XmlMimeType to specify that the Image should be encoded as a JPEG.

package blog.attachments;

import java.awt.Image;

import javax.xml.bind.annotation.XmlInlineBinaryData;
import javax.xml.bind.annotation.XmlMimeType;
import javax.xml.bind.annotation.XmlRootElement;

@XmlRootElement
public class Root {

    private byte[] a;
    private byte[] b;
    private byte[] c;
    private Image d;

    public byte[] getA() {
        return a;
    }

    public void setA(byte[] foo) {
        this.a = foo;
    }

    public byte[] getB() {
        return b;
    }

    public void setB(byte[] bar) {
        this.b = bar;
    }

    @XmlInlineBinaryData
    public byte[] getC() {
        return c;
    }

    public void setC(byte[] c) {
        this.c = c;
    }

    @XmlMimeType("image/jpeg")
    public Image getD() {
        return d;
    }

    public void setD(Image d) {
        this.d = d;
    }

}

Web Service Layer

When a JAX-WS implementation leverages attachments the XML payload will look similar to the following.  Some data will be marshalled as xs:base64Binary and other data will be marshalled as an identifier that will serve as a reference to the attachment. 

<root>
    <a>
        <xop:Include 
            href="cid:1" 
            xmlns:xop="http://www.w3.org/2004/08/xop/include"/>
    </a>
    <b>QkFS</b>
    <c>SEVMTE8gV09STEQ=</c>
    <d>
        <xop:Include
            href="cid:2"
            xmlns:xop="http://www.w3.org/2004/08/xop/include"/>
    </d>
</root>

A JAX-WS provider achieves this by leveraging JAXB's AttachmentMarshaller and AttachmentUnmarshaller mechanisms.  You do not need to write any code to make this happen.  The following code is provided to give you a behind the scenes look.

Example AttachmentMarshaller

A JAX-WS provider that wants to leverage attachments registers an implementation of javax.xml.bind.attachment.AttachmentMarshaller on the JAXB Marshaller.  The implementation is specific to the JAX-WS provider, but below is a sample of how it might look.  A JAX-WS provider can choose when to handle binary data as an attachment, in the implementation below any candidate byte[] of size greater than 10 will be treated as an attachment.

package blog.attachments;

import java.util.ArrayList;
import java.util.List;

import javax.activation.DataHandler;
import javax.xml.bind.attachment.AttachmentMarshaller;

public class ExampleAttachmentMarshaller extends AttachmentMarshaller {

    private static final int THRESHOLD = 10;

    private List<Attachment> attachments = new ArrayList<Attachment>();

    public List<Attachment> getAttachments() {
        return attachments;
    }

    @Override
    public String addMtomAttachment(DataHandler data, String elementNamespace, String elementLocalName) {
        return null;
    }

    @Override
    public String addMtomAttachment(byte[] data, int offset, int length, String mimeType, String elementNamespace, String elementLocalName) {
        if(data.length < THRESHOLD) {
            return null;
        }
        int id = attachments.size() + 1;
        attachments.add(new Attachment(data, offset, length));
        return "cid:" + String.valueOf(id);
    }

    @Override
    public String addSwaRefAttachment(DataHandler data) {
        return null;
    }

    @Override
    public boolean isXOPPackage() {
        return true;
    }

    public static class Attachment {

        private byte[] data;
        private int offset;
        private int length;

        public Attachment(byte[] data, int offset, int length) {
            this.data = data;
            this.offset = offset;
            this.length = length;
        }

        public byte[] getData() {
            return data;
        }

        public int getOffset() {
            return offset;
        }

        public int getLength() {
            return length;
        }

    }

}

Example AttachmentUnmarshaller

If a JAX-WS provider is leveraging attachments, then an implementation of javax.xml.bind.attachment.AttachmentUnmarshaller must be specified on the JAXB Unmarshaller.  Again the implementations is specific to the JAX-WS provider.  A sample implementation is shown below:

package blog.attachments;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.HashMap;
import java.util.Map;

import javax.activation.DataHandler;
import javax.activation.DataSource;
import javax.xml.bind.attachment.AttachmentUnmarshaller;

public class ExampleAttachmentUnmarshaller extends AttachmentUnmarshaller {

    private Map<String, byte[]> attachments = new HashMap<String, byte[]>();

    public Map<String, byte[]> getAttachments() {
        return attachments;
    }

    @Override
    public DataHandler getAttachmentAsDataHandler(String cid) {
        byte[] bytes = attachments.get(cid);
        return new DataHandler(new ByteArrayDataSource(bytes));
    }

    @Override
    public byte[] getAttachmentAsByteArray(String cid) {
        return attachments.get(cid);
    }

    @Override
    public boolean isXOPPackage() {
        return true;
    }

    private static class ByteArrayDataSource implements DataSource {

        private byte[] bytes;

        public ByteArrayDataSource(byte[] bytes) {
            this.bytes = bytes;
        }

        public String getContentType() {
            return  "application/octet-stream";
        }

        public InputStream getInputStream() throws IOException {
            return new ByteArrayInputStream(bytes);
        }

        public String getName() {
            return null;
        }

        public OutputStream getOutputStream() throws IOException {
            return null;
        }

    }

}

Demo Code

The following example was inspired by an answer I gave on Stack Overflow (feel free to up vote).  It covers how to leverage JAXB's AttachmentMarshaller & AttachmentUnmarshaller to produce a message in the following format:

[xml_length][xml][attach1_length][attach1]...[attachN_length][attachN]

While this example is unique to a particular use case, it does demonstrate JAXB's attachment mechanism without requiring a JAX-WS provider.

Demo

package blog.attachments;

import java.awt.image.BufferedImage;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import javax.xml.bind.JAXBContext;

public class Demo {

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

        Root root = new Root();
        root.setA("HELLO WORLD".getBytes());
        root.setB("BAR".getBytes());
        root.setC("HELLO WORLD".getBytes());
        root.setD(new BufferedImage(10, 10, BufferedImage.TYPE_INT_RGB));

        MessageWriter writer = new MessageWriter(jc);
        FileOutputStream outStream = new FileOutputStream("message.xml");
        writer.write(root, outStream);
        outStream.close();

        MessageReader reader = new MessageReader(jc);
        FileInputStream inStream = new FileInputStream("message.xml");
        Root root2 = (Root) reader.read(inStream);
        inStream.close();
        System.out.println(new String(root2.getA()));
        System.out.println(new String(root2.getB()));
        System.out.println(new String(root2.getC()));
        System.out.println(root2.getD());
    }

}

MessageWriter

package blog.attachments;

import java.io.ByteArrayOutputStream;
import java.io.ObjectOutputStream;
import java.io.OutputStream;

import javax.xml.bind.JAXBContext;
import javax.xml.bind.Marshaller;

import blog.attachments.ExampleAttachmentMarshaller.Attachment;

public class MessageWriter {

    private JAXBContext jaxbContext;

    public MessageWriter(JAXBContext jaxbContext) {
        this.jaxbContext = jaxbContext;
    }

    /**
     * Write the message in the following format:
     * [xml_length][xml][attach1_length][attach1]...[attachN_length][attachN] 
     */
    public void write(Object object, OutputStream stream) {
        try {
            Marshaller marshaller = jaxbContext.createMarshaller();
            marshaller.setProperty(Marshaller.JAXB_FRAGMENT, true);
            ExampleAttachmentMarshaller attachmentMarshaller = new ExampleAttachmentMarshaller();
            marshaller.setAttachmentMarshaller(attachmentMarshaller);
            ByteArrayOutputStream xmlStream = new ByteArrayOutputStream();
            marshaller.marshal(object, xmlStream);
            byte[] xml = xmlStream.toByteArray();
            xmlStream.close();

            ObjectOutputStream messageStream = new ObjectOutputStream(stream);

            messageStream.writeInt(xml.length); //[xml_length]
            messageStream.write(xml); // [xml]

            for(Attachment attachment : attachmentMarshaller.getAttachments()) {
                messageStream.writeInt(attachment.getLength()); // [attachX_length]
                messageStream.write(attachment.getData(), attachment.getOffset(), attachment.getLength());  // [attachX]
            }

            messageStream.flush();
        } catch(Exception e) {
            throw new RuntimeException(e);
        }
    }

}

MessageReader

package blog.attachments;

import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.io.ObjectInputStream;

import javax.xml.bind.JAXBContext;
import javax.xml.bind.Unmarshaller;

public class MessageReader {

    private JAXBContext jaxbContext;

    public MessageReader(JAXBContext jaxbContext) {
        this.jaxbContext = jaxbContext;
    }

    /**
     * Read the message from the following format:
     * [xml_length][xml][attach1_length][attach1]...[attachN_length][attachN] 
     */
    public Object read(InputStream stream) {
        try {
            ObjectInputStream inputStream = new ObjectInputStream(stream);
            int xmlLength = inputStream.readInt();  // [xml_length]

            byte[] xmlIn = new byte[xmlLength]; 
            inputStream.read(xmlIn);  // [xml]

            ExampleAttachmentUnmarshaller attachmentUnmarshaller = new ExampleAttachmentUnmarshaller();
            int id = 1;
            while(inputStream.available() > 0) {
                int length = inputStream.readInt();  // [attachX_length]
                byte[] data = new byte[length];  // [attachX]
                inputStream.read(data);
                attachmentUnmarshaller.getAttachments().put("cid:" + String.valueOf(id++), data);
            }

            Unmarshaller unmarshaller = jaxbContext.createUnmarshaller();
            unmarshaller.setAttachmentUnmarshaller(attachmentUnmarshaller);
            ByteArrayInputStream byteInputStream = new ByteArrayInputStream(xmlIn);
            Object object = unmarshaller.unmarshal(byteInputStream);
            byteInputStream.close();
            inputStream.close();
            return object;
        } catch(Exception e) {
            throw new RuntimeException(e);
        }
    }

}

3 comments:

  1. Hi Blaise,
    Thanks for the informative blog!

    Just one question:
    if I generate my Domain model from a schema and the schema defines my binary tag as
    "", then the generated model output is
    "@XmlElement(name = "BitmapData", required = true)
    protected byte[] bitmapData;"
    I have to explicitly add "@XmlInlineBinaryData" to ensure that the binary data is not sent as an attachment.

    Do you know of a schema deceleration that ensure that the "@XmlInlineBinaryData" is added to the domain model when it is generated.

    Thanks,

    Nishern

    ReplyDelete
  2. Excelent Post!

    Thanks for save me a lot of wasted time,

    Cheers

    ReplyDelete

Note: Only a member of this blog may post a comment.