August 16, 2012

Removing JAXBElement From Your Domain Model

JAXBElement is a JAXB (JSR-222) mechanism that stores name and namespace information in situations where this can not be determined from the value or mapping.  For example in the class below the elements billing-address and shipping-address both correspond to the Address class.  In order to be able to round trip the data we need to keep track of which element we unmarshalled.
import javax.xml.bind.JAXBElement;
import javax.xml.bind.annotation.*;

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

    @XmlElementRefs({
        @XmlElementRef(name = "billing-address"),
        @XmlElementRef(name = "shipping-address")
    })
    private JAXBElement<Address> address;

}
While useful JAXBElement can get in the way if you want to use your domain model with something like JPA (which doesn't know what to do with it).  In this post I will demonstrate how you can eliminate the need for JAXBElement through the use of an XmlAdapter.

Address

The element name and namespace information from JAXBElement needs to be stored somewhere.  Instead of using a JAXBElement, we will set the element name and namespace in a QName property on the Address object (line 10).  Since we will not be mapping the qName field it has been marked @XmlTransient (line 9, see JAXB and Unmapped Properties).

package blog.jaxbelement.remove;

import javax.xml.bind.annotation.*;
import javax.xml.namespace.QName;

@XmlAccessorType(XmlAccessType.FIELD)
public class Address {

    @XmlTransient
    private QName qName;

    private String street;

    private String city;

    public QName getQName() {
        return qName;
    }

    public void setQName(QName name) {
        this.qName = name;
    }

}

AddressAdapter

We will use an XmlAdapter to convert an instance of Address to/from an instance of JAXBElement.  During this conversion we must move the name and namespace information between Address and JAXBElement.

package blog.jaxbelement.remove;

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

public class AddressAdapter extends XmlAdapter<JAXBElement<Address>, Address>{

    @Override
    public JAXBElement<Address> marshal(Address address) throws Exception {
        return new JAXBElement<Address>(address.getQName(), Address.class, address); 
    }

    @Override
    public Address unmarshal(JAXBElement<Address> jaxbElement) throws Exception {
        Address address = jaxbElement.getValue();
        address.setQName(jaxbElement.getName());
        return address;
    }

}

Customer

The @XmlJavaTypeAdapter annotation is used to specify the XmlAdapter.  The XmlAdapter is responsible for converting an instance of Address to a JAXBElement to satisfy the needs of the @XmlElementRefs mapping.  The name property on an @XmlElementRef annotation must match the name specified in a @XmlRootElement or @XmlElementDecl annotation.

package blog.jaxbelement.remove;

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

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

    @XmlElementRefs({
        @XmlElementRef(name = "billing-address"),
        @XmlElementRef(name = "shipping-address")
    })
    @XmlJavaTypeAdapter(AddressAdapter.class)
    private Address address;

    public Address getAddress() {
        return address;
    }

}

ObjectFactory

The @XmlElementDecl annotation is used when a class is associated with multiple elements (if a class is associated with only one element then @XmlRootElement can be used).  It is placed on a factory method in a class annotated with @XmlRegistry (when generated from an XML schema this class is always called ObjectFactory).  The factory method returns the domain object wrapped in an instance of JAXBElement.  The JAXBElement has a QName that represents the elements name and namespace URI.
package blog.jaxbelement.remove;

import javax.xml.bind.JAXBElement;
import javax.xml.bind.annotation.*;
import javax.xml.namespace.QName;

@XmlRegistry
public class ObjectFactory {

    static final String BILLING_ADDRESS = "billing-address";
    static final String SHIPPING_ADDRESS = "shipping-address";

    @XmlElementDecl(name=BILLING_ADDRESS)
    public JAXBElement<Address> createBillingAddress(Address address) {
        return new JAXBElement<Address>(new QName(BILLING_ADDRESS), Address.class, address);
    }

    @XmlElementDecl(name=SHIPPING_ADDRESS)
    public JAXBElement<Address> createShippingAddress(Address address) {
        return new JAXBElement<Address>(new QName(SHIPPING_ADDRESS), Address.class, address);
    }

}

input.xml

Below is the input to the demo code.  Note how the address data is wrapped in the billing-address element.
  The billing-address element was one of the element names we specified in a @XmlElementRef annotation on the Customer class.  In the demo code we will change this to the shipping-address element, the other element name we specified in an @XmlElementRef annotation.
<?xml version="1.0" encoding="UTF-8"?>
<customer>
    <billing-address>
        <street>123 A Street</street>
        <city>Any Town</city>
    </billing-address>
</customer>

Demo

In the demo code below we will:
  1. Unmarshal the input document (line 14)
  2. Set a new QName on the Address object.  The QName must correspond to one of the @XmlElementDecl annotations on the ObjectFactory class (line 17).
  3. Marshal the Customer object back to XML (line 21).
package blog.jaxbelement.remove;

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

public class Demo {

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

        Unmarshaller u = jc.createUnmarshaller();
        File xml = new File("src/blog/jaxbelement/remove/input.xml");
        Customer customer = (Customer) u.unmarshal(xml);

        // Change the Wrapper Element
        customer.getAddress().setQName(new QName(ObjectFactory.SHIPPING_ADDRESS));

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

}

Output

Below is the output from running the demo code.  Note how the address data is now wrapped in the shipping-address element.
<?xml version="1.0" encoding="UTF-8"?>
<customer>
    <shipping-address>
        <street>123 A Street</street>
        <city>Any Town</city>
    </shipping-address>
</customer>

Further Reading

If you enjoyed this post then you may also be interested in:

No comments:

Post a Comment

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