August 26, 2010

Using @XmlAnyElement to Build a Generic Message

In this post we are building our own XML messaging framework.  We want to have a standard message object (similar to a SOAP envelope) that the framework can understand, and the ability to easily add domain specific payloads over time.


Message Object

Our message object needs to represent 3 things:
  1. Who the message is from.
  2. Who the message is to.
  3. The body of the message.
The first 2 items are easy enough to do, the 3rd is harder because we don't know what the payload of the message is going to be.  We will handle this by setting the type of the property to Object, and using the @XmlAnyElement(lax=true) annotation.  What this means is that when dealing with this property the @XmlRootElement of the referenced object will be used.  If lax is set to false then the content will be unmarshalled as DOM nodes.

package message;

import javax.xml.bind.annotation.*;

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

    @XmlAttribute
    private String to;

    @XmlAttribute
    private String from;

    @XmlAnyElement(lax=true)
    private Object body;

}

Customer Payload

The Customer class will be the root type for the body of the message when the payload corresponds to customer information, so we need to annotate it as @XmlRootElement.

package customer;

import javax.xml.bind.annotation.*;

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

    private String name;
    private Address address;

}

package customer;

import javax.xml.bind.annotation.*;

@XmlAccessorType(XmlAccessType.FIELD)
public class Address {

    private String street;
    private String city;

}

Product Payload

The Product class will be the root type for the body of the message when the payload corresponds to product information, so we need to annotate it with @XmlRootElement.

package product;

import javax.xml.bind.annotation.*;

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

    private String name;

}

Creating the JAXBContext

We could create the JAXBContext on an array of classes, but our goal is to be able to easily add additional payloads over time.  Here we will create the JAXBContext using a context path:

JAXBContext.newInstance("message:customer:product");

The context path includes 3 package names seperated by the colon ':' character.  In each of these packages we need to include a file named jaxb.index with a list of files.  Below is an example of the jaxb.index file in the customer package:

Address
Customer

When we want to add a model to represent orders to our framework we would extend our JAXBContext creation to like:
JAXBContext.newInstance("message:customer:product:order");


Trying it Out

Finally, we can verify that our example works with the following message.  The body of the message corresponds to a customer payload. 

<message to="john@example.com" from="jane@example.com">
    <customer>
        <name>Sue Smith</name>
        <address>
            <street>123 A Street</street>
            <city>Any Town</city>
        </address>
    </customer>
</message>

Below is some sample code to handle this message.
import java.io.File;
import javax.xml.bind.*;
import message.Message;

public class Demo {

    public static void main(String[] args) throws Exception {
        JAXBContext jc = JAXBContext.newInstance("message:customer:product");

        Unmarshaller unmarshaller = jc.createUnmarshaller();
        File file = new File("input.xml");
        Message message = (Message) unmarshaller.unmarshal(file);

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

}

4 comments:

  1. Thanks blaise. I don't suppose you could give an example of how to marshal this XML using this object structure could you? I want to do the reverse: Inject some fairly freeform XML into a field of type @XmlAnyElement.
    ReplyDelete
  2. With this example you could create an instance of Message and set an instance of Customer or Product on it. You can also set an instance of org.w3c.dom.Element on it, if you wish to include free form XML.

    If you always want the content of the property to be a DOM node you have specify that lax=false on the @XmlAnyElement annotation.
    ReplyDelete
  3. Once I have the message object,
    Message message = (Message) unmarshaller.unmarshal(file);

    What is the best way to get the Customer object out of the Message object? Do I have to check for instanceof against all object types that I have and cast it to the proper type? Eg

    if(message.getBody() instanceof Customer) {
    Customer customer = (Customer) message.getBody();
    }

    I say this because I am working on an extensible REST api with tons of objects, and thats a lot of boilerplate code that I have to write.
    ReplyDelete
  4. Since with JAXB you have control over you objects, you could leverage a common super class or interface with known API that will eliminate the need for doing the instanceof checks.

    -Blaise
    ReplyDelete