8 Advanced Concepts

This chapter includes the following sections:

8.1 Refreshing Metadata

Introduced in EclipseLink MOXy 2.3, you can refresh the JAXBContext metadata at runtime. This allows you to make changes to existing mappings in a live application environment and see those changes immediately without having to create a new JAXBContext.

In order to use the Metadata Refresh feature, your metadata information must be provided in one of the following formats:

  • javax.xml.transform.Source

  • org.w3c.dom.Node

  • org.eclipse.persistence.jaxb.metadata.MetadataSource

Example 8-1 Refreshing Metadata

This example will be bootstrapped from the following EclipseLink OXM file:

<?xml version="1.0"?>
<xml-bindings
      xmlns="http://www.eclipse.org/eclipselink/xsds/persistence/oxm"
    package-name="example">
    <java-types>
        <java-type name="Root">
            <java-attributes>
                <xml-element java-attribute="name" name="orig-name"/>
            </java-attributes>
        </java-type>
    </java-types>
</xml-bindings>
 

The JAXBContext is created in the standard way:

 
...
ClassLoader classLoader = ClassLoader.getSystemClassLoader();
 
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
dbf.setNamespaceAware(true);
DocumentBuilder db = dbf.newDocumentBuilder();
InputStream metadataStream = classLoader.getResourceAsStream("example/eclipselink-oxm.xml";
Document metadataDocument = db.parse(metadataStream);
metadataStream.close();
 
Map<String, Object> props = new HashMap<String, Object>(1);
props.put(JAXBContextProperties.OXM_METADATA_SOURCE, metadataDocument);
JAXBContext context = JAXBContextFactory.createContext(new Class[] { Root.class }, props);
...
 

At this point, if we were to marshal a Root object to XML, it would look like this:

<root>
    <orig-name>RootName</orig-name>
</root>
 

For this example, we will modify the metadata Document directly to change the XML name for the name field. We can then refresh the metadata using the refreshMetadata() API:

...
Element xmlElementElement = (Element) metadataDocument.getElementsByTagNameNS("http://www.eclipse.org/eclipselink/xsds/persistence/oxm", "xml-element").item(0);
xmlElementElement.setAttribute("name", "new-name");
JAXBHelper.getJAXBContext(jc).refreshMetadata();
...
 

After refreshing metadata, the same Root object will be marshalled as follows:

<root>
      <new-name>RootName</new-name>
</root>

8.2 Customizing XML Name Conversions

JAXB has well-established rules for converting Java names to XML names, which can be overridden through the use of annotations. This can become burdensome if your names follow common rules (such as making everything upper-case). Starting with EclipseLink MOXy 2.3, you can override this default naming algorithm.

This example will create an implementation of XMLNameTransformer to provide a naming algorithm to MOXy.

8.2.1 Using the XMLNameTransformer

The XMLNameTransformer interface defines several methods for customizing name generation:

  • transformElementName – called when creating an element from a Java field or method

  • transformAttributeName – called when creating an attribute from a Java field or method

  • transformTypeName – called when creating a simple type or complex type from a Java class

  • transformRootElementName – called when creating a (root) simple type or complex type from a Java class

Example 8-2 defines an XMLNameTransformer that does the following:

  • Root element will be the unqualified Java class name

  • Other types will be named (unqualified Java class name) + "Type"

  • Camel-case element names will be converted to lower-case, hyphenated names

  • XML attributes will appear in all upper-case

Example 8-2 Using an XMLNameTransformer

package example;
 
public class NameGenerator implements org.eclipse.persistence.oxm.XMLNameTransformer {
 
    // Use the unqualified class name as our root element name.
    public String transformRootElementName(String name) {
        return name.substring(name.lastIndexOf('.') + 1);
    }
 
    // The same algorithm as root element name plus "Type" appended to the end.
    public String transformTypeName(String name) {
        return transformRootElementName(name) + "Type";
    }
 
    // The name will be lower-case with word breaks represented by '-'.  
    // Note:  A capital letter in the original name represents the start of a new word.
    public String transformElementName(String name) {
        StringBuilder strBldr = new StringBuilder();
        for (char character : name.toCharArray()) {
            if (Character.isUpperCase(character)) {
                strBldr.append('-');
                strBldr.append(Character.toLowerCase(character));
            } else {
                strBldr.append(character);
            }
         }
        return strBldr.toString();
    }
 
    // The original name converted to upper-case.
    public String transformAttributeName(String name) {
        return name.toUpperCase();
    }
 
}
 

8.2.2 Example Model

The domain model in Example 8-3 will be used. To save space, the accessors have been omitted.

Example 8-3 Customer

import javax.xml.bind.annotation.*;
 
@XmlRootElement
@XmlType(propOrder={"fullName", "shippingAddress"})
@XmlAccessorType(XmlAccessType.FIELD)
public class Customer {
 
    @XmlAttribute
    private long id;
 
    private String fullName;     
 
    private Address shippingAddress;
 
}
 

Example 8-4 Address.java

import javax.xml.bind.annotation.*;
 
@XmlAccessorType(XmlAccessType.FIELD)
public class Address {
 
    @XmlAttribute
    private String type;
 
    private String street;
 
}
 

8.2.3 Specifying the Naming Algorithm

Our implementation of the naming algorithm can be provided via the @XmlNameTransformer annotation (package or type level) or via the external bindings file in XML.

  1. At the type level:

    @XmlNameTransformer(example.NameGenerator.class)
    public class Customer
    
  2. At the package level (package-info.java):

    @XmlNameTransformer(example.NameGenerator.class)
    package example;
    
  3. External bindings file:

    <?xml version='1.0' encoding='UTF-8'?>
    <xml-bindings xmlns='http://www.eclipse.org/eclipselink/xsds/persistence/oxm' xml-name-transformer='example.NameGenerator'>
       <xml-schema/>
       <java-types/>
    </xml-bindings>
     
    

8.2.4 XML Output

Without any customization, JAXB's default naming algorithm will produce XML that looks like the following:

<?xml version="1.0" encoding="UTF-8"?>
<customer id="123">
    <fullName>Jane Doe</fullName>
    <shippingAddress type="residential">
        <street>1 Any Street</street>
    </shippingAddress>
</customer>
 

By leveraging our customized naming algorithm we can get the following output without specifying any additional metadata on our domain classes:

<?xml version="1.0" encoding="UTF-8"?>
<Customer ID="123">
   <full-name>Jane Doe</full-name>
   <shipping-address TYPE="residential">
      <street>1 Any Street</street>
   </shipping-address>
</Customer>

8.3 Using Virtual Access Methods

In addition to standard JAXB properties (represented by Java fields and accessor methods), EclipseLink MOXy 2.3 introduced the concept of virtual properties and virtual access methods, which instead rely on special get() and set() methods to maintain mapping data. For example, you might want to use a HashMap as the underlying structure to hold data for certain mappings. The mappings that use virtual method access must be defined in EclipseLink OXM metadata.

In order to add virtual properties to an entity:

  • the Java class must be marked with an @XmlVirtualAccessMethods annotation, or <xml-virtual-access-methods> element in OXM

  • the Java class must contain getter and setter methods to access virtual property values:

    • public <ValueType> get(String propertyName)

    • public void set(String propertyName, <ValueType> value)

      • method names are configurable

      • <ValueType> can be Object, or any other Java type (if you would like to use a particular type of value class in the method signature)

Note:

By default, EclipseLink will look for methods named set and get. To customize accessor method names, see "Specifying Alternate Accessor Methods".

For an example of using virtual properties in a multi-tenant architecture, see "Using Extensible MOXy".

8.3.1 Configuring Virtual Access Methods

Virtual Access Methods can be configured either by through Java annotations (see Example 8-5) or EclipseLink OXM metadata (see Example 8-6).

Example 8-5 Using Java Annotations

package example;
 
import java.util.Map;
import java.util.HashMap;
 
import javax.xml.bind.annotation.*;
 
import org.eclipse.persistence.oxm.annotations.XmlVirtualAccessMethods;
 
@XmlRootElement
@XmlVirtualAccessMethods
@XmlAccessorType(XmlAccessType.PROPERTY)
public class Customer {
 
   private int id;
 
   private String name;
 
   private Map<String, Object> extensions = new HashMap<String, Object>();
 
   public Object get(String name) {
      return extensions.get(name);
   }
 
   public void set(String name, Object value) {
      extensions.put(name, value);
   }
 
   @XmlAttribute
   public int getId() {
   ...
 
}
 

Example 8-6 Using OXM Metadata

...
<java-types>
   <java-type name="Customer">
      <xml-virtual-access-methods />
      <java-attributes>
         <xml-attribute java-attribute="id" />
         <xml-element java-attribute="name" />
      </java-attributes>
   </java-type>
...
 

8.3.2 Example

For this example we will use the Customer class (Example 8-3), along with an EclipseLink OXM file to define our virtual mappings. Any property encountered in this file that does not have a corresponding Java attribute will be considered a virtual property and will be accessed through the virtual access methods. Because there is no associated Java field, the type information must also be provided.

Example 8-7 The virtualprops-oxm.xml File

...
<java-types>
    <java-type name="Customer">
        <java-attributes>
            <xml-element java-attribute="discountCode" name="discount-code"
                type="java.lang.String" />
        </java-attributes>
    </java-type>
</java-types>
...
 

When creating the JAXBContext, we pass in the virtualprops metadata along with our Customer class.

To set the values for virtual properties, we will use the aforementioned set() method.

InputStream oxm = classLoader.getResourceAsStream("virtualprops-oxm.xml");
Map<String, Object> properties = new HashMap<String, Object>();
properties.put(JAXBContextProperties.OXM_METADATA_SOURCE, oxm);
 
Class[] classes = new Class[] { Customer.class };
JAXBContext ctx = JAXBContext.newInstance(classes, properties);
 
Customer c = new Customer();
c.setId(7761);
c.setName("Bob Smith");
c.set("discountCode", "SIUB372JS7G2IUDS7");
 
ctx.createMarshaller().marshal(e, System.out);
 

This will produce the following XML:

<customer id="7761">
   <name>Bob Smith</name>
   <discount-code>SIUB372JS7G2IUDS7</discount-code>
</customer>
 

Conversely, we use the get(String) method to access virtual properties:

...
Customer c = (Customer) ctx.createUnmarshaller().unmarshal(CUSTOMER_URL);
 
// Populate UI
customerWindow.getTextField(ID).setText(String.valueOf(c.getId()));
customerWindow.getTextField(NAME).setText(c.getName());
customerWindow.getTextField(DCODE).setText(c.get("discountCode"));
...
 

8.3.3 Using XmlAccessType.FIELD and XmlTransient

If you are using an @XmlAccessorType of XmlAccessType.FIELD, you will need to mark your virtual properties Map attribute to be @XmlTransient, to prevent the Map itself from being bound to XML:

Example 8-8 Marking the Map Attribute

package example;
 
import javax.xml.bind.annotation.*;
 
import org.eclipse.persistence.oxm.annotations.XmlVirtualAccessMethods;
 
@XmlRootElement
@XmlVirtualAccessMethods
@XmlAccessorType(XmlAccessType.FIELD)
public class Customer {
 
   @XmlTransient
   private Map<String, Object> extensions;
   ...
 

8.3.4 Options

8.3.4.1 Specifying Alternate Accessor Methods

To use different method names as your virtual method accessors, specify them using the getMethodName and setMethodName attributes on @XmlVirtualAccessMethods:

Example 8-9 Using Alternate Accessor Methods

package example;
 
import java.util.Properties;
 
import javax.xml.bind.annotation.*;
 
import org.eclipse.persistence.oxm.annotations.XmlVirtualAccessMethods;
 
@XmlRootElement
@XmlVirtualAccessMethods(getMethod = "getCustomProps", setMethod = "putCustomProps")
@XmlAccessorType(XmlAccessType.FIELD)
public class Customer {
 
   @XmlAttribute
   private int id;
 
   private String name;
 
   @XmlTransient
   private Properties<String, Object> props = new Properties<String, Object>();
 
   public Object getCustomProps(String name) {
      return props.getProperty(name);
   }
 
   public void putCustomProps(String name, Object value) {
      props.setProperty(name, value);
   }
 
}
 

In OXM:

Example 8-10 Using the xml-virtual-access-methods Element

...
<java-types>
  <java-type name="Customer">
    <xml-virtual-access-methods get-method="getCustomProps" set-method="putCustomProps" />
    <java-attributes>
      <xml-attribute java-attribute="id" />
      <xml-element java-attribute="name" />
      <!-- virtual -->
      <xml-element java-attribute="discountCode" name="discount-code"
        type="java.lang.String" />
    </java-attributes>
  </java-type>
...
 

8.3.4.2 Specifying Schema Generation Options

You can configure how virtual properties should appear in generated schemas using the schema attribute on @XmlVirtualAccessMethods. EclipseLink offers two options. Virtual properties can be:

  • written as individual nodes, or

  • consolidated into a single <any> element.

8.3.4.2.1 Virtual Properties as Individual Nodes

This is EclipseLink's default behavior, or can be specified explicitly as an override as follows:

Example 8-11 Mapping as Individual Nodes

package example;
 
@XmlRootElement
@XmlVirtualAccessMethods(schema = XmlVirtualAccessMethodsSchema.NODES)
@XmlAccessorType(XmlAccessType.FIELD)
public class Customer {
 
   ...
 

For example:

Example 8-12 Original Customer Schema

<xs:schema ...>
 
    <xs:element name="customer">
        <xs:complexType>
            <xs:sequence>
                <xs:element name="first-name" type="xs:string" />
                <xs:element name="last-name" type="xs:string" />
            </xs:sequence>
        </xs:complexType>
    </xs:element>
 
</xs:schema>
 

Example 8-13 Generated Schema (After adding middle-initial and phone-number)

<xs:schema ...>
 
    <xs:element name="customer">
        <xs:complexType>
            <xs:sequence>
                <xs:element name="first-name" type="xs:string" />
                <xs:element name="last-name" type="xs:string" />
                <xs:element name="middle-initial" type="xs:string" />
                <xs:element name="phone-number" type="xs:string" />
            </xs:sequence>
        </xs:complexType>
    </xs:element>
 
</xs:schema>
 
8.3.4.2.2 Virtual Properties in an <any> Element

EclipseLink can also use an <any> element to hold all of the virtual properties in one node:

Example 8-14 Using an <any> Element

package example;
 
@XmlRootElement
@XmlVirtualAccessMethods(schema = XmlVirtualAccessMethodsSchema.ANY)
@XmlAccessorType(XmlAccessType.FIELD)
public class Customer {
 
   ...
 

From Example 8-14, a newly generated schema using this approach would look like:

Example 8-15 Generated Schema

<xs:schema ...>
 
    <xs:element name="customer">
        <xs:complexType>
            <xs:sequence>
                <xs:element name="first-name" type="xs:string" />
                <xs:element name="last-name" type="xs:string" />
                <xs:any minOccurs="0" />
            </xs:sequence>
        </xs:complexType>
   </xs:element>
 
</xs:schema>

8.4 Using Extensible MOXy

In a multi-tenant architecture, a single application runs on a server serving multiple client organizations (tenants). Good multi-tenant applications allow per-tenant customizations. When these customizations are made to data, it can be difficult for the binding layer to handle them.

JAXB is designed to work with domain models that have real fields and properties. EclipseLink MOXy virtual properties provide a way to extend a class without modifying the source.

8.4.1 Using the @XmlVirtualAccessMethods Annotation

The @XmlVirtualAccessMethods annotation is used to specify that a class is extensible. An extensible class is required to have a get method that returns a value by property name, and a set method that stores a value by property name. The default names for these methods are get and set, and can be overridden with the @XmlVirtualAccessMethods annotation.

Since we will have multiple extensible classes in this example, we'll configure a base class for this behavior that extensible classes can extend. We will use the @XmlTransient annotation to prevent ExtensibleBase from being mapped as an inheritance relationship. The real properties represent the parts of the model that will be common to all tenants. The per-tenant extensions will be represented as virtual properties.

Example 8-16 Sample ExtensibleBase

package examples.virtual;
 
import java.util.HashMap;
import java.util.Map;
 
import javax.xml.bind.annotation.XmlTransient;
 
import org.eclipse.persistence.oxm.annotations.XmlVirtualAccessMethods;
 
@XmlTransient
@XmlVirtualAccessMethods(setMethod="put")
public class ExtensibleBase {
 
    private Map<String, Object> extensions = new HashMap<String, Object>();
 
    public <T> T get(String property) {
        return (T) extensions.get(property);
    }
 
    public void put(String property, Object value) {
        extensions.put(property, value);
    }
}
 

Example 8-17 Customer

The Customer class will be extensible since it inherits from a domain class that has been annotated with @XmlVirtualAccessMethods.

package examples.virtual;
 
import javax.xml.bind.annotation.XmlRootElement;
 
@XmlRootElement
public class Customer extends ExtensibleBase {
 
    private String firstName;
    private String lastName;
    private Address billingAddress;
 
    public String getFirstName() {
        return firstName;
    }
 
    public void setFirstName(String firstName) {
        this.firstName = firstName;
    }
 
    public String getLastName() {
        return lastName;
    }
 
    public void setLastName(String lastName) {
        this.lastName = lastName;
    }
 
    public Address getBillingAddress() {
        return billingAddress;
    }
 
    public void setBillingAddress(Address billingAddress) {
        this.billingAddress = billingAddress;
    }
 
}
 

Example 8-18 Address

It is not necessary to have every class in your model be extensible. In this example the Address class will not have any virtual properties.

package examples.virtual;
 
public class Address {
 
    private String street;
 
    public String getStreet() {
        return street;
    }
 
    public void setStreet(String street) {
        this.street = street;
    }
 
}
 

Example 8-19 PhoneNumber

Like Customer, PhoneNumber will be an extensible class.

package examples.virtual;
 
import javax.xml.bind.annotation.XmlValue;
 
public class PhoneNumber extends ExtensibleBase {
 
    private String number;
 
    @XmlValue
    public String getNumber() {
        return number;
    }
 
    public void setNumber(String number) {
        this.number = number;
    }
 
}
 

8.4.2 Creating Tenant 1

The first tenant is an online sporting goods store that requires the following extensions to the model:

  • Customer ID

  • Customer's middle name

  • Shipping address

  • A collection of contact phone numbers

  • Type of phone number (i.e. home, work, or cell)

The metadata for the virtual properties is supplied through MOXy's XML mapping file. Virtual properties are mapped in the same way as real properties. Some additional information is required including type (since this cannot be determined via reflection), and for collection properties a container type.

The virtual properties defined in Example 8-20 for Customer are: middleName, shippingAddress, and phoneNumbers. For PhoneNumber the virtual property is the type property.

Example 8-20 binding-tenant1.xml

<?xml version="1.0"?>
<xml-bindings
    xmlns="http://www.eclipse.org/eclipselink/xsds/persistence/oxm"
    package-name="examples.virtual">
    <java-types>
        <java-type name="Customer">
            <xml-type prop-order="firstName middleName lastName billingAddress shippingAddress phoneNumbers"/>
            <java-attributes>
                <xml-attribute
                    java-attribute="id"
                    type="java.lang.Integer"/>
                <xml-element
                    java-attribute="middleName"
                    type="java.lang.String"/>
                <xml-element
                    java-attribute="shippingAddress"
                    type="examples.virtual.Address"/>
                <xml-element
                    java-attribute="phoneNumbers"
                    name="phoneNumber"
                    type="examples.virtual.PhoneNumber"
                    container-type="java.util.List"/>
            </java-attributes>
        </java-type>
        <java-type name="PhoneNumber">
            <java-attributes>
                <xml-attribute
                    java-attribute="type"
                    type="java.lang.String"/>
            </java-attributes>
        </java-type>
    </java-types>
</xml-bindings>
 

The get/set methods are used on the domain model to interact with the real properties and the accessors defined on the @XmlVirtualAccessMethods annotation are used to interact with the virtual properties. The normal JAXB mechanisms are used for marshal and unmarshall operations:

Customer customer = new Customer();
 
//Set Customer's real properties
customer.setFirstName("Jane");
customer.setLastName("Doe");
 
Address billingAddress = new Address();
billingAddress.setStreet("1 Billing Street");
customer.setBillingAddress(billingAddress);
 
//Set Customer's virtual 'middleName' property
customer.put("middleName", "Anne");
 
//Set Customer's virtual 'shippingAddress' property
Address shippingAddress = new Address();
shippingAddress.setStreet("2 Shipping Road");
customer.put("shippingAddress", shippingAddress);
 
List<PhoneNumber> phoneNumbers = new ArrayList<PhoneNumber>();
customer.put("phoneNumbers", phoneNumbers);
 
PhoneNumber workPhoneNumber = new PhoneNumber();
workPhoneNumber.setNumber("555-WORK");
//Set the PhoneNumber's virtual 'type' property
workPhoneNumber.put("type", "WORK");
phoneNumbers.add(workPhoneNumber);
 
PhoneNumber homePhoneNumber = new PhoneNumber();
homePhoneNumber.setNumber("555-HOME");
//Set the PhoneNumber's virtual 'type' property
homePhoneNumber.put("type", "HOME");
phoneNumbers.add(homePhoneNumber);
 
Map<String, Object> properties = new HashMap<String, Object>();
properties.put(JAXBContextProperties.OXM_METADATA_SOURCE, "examples/virtual/binding-tenant1.xml");
JAXBContext jc = JAXBContext.newInstance(new Class[] {Customer.class, Address.class}, properties);
 
Marshaller marshaller = jc.createMarshaller();
marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true);
marshaller.marshal(customer, System.out);
 

Example 8-21 Output

<?xml version="1.0" encoding="UTF-8"?>
<customer>
   <firstName>Jane</firstName>
   <middleName>Anne</middleName>
   <lastName>Doe</lastName>
   <billingAddress>
      <street>1 Billing Street</street>
   </billingAddress>
   <shippingAddress>
      <street>2 Shipping Road</street>
   </shippingAddress>
   <phoneNumber type="WORK">555-WORK</phoneNumber>
   <phoneNumber type="HOME">555-HOME</phoneNumber>
</customer>

8.4.3 Creating Tenant 2

The second tenant is a streaming media provider that offers on-demand movies and music to it's subscribers. It requires a different set of extensions to the core model: a single contact phone number

For this tenant we will also leverage the mapping file to customize the mapping of the real properties, as shown in Example 8-22:

Example 8-22 binding-tenant2.xml

<?xml version="1.0"?>
<xml-bindings
    xmlns="http://www.eclipse.org/eclipselink/xsds/persistence/oxm"
    package-name="examples.virtual">
    <xml-schema namespace="urn:tenant1" element-form-default="QUALIFIED"/>
    <java-types>
        <java-type name="Customer">
            <xml-type prop-order="firstName lastName billingAddress phoneNumber"/>
            <java-attributes>
                <xml-attribute java-attribute="firstName"/>
                <xml-attribute java-attribute="lastName"/>
                <xml-element java-attribute="billingAddress" name="address"/>
                <xml-element
                    java-attribute="phoneNumber"
                    type="examples.virtual.PhoneNumber"/>
            </java-attributes>
        </java-type>
    </java-types>
</xml-bindings>
 
Customer customer = new Customer();
customer.setFirstName("Jane");
customer.setLastName("Doe");
 
Address billingAddress = new Address();
billingAddress.setStreet("1 Billing Street");
customer.setBillingAddress(billingAddress);
 
PhoneNumber phoneNumber = new PhoneNumber();
phoneNumber.setNumber("555-WORK");
customer.put("phoneNumber", phoneNumber);
 
Map<String, Object> properties = new HashMap<String, Object>();
properties.put(JAXBContextProperties.OXM_METADATA_SOURCE, "examples/virtual/binding-tenant2.xml");
JAXBContext jc = JAXBContext.newInstance(new Class[] {Customer.class, Address.class}, properties);
 
Marshaller marshaller = jc.createMarshaller();
marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true);
marshaller.marshal(customer, System.out);

Example 8-23 Output

Note that even though both tenants share several real properties, the corresponding XML representation can be quite different due to virtual properties:

 
<?xml version="1.0" encoding="UTF-8"?>
<customer xmlns="urn:tenant1" firstName="Jane" lastName="Doe">
   <address>
      <street>1 Billing Street</street>
   </address>
   <phoneNumber>555-WORK</phoneNumber>
</customer>

8.5 Mapping Using XPath Predicates

By default, JAXB will use the Java field name as the XML element name:

Example 8-24 Sample Java Code and XML Schema

public class Customer {
   @XmlElement
   private String firstName;
   @XmlElement
   private String lastName;
}
 
 

 
<?xml version="1.0" encoding="UTF-8"?>
<customer>
   <firstName>Bob</firstName>
   <lastName>Roberts</lastName>
</customer>
 

Or, the XML name can be customized using the name attribute of the @XmlElement annotation:

Example 8-25 Sample Java Code and XML Schema

public class Customer {
   @XmlElement(name="f-name")
   private String firstName;
   @XmlElement(name="l-name")
   private String lastName;
}
 


 
<?xml version="1.0" encoding="UTF-8"?>
<customer>
   <f-name>Bob</f-name>
   <l-name>Roberts</l-name>
</customer>
 

However, sometimes elements need to be mapped based on their position in the document, or based on an attribute value of an element:

<?xml version="1.0" encoding="UTF-8"?>
<node>
   <name>Jane</name>
   <name>Doe</name>
   <node name="address">
      <node name="street">123 A Street</node>
   </node>
   <node name="phone-number" type="work">555-1111</node>
   <node name="phone-number" type="cell">555-2222</node>
</node>
 

For cases like this, EclipseLink MOXy allows you to use XPath predicates to define an expression that will specify the XML element's name.

8.5.1 Mapping with XPath Predicates

An XPath predicate represents an expression that will be evaluated against the element specified. For example, the XPath statement:

node[2]

Would match the second occurrence of the node element ("DEF"):

<?xml version="1.0" encoding="UTF-8"?>
<data>
   <node>ABC</node>
   <node>DEF</node>
</data>
 

Predicates can also match based on an attribute value:

node[@name='foo']

Would match the node element with the attribute name="foo" (that is, ABC). It would not match the node that contains DEF.

<?xml version="1.0" encoding="UTF-8"?>
<data>
   <node name="foo">ABC</node>
   <node name="bar">DEF</node>
</data>
 

Note:

For more information on XPath Predicates, see "2.4 Predicates" of the XML Path Language (XPath) specification (http://www.w3.org/TR/xpath).

8.5.2 Mapping Based on Position

In the following example, our XML contains two name elements; the first occurrence of name should represent the Customer's first name, and the second name will be their last name. To map this, we will specify XPath expressions for each property that will match the appropriate XML element. Note that we also use @XmlType(propOrder) to ensure that our elements will always be in the proper positions.

Example 8-26 Using the @XmlType(propOrder) Annotation

package example;
 
import javax.xml.bind.annotation.*;
 
import org.eclipse.persistence.oxm.annotations.XmlPath;
 
@XmlRootElement
@XmlType(propOrder={"firstName", "lastName"})
@XmlAccessorType(XmlAccessType.FIELD)
public class Customer {
    @XmlPath("name[1]/text()")
    private String firstName;
 
    @XmlPath("name[2]/text()")
    private String lastName;
 
    ...
}
 

This same configuration can be expressed in an EclipseLink XML Bindings document as follows:

Example 8-27 Using the prop-order Attribute

...
<java-type name="Customer">
   <xml-root-element/>
   <xml-type prop-order="firstName lastName"/>
   <java-attributes>
      <xml-element java-attribute="firstName" xml-path="name[1]/text()"/>
      <xml-element java-attribute="lastName" xml-path="name[2]/text()"/>
   </java-attributes>
</java-type>
...
 

This will give us the desired XML representation:

Example 8-28 Resulting XML

<?xml version="1.0" encoding="UTF-8"?>
<customer>
   <name>Bob</name>
   <name>Smith</name>
</customer>
 

8.5.3 Mapping Based on an Attribute Value

Since EclipseLink MOXy 2.3, you can also map to an XML element based on an Attribute value. In this example, all of our XML elements are named node, differentiated by the value of their name attribute:

Example 8-29 Sample XML Schema

<?xml version="1.0" encoding="UTF-8"?>
<node>
   <node name="first-name">Bob</node>
   <node name="last-name">Smith</node>
   <node name="address">
      <node name="street">123 A Street</node>
   </node>
   <node name="phone-number" type="work">555-1111</node>
   <node name="phone-number" type="cell">555-2222</node>
</node>
 

We can use an XPath in the form of element-name[@attribute-name='value'] to map each Java field:

Example 8-30 Sample Mappings

package example;
 
import javax.xml.bind.annotation.*;
 
import org.eclipse.persistence.oxm.annotations.XmlPath;
 
@XmlRootElement(name="node")
@XmlAccessorType(XmlAccessType.FIELD)
public class Customer {
 
    @XmlPath("node[@name='first-name']/text()")
    private String firstName;
 
    @XmlPath("node[@name='last-name']/text()")
    private String lastName;
 
    @XmlPath("node[@name='address']")
    private Address address;
 
    @XmlPath("node[@name='phone-number']")
    private List<PhoneNumber> phoneNumbers = new ArrayList<PhoneNumber>();
 
    ...
}
 
package example;
 
import javax.xml.bind.annotation.*;
 
import org.eclipse.persistence.oxm.annotations.XmlPath;
 
@XmlAccessorType(XmlAccessType.FIELD)
public class Address {
 
    @XmlPath("node[@name='street']/text()")
    private String street;
 
    ...
}
 
package example;
 
import javax.xml.bind.annotation.*;
 
@XmlAccessorType(XmlAccessType.FIELD)
public class PhoneNumber {
 
    @XmlAttribute
    private String type;
 
    @XmlValue
    private String number;
 
    ...
}
 

8.5.4 Creating "Self" Mappings

EclipseLink allows you to configure your one-to-one mappings so the data from the target object will appear inside the source object's XML element. Expanding on the previous example, we could map the Address information so that it would appear directly under the customer element, and not wrapped in its own element. This is referred to as a "self" mapping, and is achieved by setting the target object's XPath to . (dot).

Example 8-31 demonstrates a self mapping declared in annotations.

Example 8-31 Self Mapping Example

package example;
 
import javax.xml.bind.annotation.*;
 
import org.eclipse.persistence.oxm.annotations.XmlPath;
 
@XmlRootElement(name="node")
@XmlAccessorType(XmlAccessType.FIELD)
public class Customer {
 
    @XmlPath("node[@name='first-name']/text()")
    private String firstName;
 
    @XmlPath("node[@name='last-name']/text()")
    private String lastName;
 
    @XmlPath(".")
    private Address address;
 
    @XmlPath("node[@name='phone-number']")
    private List<PhoneNumber> phoneNumbers = new ArrayList<PhoneNumber>();
 
    ...
}
 

Using a self mapping, EclipseLink produces the desired XML. The street data is stored in the root node.

<?xml version="1.0" encoding="UTF-8"?>
<node>
   <node name="first-name">Bob</node>
   <node name="last-name">Smith</node>
   <node name="street">123 A Street</node>
   <node name="phone-number" type="work">555-1111</node>
   <node name="phone-number" type="cell">555-2222</node>
</node>

8.6 Using an XmlAdapter

Some Java classes may not be well suited for use with JAXB and at first glance may seem "unmappable" – for example, classes that do not have a default no-arg constructor, or classes for which an XML representation cannot be automatically determined. Using JAXB's XmlAdapter, you can define custom code to convert the unmappable class into something that JAXB can handle. Then, you can use the @XmlJavaTypeAdapter annotation to indicate that your adapter should be used when working with the unmappable class.

XmlAdapter uses the following terminology:

  • ValueType – The type that JAXB knows how to handle out of the box.

  • BoundType – The type that JAXB doesn't know how to handle. An adapter is written to allow this type to be used as an in-memory representation through the ValueType.

The outline of an XmlAdapter class is as follows:

Example 8-32 XmlAdapter Class Outline

public class AdapterName extends XmlAdapter<ValueType, BoundType> {
 
   public BoundType unmarshal(ValueType value) throws Exception {
      ...
   }
 
   public ValueType marshal(BoundType value) throws Exception {
      ...
   }
 
}
 

8.6.1 Using java.util.Currency

Our first example will use the following domain class:

Example 8-33 Sample Domain Class

package example;
 
import java.util.Currency;
 
import javax.xml.bind.annotation.*;
 
@XmlRootElement
@XmlAccessorType(XmlAccessType.FIELD)
public class PurchaseOrder {
 
   private Double amount;
 
   private Currency currency;
 
   ...
}
 

Here, the Currency cannot be automatically mapped with JAXB because it does not contain a no-argument constructor. However, we can write an adapter that will convert the Currency into something that JAXB does know how to handle – a simple String. Luckily, in this case the Currency's toString() method returns the currency code, which can also be used to create a new Currency:

Example 8-34 Using an Adapter

package example;
 
import java.util.Currency;
 
import javax.xml.bind.annotation.adapters.XmlAdapter;
 
public class CurrencyAdapter extends XmlAdapter<String, Currency> {
 
   /*
    * Java => XML
    * Given the unmappable Java object, return the desired XML representation.
    */
   public String marshal(Currency val) throws Exception {
      return val.toString();
   }
 
   /*
    * XML => Java
    * Given an XML string, use it to build an instance of the unmappable class.
    */
   public Currency unmarshal(String val) throws Exception {
      return Currency.getInstance(val);
   }
 
}
 

To indicate that our adapter should be used for the Currency property, we annotate it with @XmlJavaTypeAdapter and provide the class name of our adapter:

Example 8-35 Using the @XmlJavaTypeAdapter Annotation

package example;
 
import java.util.Currency;
 
import javax.xml.bind.annotation.*;
 
@XmlRootElement
@XmlAccessorType(XmlAccessType.FIELD)
public class PurchaseOrder {
 
   private Double amount;
 
   @XmlJavaTypeAdapter(CurrencyAdapter.class)
   private Currency currency;
 
   ...
}
 

8.6.2 Using java.awt.Point

Sometimes the best way to handle an unmappable class is to write a "stand-in" class which can be mapped with JAXB, and convert between the two classes in the XmlAdapter. In this example, we want to use the Point class. Because of that class' getLocation() method (which JAXB will pickup automatically and map), an infinite loop will occur during marshalling. Because we cannot change the Point class, we will write a new class, MyPoint, and use it in the adapter.

Example 8-36 Using java.awt.Point

package example;
 
public class MyPoint {
 
   private int x, y;
 
   public MyPoint() {
      this(0, 0);
   }
 
   public MyPoint(int x, int y) {
      this.x = x;
      this.y = y;
   }
 
   public int getX() {
      return x;
   }
 
   ...
}
 
package example;
 
import java.awt.Point;
 
import javax.xml.bind.annotation.adapters.XmlAdapter;
 
public class MyPointAdapter extends XmlAdapter<MyPoint, Point> {
 
   /*
    * Java => XML
    */
   public MyPoint marshal(Point val) throws Exception {
      return new MyPoint((int) val.getX(), (int) val.getY());
   }
 
   /*
    * XML => Java
    */
   public Point unmarshal(MyPoint val) throws Exception {
      return new Point(val.getX(), val.getY());
   }
 
}
 

Finally, our Point properties are marked with @XmlJavaTypeAdapter:

Example 8-37 Using the @XmlJavaTypeAdapter Annotation

package example;
 
import java.awt.Point;
 
import javax.xml.bind.annotation.*;
 
@XmlRootElement
@XmlAccessorType(XmlAccessType.FIELD)
public class Zone {
 
   private String name;
 
   @XmlJavaTypeAdapter(MyPointAdapter.class)
   private Point startCoord;
 
   @XmlJavaTypeAdapter(MyPointAdapter.class)
   private Point endCoord;
 
   ...
}

8.6.3 Specifying Package-Level Adapters

In Example 8-37, we annotated both Point properties with the @XmlJavaTypeAdapter annotation. If you have many of these types of properties – for example, in other domain classes – it can be more convenient to specify the @XmlJavaTypeAdapters at the package level.

We could define both of the adapter classes in package-info.java, and would no longer have to annotate any further Currency or Point properties:

@XmlJavaTypeAdapters({
   @XmlJavaTypeAdapter(value=CurrencyAdapter.class,type=Currency.class),
   @XmlJavaTypeAdapter(value=MyPointAdapter.class,type=Point.class)
})
package example;
 

8.6.4 Specifying Class-Level @XmlJavaTypeAdapters

If you have a Java class and you would like to always use an XmlAdapter during marshalling and unmarshalling, then you can specify the @XmlJavaTypeAdapter directly at the class level:

package example;
 
import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
 
@XmlJavaTypeAdapter(DataStructureAdapter.class)
public class DataStructure {
 
   ...
 
}
 

Now, any object that has a DataStructure property will automatically use the DataStructureAdapter, without the need for an annotation on the property itself.

8.7 Using XML Transformations

In many cases, you can use MOXy's @XmlTransformation annotation to give you considerably more control over the marshalling and unmarshalling of your objects. @XmlTransformation can be used to create a custom mapping where one or more XML nodes can be used to create the value for the Java attribute.

To handle the custom requirements at marshal (write) and unmarshall (read) time, @XmlTransformation uses instances of org.eclipse.persistence.mappings.transformers (such as AttributeTransformer and FieldTransformer), providing a non-intrusive solution that avoids the need for domain objects to implement any 'special' interfaces.

For example, if you wanted to map the following XML to objects and combine the values of DATE and TIME into a single java.util.Date object, you can use an @XmlTransformation:

<ELEM_B>
   <B_DATE>20100825</B_DATE>
   <B_TIME>153000</B_TIME>
   <NUM>123</NUM>
   <C_DATE>20100825</C_DATE>
   <C_TIME>154500</C_TIME>
</ELEM_B>

Note:

Ordinarily, you would use @XmlAdapter. However:

  • Although the DATE/TIME pairings are repeated throughout the document, the element name changes each time (such as B_DATE/B_TIME, C_DATE/C_TIME, and so on).

  • Because each pairing is missing a grouping element, you would need to adapt the entire ElemB class.

Because of these issues, MOXy's transformation mapping is much easier to implement:

Example 8-38 Mapping Example

 
package example;
 
import java.util.Date;
 
import javax.xml.bind.annotation.*;
import org.eclipse.persistence.oxm.annotations.*;
 
@XmlAccessorType(XmlAccessType.FIELD)
@XmlRootElement(name="ELEM_B")
public class ElemB {
 
    @XmlTransformation
    @XmlReadTransformer(transformerClass=DateAttributeTransformer.class)
    @XmlWriteTransformers({
        @XmlWriteTransformer(xmlPath="B_DATE/text()", transformerClass=DateFieldTransformer.class),
        @XmlWriteTransformer(xmlPath="B_TIME/text()", transformerClass=TimeFieldTransformer.class),
    })
    private Date bDate;
 
    @XmlElement(name="NUM")
    private int num;
 
    @XmlTransformation
    @XmlReadTransformer(transformerClass=DateAttributeTransformer.class)
    @XmlWriteTransformers({
        @XmlWriteTransformer(xmlPath="C_DATE/text()", transformerClass=DateFieldTransformer.class),
        @XmlWriteTransformer(xmlPath="C_TIME/text()", transformerClass=TimeFieldTransformer.class),
    })
    private Date cDate;
 
}
 

8.7.1 Using an AttributeTransformer

Use an AttributeTransformer to construct the Java attribute value:

Example 8-39 Sample AttributeTransfomer

package example;
 
import java.text.ParseException;
import java.text.SimpleDateFormat;
 
import org.eclipse.persistence.internal.helper.DatabaseField;
import org.eclipse.persistence.mappings.foundation.AbstractTransformationMapping;
import org.eclipse.persistence.mappings.transformers.AttributeTransformer;
import org.eclipse.persistence.sessions.Record;
import org.eclipse.persistence.sessions.Session;
 
public class DateAttributeTransformer implements AttributeTransformer {
 
    private AbstractTransformationMapping mapping;
    private SimpleDateFormat yyyyMMddHHmmss = new SimpleDateFormat("yyyyMMddHHmmss");
 
    public void initialize(AbstractTransformationMapping mapping) {
        this.mapping = mapping;
    }
 
    public Object buildAttributeValue(Record record, Object instance, Session session) {
        try {
            String dateString = null;
            String timeString = null;
 
            for (DatabaseField field : mapping.getFields()) {
                if (field.getName().contains("DATE")) {
                    dateString = (String) record.get(field);
                } else {
                    timeString = (String) record.get(field);
                }
            }
            return yyyyMMddHHmmss.parseObject(dateString + timeString);
        } catch(ParseException e) {
            throw new RuntimeException(e);
        }
    }
 
}
 

8.7.2 Using a FieldTransformer

Use a FieldTransformer to construct the XML field value from the Java object.

Each transformation mapping may have multiple write transformers. In this example, you will need two:

  • The first write transformer writes the year, month, and day in yyMMdd format:

    Example 8-40 First Write Transformer

    package example;
     
    import java.text.SimpleDateFormat;
    import java.util.Date;
     
    import org.eclipse.persistence.mappings.foundation.AbstractTransformationMapping;
    import org.eclipse.persistence.mappings.transformers.FieldTransformer;
    import org.eclipse.persistence.sessions.Session;
     
    public class DateFieldTransformer implements FieldTransformer {
     
        private AbstractTransformationMapping mapping;
        private SimpleDateFormat yyyyMMdd = new SimpleDateFormat("yyyyMMdd");
     
        public void initialize(AbstractTransformationMapping mapping) {
            this.mapping = mapping;
        }
     
        public Object buildFieldValue(Object instance, String xPath, Session session) {
            Date date = (Date) mapping.getAttributeValueFromObject(instance);
            return yyyyMMdd.format(date);
        }
     
    }
    
  • The second write transformer writes out the hour, minutes, and seconds in HHmmss format.

    Example 8-41 Second Write Transformer

    package example;
     
    import java.text.SimpleDateFormat;
    import java.util.Date;
     
    import org.eclipse.persistence.mappings.foundation.AbstractTransformationMapping;
    import org.eclipse.persistence.mappings.transformers.FieldTransformer;
    import org.eclipse.persistence.sessions.Session;
     
    public class TimeFieldTransformer implements FieldTransformer {
     
        private AbstractTransformationMapping mapping;
        private SimpleDateFormat HHmmss = new SimpleDateFormat("HHmmss");
     
        public void initialize(AbstractTransformationMapping mapping) {
            this.mapping = mapping;
        }
     
        public Object buildFieldValue(Object instance, String xPath, Session session) {
            Date date = (Date) mapping.getAttributeValueFromObject(instance);
            return HHmmss.format(date);
        }
     
    }
    

8.8 Generating Java Classes from an XML Schema

Use the JAXB Compiler to generate Java classes from an XML schema. The generated classes will contain JAXB annotations that represent the XML binding metadata.

8.8.1 Running the JAXB Compiler

Use the .sh or .cmd file to run the JAXB Compiler:

<ECLIPSELINK_HOME>/eclipselink/bin/jaxb-compiler.sh <source-file.xsd> [-options]

or

<ECLIPSELINK_HOME>\eclipselink\bin\jaxb-compiler.cmd <source-file.xsd> [-options]

The JAXB Compiler supports the following options:

Option Description

-nv

Do not perform strict validation of the input schemas.

-extension

Allow vender-specific extensions; do not strictly follow the Compatibility Rules in Appendix E.2 of the JAXB specification

-b <file/directory>

Specify the external binding files to process.

Note: Each file must use its own -b flag.

-d <directory>

Specify the output directory for the generated files.

-p <package>

Specify the target package.

-classpath <arg>

Specify where to find user class files.

-verbose

Enable additional compiler output, such as informational messages.

-quiet

Disable compiler output.

-version

Display the compiler version.


For example:

jaxb-compiler.sh -d jaxb-compiler-output config/Customer.xsd

To display a complete list of compiler options, use:

jaxb-compiler.sh -help

8.9 Customizing Generated Mappings

When bootstrapping from an XML Schema (or an EclipseLink project from sessions.xml), you can customize the mappings that EclipseLink generates by using your own EclipseLink OXM Bindings file. This file contains your additional mappings and allows you to combine OXM with XSD bootstrapping. This means that you can use EclipseLink mappings to customize an existing XML schema.

This section shows how to override mappings defined in the schema. Although the schema defines addresses in Canadian format (with province and postal code), you can use XML that contains the address is USA format (with state and zip code).

First, you must create an eclipselink-oxm.xml file that contains the mapping overrides. In Example 8-42, we modify the XPaths for province and postalCode:

Example 8-42 Sample eclipselink-oxm.xml File

<?xml version="1.0" encoding="US-ASCII"?>
<xml-bindings xmlns="http://www.eclipse.org/eclipselink/xsds/persistence/oxm" package-name="example">
      <java-types>
        <java-type name="Address">
            <java-attributes>
                <xml-element java-attribute="province" xml-path="state/text()"/>
                <xml-element java-attribute="postalCode" xml-path="zip-code/text()"/>
            </java-attributes>
        </java-type>
    </java-types>
</xml-bindings>
 

When you create a DynamicJAXBContext, use the properties argument to pass this binding file to the DynamicJAXBContextFactory (in addition to the Schema):

 
// Load Schema
InputStream xsdStream = myClassLoader.getSystemResourceAsStream("example/resources/xsd/customer.xsd");
 
// Load OXM with customizations, put into Properties
InputStream oxmStream = myClassLoader.getSystemResourceAsStream("example/resources/eclipselink/eclipselink-oxm.xml");
Map<String, Object> props = new HashMap<String, Object>();
props.put(JAXBContextProperties.OXM_METADATA_SOURCE, oxmStream);
 
// Create Context
DynamicJAXBContext dContext = DynamicJAXBContextFactory.createContextFromXSD(inputStream, null, myClassLoader, props);