June 14, 2011

MOXy Extensible Models - Refresh Example

In this example we will leverage EclipseLink JAXB (MOXy)'s concepts of externalized metadata (represented as a MetadataSource), and extensible models.  The MetadataSource will be used to define the metadata for the extensions.  In EclipseLink 2.3 we have introduced the ability for a JAXBContext to be "refreshed".  This means that without stopping the application we can supply metadata about new extensions.


Demo

MOXy provides a class called JAXBHelper to easily obtain MOXy's implementation of JAXBContext.  This exposes the refreshMetadata() method.  The refreshMetadata() call does not affect any marshal or unmarshal operations that may currently be in progress, and once the refresh is complete the new metadata will be used.

package blog.metadatasource.refresh;

import java.util.HashMap;
import java.util.Map;

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

import org.eclipse.persistence.jaxb.JAXBContextFactory;
import org.eclipse.persistence.jaxb.JAXBHelper;

public class Demo {

    public static void main(String[] args) throws Exception {
        Map<String, Object> properties = new HashMap<String, Object>(1);
        ExtensionsMetadataSource extensions = new ExtensionsMetadataSource();
        properties.put(JAXBContextFactory.ECLIPSELINK_OXM_XML_KEY, extensions);

        JAXBContext jc = JAXBContext.newInstance(new Class[] {Customer.class}, properties);
        Marshaller marshaller = jc.createMarshaller();
        marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true);

        Customer customer = new Customer();
        customer.setFirstName("Jane");
        customer.set("middleName", "Anne");
        customer.setLastName("Doe");

        Address billingAddress = new Address();
        billingAddress.setStreet("123 Billing Address");
        customer.setBillingAddress(billingAddress);

        Address shippingAddress = new Address();
        shippingAddress.setStreet("456 Shipping Address");
        customer.set("shippingAddress", shippingAddress);

        marshaller.marshal(customer, System.out);

        extensions.addXmlElement(Customer.class, "middleName", String.class);
        extensions.addXmlElement(Customer.class, "shippingAddress", Address.class);
        JAXBHelper.getJAXBContext(jc).refeshMetadata();

        marshaller.marshal(customer, System.out);
    }

}

Output

In the first document that is marshalled the extensions are not included, this is because the JAXBContext is not yet aware of the metadata.  Once the metadata has been added and the JAXBContext refreshed the new properties are included.  One thing to note is that we did not need to obtain a new instance of Marshaller.

<?xml version="1.0" encoding="UTF-8"?>
<customer>
   <firstName>Jane</firstName>
   <lastName>Doe</lastName>
   <billingAddress>
      <street>123 Billing Address</street>
   </billingAddress>
</customer>
<?xml version="1.0" encoding="UTF-8"?>
<customer>
   <firstName>Jane</firstName>
   <lastName>Doe</lastName>
   <billingAddress>
      <street>123 Billing Address</street>
   </billingAddress>
   <middleName>Anne</middleName>
   <shippingAddress>
      <street>456 Shipping Address</street>
   </shippingAddress>
</customer>

ExtensionMetadataSource

I introduced the concept of a MetadataSource in a previous post.  In that post the metadata was in the form of an XML document, in this example we will use the corresponding object model.  Using the model provides us an easy means to programmatically update it.  The XmlBindings model was generated from the schema for MOXy's externalized metadata format:  http://www.eclipse.org/eclipselink/xsds/eclipselink_oxm_2_3.xsd.

package blog.metadatasource.refresh;

import java.util.HashMap;
import java.util.Map;

import org.eclipse.persistence.jaxb.metadata.MetadataSourceAdapter;
import org.eclipse.persistence.jaxb.xmlmodel.JavaType;
import org.eclipse.persistence.jaxb.xmlmodel.JavaType.JavaAttributes;
import org.eclipse.persistence.jaxb.xmlmodel.ObjectFactory;
import org.eclipse.persistence.jaxb.xmlmodel.XmlBindings;
import org.eclipse.persistence.jaxb.xmlmodel.XmlBindings.JavaTypes;
import org.eclipse.persistence.jaxb.xmlmodel.XmlElement;

public class ExtensionsMetadataSource extends MetadataSourceAdapter {

    private ObjectFactory objectFactory;
    private Map<Class<?>, JavaType> javaTypes;
    private XmlBindings xmlBindings;

    public ExtensionsMetadataSource() {
        objectFactory = new ObjectFactory();
        javaTypes = new HashMap<Class<?>, JavaType>();

        xmlBindings = new XmlBindings();
        xmlBindings.setPackageName("blog.metadatasource.refresh");
        xmlBindings.setJavaTypes(new JavaTypes());
    }

    @Override
    public XmlBindings getXmlBindings(Map<String, ?> properties, ClassLoader classLoader) {
        return xmlBindings;
    }

    public JavaType getJavaType(Class<?> clazz) {
        JavaType javaType = javaTypes.get(clazz);
        if(null == javaType) {
            javaType = new JavaType();
            javaType.setName(clazz.getSimpleName());
            javaType.setJavaAttributes(new JavaAttributes());
            xmlBindings.getJavaTypes().getJavaType().add(javaType);
            javaTypes.put(clazz, javaType);
        }
        return javaType;
    }

    public void addXmlElement(Class<?> domainClass, String propertyName, Class<?> type) {
        XmlElement xmlElement = new XmlElement();
        xmlElement.setJavaAttribute(propertyName);
        xmlElement.setType(type.getName());
        JavaType javaType = getJavaType(domainClass);
        javaType.getJavaAttributes().getJavaAttribute().add(objectFactory.createXmlElement(xmlElement));
    }

}

Customer

The @XmlVirtualAccessMethods annotation is used to specify that a class is extensible.  An extensible class is required to have a "get" method that returns a value by property name, and a "set" method that stores a value by property name.  The default names for these methods are "get" and "set", and can be overridden with the @XmlVirtualAccessMethods annotation.

package blog.metadatasource.refresh;

import java.util.HashMap;
import java.util.Map;

import javax.xml.bind.annotation.XmlRootElement;
import javax.xml.bind.annotation.XmlType;

import org.eclipse.persistence.oxm.annotations.XmlVirtualAccessMethods;

@XmlRootElement
@XmlType(propOrder={"firstName", "lastName", "address"})
@XmlVirtualAccessMethods
public class Customer {

    private String firstName;
    private String lastName;
    private Address billingAddress;
    private Map<String, Object> extensions = new HashMap<String, Object>();

    public String getFirstName() {
        return firstName;
    }

    public void setFirstName(String firstName) {
        this.firstName = firstName;
    }

    public String getLastName() {
        return lastName;
    }

    public void setLastName(String lastName) {
        this.lastName = lastName;
    }

    public Address getBillingAddress() {
        return billingAddress;
    }

    public void setBillingAddress(Address billingAddress) {
        this.billingAddress = billingAddress;
    }

    public Object get(String key) {
        return extensions.get(key);
    }

    public void set(String key, Object value) {
        extensions.put(key, value);
    }

}

Address

package blog.metadatasource.refresh;

public class Address {

    private String street;

    public String getStreet() {
        return street;
    }

    public void setStreet(String street) {
        this.street = street;
    }

}

Further Reading

No comments:

Post a Comment

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