September 28, 2010

JAXB & XML Infoset Preservation

In this post we will discuss how to use JAXB to preserve the XML Infoset. 


XML

The XML we will use for this example contains comments and unmapped elements.  Even though we will only map the highlighted lines, this example will demonstrate how to preserve the rest of the XML document using the JAXB Binder.

<?xml version="1.0" encoding="UTF-8"?>
<customer>
    <UNMAPPED_ELEMENT_1/>
    <name>Jane Doe</name>
    <!-- COMMENT #1 -->
    <address>
        <UNMAPPED_ELEMENT_2/>
        <street>1 A Street</street>
        <!-- COMMENT #2 -->
        <UNMAPPED_ELEMENT_3/>
        <city>Any Town</city>
    </address>
    <!-- COMMENT #3 -->
    <UNMAPPED_ELEMENT_4/>
    <phone-number type="home">555-HOME</phone-number>
    <!-- COMMENT #4 -->
    <phone-number type="cell">555-CELL</phone-number>
    <UNMAPPED_ELEMENT_5/>
    <!-- COMMENT #5 -->
</customer>

Java Model

We will use the following Java model for this example.  To save space the get/set methods have been omitted.

Customer

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

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

    private String name;

    private Address address;

    @XmlElement(name="phone-number")
    private List<PhoneNumber> phoneNumbers;

}

Address

import javax.xml.bind.annotation.*;

@XmlAccessorType(XmlAccessType.FIELD)
public class Address {

    private String street;

    private String city;

}

Phone Number

import javax.xml.bind.annotation.*;

@XmlAccessorType(XmlAccessType.FIELD)
public class PhoneNumber {

    @XmlAttribute
    private String type;
    
    @XmlValue
    private String value;
}

Unmarshaller & Marshaller

The following code is familiar to all JAXB users.  An Unmarshaller is used to load the XML into objects, and then a Marshaller is used to save the objects back to XML.

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);

        File xml = new File("input.xml");
        Unmarshaller unmarshaller = jc.createUnmarshaller();
        Customer customer = (Customer) unmarshaller.unmarshal(xml);

        customer.getAddress().setStreet("2 NEW STREET");
        PhoneNumber workPhone = new PhoneNumber();
        workPhone.setType("work");
        workPhone.setValue("555-WORK");
        customer.getPhoneNumbers().add(workPhone);

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

}

Note that when the objects are saved back to XML using the Marshaller the unmapped content is lost:

<?xml version="1.0" encoding="UTF-8"?>
<customer>
   <name>Jane Doe</name>
   <address>
      <street>2 NEW STREET</street>
      <city>Any Town</city>
   </address>
   <phone-number type="home">555-HOME</phone-number>
   <phone-number type="cell">555-CELL</phone-number>
   <phone-number type="work">555-WORK</phone-number>
</customer>

Binder

The Binder interface was introduced in JAXB 2.0, yet many developers are not familiar with this JAXB feature. It is created from the JAXBContext, and interacts with XML in the form of a DOM. The Binder maintains an association between the Java objects and their corresponding DOM nodes.

import java.io.File;
import javax.xml.bind.*;
import javax.xml.parsers.*;
import javax.xml.transform.*;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import org.w3c.dom.*;

public class BinderDemo {

    public static void main(String[] args) throws Exception {
        DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
        DocumentBuilder db = dbf.newDocumentBuilder();
        File xml = new File("input.xml");
        Document document = db.parse(xml);

        JAXBContext jc = JAXBContext.newInstance(Customer.class);

        Binder<Node> binder = jc.createBinder();
        Customer customer = (Customer) binder.unmarshal(document);
        customer.getAddress().setStreet("2 NEW STREET");
        PhoneNumber workPhone = new PhoneNumber();
        workPhone.setType("work");
        workPhone.setValue("555-WORK");
        customer.getPhoneNumbers().add(workPhone);
        binder.updateXML(customer);

        TransformerFactory tf = TransformerFactory.newInstance();
        Transformer t = tf.newTransformer();
        t.transform(new DOMSource(document), new StreamResult(System.out));
    }

}

Instead of creating a new XML document, Binder applies the changes made to the object back to the original DOM.  This is how JAXB is able to preserve the XML infoset.

<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<customer>
    <UNMAPPED_ELEMENT_1/>
    <name>Jane Doe</name>
    <!-- COMMENT #1 -->
    <address>
        <UNMAPPED_ELEMENT_2/>
        <street>2 NEW STREET</street>
        <!-- COMMENT #2 -->
        <UNMAPPED_ELEMENT_3/>
        <city>Any Town</city>
    </address>
    <!-- COMMENT #3 -->
    <UNMAPPED_ELEMENT_4/>
    <phone-number type="home">555-HOME</phone-number>
    <!-- COMMENT #4 -->
    <phone-number type="cell">555-CELL</phone-number>
    <phone-number type="work">555-WORK</phone-number>
    <UNMAPPED_ELEMENT_5/>
    <!-- COMMENT #5 -->
</customer>

Note

There appears to be a bug in the Metro version of JAXB regarding this example.  You will need to ensure you use EclipseLink JAXB (MOXy).  To use the MOXy implementation you will need to add a file named jaxb.properties in with your model classes that contains the following entry:


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

14 comments:

  1. Hi,
    Binder is a very good thing, but when I test your sample, all comments are removed ! And idea for the issue ?
    Thanks advance

    ReplyDelete
  2. This example was created with MOXy JAXB you will need to ensure that you specify the necessary jaxb.properties file. For more information see my post: JAXB - The XML Binding Standard.

    There appears to be a bug with Metro JAXB (the reference implementation) regarding the Binder.

    ReplyDelete
  3. I'm facing the same issue with the comments being omitted but the problem doesn't seem to be with JAXB. I'm at a point where I've got an instance of org.w3c.dom.Document that has comments in it (saw it using debugger)
    but printing using the transformer (as your code) doesn't show the comments.
    I created the document from the xml using

    DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
    dbf.setNamespaceAware(true);
    // Preserve Lexical Info
    dbf.setCoalescing(false);
    dbf.setExpandEntityReferences(false);
    dbf.setIgnoringElementContentWhitespace(false);
    dbf.setIgnoringComments(false);

    This made the Document keep the comments. Any idea how I can print the document WITH the comments?

    ReplyDelete
  4. Hi Enrico,

    If you are just creating a DOM with comments and using a Transformer to write the document to an OutputStream, there shouldn't be anything special you need to do. It may be a bug in the implementation of Transformer you are using.

    If you are seeing the comments disappear when running the Binder example you may be using the Metro JAXB implemenation (which is included with Java SE 6), instead of the MOXy implementation. To use MOXy you will need to add the necessary jaxb.properties file. I have updated the example to include this step.

    -Blaise

    ReplyDelete
  5. YES!
    Thank you!

    My new problem is that the new element I created with java code was added to the xml with a new namespace ns0 and the root node of the new element as new attributes defining this new namespace...

    But I'll figure it out ...

    Thanks again!

    ReplyDelete
  6. While using a XML file under action script, the xml file is modified and I am unable to use the modified XM?

    ReplyDelete
  7. Hi Silver,

    If you make changes to the XML, you can apply it back to the object model using updateJAXB on Binder.

    -Blaise

    ReplyDelete
  8. Hi Doughan,
    How can append the data into existing xml file.
    existing.xml

    Simple text


    following this example http://blogs.oracle.com/CoreJavaTechTips/entry/exchanging_data_with_xml_and1 and getting result e.g.

    Simple text


    Simple text


    but i am expecting as,

    Simple text
    Simple text

    ReplyDelete
  9. I believe some relevant information from your comment was lost due to the formatting constraints of the comment section. Feel free to send me a message via my Contact Me page to start an email discussion on this issue.

    -Blaise

    ReplyDelete
  10. Binder is very good for using. but i have one issue while using binder. i did the above sample and removed the value of name and run again. The empty name tag created again and again. can any one please help me? thanks in advance.

    ReplyDelete