June 21, 2013

Mapping Bad XML - Enumerated Collection Elements

In a previous post I introduced EclipseLink JAXB (MOXy)'s @XmlVariableNode extension.  In this post I'll demonstrate how @XmlVariableNode could be leveraged to handle an interesting question I came across on Stack Overflow.  In that question instead of a collection being represented with an element that appeared multiple times, the element name contained the index.  While I would never recommend structuring your XML document this way sometimes you encounter it and need to be able to map it.


XML Input (input.xml)/Output

Below is what the problematic XML looks like.  In this example the number of different elements prefixed with phone-number is not known.

<?xml version="1.0" encoding="UTF-8"?>
<customer>
    <phone-number1 type="work">555-1111</phone-number1>
    <phone-number2 type="home">555-2222</phone-number2>
    <phone-number3 type="cell">555-3333</phone-number3>
</customer>

Java Model

Below is the Java model that we will use for this example.

Customer

Ultimately we wish to represent the XML in Java as a Customer that has a collection of PhoneNumber instances.  For this use case we will leverage MOXy's @XmlVariableNode extension.  The referenced object does not have a property that we can use to store the node name, so we will use an XmlAdapter to convert PhoneNumber into an object that does.

package blog.variablenode.enumeratedlist;

import java.util.*;
import javax.xml.bind.annotation.*;
import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
import org.eclipse.persistence.oxm.annotations.XmlVariableNode;

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

    @XmlVariableNode("nodeName")
    @XmlJavaTypeAdapter(PhoneNumberAdapter.class)
    private List<PhoneNumber> phoneNumbers = new ArrayList<PhoneNumber>();

}

PhoneNumber

Note how PhoneNumber does not have a field to store the node name. 

package blog.variablenode.enumeratedlist;

import javax.xml.bind.annotation.*;

@XmlAccessorType(XmlAccessType.FIELD)
public class PhoneNumber {

    @XmlAttribute
    private String type;

    @XmlValue
    private String number;

}

XMLAdapter (PhoneNumberAdapter)

We will use an XmlAdapter to convert the PhoneNumber into an AdaptedPhoneNumberAdaptedPhoneNumber has the nodeName field (line 14) that we referenced in the @XmlVariableNode annotation that we used on the phoneNumbers field in the Customer class.  We will annotate this field with @XmlTransient to prevent it from being marshalled/unmarshalled (see: JAXB and Unmapped Properties).  We will leverage MOXy's @XmlPath extension to avoid having to copy the contents of PhoneNumber into the AdaptedPhoneNumber (see:  XPath Based Mapping).

package blog.variablenode.enumeratedlist;

import javax.xml.bind.annotation.XmlTransient;
import javax.xml.bind.annotation.adapters.XmlAdapter;
import org.eclipse.persistence.oxm.annotations.XmlPath;

public class PhoneNumberAdapter extends XmlAdapter<PhoneNumberAdapter.AdaptedPhoneNumber, PhoneNumber>{

    private int counter = 1;

    public static class AdaptedPhoneNumber {

        @XmlTransient
        public String nodeName;

        @XmlPath(".")
        public PhoneNumber phoneNumber;

    }

    @Override
    public AdaptedPhoneNumber marshal(PhoneNumber phoneNumber) throws Exception {
        AdaptedPhoneNumber adaptedPhoneNumber = new AdaptedPhoneNumber();
        adaptedPhoneNumber.nodeName = "phone-number" + counter++;
        adaptedPhoneNumber.phoneNumber = phoneNumber;
        return adaptedPhoneNumber;
    }

    @Override
    public PhoneNumber unmarshal(AdaptedPhoneNumber adaptedPhoneNumber) throws Exception {
        return adaptedPhoneNumber.phoneNumber;
    }

}

Demo

The following demo code can be used to prove that everything works.  Since the PhoneNumberAdapter is stateful we need to set and instance of it on the Marshaller (line 17).

package blog.variablenode.enumeratedlist;

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/variablenode/enumeratedlist/input.xml");
        Customer customer = (Customer) unmarshaller.unmarshal(xml);
        
        Marshaller marshaller = jc.createMarshaller();
        PhoneNumberAdapter phoneNumberAdapter = new PhoneNumberAdapter();
        marshaller.setAdapter(phoneNumberAdapter);
        marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true);
        marshaller.marshal(customer, System.out);
    }

}


Further Reading

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

No comments:

Post a Comment

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