org.eclipse.persistence.oxm.XMLField Maven / Gradle / Ivy
/*
* Copyright (c) 1998, 2021 Oracle and/or its affiliates. All rights reserved.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v. 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0,
* or the Eclipse Distribution License v. 1.0 which is available at
* http://www.eclipse.org/org/documents/edl-v10.php.
*
* SPDX-License-Identifier: EPL-2.0 OR BSD-3-Clause
*/
// Contributors:
// Oracle - initial API and implementation from Oracle TopLink
package org.eclipse.persistence.oxm;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.StringTokenizer;
import javax.xml.datatype.XMLGregorianCalendar;
import javax.xml.namespace.QName;
import org.eclipse.persistence.exceptions.XMLMarshalException;
import org.eclipse.persistence.internal.core.helper.CoreClassConstants;
import org.eclipse.persistence.internal.core.sessions.CoreAbstractSession;
import org.eclipse.persistence.internal.helper.DatabaseField;
import org.eclipse.persistence.internal.helper.DatabaseTable;
import org.eclipse.persistence.internal.oxm.ConversionManager;
import org.eclipse.persistence.internal.oxm.XMLConversionManager;
import org.eclipse.persistence.internal.oxm.XMLConversionPair;
import org.eclipse.persistence.internal.oxm.XPathFragment;
import org.eclipse.persistence.internal.oxm.XPathPredicate;
import org.eclipse.persistence.internal.oxm.mappings.Field;
import org.eclipse.persistence.internal.oxm.record.AbstractUnmarshalRecord;
/**
* TopLink XML mappings make use of XMLFields based on XPath statements to find the relevant
* data in an XML document. The XPath statement is relative to the context node specified in the descriptor.
* The XPath may contain node type, path, and positional information. The XPath is specified on the
* field using the setXPath
method or by using the appropriate constructor.
*
* The following XPath statements may be used to specify the location of XML data relating to an object's
* name attribute:
*
*
* XPath statements
*
* XPath
* Description
*
*
* @name
* The "@" character indicates that the node is an attribute.
*
*
* text()
* "text()" indicates that the node is a text node. In this case the name value in the
* text node belongs to the context node.
*
*
* full-name/text()
* The name information is stored in the text node of the full-name element.
*
*
* personal-info/name/text()
* The XPath statement may be used to specify any valid path.
*
*
* name[2]/text()
* The XPath statement may contain positional information. In this case the name
* information is stored in the text node of the second occurrence of the name element.
*
*
* Mapping to a Specific Schema Type: In most cases TopLink can determine the target format in the
* XML document. However, there are cases where you must specify which one of a number of possible targets
* TopLink should use. For example, a java.util.Calendar could be marshalled to a schema date, time, or dateTime,
* or a byte[] could be marshalled to a schema hexBinary or base64Binary node.
*
*
*
*
XML Schema
*
* <?xml version="1.0" encoding="UTF-8"?>
* <xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema">
* <xsd:element name="customer" type="customer-type"/>
* <xsd:complexType name="customer-type">
* <xsd:sequence>
* <xsd:element name="picture" type="xsd:hexBinary"/>
* <xsd:element name="resume" type="xsd:base64Binary"/>
* </xsd:sequence>
* </xsd:complexType>
* </xsd:schema>
*
*
*
Code Sample
*
* XMLField pictureField = new XMLField("picture/text()")
* pictureField.setSchemaType(XMLConstants.HEX_BINARY_QNAME);
*
*
*
Setting custom conversion pairs: By default in TopLink XML built-in schema types are associated with
* java classes and vice versa. These default pairs can be modified by the user using the addJavaConversion and
* addXMLConversion api. For example by default a java.util.Calendar is mapped to the dateTime schema type
* so the XML will be formated based on that type. Below are the default schema type to java type conversion pairs
* and the default java type to schema type conversion pairs.
*
* XML schema type to Java type default conversion pairs
*
* Schema Type
* Java Type
*
*
* base64Binary
* byte[]
*
*
* boolean
* boolean
*
*
* byte
* byte
*
*
* date
* java.util.Calendar
*
*
* dateTime
* java.util.Calendar
*
*
* decimal
* java.math.BigDecimal
*
*
* double
* double
*
*
* float
* float
*
*
* hexBinary
* byte[]
*
*
* int
* int
*
*
* integer
* java.math.BigInteger
*
*
* long
* long
*
*
* QName
* javax.xml.namespace.QName
*
*
* time
* java.util.Calendar
*
*
* unsignedByte
* short
*
*
* unsignedInt
* long
*
*
* unsignedShort
* int
*
*
* anySimpleType
* java.lang.String
*
*
*
*
* Java type to XML schema type default conversion pairs
*
* Java Type
* Schema Type
*
*
* byte[]
* hexBinary
*
*
* java.lang.Byte[]
* hexBinary
*
*
* java.math.BigDecimal
* decimal
*
*
* java.math.BigInteger
* integer
*
*
* boolean
* boolean
*
*
* java.lang.Boolean
* boolean
*
*
* java.lang.Byte
* Byte
*
*
* byte
* byte
*
*
* java.util.Calendar
* dateTime
*
*
* java.util.GregorianCalendar
* dateTime
*
*
* double
* double
*
*
* java.lang.Double
* double
*
*
* float
* float
*
*
* java.lang.Float
* float
*
*
* int
* int
*
*
* java.lang.Integer
* int
*
*
* long
* long
*
*
* java.lang.Long
* long
*
*
* short
* short
*
*
* java.lang.Short
* short
*
*
* javax.xml.namespace.QName
* QName
*
*
* java.lang.String
* string
*
*
* @see org.eclipse.persistence.oxm.XMLUnionField
*/
public class XMLField extends DatabaseField implements Field {
private NamespaceResolver namespaceResolver;
private QName schemaType;
private XPathFragment xPathFragment;
private XPathFragment lastXPathFragment;
private boolean isCDATA = false;
private boolean isRequired = false;
private boolean isInitialized = false;
private boolean nestedArray = false;
/** Makes this maintain the collection of items in a single attribute or element instead of having one element per item in the collection.
* Default is false */
private boolean usesSingleNode;
// Hashtables for the non-default conversions, these Hashtables are
// used if the user has made any modifications to the pairs
protected HashMap userXMLTypes;
protected HashMap userJavaTypes;
protected boolean isTypedTextField;
protected QName leafElementType;
/**
* Default constructor, create a new XMLField
*/
public XMLField() {
super();
isTypedTextField = false;
}
/**
* Default constructor, create a new XMLField based on the specified xPath
* @param xPath The xPath statement for this field
*/
public XMLField(String xPath) {
super(xPath, new DatabaseTable());
isTypedTextField = false;
}
@Override
public void initialize() {
if(null != xPathFragment) {
initializeXPathFragment(xPathFragment);
}
isInitialized = true;
}
private void initializeXPathFragment(XPathFragment xPathFragment) {
XPathPredicate predicate = xPathFragment.getPredicate();
if(null != predicate) {
initializeXPathFragment(predicate.getXPathFragment());
}
String localName = xPathFragment.getLocalName();
if(localName !=null && !localName.equals(XMLConstants.EMPTY_STRING)){
if(null == xPathFragment.getNamespaceURI()) {
if(xPathFragment.hasNamespace()) {
if(null == namespaceResolver) {
throw XMLMarshalException.namespaceNotFound(xPathFragment.getShortName());
} else {
String uri = namespaceResolver.resolveNamespacePrefix(xPathFragment.getPrefix());
if(null == uri && null != xPathFragment.getPrefix()) {
throw XMLMarshalException.namespaceNotFound(xPathFragment.getShortName());
}
xPathFragment.setNamespaceURI(uri);
}
}
else if(!xPathFragment.isAttribute() && null != namespaceResolver) {
xPathFragment.setNamespaceURI(namespaceResolver.getDefaultNamespaceURI());
}
}
}
XPathFragment nextXPathFragment = xPathFragment.getNextFragment();
if(null != nextXPathFragment) {
initializeXPathFragment(nextXPathFragment);
}
}
/**
* Returns the xpath statement associated with this XMLField
* @return The xpath statement associated with this XMLField
*/
@Override
public String getXPath() {
return getName();
}
/**
* Set the xpath statment for this XMLField.
* @param xPath The xpath statement to be associated with this XMLField
*/
@Override
public void setXPath(String xPath) {
setName(xPath);
}
/**
* Get the NamespaceResolver associated with this XMLField
* @return The NamespaceResolver associated with this XMLField
* @see org.eclipse.persistence.oxm.NamespaceResolver
*/
@Override
public NamespaceResolver getNamespaceResolver() {
return namespaceResolver;
}
/**
* Set the NamespaceResolver associated with this XMLField
* @param newNamespaceResolver The namespaceResolver to be associated with this XMLField
* @see org.eclipse.persistence.oxm.NamespaceResolver
*/
@Override
public void setNamespaceResolver(NamespaceResolver newNamespaceResolver) {
namespaceResolver = newNamespaceResolver;
}
/**
* PUBLIC:
* Sets whether the mapping uses a single node.
* @param usesSingleNode True if the items in the collection are in a single node or false if each of the items in the collection is in its own node
*/
@Override
public void setUsesSingleNode(boolean usesSingleNode) {
this.usesSingleNode = usesSingleNode;
}
/**
* PUBLIC:
* Checks whether the mapping uses a single node.
*
* @return True if the items in the collection are in a single node or false if each of the items in the collection is in its own node.
*/
@Override
public boolean usesSingleNode() {
return usesSingleNode;
}
/**
* Sets the schematype associated with this XMLField
* This is an optional setting; when set the schema type will be used to format the XML appropriately
* @param value QName to be added to the list of schema types
*/
@Override
public void setSchemaType(QName value) {
this.schemaType = value;
}
/**
* Return the schema type associated with this field
* @return the schema type
*/
@Override
public QName getSchemaType() {
return schemaType;
}
/**
* Returns if the field is a typed text field
* True when we should base conversions on the "type" attribute on elements
* @return True when we should base conversions on the "type" attribute on elements, otherwise false
*/
@Override
public boolean isTypedTextField() {
return isTypedTextField;
}
/**
* Set if the field is a typed text field
* True when we should base conversions on the "type" attribute on elements
* @param value The boolean value specifiy if this is a typed text field
*/
@Override
public void setIsTypedTextField(boolean value) {
isTypedTextField = value;
}
/**
* INTERNAL:
* Indicates if the xpath for this field is "."
*
* @return true if the xpath is ".", false otherwise
*/
@Override
public boolean isSelfField() {
if (null == xPathFragment) {
return false;
}
return xPathFragment.isSelfFragment();
}
/**
* INTERNAL:
* Returns false since this is a union field
* The subclass XMLUnionField returns true for this
*/
@Override
public boolean isUnionField() {
return false;
}
/**
* Override setName in superclass
*/
@Override
public void setName(String xPath, String startDelimiter, String endDelimiter) {
super.setName(xPath, null, null);
if (hasPath(xPath)) {
buildFragments(xPath);
} else {
XPathFragment xPathFragment = new XPathFragment(xPath.intern());
xPathFragment.setXMLField(this);
setXPathFragment(xPathFragment);
setLastXPathFragment(xPathFragment);
}
}
/**
* This has the same effect as calling the setXPath method
*
* @param xPath The xPath associated with this XMLField
*/
@Override
public void setName(String xPath) {
setName(xPath, null, null);
}
private boolean hasPath(String xpathString) {
return ((xpathString != null) && (xpathString.indexOf('/') != -1));
}
private void buildFragments(String xpathString) {
StringTokenizer st = new StringTokenizer(xpathString, "/", true);
String next;
int i = 0;
XPathFragment currentXPathFragment = null;
XPathFragment nextXPathFragment = null;
while (st.hasMoreTokens()) {
next = st.nextToken();
if (null != next) {
if ("/".equals(next)) {
if (0 == i) {
next = "/" + st.nextToken();
} else {
continue;
}
}
if (next.contains("[") && !next.contains("]")) {
StringBuilder sb = new StringBuilder(next);
String more;
while (st.hasMoreTokens()) {
more = st.nextToken();
sb.append(more);
if (more.contains("]"))
break;
}
next = sb.toString().intern();
} else {
next = next.intern();
}
nextXPathFragment = new XPathFragment(next);
if (0 == i) {
setXPathFragment(nextXPathFragment);
} else {
currentXPathFragment.setNextFragment(nextXPathFragment);
if (nextXPathFragment.isAttribute() || nextXPathFragment.nameIsText()) {
currentXPathFragment.setHasText(true);
}
}
nextXPathFragment.setXMLField(this);
currentXPathFragment = nextXPathFragment;
i++;
}
setLastXPathFragment(currentXPathFragment);
}
}
/**
* INTERNAL:
* Maintain a direct pointer to the first XPathFragment. For example given
* the following XPath first/middle/@last, first is the first XPathFragment.
*/
@Override
public XPathFragment getXPathFragment() {
return xPathFragment;
}
/**
* INTERNAL:
* Return the first XPathFragment.
*/
public void setXPathFragment(XPathFragment xPathFragment) {
this.xPathFragment = xPathFragment;
}
/**
* INTERNAL:
* Return the last XPathFragment.
*/
@Override
public XPathFragment getLastXPathFragment() {
return lastXPathFragment;
}
/**
* INTERNAL:
* Maintain a direct pointer to the last XPathFragment. For example given
* the following XPath first/middle/@last, @last is the last XPathFragment.
*/
public void setLastXPathFragment(XPathFragment lastXPathFragment) {
this.lastXPathFragment = lastXPathFragment;
}
/**
* INTERNAL
* Return the class for a given qualified XML Schema type
* @param qname The qualified name of the XML Schema type to use as a key in the lookup
* @return The class corresponding to the specified schema type, if no corresponding match found returns null
*/
public Class getJavaClass(QName qname) {
return getJavaClass(qname, XMLConversionManager.getDefaultXMLManager());
}
/**
* INTERNAL
* @return the class for a given qualified XML Schema type.
* @since EclipseLink 2.6.0
*/
@Override
public Class getJavaClass(QName qname, ConversionManager conversionManager) {
if (userXMLTypes != null) {
Class theClass = (Class)userXMLTypes.get(qname);
if(theClass != null){
return theClass;
}
}
return conversionManager.javaType(qname);
}
/**
* Return the qualified XML Schema type for a given class
* @param javaClass The class to use as a key in the lookup
* @return QName The qualified XML Schema type, if no corresponding match found returns null
*/
public QName getXMLType(Class javaClass) {
return getXMLType(javaClass, XMLConversionManager.getDefaultXMLManager());
}
/**
* @return the XML Schema type for a given class.
* @since EclipseLink 2.6.0
*/
@Override
public QName getXMLType(Class javaClass, ConversionManager conversionManager) {
if (userJavaTypes != null) {
QName theQName = (QName)userJavaTypes.get(javaClass);
if (theQName !=null) {
return theQName;
}
}
return conversionManager.schemaType(javaClass);
}
/**
* @return a HashMap of Java to XML Schema type conversion pairs
*/
private HashMap getUserJavaTypes() {
// If no manual modifications have been made to the conversion pairs yet,
// userJavaTypes will be null and needs to be built
if (userJavaTypes == null) {
userJavaTypes = new HashMap();
}
return userJavaTypes;
}
/**
* @return a HashMap of XML Schema types to Java types conversion pairs
*/
private HashMap getUserXMLTypes() {
// If no manual modifications have been made to the conversion pairs yet,
// userXMLTypes will be null and needs to be built
if (userXMLTypes == null) {
userXMLTypes = new HashMap();
}
return userXMLTypes;
}
/**
* INTERNAL:
*/
public ArrayList getUserXMLTypesForDeploymentXML() {
if (userXMLTypes != null) {
ArrayList types = new ArrayList(userXMLTypes.size());
Iterator iter = userXMLTypes.entrySet().iterator();
while (iter.hasNext()) {
Map.Entry entry = (Map.Entry)iter.next();
XMLConversionPair pair = new XMLConversionPair((QName)entry.getKey(),((Class)entry.getValue()).getName());
types.add(pair);
}
return types;
}
return null;
}
/**
* INTERNAL:
*/
public void setUserXMLTypesForDeploymentXML(ArrayList pairs) throws Exception {
if (pairs.size() > 0) {
userXMLTypes = new HashMap();
Iterator iter = pairs.iterator();
while (iter.hasNext()) {
XMLConversionPair pair = (XMLConversionPair)iter.next();
if ((pair.getXmlType() != null) && (pair.getJavaType() != null)) {
userXMLTypes.put(pair.getXmlType(), Class.forName(pair.getJavaType()));
}
}
}
}
/**
* INTERNAL:
*/
public ArrayList getUserJavaTypesForDeploymentXML() {
if (userJavaTypes != null) {
ArrayList types = new ArrayList(userJavaTypes.size());
Iterator iter = userJavaTypes.entrySet().iterator();
while (iter.hasNext()) {
Map.Entry entry = (Map.Entry)iter.next();
XMLConversionPair pair = new XMLConversionPair((QName)entry.getValue(), ((Class)entry.getKey()).getName());
types.add(pair);
}
return types;
}
return null;
}
/**
* INTERNAL:
*/
public void setUserJavaTypesForDeploymentXML(ArrayList pairs) throws Exception {
if (pairs.size() > 0) {
userJavaTypes = new HashMap();
Iterator iter = pairs.iterator();
while (iter.hasNext()) {
XMLConversionPair pair = (XMLConversionPair)iter.next();
if ((pair.getXmlType() != null) && (pair.getJavaType() != null)) {
userJavaTypes.put(Class.forName(pair.getJavaType()), pair.getXmlType());
}
}
}
}
/**
* INTERNAL:
* Called from DOMRecord and XMLReader. MappingNodeValues call XMLReader which calls this method so that other XMLReader subclasses can override.
*/
@Override
public Object convertValueBasedOnSchemaType(Object value, XMLConversionManager xmlConversionManager, AbstractUnmarshalRecord record) {
if (schemaType != null) {
if(XMLConstants.QNAME_QNAME.equals(schemaType)){
return xmlConversionManager.buildQNameFromString((String)value, record);
}else{
Class fieldType = getType();
if (fieldType == null) {
fieldType = getJavaClass(schemaType, xmlConversionManager);
}
return xmlConversionManager.convertObject(value, fieldType, schemaType);
}
}
return value;
}
/**
* Add an XML to Java Conversion pair entry
* @param qname The qualified name of the XML schema type
* @param javaClass The class to add
*/
public void addXMLConversion(QName qname, Class javaClass) {
getUserXMLTypes().put(qname, javaClass);
}
/**
* Add a Java to XML Conversion pair entry
* @param javaClass The class to add
* @param qname The qualified name of the XML schema type
*/
public void addJavaConversion(Class javaClass, QName qname) {
getUserJavaTypes().put(javaClass, qname);
}
/**
* Add an entry for both an XML Conversion and a Java Conversion entry
* @param qname The qualified name of the XML schema type
*/
public void addConversion(QName qname, Class javaClass) {
addJavaConversion(javaClass, qname);
addXMLConversion(qname, javaClass);
}
/**
* Remove an XML to Java Conversion entry
*/
public void removeXMLConversion(QName qname) {
getUserXMLTypes().remove(qname);
}
/**
* Remove a Java to XML Conversion entry
*
*/
public void removeJavaConversion(Class javaClass) {
getUserJavaTypes().remove(javaClass);
}
/**
* Remove both a Java to XML Conversion and the corresponding XML to Java Conversion entry
*
*/
public void removeConversion(QName qname, Class javaClass) {
removeJavaConversion(javaClass);
removeXMLConversion(qname);
}
/**
* Assumes type is in the format prefix:localPart, or localPart.
*
*/
public void setLeafElementType(QName type) {
leafElementType = type;
if (hasLastXPathFragment()) {
getLastXPathFragment().setLeafElementType(type);
}
}
@Override
public QName getLeafElementType() {
if (lastXPathFragment != null) {
return lastXPathFragment.getLeafElementType();
}
return leafElementType;
}
/**
* INTERNAL:
*/
@Override
public boolean hasLastXPathFragment() {
return lastXPathFragment != null;
}
/**
* INTERNAL:
*/
@Override
public QName getSchemaTypeForValue(Object value, CoreAbstractSession session) {
if(leafElementType != null){
return leafElementType;
}else if (isTypedTextField) {
if (CoreClassConstants.XML_GREGORIAN_CALENDAR.isAssignableFrom(value.getClass())){
return ((XMLGregorianCalendar) value).getXMLSchemaType();
}else if (CoreClassConstants.DURATION.isAssignableFrom(value.getClass())){
ConversionManager conversionManager = (ConversionManager) session.getDatasourcePlatform().getConversionManager();
return getXMLType(CoreClassConstants.DURATION, conversionManager);
}
ConversionManager conversionManager = (ConversionManager) session.getDatasourcePlatform().getConversionManager();
return getXMLType(value.getClass(), conversionManager);
}
return schemaType;
}
/**
* INTERNAL:
*/
@Override
public void setIsCDATA(boolean CDATA) {
this.isCDATA = CDATA;
}
/**
* INTERNAL:
*/
@Override
public boolean isCDATA() {
return isCDATA;
}
/**
* INTERNAL
*/
@Override
public boolean isSchemaType(QName schemaType){
if(getSchemaType() == null){
return false;
}
return getSchemaType().equals(schemaType);
}
/**
* Indicates if this XMLField represents a "required" XML element or attribute
* ([minOccurs="1"] for elements, [use="required"] for attributes). NOTE: This
* API is used only for Schema Generation.
*
* @see org.eclipse.persistence.internal.oxm.schema.SchemaModelGenerator
*/
@Override
public boolean isRequired() {
return isRequired;
}
/**
* Set whether this XMLField represents a "required" XML element or attribute
* ([minOccurs="1"] for elements, [use="required"] for attributes). NOTE: This
* API is used only for Schema Generation.
*
* @see org.eclipse.persistence.internal.oxm.schema.SchemaModelGenerator
*/
@Override
public void setRequired(boolean isRequired) {
this.isRequired = isRequired;
}
@Override
public boolean equals(Object object) {
try {
if(!isInitialized) {
return super.equals(object);
}
if(this == object) {
return true;
}
XMLField xmlField = (XMLField) object;
if(!xPathFragment.equals(xmlField.getXPathFragment())) {
return false;
}
XPathFragment xpf = xPathFragment;
XPathFragment xpf2 = xmlField.getXPathFragment();
while(xpf.getNextFragment() != null) {
xpf = xpf.getNextFragment();
xpf2 = xpf2.getNextFragment();
if(!xpf.equals(xpf2)) {
return false;
}
}
return null == xpf2.getNextFragment();
} catch(ClassCastException e) {
return false;
}
}
/**
* Set nested array flag. Used in JSON marshalling.
*
* @param nestedArray flag.
*/
@Override
public void setNestedArray(boolean nestedArray) {
this.nestedArray = nestedArray;
}
/**
* INTERNAL:
*
* @return True if content is nested array.
*/
@Override
public boolean isNestedArray() {
return this.nestedArray;
}
@Override
public int hashCode() {
if(null == xPathFragment && null == xPathFragment.getXPath()) {
return 1;
}
return (xPathFragment.getXPath().replace((xPathFragment.getPrefix() != null) ? xPathFragment.getPrefix() + xPathFragment.getNamespaceSeparator() : "", "")).hashCode();
}
}