February 6, 2012

JAXB and Inheritance - EclipseLink MOXy's @XmlClassExtractor

In a previous post I covered how JAXB's XmlAdapter could be leveraged to use a node unique to the subtype as the inheritance indicator.  In this post I'll demonstrate that if that node is an XML attribute, then EclipseLink JAXB (MOXy)'s @XmlClassExtractor could be leveraged to do the same thing in fewer lines of code.

Input (input.xml)

In this example the possible contact methods are Address and PhoneNumber.  If the street attribute is present on the contact-method element we will instantiate an Address object, and if the number attribute is present we will instantiate a PhoneNumber object.

<?xml version="1.0" encoding="UTF-8"?>
<customer>
    <contact-method 
        number="555-1111"/>
    <contact-method 
        street="1 A St" 
        city = "Any Town"/>
    <contact-method 
        number="555-2222"/>
</customer>

Java Model

Below is the domain model that will be used for this example.

Customer

package blog.inheritance.xmlclassextractor;

import java.util.List;
import javax.xml.bind.annotation.*;

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

    @XmlElement(name="contact-method")
    private List<ContactMethod> contactMethods;

}

ContactMethod

Since we are using a custom mechanism to indicate the subtype, we can use MOXy's @XmlClassExtractor extension to plug in the custom logic.

package blog.inheritance.xmlclassextractor;

import javax.xml.bind.annotation.XmlSeeAlso;
import org.eclipse.persistence.oxm.annotations.XmlClassExtractor;

@XmlClassExtractor(ContactMethodClassExtractor.class)
@XmlSeeAlso({Address.class, PhoneNumber.class})
public abstract class ContactMethod {

}

Address

package blog.inheritance.xmlclassextractor;

import javax.xml.bind.annotation.*;

@XmlAccessorType(XmlAccessType.FIELD)
public class Address extends ContactMethod {

    @XmlAttribute
    protected String street;

    @XmlAttribute
    protected String city;

}

PhoneNumber

package blog.inheritance.xmlclassextractor;

import javax.xml.bind.annotation.*;

@XmlAccessorType(XmlAccessType.FIELD)
public class PhoneNumber extends ContactMethod {

    @XmlAttribute
    protected String number;

}

ClassExtractor (ContactMethodClassExtractor)

ContactMethodClassExtractor is an implementation of ClassExtractor.  Its role is to decide which subtype should be returned based on an instance of Record.  The Record class represents the XML element currently being unmarshalled.  This class serves a similar role to the ContactMethodAdapter from the JAXB and Inheritance - Using XmlAdapter example but requires much less code.

package blog.inheritance.xmlclassextractor;

import org.eclipse.persistence.descriptors.ClassExtractor;
import org.eclipse.persistence.sessions.Record;
import org.eclipse.persistence.sessions.Session;

public class ContactMethodClassExtractor extends ClassExtractor{

    @Override
    public Class extractClassFromRow(Record record, Session session) {
        if(null != record.get("@street")) {
            return Address.class;
        } else if(null != record.get("@number")) {
            return PhoneNumber.class;
        }
        return null;
    }

}

Demo Code

The following demo code will be used for this example.  We will unmarshal the input document, output the type of each object in the collection, and then marshal the objects back to XML.

package blog.inheritance.xmlclassextractor;

import java.io.File;
import javax.xml.bind.*;
 
public class Demo {
 
    public static void main(String[] args) throws Exception  {
        JAXBContext jc = JAXBContext.newInstance(Customer.class);
 
        Unmarshaller unmarshaller = jc.createUnmarshaller();
        File xml = new File("src/blog/inheritance/xmlclassextractor/input.xml");
        Customer customer = (Customer) unmarshaller.unmarshal(xml);
 
        for(ContactMethod contactMethod : customer.getContactMethods()) {
            System.out.println(contactMethod.getClass());
        }
 
        Marshaller marshaller = jc.createMarshaller();
        marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true);
        marshaller.marshal(customer, System.out);
    }
 
}

Output

The following is the output from running the demo code.  Note how each of the instances of ContactMethod in the collection are of the appropriate sub-type.

class blog.inheritance.xmlclassextractor.PhoneNumber
class blog.inheritance.xmlclassextractor.Address
class blog.inheritance.xmlclassextractor.PhoneNumber
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<customer>
    <contact-method number="555-1111"/>
    <contact-method city="Any Town" street="1 A St"/>
    <contact-method number="555-2222"/>
</customer>

Further Reading

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

2 comments:

  1. if the subtype is an element and not a attribut like

    <?xml version="1.0" encoding="UTF-8" standalone="yes"?>
    <customer>
    <contact-method>
    <number>555-1111"</number>
    </contact-method>
    <contact-method>
    <city>Any Town</city>
    <street>=1 A St</street>
    </contact-method>
    <contact-method>
    <number>555-1111"</number>
    </contact-method>
    </customer>

    I haven't seen this example.

    ReplyDelete
    Replies
    1. Currently MOXy requires that the inheritance indicator be in an XML attribute. If it is in an XML element you could use the following approach with an XmlAdapter:
      - JAXB and Inheritance - Using XmlAdapter

      -Blaise

      Delete