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.
Difference between revisions of "EclipseLink/Development/339381"
Line 250: | Line 250: | ||
<java-types> | <java-types> | ||
<java-type name="Customer"> | <java-type name="Customer"> | ||
− | <xml-extensible | + | <xml-extensible get-method-name="getCustomProps" set-method-name="putCustomProps" /> |
<java-attributes> | <java-attributes> | ||
<xml-attribute java-attribute="id" type="java.lang.Integer" /> | <xml-attribute java-attribute="id" type="java.lang.Integer" /> |
Revision as of 16:12, 13 April 2011
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 rely on special get() and set() methods to maintain extension data.
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 specify that a Java type is extensible, using either Annotations or EclipseLink OXM
Configuration
In order to add extensions to an entity:
- the Java class must be marked with an @XmlExtensible annotation, or <xml-extensible> element in OXM
- the Java class must contain getter and setter methods to access extension values
- public Object get(String extensionName)
- public void set(String extensionName, Object extensionValue)
- method names are configurable but must have the same method signatures as above
Annotations
The user can specify that a Java class may hold extensions by using the @XmlExtensible annotation:
@Target({TYPE}) @Retention(RUNTIME) public @interface XmlExtensible { String getMethodName() default "get"; String setMethodName() default "set"; XmlExtensibleSchema schemaGenerationPolicy() default XmlExtensibleSchema.ELEMENTS; }
public enum XmlExtensibleSchema { /** * 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-extensible element in their metadata file:
eclipselink_oxm_2_3.xsd:
... <xs:element name="java-type"> <xs:complexType> <xs:all> ... <xs:element ref="xml-extensible" minOccurs="0"/> ... ... <xs:element name="xml-extensible"> <xs:complexType> <xs:attribute name="get-method-name" type="xs:string" default="get" /> <xs:attribute name="set-method-name" type="xs:string" default="set" /> <xs:attribute name="schema-generation-policy" type="xs:string" default="ELEMENTS" /> </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.
@XmlRootElement @XmlExtensible @XmlAccessorType(AccessType.PROPERTY) public class Customer { private int id; private String name; 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); } @XmlAttribute public int getId() { ... }
The class above can be expressed in EclipseLink OXM metadata as follows:
... <java-types> <java-type name="Customer"> <xml-extensible /> <java-attributes> <xml-attribute java-attribute="id" type="java.lang.Integer" /> <xml-element java-attribute="name" type="java.lang.String" /> </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 use the aforementioned set() method:
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 using the getMethodName and setMethodName attributes on @XmlExtensible:
@XmlRootElement @XmlExtensible(getMethodName = "getCustomProps", setMethodName = "putCustomProps") @XmlAccessorType(AccessType.FIELD) public class Customer { @XmlAttribute private int id; private String name; @XmlTransient private Map<String, Object> extensions; public Object getCustomProps(String name) { if (extensions == null) { extensions = new HashMap<String, Object>(); } return extensions.get(name); } public void putCustomProps(String name, Object value) { if (extensions == null) { extensions = new HashMap<String, Object>(); } extensions.put(name, value); } }
In OXM:
... <java-types> <java-type name="Customer"> <xml-extensible get-method-name="getCustomProps" set-method-name="putCustomProps" /> <java-attributes> <xml-attribute java-attribute="id" type="java.lang.Integer" /> <xml-element java-attribute="name" type="java.lang.String" /> </java-attributes> </java-type> ...
Schema Generation Options
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.
To configure how these new extensions should appear in future generated schemas, use the schemaGenerationPolicy attribute on @XmlExtensible.
Extensions as individual Elements
This is EclipseLink's default behaviour, or can be specified explicitly as an override as follows:
@XmlRootElement @XmlExtensible(schemaGenerationPolicy = XmlExtensibleSchema.ELEMENTS) @XmlAccessorType(AccessType.FIELD) public class Customer { ...
For example:
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>
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>
All Extensions in an <any> Element
EclipseLink can also use an <any> element to hold all of the extensions in one node:
@XmlRootElement @XmlExtensible(schemaGenerationPolicy = XmlExtensibleSchema.ANY) @XmlAccessorType(AccessType.FIELD) public class Customer { ...
Taking the example from above, a newly generated schema using this approach would look like:
<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>
XmlAccessorType and XmlTransient
If you are using an @XmlAccessorType other than AccessType.PROPERTY, you will need to mark your extensions Map attribute to be @XmlTransient, to prevent the Map itself from being bound to XML.
@XmlRootElement @XmlExtensible @XmlAccessorType(AccessType.FIELD) public class Customer { @XmlTransient private Map<String, Object> extensions; ...
Design
- org.eclipse.persistence.jaxb.compiler.Property
- A Property will now know if it is an extension Property
- org.eclipse.persistence.jaxb.compiler.TypeInfo
- A TypeInfo will now know if it is extensible
- 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 is extensible, then create a new "foo" Property, and setup an org.eclipse.persistence.internal.descriptors.VirtualAttributeAccessor for its mapping
- If the TypeInfo is not extensible, 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:
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 |
110413 | Rick Barkhouse | 1.06 : Modified document to reflect new @XmlExtensible design |