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.
EclipseLink/Development/339381
Design Specification: XML Extensions
Currently, EclipseLink MOXy supports the mapping of Java fields and properties to XML. Said another way; in order to map data to XML, the user must have an existing Java field or property to map.
To support multi-tenancy, we will be allowing the user to add additional mappings at runtime. Because these new mappings would not have existing fields / properties on the Java class to map to, we will introduce the concept of "extensions", where we can instead map the elements of a Java Map to the desired XML.
Requirements
- Users must be able to add new mappings at runtime through EclipseLink OXM
- Users should be able to add any type of MOXy mapping as an extension
- Users must be able to annotate a field on their Java objects to hold extensions
- The Java field must be of type Map<String, Object>
- Users must be able to specify a field in EclipseLink OXM to hold extensions
Configuration
In order to use this feature, the user will have to have defined a field on their Java object to be an extensions holder. This can be done either with an annotation in the Java class, or through EclipseLink OXM.
There can be at most one extensions holder on any given Java class. Attempting to define multiple extensions holders should result in a validation error.
Annotations
The user can specify a field on their Java object to hold extensions by using the @XmlExtensions annotation.
@Target({METHOD, FIELD}) @Retention(RUNTIME) public @interface XmlExtensions { String getMethodName default "get"; String setMethodName default "set"; XmlExtensionSchemaGenerationPolicy schemaGenerationPolicy() default XmlExtensionSchemaGenerationPolicy.ELEMENTS; } --- public enum XmlExtensionSchemaGenerationPolicy { /** * XML Extensions are written to the schema as individual elements (default). */ ELEMENTS, /** * An XML <any> element will be written to the schema to represent all * of the defined Extensions. */ ANY }
OXM Metadata
To indicate an extensions field in EclipseLink OXM, the user can specify an xml-extensions element in their metadata file:
eclipselink_oxm_2_3.xsd:
... <xs:element name="xml-extensions" substitutionGroup="java-attribute"> <xs:complexType> <xs:complexContent> <xs:extension base="java-attribute"> </xs:complexContent> </xs:complexType> </xs:element> ...
Example
The following domain class is annotated with @XmlExtensible, indicating that it has special accessor methods to handle additional mappings. EclipseLink's default behaviour will look for the methods public Object get(String) and public void set(String, Object) to be the accessors of the extensions map. The extensions field is marked as @XmlTransient to prevent the extensions map itself from being bound to XML.
@XmlRootElement @XmlExtensible @XmlAccessorType(AccessType.FIELD) public class Customer { @XmlAttribute private int id; private String name; @XmlTransient private Map<String, Object> extensions; public Object get(String name) { if (extensions == null) { extensions = new HashMap<String, Object>(); } return extensions.get(name); } public void set(String name, Object value) { if (extensions == null) { extensions = new HashMap<String, Object>(); } extensions.put(name, value); } }
The class above can be expressed in EclipseLink OXM metadata as follows:
... <java-types> <java-type name="Customer"> <java-attributes> <xml-attribute java-attribute="name" type="java.lang.String" /> <xml-extensions java-attribute="extensions" /> </java-attributes> </java-type> ...
In a secondary metadata file, we will define additional mappings that we would like to add to Customer:
... <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> ...
(Note that there is no special configuration needed for additional mappings; they are specified in the same way as "normal" mappings.)
To set the values for these additional mappings, we will add values into the extensions Map, using the property name as the Map key:
InputStream oxm = classLoader.getResourceAsStream("eclipselink-oxm.xml"); Map<String, Object> properties = new HashMap<String, Object>(); properties.put(JAXBContextFactory.ECLIPSELINK_OXM_XML_KEY, oxm); Class[] classes = new Class[] { Customer.class }; JAXBContext ctx = JAXBContext.newInstance(classes, properties); Customer c = new Customer(); c.setName("Dan Swano"); c.set("discountCode", "SIUB372JS7G2IUDS7"); ctx.createMarshaller().marshal(e, System.out);
This will produce the following XML:
<customer name="Dan Swano"> <discount-code>SIUB372JS7G2IUDS7</discount-code> </customer>
Config Options
Specifying Alternate Accessor Methods
To use different method names as your extensions accessors, specify them in the
@XmlRootElement @XmlAccessorType(XmlAccessType.FIELD) @XmlExtensible public class Customer { private String name; @XmlExtensions private Map<String, Object> extensions; public Object get(String name) { if (extensions == null) { extensions = new HashMap<String, Object>(); } return extensions.get(name); } public void set(String name, Object value) { if (extensions == null) { extensions = new HashMap<String, Object>(); } extensions.put(name, value); } }
@XmlRootElement @XmlAccessorType(XmlAccessType.PROPERTY) @XmlExtensible(schemaGenerationPolicy = XmlExtensionSchemaGenerationPolicy.ANY) public class Customer { private String name; private Map<String, Object> extensions; @XmlExtensions public Object getExt(String name) { if (extensions == null) { extensions = new HashMap<String, Object>(); } return extensions.get(name); } public void setExt(String name, Object value) { if (extensions == null) { extensions = new HashMap<String, Object>(); } extensions.put(name, value); } public String getName() { // ... }
Design
- org.eclipse.persistence.jaxb.compiler.Property
- A Property will now know if it is an extensions holder
- org.eclipse.persistence.jaxb.compiler.TypeInfo
- A TypeInfo can now have an extensions Property
- org.eclipse.persistence.internal.jaxb.XmlExtensionsAttributeAccessor
- New AttributeAccessor to handle getting/setting values from the extensions Map.
- org.eclipse.persistence.jaxb.compiler.XMLProcessor
- When processing an OXM file, if a Property is encountered (e.g. "foo") that does not have a corresponding property in Java:
- If the TypeInfo has an extensions Property, then create a new "foo" Property, and setup an XmlExtensionsAttributeAccessor for its mapping
- If the TypeInfo does not have an extensions Property, a "No such property exists" exception will be thrown
- When processing an OXM file, if a Property is encountered (e.g. "foo") that does not have a corresponding property in Java:
Impact on Schema Generation
If the user generates an XML Schema from the JAXBContext after extensions have been added, then the resulting schema will obviously be different from any Schema that may have been used to generate the initial domain objects.
Consider the following example Schema for a Customer:
<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>
Now, consider that the user adds two extensions, middle-initial and phone-number. There are two ways we can handle this:
Extensions as Individual Elements
One approach is to add each new extension into the schema as a new element. Using this approach, a newly-generated schema would look like this:
<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>
All Extensions in an <any> Element
Another option would be to create an <any> element to hold all of the extensions in one node:
<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>
Configuration
The user could specify which schema generation strategy to use by way of an optional property on the @XmlExtensions annotation, e.g.:
- @XmlExtensions(schemaGenerationPolicy = XmlExtensionSchemaGenerationPolicy.ELEMENTS) (default behaviour)
- @XmlExtensions(schemaGenerationPolicy = XmlExtensionSchemaGenerationPolicy.ANY)
Document History
Date | Author | Version Description & Notes |
---|---|---|
110323 | Rick Barkhouse | 1.00 |
110329 | Rick Barkhouse | 1.01 : Input from Doug, added Action Items |
110331 | Rick Barkhouse | 1.02 : Moved open items to Discussion page |
110404 | Rick Barkhouse | 1.03 : Changed to "XML Flex Extensions", modified OXM configuration |
110406 | Rick Barkhouse | 1.04 : Changed to "XML Extensions", added Schema Generation section |
110407 | Rick Barkhouse | 1.05 : Added XmlExtensionSchemaGenerationPolicy information |