Team lead for the TopLink/EclipseLink JAXB & SDO implementations, and the Oracle representative on those specifications. Blaise is a DZone MVB and is not an employee of DZone and has posted 44 posts at DZone. You can read more from them at their website. View Full User Profile

Mixing Nesting and References with JAXB's XmlAdapter

10.20.2011
| 6357 views |
  • submit to reddit

Recently I came across a question on Stack Overflow asking if JAXB could marshal the first occurrence of an object as containment and all subsequent occurrences as a reference.  Below is an expanded version of my answer (up votes appreciated) demonstrating how this can be achieved by leveraging JAXB's XmlAdapter.

input.xml

The following is the XML document I will use for this example. The 2nd "other-phone-number" element refers to the same data as the "primary-phone-number" element, and the 3rd "other-phone-number" element refers to the same data as the 1st "other-phone-number" element.

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<customer>
    <primary-phone-number id="B">555-BBBB</primary-phone-number>
    <other-phone-number id="A">555-AAAA</other-phone-number>
    <other-phone-number reference="B"/>
    <other-phone-number reference="A"/>
</customer>

Customer

The Customer class maintains references to PhoneNumber objects. The same instance of PhoneNumber may be referenced multiple times.

package package blog.xmladapter.stateful;
 
import java.util.List;
import javax.xml.bind.annotation.*;
 
@XmlRootElement
@XmlType(propOrder={"primaryPhoneNumber", "otherPhoneNumbers"})
public class Customer {
 
    private PhoneNumber primaryPhoneNumber;
    private List<PhoneNumber> otherPhoneNumbers;
 
    @XmlElement(name="primary-phone-number")
    public PhoneNumber getPrimaryPhoneNumber() {
        return primaryPhoneNumber;
    }
 
    public void setPrimaryPhoneNumber(PhoneNumber primaryPhoneNumber) {
        this.primaryPhoneNumber = primaryPhoneNumber;
    }
 
    @XmlElement(name="other-phone-number")
    public List<PhoneNumber> getOtherPhoneNumbers() {
        return otherPhoneNumbers;
    }
 
    public void setOtherPhoneNumbers(List<PhoneNumber> phoneNumbers) {
        this.otherPhoneNumbers = phoneNumbers;
    }
 
}

PhoneNumber

This is a class that can either appear in the document itself or as a reference. This will be handled using an XmlAdapter. An XmlAdapter is configured using the @XmlJavaTypeAdapter annotation. Since we have specified this adapter at the type/class level it will apply to all properties referencing the PhoneNumber class:

package blog.xmladapter.stateful;
 
import javax.xml.bind.annotation.XmlAttribute;
import javax.xml.bind.annotation.XmlTransient;
import javax.xml.bind.annotation.XmlValue;
import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
 
@XmlJavaTypeAdapter(PhoneNumberAdapter.class)
@XmlTransient
public class PhoneNumber {
 
    private String id;
    private String number;
 
    @XmlAttribute
    public String getId() {
        return id;
    }
 
    public void setId(String id) {
        this.id = id;
    }
 
    @XmlValue
    public String getNumber() {
        return number;
    }
 
    public void setNumber(String number) {
        this.number = number;
    }
 
    @Override
    public boolean equals(Object object) {
        if(null == object || object.getClass() != this.getClass()) {
            return false;
        }
        PhoneNumber test = (PhoneNumber) object;
        if(!equals(id, test.getId())) {
            return false;
        }
        return equals(number, test.getNumber());
    }
 
    private boolean equals(String control, String test) {
        if(null == control) {
            return null == test;
        } else {
            return control.equals(test);
        }
    }
 
    @Override
    public int hashCode() {
        return id.hashCode();
    }
 
}


PhoneNumberAdapter

Below is the implementation of the XmlAdapter. Note that we must maintain if the PhoneNumber object has been seen before. If it has we only populate the id portion of the AdaptedPhoneNumber object.

package blog.xmladapter.stateful;
 
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
 
import javax.xml.bind.annotation.XmlAttribute;
import javax.xml.bind.annotation.adapters.XmlAdapter;
 
public class PhoneNumberAdapter extends XmlAdapter<PhoneNumberAdapter.AdaptedPhoneNumber, PhoneNumber>{
 
    private List<PhoneNumber> phoneNumberList = new ArrayList<PhoneNumber>();
    private Map<String, PhoneNumber> phoneNumberMap = new HashMap<String, PhoneNumber>();
 
    public static class AdaptedPhoneNumber extends PhoneNumber {
        @XmlAttribute
        public String reference;
    }
 
    @Override
    public AdaptedPhoneNumber marshal(PhoneNumber phoneNumber) throws Exception {
        AdaptedPhoneNumber adaptedPhoneNumber = new AdaptedPhoneNumber();
        if(phoneNumberList.contains(phoneNumber)) {
            adaptedPhoneNumber.reference = phoneNumber.getId();
        } else {
            adaptedPhoneNumber.setId(phoneNumber.getId());
            adaptedPhoneNumber.setNumber(phoneNumber.getNumber());
            phoneNumberList.add(phoneNumber);
        }
        return adaptedPhoneNumber;
    }
 
    @Override
    public PhoneNumber unmarshal(AdaptedPhoneNumber adaptedPhoneNumber) throws Exception {
        PhoneNumber phoneNumber = phoneNumberMap.get(adaptedPhoneNumber.reference);
        if(null == phoneNumber) {
            phoneNumber = new PhoneNumber();
            phoneNumber.setId(adaptedPhoneNumber.getId());
            phoneNumber.setNumber(adaptedPhoneNumber.getNumber());
            phoneNumberMap.put(phoneNumber.getId(), phoneNumber);
        }
        return phoneNumber;
    }
 
}


Demo

To ensure the same instance of the XmlAdapter is used for the entire marshal and unmarshal operations we must specifically set an instance of the XmlAdapter on both the Marshaller and Unmarshaller:

package blog.xmladapter.stateful;
 
import java.io.File;
 
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 jc = JAXBContext.newInstance(Customer.class);
 
        Unmarshaller unmarshaller = jc.createUnmarshaller();
        unmarshaller.setAdapter(new PhoneNumberAdapter());
        File xml = new File("src/blog/xmladapter/stateful/input.xml");
        Customer customer = (Customer) unmarshaller.unmarshal(xml);
 
        PhoneNumber primaryPN = customer.getPrimaryPhoneNumber();
        PhoneNumber otherPN1 = customer.getOtherPhoneNumbers().get(0);
        PhoneNumber otherPN2 = customer.getOtherPhoneNumbers().get(1);
        PhoneNumber otherPN3 = customer.getOtherPhoneNumbers().get(2);
        System.out.println(primaryPN == otherPN2);
        System.out.println(otherPN1 == otherPN3);
 
        Marshaller marshaller = jc.createMarshaller();
        marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true);
        marshaller.setAdapter(new PhoneNumberAdapter());
        marshaller.marshal(customer, System.out);
    }
 
}


Output


Below is the output from running the demo code:

true
true
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<customer>
    <primary-phone-number id="B">555-BBBB</primary-phone-number>
    <other-phone-number id="A">555-AAAA</other-phone-number>
    <other-phone-number reference="B"/>
    <other-phone-number reference="A"/>
</customer>

 

From http://blog.bdoughan.com/2011/09/mixing-nesting-and-references-with.html

Published at DZone with permission of Blaise Doughan, author and DZone MVB.

(Note: Opinions expressed in this article and its replies are the opinions of their respective authors and not those of DZone, Inc.)

Tags:

Comments

Ed Staub replied on Fri, 2011/10/21 - 3:39pm

15K+ and still begging for up-votes?  ;-)

Nice article, as usual!

Blaise Doughan replied on Fri, 2011/10/21 - 3:45pm in response to: Ed Staub

Thanks Ed.

Robert Craft replied on Thu, 2012/01/26 - 5:56am

Java has several XML binding frameworks that map Java objects into XML and back. These frameworks allows one to map various Java hierarchies and classes into the chosen XML structure. They can be configured through a separate configuration file. These frameworks include Castor, JiBX and the like. Other frameworks can be configured programmatically. Most JSON binding frameworks simply map the Java object directly into json. Most do not extend the flexibility of mapping complex Java hierarchies into any arbitrary chosen JSON object representation. The configuration is usually pretty limited. I'm currently trying to build a web service that returns json responses. If my Java domain classes changes in any way, my json responses will change as well, causing any service clients, expecting the old version of json responses, to break. Are there any fully flexible JSON binding frameworks out there that can map multiple bindings into the same set of classes? Or (perhaps the more fundamental question is) how do I support different versions of JSON bindings on the same set of Java classes? Or should I simply make sure my domain classes never change? (This does not sound feasible)

Spring Security

Blaise Doughan replied on Wed, 2012/02/01 - 11:04am

Hi Robert, 

Are there any fully flexible JSON binding frameworks out there that can map multiple bindings into the same set of classes?

 Check out the JSON-binding that we are adding to MOXy in EclipseLink 2.4.0:

The same external mapping document is used as for the XML binding which allows you to apply multiple mappings:

 -Blaise

Comment viewing options

Select your preferred way to display the comments and click "Save settings" to activate your changes.