July 23, 2010

MOXy JAXB - Map Interfaces to XML

I recently came across a Twitter message (tweet) complaining that JAXB does not support binding Java interfaces to XML. Below I'll describe how this can be accomplished using MOXy in the upcoming EclipseLink 2.1.1 and 2.2 releases (try it now).


Below is the model we'll be using, notice how everything is an interface:

package example.interfaces;

import java.util.List;
import javax.xml.bind.annotation.XmlRootElement;
import javax.xml.bind.annotation.XmlType;

@XmlRootElement()
@XmlType(propOrder={"name", "address"})
public interface Customer {

    String getName();
    void setName(String name);

    Address getAddress();
    void setAddress(Address address);

    public List<PhoneNumber> getPhoneNumbers();
    public void setPhoneNumbers(List<PhoneNumber> phoneNumbers);

}

package example.interfaces;

public interface Address {

    public String getStreet();
    public void setStreet(String street);
 
    public String getCity();
    public void setCity(String city);

}

package example.interfaces;

import javax.xml.bind.annotation.XmlAttribute;
import javax.xml.bind.annotation.XmlValue;

public interface PhoneNumber {

    @XmlAttribute()
    String getType();
    void setType(String type);

    @XmlValue()
    String getValue();
    void setValue(String value);

}

We need a way to create an instance of the interface.  For this we'll leverage an ObjectFactory.  There is some flexibility on how this could be done, but I'll use Java's proxy mechanism with an InvocationHandler.

package example.interfaces;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.HashMap;
import java.util.Map;
import javax.xml.bind.annotation.XmlRegistry;
 
@XmlRegistry
public class ObjectFactory {
 
    public Customer createCustomer() {
        return createInstance(Customer.class);
    }
 
    public Address createAddress() {
        return createInstance(Address.class);
    }
 
    public PhoneNumber createPhoneNumber() {
        return createInstance(PhoneNumber.class);
    }
 
    private <T> T createInstance(Class<T> anInterface) {
        return (T) Proxy.newProxyInstance(anInterface.getClassLoader(), new Class[] {anInterface}, new InterfaceInvocationHandler());
    }
 
    private static class InterfaceInvocationHandler implements InvocationHandler {
 
        private Map<String, Object> values = new HashMap<String, Object>();
 
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            String methodName = method.getName();
            if(methodName.startsWith("get")) {
                return values.get(methodName.substring(3));
            } else {
                values.put(methodName.substring(3), args[0]);
                return null;
            }
        }
 
    }

}

Now we can run the following code to see everything working:

package example.interfaces;

import java.io.FileInputStream;
import javax.xml.bind.JAXBContext;
import javax.xml.bind.Marshaller;
import javax.xml.bind.Unmarshaller;

public class Demo {

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

        FileInputStream xml = new FileInputStream("input.xml");
        Unmarshaller unmarshaller = jaxbContext.createUnmarshaller();
        Customer customer = (Customer) unmarshaller.unmarshal(xml);

        ObjectFactory objectFactory = new ObjectFactory();
        PhoneNumber homePhoneNumber = objectFactory.createPhoneNumber();
        homePhoneNumber.setType("home");
        homePhoneNumber.setValue("613-555-3333");
        customer.getPhoneNumbers().add(homePhoneNumber);

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

}

To use the MOXy runtime, you need to add a file named jaxb.properties in with your model classes (or interfaces) with the following entry:

javax.xml.bind.context.factory=org.eclipse.persistence.jaxb.JAXBContextFactory

Now back to Twitter to mine more topics for future blog posts.


5 comments:

  1. So this is MOXy/EclipseLink only, not standard JAXB? Even though you only use JAXB-defined types, would other JAXB implementations fail to handle this? If so, why specifically?

    ReplyDelete
    Replies
    1. Hi Trevor,

      The root problem with handling interfaces is that a JAXB (JSR-222) implementation requires a way to instantiate a concrete instance during unmarshalling. By default MOXy will leverage the ObjectFactory to do this if it is present, otherwise you can configure a factory class/method using the @XmlType annotation (JAXB and Factory Methods). Once MOXy knows how to instantiate a concrete instance it stops caring that it is an interface, other JAXB implementations such as the RI will thrown an exception.

      -Blaise

      Delete
  2. How Could I avoid to define as many binding xml file as many class implements my interface?
    E.g. If I have an interface and my class implements that interface I would like to have a way to marshal my different instances of that interface getting as output an xml or json file that in his nodes has the name of the kind of the class that has been marshalled. If I haven't been clear I'll create a question on stacked overflow.
    Thank you

    Sergio

    ReplyDelete
    Replies
    1. Hi Sergio,

      Here is a link to an answer that I gave to a similar question on Stack Overflow:
      - http://stackoverflow.com/questions/16878949/moxy-jaxb-interface-annotation/16903047#16903047

      -Blaise

      Delete