Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

JAXB Annotations - How do I make a list of XmlIDRef elements have the id value as an attribute instead of element body text?

update - see edit at the bottom

IDRefs/keyrefs seem to be possible in JAXB annotations, but the ref ends up being element text.

I would like the ref to be an attribute of an element.

For example, given this object model:

@XmlType
public class Employee {
    @XmlID
    @XmlAttribute
    String name;
    @XmlAttribute
    int years;
    @XmlAttribute
    String foo;
}

@XmlType
public class Office {
    @XmlAttribute
    String name;
    @XmlElementWrapper
    @XmlElement(name = "employee")
    List<Employee> employees;
}

@XmlRootElement
public class Company {
    @XmlElementWrapper
    @XmlElement(name = "office")
    List<Office> offices;
    @XmlElementWrapper
    @XmlElement(name = "employee")
    List<Employee> employees;
}

I would like the externalized xml format to end up looking like this:

<company>
    <offices>
        <office name="nyc">
            <employees>
                <!--*** id ref to employee name ***-->
                <employee ref="alice"/>
                <employee ref="bob"/>
            </employees>
        </office>
        <office name="sf">
            <employees>
                <employee ref="connie"/>
                <employee ref="daphne"/>
            </employees>
        </office>
    </offices>
    <employees>
        <!-- *** name is the id *** -->
        <employee name="alice" years="3" foo="bar"/>
        <employee name="bob" years="3" foo="bar"/>
        <employee name="connie" years="3" foo="bar"/>
        <employee name="daphne" years="3" foo="bar"/>
    </employees>
</company>

Instead, the best I can do is this (with the annotations listed above in the java code):

<company>
    <offices>
        <office name="nyc">
            <employees>
                <employee>alice</employee>
                <employee>bob</employee>
            </employees>
        </office>
        <office name="sf">
            <employees>
                <employee>connie</employee>
                <employee>daphne</employee>
            </employees>
        </office>
    </offices>
    <employees>
        <employee name="alice" years="3" foo="bar"/>
        <employee name="bob" years="3" foo="bar"/>
        <employee name="connie" years="3" foo="bar"/>
        <employee name="daphne" years="3" foo="bar"/>
    </employees>
</company>

Is there a way I can force the idref value to be an attribute of employee, rather than element body text? I know I can do this with an XML Schema, but I'd like to stick to annotations if at all possible.

Thank you.

Edit The solution by Torious below almost works, but it doesn't quite work under some circumstances.

unmarshalling fails if the "offices" elements come before (in the xml file) the "employee" elements that the office references. The employee references are not found, and the EmployeeRef wrapper has a null employee object. If the "employee" are first, it works.

This wouldn't be so much of a problem, but the marshal method will put "offices" first, so that trying to unmarshal what has just been marshalled fails.

Edit 2 comment in Torious' answer solves the ordering problem.

like image 886
marathon Avatar asked Apr 14 '12 01:04

marathon


2 Answers

The solution is to use an XmlAdapter which wraps an Employee in an instance of a new type, EmployeeRef, which specifies how to map the XML id ref:

@XmlType
public class Office {

    @XmlAttribute
    String name;

    @XmlElementWrapper
    @XmlElement(name="employee")
    @XmlJavaTypeAdapter(EmployeeAdapter.class) // (un)wraps Employee
    List<Employee> employees;
}

@XmlType
public class EmployeeRef {

    @XmlIDREF
    @XmlAttribute(name="ref")
    Employee employee;

    public EmployeeRef() {
    }

    public EmployeeRef(Employee employee) {
        this.employee = employee;
    }
}

public class EmployeeAdapter extends XmlAdapter<EmployeeRef, Employee> {

    @Override
    public EmployeeRef marshal(Employee employee) throws Exception {
        return new EmployeeRef(employee);
    }

    @Override
    public Employee unmarshal(EmployeeRef ref) throws Exception {
        return ref.employee;
    }
}

Good luck.

like image 107
Torious Avatar answered Oct 06 '22 01:10

Torious


Note: I'm the EclipseLink JAXB (MOXy) lead and a member of the JAXB (JSR-222) expert group.

Below is an example of how this can be done with MOXy by leveraging the @XmlPath annotation. Due to a bug, you will need to use an EclipseLink 2.4.0 nightly label starting with May 17, 2012. This fix has also be added to the EclipseLink 2.3.3 (starting with May 18, 2012) stream. You can download a nightly label from the following location:

  • http://www.eclipse.org/eclipselink/downloads/nightly.php

Office

On the employees property you can use the @XmlIDREF annotation in combination with the @XmlPath annotation to get the desired mapping. @XmlIDREF tells the JAXB implementation to write out a foreign key instead of the object.

package forum10150263;

import java.util.List;
import javax.xml.bind.annotation.*;
import org.eclipse.persistence.oxm.annotations.XmlPath;

@XmlType
public class Office {
    @XmlAttribute
    String name;

    @XmlPath("employees/employee/@ref")
    @XmlIDREF
    List<Employee> employees;
}

Employee

The counterpart to @XmlIDREF is @XmlID. @XmlID is used to specify the primary key for an object.

package forum10150263;

import javax.xml.bind.annotation.*;

@XmlType
public class Employee {
    @XmlID
    @XmlAttribute
    String name;

    @XmlAttribute
    int years;

    @XmlAttribute
    String foo;
}

Company

Each object referenced via the @XmlIDREF mechanism also needs to be referenced by a containment relationship. In this example this is accomplished by the employees property.

package forum10150263;

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

@XmlRootElement
public class Company {
    @XmlElementWrapper
    @XmlElement(name = "office")
    List<Office> offices;

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

jaxb.properties

To specify MOXy as your JAXB provider you need to add a file named jaxb.properties in the same package as your domain model with the following entry (see Specifying EclipseLink MOXy as Your JAXB Provider)

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

Demo

The standard JAXB APIs are used for unmarshalling/marshalling the XML.

package forum10150263;

import java.io.File;
import javax.xml.bind.*;

public class Demo {

    public static void main(String[] args) throws Exception {
        JAXBContext jc = JAXBContext.newInstance(Company.class);

        Unmarshaller unmarshaller = jc.createUnmarshaller();
        File xml = new File("src/forum10150263/input.xml");
        Company company = (Company) unmarshaller.unmarshal(xml);

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

}

input.xml/Output

<?xml version="1.0" encoding="UTF-8"?>
<company>
   <offices>
      <office name="nyc">
         <employees>
            <employee ref="alice"/>
            <employee ref="bob"/>
         </employees>
      </office>
      <office name="sf">
         <employees>
            <employee ref="connie"/>
            <employee ref="daphne"/>
         </employees>
      </office>
   </offices>
   <employees>
      <employee name="alice" years="3" foo="bar"/>
      <employee name="bob" years="3" foo="bar"/>
      <employee name="connie" years="3" foo="bar"/>
      <employee name="daphne" years="3" foo="bar"/>
   </employees>
</company>

For More Information

  • Specifying EclipseLink MOXy as Your JAXB Provider
  • JAXB and Shared References: @XmlID and @XmlIDREF
  • XPath Based Mapping
like image 37
bdoughan Avatar answered Oct 06 '22 02:10

bdoughan