October 7, 2010

JAXB and Shared References: @XmlID and @XmlIDREF

When converting objects to XML, privately owned data can easily be represented through nested elements.  When there are multiple references to an object another mechanism needs to be used.  In JAXB this mechanism is @XmlID and @XmlIDREF.

XML is a tree structure so each object has to be represented as a nested element.  In our example we will nest all employees under the company.   This is achieved using @XmlElement.  Note any unnannotated field/property is considered to be @XmlElement.

package blog.shared;

import java.util.ArrayList;
import java.util.List;

import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlRootElement;

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

    @XmlElement(name="employee")
    private List<Employee> employees;

    public Company() {
        employees = new ArrayList<Employee>();
    }

}

In addition to each employee object being referenced by a company object, there are also references between employee objects.  Each employee may have one manager and multiple reports.  These relationships will be represented using keys (@XmlID) and foreign keys (@XmlIDREF).

MOXy Extenstion:  The JAXB specification requires that the property marked with @XmlID be a String property, MOXy JAXB does not enforce this restriction.

package blog.shared;

import java.util.ArrayList;
import java.util.List;

import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAttribute;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlID;
import javax.xml.bind.annotation.XmlIDREF;

@XmlAccessorType(XmlAccessType.FIELD)
public class Employee {

    @XmlAttribute
    @XmlID
    private String id;

    @XmlAttribute
    private String name;

    @XmlIDREF
    private Employee manager;

    @XmlElement(name="report")
    @XmlIDREF
    private List<Employee> reports;

    public Employee() {
        reports = new ArrayList<Employee>();
    }

}

The following code will be used to demonstrate the concept.

package blog.shared;

import javax.xml.bind.JAXBContext;
import javax.xml.bind.Marshaller;

public class Demo {

    public static void main(String[] args) throws Exception {
        Company company = new Company();

        Employee employee1 = new Employee();
        employee1.setId("1");
        employee1.setName("Jane Doe");
        company.getEmployees().add(employee1);

        Employee employee2 = new Employee();
        employee2.setId("2");
        employee2.setName("John Smith");
        employee2.setManager(employee1);
        employee1.getReports().add(employee2);
        company.getEmployees().add(employee2);

        Employee employee3 = new Employee();
        employee3.setId("3");
        employee3.setName("Anne Jones");
        employee3.setManager(employee1);
        employee1.getReports().add(employee3);
        company.getEmployees().add(employee3);

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

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

}

In the XML representation the manager and reports are marshalled with the ID of the employee instance they are referencing.

<company>
    <employee id="1" name="Jane Doe">
        <report>2</report>
        <report>3</report>
    </employee>
    <employee id="2" name="John Smith">
        <manager>1</manager>
    </employee>
    <employee id="3" name="Anne Jones">
        <manager>1</manager>
    </employee>
</company> 
 
The @XmlIDREF annotation is also compatible with the @XmlList annotation. Alternatively we could model our Employee object as:

package blog.shared;

import java.util.ArrayList;
import java.util.List;

import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAttribute;
import javax.xml.bind.annotation.XmlID;
import javax.xml.bind.annotation.XmlIDREF;
import javax.xml.bind.annotation.XmlList;

@XmlAccessorType(XmlAccessType.FIELD)
public class Employee {

    @XmlID
    @XmlAttribute
    private String id;

    @XmlAttribute
    private String name;

    @XmlIDREF
    private Employee manager;

    @XmlIDREF
    @XmlList
    private List<Employee> reports;

    public Employee() {
        reports = new ArrayList<Employee>();
    }

}

Which would produce the following XML:

<company>
   <employee id="1" name="Jane Doe">
      <reports>2 3</reports>
   </employee>
   <employee id="2" name="John Smith">
      <manager>1</manager>
   </employee>
   <employee id="3" name="Anne Jones">
      <manager>1</manager>
   </employee>
</company>

5 comments:

  1. Dear, Mr. Blaise Doughan.
    I just wrote an entry based on this blog about JPA/JAXB mixup.
    Can you please review that?
    http://jinahya.wordpress.com/2012/03/09/using-xmlid-and-xmlidref-in-jaxb-for-self-referencing-relationships/

    ReplyDelete
    Replies
    1. Hi Jin,

      For that use case I would recommend using an XmlAdapter instead of the events:
      - http://stackoverflow.com/a/9633097/383861

      -Blaise

      Delete
  2. Hi Blaise,

    I have two classes which is related to each other in this way.

    class User {
    int id;
    Set usrgrp;
    }

    Class UserGroup {
    int id;
    Set usr;
    }

    My actual issue is I am trying to marshal/unmarshal these two classes and that was resulting in cyclic object graph. I do not want to use eclipseLink annotations, I want to do it using JPA and java annotations.

    Currently I tried to use the @XmlID, @XmlIDRef and created a IAdapter class to convert the int to String and back, On this line
    http://stackoverflow.com/questions/12914382/marshalling-unmarshalling-fields-to-tag-with-attributes-using-jaxb

    Now I am getting the folowing error...
    Adapter entities.tr.utils.IDAdapter is not applicable to the field type int.
    this problem is related to the following location:
    at @javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter(type=class javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter$DEFAULT, value=class entities.tr.utils.IDAdapter)
    at private int entities.tr.user.User.id
    at entities.tr.user.User
    at private java.util.Set entities.tr.user.UserGroup.usr
    at entities.tr.user.UserGroup

    Please help me to find the solution of this. Without using the eclipse link annotations.

    ReplyDelete
    Replies
    1. Hi,

      You will need to change int to Integer in order to apply an XmlAdapter to it. If you use EclipseLink JAXB (MOXy) as your JAXB (JSR-222) provider then you can use @XmlID on a non-String field/property.

      -Blaise

      Delete
  3. Hi Blaise,

    Instead of using annotations to manage the report references; I'm trying to use an external XML mapping. The problem; the XmlIdRef is not used properly and the list of reports are not produced.

    Here is the XML mapping.

    <?xml version="1.0"?>
    <xml-bindings xmlns="http://www.eclipse.org/eclipselink/xsds/persistence/oxm"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://www.eclipse.org/eclipselink/xsds/persistence/oxm http://www.eclipse.org/eclipselink/xsds/eclipselink_oxm_2_4.xsd"
    package-name="test">

    <java-types>
    <java-type name="Employee">
    <java-attributes>
    <xml-attribute java-attribute="id" xml-id="true" />

    <!--xml-element java-attribute="reports" name="report" type="test.Employee" xml-idref="true" / -->
    <xml-elements java-attribute="reports" xml-idref="true">
    <xml-element type="test.Employee" name="report" />
    </xml-elements>
    </java-attributes>
    </java-type>

    </java-types>
    </xml-bindings>

    If I add @XmlIDREF to the list of reports then it's fine... The idea was to remove all the javax.xml and eclipselink annotations for my JPA objects.

    Cheers,
    Ced.

    ReplyDelete

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