Skip to main content

Notice: This Wiki is now read only and edits are no longer possible. Please see: https://gitlab.eclipse.org/eclipsefdn/helpdesk/-/wikis/Wiki-shutdown-plan for the plan.

Jump to: navigation, search

Difference between revisions of "EclipseLink/Development/2.1/DynamicMOXy/296967/Documentation"

(Customizing the Generated Mappings with eclipselink-oxm.xml)
Line 330: Line 330:
 
Another way to customize the mappings that EclipseLink generates is to inject your own EclipseLink OXM Bindings file, containing any additional mapping configurations you wish to make.
 
Another way to customize the mappings that EclipseLink generates is to inject your own EclipseLink OXM Bindings file, containing any additional mapping configurations you wish to make.
  
Again taking our <tt>Customer</tt> / <tt>Address</tt> example, lets say that we wanted to express our Address in US format, as opposed to the Canadian format that is defined in our schema.  First, we create an <tt>eclipselink-oxm.xml</tt> file that contains the mapping overrides we want to make:
+
Again taking our <tt>Customer</tt> / <tt>Address</tt> example, lets say that we wanted our XML to contain <tt>Addresses</tt> in US format, as opposed to the Canadian format that is defined in our schema.  First, we create an <tt>eclipselink-oxm.xml</tt> file that contains the mapping overrides we want to make (in this case, modifying the XPaths for <tt>province</tt> and <tt>postalCode</tt>):
  
 
<div style="width:80%"><source lang="xml">
 
<div style="width:80%"><source lang="xml">

Revision as of 12:02, 30 April 2010

Dynamic MOXy - JAXB with Dynamically Generated Java Classes

EclipseLink Dynamic MOXy introduces a new concept in JAXB development - the freedom to bootstrap a JAXBContext from a variety of metadata sources and use familiar JAXB APIs to marshal and unmarshal data, all without having actual compiled Java class files on the classpath. This gives the user the flexibility to alter their metadata and not have to worry about updating and recompiling the previously-generated Java source code.



Dynamic Entities

Instead of actual Java classes (e.g. Customer.class, Address.class, etc), the "domain" objects used in Dynamic MOXy will be subclasses of DynamicEntity. DynamicEntities offer a simple get(propertyName) / set(propertyName, propertyValue) API to manipulate their data. DynamicEntities have an associated DynamicType, which will be generated in memory when the metadata is parsed.

Idea.png
DynamicTypes are analogous to Java Classes, whereas DynamicEntities can be though of as instances of a DynamicType.


Idea.png
XML names found in the metadata (complex type names, element names, attribute names) will be translated to Java identifiers according to the algorithms described in "Appendix D: Binding XML Names to Java Identifiers" of the Java Architecture for XML Binding (JAXB) 2.2 Specification. In the example above, "first-name" in XML was translated to "firstName" for the Java object.


Following is an example of using the DynamicEntity APIs:

DynamicEntity customer = (DynamicEntity) dynamicJAXBContext.createUnmarshaller().unmarshal(instanceDoc);
 
String lastName = customer.get("lastName");
List orders = customer.get("orders");
...
DynamicEntity address = dContext.newDynamicEntity("mynamespace.Address");
address.set("street", "1001 Fleet St.");
 
customer.set("lastName", lastName + "Jr.");
customer.set("address", address);



Getting Started

As with conventional JAXB, the first step is to create a JAXBContext. This is achieved by use of the DynamicJAXBContextFactory class. DynamicJAXBContexts cannot be instantiated directly, they must be created through the factory API.

Currently, a DynamicJAXBContext can be created with metadata from an XML Schema file (XSD), or from an EclipseLink Project specified in the EclipseLink sessions.xml file.



Bootstrapping from XML Schema (XSD)

If you have an existing XML Schema that you would like to map to, you can provide that Schema to DynamicJAXBContextFactory to create a DynamicJAXBContext. The schema will be parsed and DynamicTypes will be generated for the complex types contained within it. EclipseLink MOXy uses Sun's XJC (XML-to-Java Compiler) APIs to parse the schema into an in-memory representation, and then generates DynamicTypes and Mappings.

There are three ways that you can pass your XML Schema metadata to DynamicJAXBContextFactory - using an InputStream, Node, or Source:

/**
 * Create a DynamicJAXBContext, using XML Schema as the metadata source.
 *
 * @param schemaStream
 *      java.io.InputStream from which to read the XML Schema.
 * @param resolver
 *      An org.xml.sax.EntityResolver, used to resolve schema imports.  Can be null.
 * @param classLoader
 *      The application's current class loader, which will be used to first lookup
 *      classes to see if they exist before new DynamicTypes are generated.  Can be
 *      null, in which case Thread.currentThread().getContextClassLoader() will be used.
 * @param properties
 *      Map of properties to use when creating a new DynamicJAXBContext.  Can be null.
 *
 * @return
 *      A new instance of DynamicJAXBContext.
 *
 * @throws JAXBException
 *      if an error was encountered while creating the DynamicJAXBContext.
 */
public static DynamicJAXBContext createContextFromXSD(java.io.InputStream schemaStream, EntityResolver resolver,
   ClassLoader classLoader, Map<String, ?> properties) throws JAXBException
 
public static DynamicJAXBContext createContextFromXSD(org.w3c.dom.Node schemaDOM, EntityResolver resolver,
   ClassLoader classLoader, Map<String, ?> properties) throws JAXBException
 
public static DynamicJAXBContext createContextFromXSD(javax.xml.transform.Source schemaSource, EntityResolver resolver,
   ClassLoader classLoader, Map<String, ?> properties) throws JAXBException
Idea.png
The classLoader parameter is your application's current class loader, and will be used to first lookup classes to see if they exist before new DynamicTypes are generated. In most cases the user can pass in null for this parameter, and Thread.currentThread().getContextClassLoader() will be used instead.


Idea.png
The properties parameter can be used to pass additional properties to DynamicJAXBContextFactory. For more information on these properties see Advanced Topics.




Example

Using the following example schema, we will show an example of how to create and marshall a new object using Dynamic MOXy:

<?xml version="1.0" encoding="UTF-8"?>
<xs:schema targetNamespace="myNamespace" xmlns:myns="myNamespace" xmlns:xs="http://www.w3.org/2001/XMLSchema"
    attributeFormDefault="qualified" elementFormDefault="qualified">
 
    <xs:element name="customer" type="myns:customer"/>
 
    <xs:complexType name="customer">
        <xs:sequence>
            <xs:element name="first-name" type="xs:string"/>
            <xs:element name="last-name" type="xs:string"/>
            <xs:element name="address" type="myns:address"/>
        </xs:sequence>
    </xs:complexType>
 
    <xs:complexType name="address">
        <xs:sequence>
            <xs:element name="street" type="xs:string"/>
            <xs:element name="city" type="xs:string"/>
            <xs:element name="province" type="xs:string"/>
            <xs:element name="postal-code" type="xs:string"/>
        </xs:sequence>
    </xs:complexType>
 
</xs:schema>

The following code demonstrates:

  • Passing the XML Schema to DynamicJAXBContextFactory to create a DynamicJAXBContext
  • Creating new DynamicEntities and setting their properties
  • Creating a JAXBMarshaller and marshalling the Java objects to XML
InputStream inputStream = ClassLoader.getSystemResourceAsStream("com/foo/sales/xsd/customer.xsd");
DynamicJAXBContext dContext = DynamicJAXBContextFactory.createContextFromXSD(inputStream, null, null, null);
 
DynamicEntity newCustomer = dContext.newDynamicEntity("mynamespace.Customer");
newCustomer.set("firstName", "George");
newCustomer.set("lastName", "Jones");
 
DynamicEntity newAddress = dContext.newDynamicEntity("mynamespace.Address");
newAddress.set("street", "227 Main St.");
newAddress.set("city", "Toronto");
newAddress.set("province", "Ontario");
newAddress.set("postalCode", "M5V1E6");
 
newCustomer.set("address", address);
 
dContext.createMarshaller().marshal(newCustomer, System.out);



Bootstrapping from an EclipseLink Project

If you would like to have more control over how your DynamicEntities will be mapped to XML, you also have the option of bootstrapping from an EclipseLink Project. Using this approach, you can take advantage of EclipseLink's robust mappings framework and explicitly specify how each complex type in XML maps to its Java counterpart. Please consult the EclipseLink MOXy documentation for information on constructing Projects. (*** TODO: Add Link Here ***)

Warning2.png
The most important thing to remember when bootstrapping from an EclipseLink project is that your project must only specify Java class names, and not actual Java classes. Keep in mind that you are mapping "imaginary" classes to XML, and these classes will be dynamically generated in memory when the DynamicJAXBContext is built.

CORRECT:

customerDescriptor.setJavaClassName("mynamespace.Customer");
...
addressMapping.setReferenceClassName("mynamespace.Address");

INCORRECT:

customerDescriptor.setJavaClass(mynamespace.Customer.class);
...
addressMapping.setReferenceClass(mynamespace.Address.class);


Once you have your EclipseLink Project, you will need to include it in your EclipseLink sessions.xml file (for more information on sessions.xml please see the EclipseLink documentation). (*** TODO: Add Link Here ***) You can then pass the Session's name to DynamicJAXBContextFactory to create your DynamicJAXBContext using the following API:

/**
 * Create a <tt>DynamicJAXBContext</tt>, using an EclipseLink <tt>sessions.xml</tt> as the metadata source.
 * The <tt>sessionNames</tt> parameter is a colon-delimited list of session names within the
 * <tt>sessions.xml</tt> file.  <tt>Descriptors</tt> in this session's <tt>Project</tt> must <i>not</i>
 * have <tt>javaClass</tt> set, but <i>must</i> have <tt>javaClassName</tt> set.
 *
 * @param sessionNames
 *      A colon-delimited <tt>String</tt> specifying the session names from the <tt>sessions.xml</tt> file.
 * @param classLoader
 *      The application's current class loader, which will be used to first lookup
 *      classes to see if they exist before new <tt>DynamicTypes</tt> are generated.  Can be
 *      <tt>null</tt>, in which case <tt>Thread.currentThread().getContextClassLoader()</tt> will be used.
 * @param properties
 *      Map of properties to use when creating a new <tt>DynamicJAXBContext</tt>.  Can be null.
 *
 * @return
 *      A new instance of <tt>DynamicJAXBContext</tt>.
 *
 * @throws JAXBException
 *      if an error was encountered while creating the <tt>DynamicJAXBContext</tt>.
 */
public static DynamicJAXBContext createContext(String sessionNames, ClassLoader classLoader, 
   Map<String, ?> properties) throws JAXBException



Example

Here is an example of an EclipseLink MOXy Project in code. Notice that we have customizations that are made:

  • We have specified that the package name of the generated classes should be com.mypackage. If importing from XSD, we would have used the default namespace to build the package name instead, which would result in a package name of mynamespace.
  • We have specified that the first-name element in XML should be mapped to a field called fName in Java. If importing from XSD we would have generated a field called firstName.
  • We have specified that a null address will be represented by xsi:nil="true" in XML.
package com.mypackage;
 
import org.eclipse.persistence.oxm.NamespaceResolver;
import org.eclipse.persistence.oxm.XMLConstants;
import org.eclipse.persistence.oxm.XMLDescriptor;
 
import org.eclipse.persistence.oxm.mappings.XMLDirectMapping;
import org.eclipse.persistence.oxm.mappings.XMLCompositeObjectMapping;
 
public class CustomerProject extends org.eclipse.persistence.sessions.Project {
 
   public CustomerProject() {
      super();
 
      NamespaceResolver nsResolver = new NamespaceResolver();
      nsResolver.put("ns0", "myNamespace");
      nsResolver.put("xsi", XMLConstants.SCHEMA_INSTANCE_URL);
 
      XMLDescriptor customerDescriptor = new XMLDescriptor();
      customerDescriptor.setJavaClassName("com.mypackage.Customer");
      customerDescriptor.setDefaultRootElement("customer");
      customerDescriptor.setNamespaceResolver(nsResolver);
 
      XMLDirectMapping firstNameMapping = new XMLDirectMapping();
      firstNameMapping.setAttributeName("fName");
      firstNameMapping.setXPath("first-name/text()");
      customerDescriptor.addMapping(firstNameMapping);
      // ...
 
      XMLCompositeObjectMapping addressMapping = new XMLCompositeObjectMapping();
      addressMapping.setAttributeName("address");
      addressMapping.setXPath("address");
      addressMapping.setReferenceClassName("com.mypackage.Address");
      addressMapping.getNullPolicy().setNullRepresentedByXsiNil(true);
      addressMapping.getNullPolicy().setMarshalNullRepresentation(XMLNullRepresentationType.XSI_NIL);
      customerDescriptor.addMapping(addressMapping);
 
      XMLDescriptor addressDescriptor = new XMLDescriptor();
      addressDescriptor.setJavaClassName("com.mypackage.Address");
      // ...
 
      this.addDescriptor(customerDescriptor);
      this.addDescriptor(addressDescriptor);
   }
 
}

Next, here is an example sessions.xml that includes our Project:

<?xml version="1.0" encoding="US-ASCII"?>
<sessions xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" ...>
   <session xsi:type="database-session">
      <name>MyCustomerProject</name>
      <primary-project xsi:type="class">com.mypackage.CustomerProject</primary-project>
   </session>
</sessions>

Finally, we can now instantiate a DynamicJAXBContext using the session name, and begin working with our DynamicEntities:

DynamicJAXBContext dContext = DynamicJAXBContextFactory.createContext("MyCustomerProject", null, null);
 
DynamicEntity newCustomer = dContext.newDynamicEntity("com.mypackage.Customer");
newCustomer.set("fName", "Bob");
...
dContext.createMarshaller().marshal(newCustomer, System.out);
Idea.png
You may specify multiple Session names when bootstrapping from an EclipseLink Project. For example,
DynamicJAXBContextFactory.createContext("ProjectA:ProjectB", null, null);
would create a single DynamicJAXBContext that is aware of Mappings and Descriptors from both Projects.




Advanced Topics

Dynamic MOXy provides further functionality for more advanced use cases.



Customizing the Generated Mappings with eclipselink-oxm.xml

Another way to customize the mappings that EclipseLink generates is to inject your own EclipseLink OXM Bindings file, containing any additional mapping configurations you wish to make.

Again taking our Customer / Address example, lets say that we wanted our XML to contain Addresses in US format, as opposed to the Canadian format that is defined in our schema. First, we create an eclipselink-oxm.xml file that contains the mapping overrides we want to make (in this case, modifying the XPaths for province and postalCode):

<?xml version="1.0" encoding="US-ASCII"?>
<xml-bindings xmlns="http://www.eclipse.org/eclipselink/xsds/persistence/oxm">
    <java-types>
        <java-type name="mynamespace.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>

Then, when we create a DynamicJAXBContext, we pass this binding file information in to the DynamicJAXBContextFactory via the Properties argument:

// First construct the Properties map.
String metadataFile = "resources/eclipselink/eclipselink-oxm.xml";
InputStream iStream = ClassLoader.getSystemResourceAsStream(metadataFile);
HashMap<String, Source> metadataSourceMap = new HashMap<String, Source>();
metadataSourceMap.put("mynamespace", new StreamSource(iStream));
Map<String, Map<String, Source>> props = new HashMap<String, Map<String, Source>>();
props.put(JAXBContextFactory.ECLIPSELINK_OXM_XML_KEY, metadataSourceMap);
 
InputStream inputStream = ClassLoader.getSystemResourceAsStream("com/foo/sales/xsd/customer.xsd");
jaxbContext = DynamicJAXBContextFactory.createContextFromXSD(inputStream, null, null, props);



Bootstrapping from jaxb.properties



Importing other Schemas / EntityResolvers

If the XML Schema that you wish to use to bootstrap contains imports of other schemas, then you must configure an org.xml.sax.EntityResolver that will resolve the locations of the imported schemas, and pass the EntityResolver to DynamicJAXBContextFactory.

Taking our Customer / Address example, let's say that we wish to define these two types in their own schemas:

<!-- customer.xsd -->
 
<?xml version="1.0" encoding="UTF-8"?>
<xs:schema xmlns:myns="myNamespace" xmlns:add="addressNamespace"
   xmlns:xs="http://www.w3.org/2001/XMLSchema" targetNamespace="myNamespace">
 
    <xs:import namespace="addressNamespace" schemaLocation="address.xsd"/>
 
    <xs:element name="customer" type="myns:customer"/>
 
    <xs:complexType name="customer">
        <xs:sequence>
            <xs:element name="first-name" type="xs:string"/>
            <xs:element name="last-name" type="xs:string"/>
            <xs:element name="address" type="add:address"/>
        </xs:sequence>
    </xs:complexType>
 
</xs:schema>
<!-- address.xsd -->
 
<?xml version="1.0" encoding="UTF-8"?>
<xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema" targetNamespace="addressNamespace"
   xmlns="addressNamespace" elementFormDefault="unqualified">
 
    <xsd:complexType name="address">
        <xs:sequence>
            <xs:element name="street" type="xs:string"/>
            <xs:element name="city" type="xs:string"/>
            <xs:element name="province" type="xs:string"/>
            <xs:element name="postal-code" type="xs:string"/>
        </xs:sequence>
    </xsd:complexType>
 
</xsd:schema>

In this case, we must supply an EntityResolver implementation which will be able to resolve the location of the imported schema. First, the EntityResolver code:

class MyEntityResolver implements EntityResolver {
 
   public InputSource resolveEntity(String publicId, String systemId) throws SAXException, IOException {
      // Imported schemas are located in ext\appdata\xsd\
 
      // Grab only the filename part from the full path
      String filename = new File(systemId).getName();
 
      // Now prepend the correct path
      String correctedId = "ext/appdata/xsd/" + filename;
 
      InputSource is = new InputSource(ClassLoader.getSystemResourceAsStream(correctedId));
      is.setSystemId(correctedId);
 
      return is;
   }
 
}

Now, when you create your DynamicJAXBContext, you can pass your EntityResolver in as well:

InputStream inputStream = ClassLoader.getSystemResourceAsStream("com/foo/sales/xsd/customer.xsd");
DynamicJAXBContext dContext = DynamicJAXBContextFactory.createContextFromXSD(inputStream, new MyEntityResolver(), null, null);
Warning2.png
If, when importing another schema, you see the following exception:
Internal Exception: org.xml.sax.SAXParseException: schema_reference.4: Failed to read schema
document '<imported-schema-name>', because 1) could not find the document; 2) the document could
not be read; 3) the root element of the document is not <xsd:schema>.

then you should try disabling XJC's "schema correctness check" by setting the following Java property:

In Code:

System.setProperty("com.sun.tools.xjc.api.impl.s2j.SchemaCompilerImpl.noCorrectnessCheck", "true")

Command Line:

-Dcom.sun.tools.xjc.api.impl.s2j.SchemaCompilerImpl.noCorrectnessCheck=true


Back to the top