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.xmladapter; 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
ContactMethod and its subclasses (Address & PhoneNumber) will be handled by an XmlAdapter, so the only mapping required is @XmlJavaTypeAdapter to specify the implementation of XmlAdapter.
ContactMethod and its subclasses (Address & PhoneNumber) will be handled by an XmlAdapter, so the only mapping required is @XmlJavaTypeAdapter to specify the implementation of XmlAdapter.
package blog.inheritance.xmladapter; import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter; @XmlJavaTypeAdapter(ContactMethodAdapter.class) public abstract class ContactMethod { }
Address
package blog.inheritance.xmladapter; public class Address extends ContactMethod { protected String street; protected String city; }
PhoneNumber
package blog.inheritance.xmladapter; public class PhoneNumber extends ContactMethod { protected String number; }
XmlAdapter (ContactMethodAdapter)
The AdaptedContactMethod class has been created and represents the combined properties of ContactMethod, Address, and PhoneNumber. In a marshal operation only the properties corresponding to the type being marshalled are populated. During the unmarshal operation after the AdaptedContactMethod has been built, we can look at which properties have been populated to determine the appropriate subtype that should be returned.
package blog.inheritance.xmladapter; import javax.xml.bind.annotation.XmlAttribute; import javax.xml.bind.annotation.adapters.XmlAdapter; public class ContactMethodAdapter extends XmlAdapter<ContactMethodAdapter.AdaptedContactMethod, ContactMethod> { @Override public AdaptedContactMethod marshal(ContactMethod contactMethod) throws Exception { if (null == contactMethod) { return null; } AdaptedContactMethod adaptedContactMethod = new AdaptedContactMethod(); if (contactMethod instanceof Address) { Address address = (Address) contactMethod; adaptedContactMethod.street = address.street; adaptedContactMethod.city = address.city; } else { PhoneNumber phoneNumber = (PhoneNumber) contactMethod; adaptedContactMethod.number = phoneNumber.number; } return adaptedContactMethod; } @Override public ContactMethod unmarshal(AdaptedContactMethod adaptedContactMethod) throws Exception { if (null == adaptedContactMethod) { return null; } if (null != adaptedContactMethod.number) { PhoneNumber phoneNumber = new PhoneNumber(); phoneNumber.number = adaptedContactMethod.number; return phoneNumber; } else { Address address = new Address(); address.street = adaptedContactMethod.street; address.city = adaptedContactMethod.city; return address; } } public static class AdaptedContactMethod { @XmlAttribute public String number; @XmlAttribute public String street; @XmlAttribute public String city; } }
Demo Code
package blog.inheritance.xmladapter; 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/xmladapter/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.xmladapter.PhoneNumber class blog.inheritance.xmladapter.Address class blog.inheritance.xmladapter.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>
Related Forum Questions
Below are a couple of use cases that appeared on Stack Overflow that can be implemented using this approach:
- eclipselink/Moxy : inheritance and attribute name overloading based on type
- Jaxb objects with the same name
- Java/JAXB: Unmarshal XML attributes to specific Java object attributes
Further Reading
If you enjoyed this post, then you also be interested in:
Thanks for writing this post!
ReplyDeleteThe one thing that concerns me is that AdaptedContactMethod is basically a superset of Address and PhoneNumber, so this is code duplication. Could this be avoided?
I have implemented an XmlAdapter that returns null in lieu of an empty string; similar to the example above in that the marshall method returns null.
DeleteThe problem:
I get a NullPointer exception now. If I convert the marshall method to return an empty string, it prevents the exception; however, I want to return null.
Does anyone hava a solution to this?
Also, I found that it is an issue that was fixed in certain versions of JaxB but when I use those versions (i.e. 2.2.5), it doesn't appear to change the behavior.
http://java.net/jira/browse/JAXB-415