/******************************************************************************* | |
* Copyright (c) 1998, 2013 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 v1.0 and Eclipse Distribution License v. 1.0 | |
* which accompanies this distribution. | |
* The Eclipse Public License is available at http://www.eclipse.org/legal/epl-v10.html | |
* and the Eclipse Distribution License is available at | |
* http://www.eclipse.org/org/documents/edl-v10.php. | |
* | |
* Contributors: | |
* Oracle - initial API and implementation from Oracle TopLink | |
******************************************************************************/ | |
package org.eclipse.persistence.oxm.mappings; | |
import org.eclipse.persistence.descriptors.ClassDescriptor; | |
import org.eclipse.persistence.exceptions.ConversionException; | |
import org.eclipse.persistence.exceptions.DescriptorException; | |
import org.eclipse.persistence.internal.core.sessions.CoreAbstractSession; | |
import org.eclipse.persistence.internal.helper.DatabaseField; | |
import org.eclipse.persistence.internal.identitymaps.CacheKey; | |
import org.eclipse.persistence.internal.oxm.XMLConversionManager; | |
import org.eclipse.persistence.internal.oxm.mappings.DirectMapping; | |
import org.eclipse.persistence.internal.oxm.mappings.Field; | |
import org.eclipse.persistence.internal.oxm.record.AbstractMarshalRecord; | |
import org.eclipse.persistence.internal.oxm.record.AbstractUnmarshalRecord; | |
import org.eclipse.persistence.internal.queries.ContainerPolicy; | |
import org.eclipse.persistence.internal.queries.JoinedAttributeManager; | |
import org.eclipse.persistence.internal.sessions.AbstractRecord; | |
import org.eclipse.persistence.internal.sessions.AbstractSession; | |
import org.eclipse.persistence.oxm.XMLConstants; | |
import org.eclipse.persistence.oxm.XMLField; | |
import org.eclipse.persistence.oxm.XMLMarshaller; | |
import org.eclipse.persistence.oxm.XMLUnmarshaller; | |
import org.eclipse.persistence.mappings.AttributeAccessor; | |
import org.eclipse.persistence.mappings.converters.Converter; | |
import org.eclipse.persistence.mappings.foundation.AbstractDirectMapping; | |
import org.eclipse.persistence.oxm.mappings.converters.XMLConverter; | |
import org.eclipse.persistence.oxm.mappings.nullpolicy.AbstractNullPolicy; | |
import org.eclipse.persistence.oxm.mappings.nullpolicy.NullPolicy; | |
import org.eclipse.persistence.oxm.record.DOMRecord; | |
import org.eclipse.persistence.oxm.record.XMLRecord; | |
import org.eclipse.persistence.queries.ObjectBuildingQuery; | |
import org.eclipse.persistence.sessions.Session; | |
/** | |
* <p>XML Direct Mappings map a Java attribute directly to XML attribute or text node. | |
* | |
* <p><b>XML Direct Mappings can be used in the following scenarios</b>:<ul> | |
* <li>Mapping to a Text Node</li> | |
* <li>Mapping to an Attribute</li> | |
* <li>Mapping to a Specified Schema Type</li> | |
* <li>Mapping to a List Field</li> | |
* <li>Mapping to a Union Field</li> | |
* <li>Mapping to a Union of Lists</li> | |
* <li>Mapping to a Union of Unions</li> | |
* <li>Mapping with a Simple Type Translator</li> | |
* </ul> | |
* | |
* <p><b>Setting the XPath</b>: TopLink XML mappings make use of 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 | |
* mapping using the <code>setXPath</code> method. | |
* | |
* <p>The following XPath statements may be used to specify the location of XML data relating to an object's | |
* name attribute: | |
* | |
* <p><table border="1"> | |
* <tr> | |
* <th id="c1" align="left">XPath</th> | |
* <th id="c2" align="left">Description</th> | |
* </tr> | |
* <tr> | |
* <td headers="c1">@name</td> | |
* <td headers="c2">The "@" character indicates that the node is an attribute.</td> | |
* </tr> | |
* <tr> | |
* <td headers="c1">text()</td> | |
* <td headers="c2">"text()" indicates that the node is a text node. In this case the name value in the | |
* text node belongs to the context node.</td> | |
* </tr> | |
* <tr> | |
* <td headers="c1">full-name/text()</td> | |
* <td headers="c2">The name information is stored in the text node of the full-name element.</td> | |
* </tr> | |
* <tr> | |
* <td headers="c1" nowrap="true">personal-info/name/text()</td> | |
* <td headers="c2">The XPath statement may be used to specify any valid path.</td> | |
* </tr> | |
* <tr> | |
* <td headers="c1">name[2]/text()</td> | |
* <td headers="c2">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.</td> | |
* </tr> | |
* </table> | |
* | |
* <p><b>Mapping to a Specific Schema Type</b>: 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 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> | |
* --> | |
* | |
* <p><em>XML Schema</em><br> | |
* <code> | |
* <?xml version="1.0" encoding="UTF-8"?><br> | |
* <xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema"><br> | |
* <xsd:element name="customer" type="customer-type"/><br> | |
* <xsd:complexType name="customer-type"><br> | |
* <xsd:sequence><br> | |
* <xsd:element name="picture" type="xsd:hexBinary"/><br> | |
* <xsd:element name="resume" type="xsd:base64Binary"/><br> | |
* </xsd:sequence><br> | |
* </xsd:complexType><br> | |
* </xsd:schema><br> | |
* </code> | |
* | |
* <p><em>Code Sample</em><br> | |
* <code> | |
* XMLDirectMapping pictureMapping = new XMLDirectMapping();<br> | |
* pictureMapping.setAttributeName("picture");<br> | |
* pictureMapping.setXPath("picture/text()");<br> | |
* XMLField pictureField = (XMLField) pictureMapping.getField();<br> | |
* pictureField.setSchemaType(XMLConstants.HEX_BINARY_QNAME);<br> | |
* </code> | |
* | |
* <p><b>Mapping to a Union Field</b>: | |
* | |
* <!-- | |
* <?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="shoe-size" type="size-type"/> | |
* </xsd:sequence> | |
* </xsd:complexType> | |
* <xsd:simpleType name="size-type"> | |
* <xsd:union memberTypes="xsd:decimal xsd:string"/> | |
* </xsd:simpleType> | |
* </xsd:schema> | |
* --> | |
* | |
* <p><em>XML Schema</em><br> | |
* <code> | |
* <?xml version="1.0" encoding="UTF-8"?><br> | |
* <xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema"><br> | |
* <xsd:element name="customer" type="customer-type"/><br> | |
* <xsd:complexType name="customer-type"><br> | |
* <xsd:sequence><br> | |
* <xsd:element name="shoe-size" type="size-type"/><br> | |
* </xsd:sequence><br> | |
* </xsd:complexType><br> | |
* <xsd:simpleType name="size-type"><br> | |
* <xsd:union memberTypes="xsd:decimal xsd:string"/><br> | |
* </xsd:simpleType><br> | |
* </xsd:schema><br> | |
* </code> | |
* | |
* <p><em>Code Sample</em><br> | |
* <code> | |
* XMLDirectMapping shoeSizeMapping = new XMLDirectMapping();<br> | |
* shoeSizeMapping.setAttributeName("shoeSize");<br> | |
* XMLUnionField shoeSizeField = new XMLUnionField();<br> | |
* shoeSizeField.setXPath("shoe-size/text()");<br> | |
* shoeSizeField.addSchemaType(XMLConstants.DECIMAL_QNAME);<br> | |
* shoeSizeField.addSchemaType(XMLConstants.STRING_QNAME);<br> | |
* shoeSizeMapping.setField(shoeSizeField);<br> | |
* </code> | |
* | |
* <p><b>Preserving the Node Type</b>: If the type of a node is not defined in your XML schema, | |
* you can configure an XML Direct Mapping to use the xsi:type attribute to provide type information. | |
* | |
* <p><em>Code Sample</em><br> | |
* <code> | |
* XMLDirectMapping numberMapping = new XMLDirectMapping();<br> | |
* numberMapping.setAttributeName("number");<br> | |
* numberMapping.setXPath("number/text()");<br> | |
* XMLField numberField = (XMLField) numberMapping.getField();<br> | |
* numberField.setIsTypedTextField(true);<br> | |
* </code> | |
* | |
* <p><b>More Information</b>: For more information about using the XML Direct Mapping, see the | |
* "Understanding XML Mappings" chapter of the Oracle TopLink Developer's Guide. | |
* | |
* @since Oracle TopLink 10<i>g</i> Release 2 (10.1.3) | |
*/ | |
public class XMLDirectMapping extends AbstractDirectMapping implements XMLMapping, DirectMapping<AbstractSession, AttributeAccessor, ContainerPolicy, Converter, ClassDescriptor, DatabaseField, XMLMarshaller, Session, XMLUnmarshaller, XMLRecord>, XMLNillableMapping { | |
AbstractNullPolicy nullPolicy; | |
public boolean isCDATA; | |
private boolean isWriteOnly; | |
private boolean isCollapsingStringValues; | |
private boolean isNormalizingStringValues; | |
private boolean isNullValueMarshalled = false; | |
public XMLDirectMapping() { | |
super(); | |
// The default policy is NullPolicy | |
nullPolicy = new NullPolicy(); | |
nullPolicy.setNullRepresentedByEmptyNode(true); | |
isCDATA = false; | |
} | |
/** | |
* Set the AbstractNullPolicy on the mapping<br> | |
* The default policy is NullPolicy.<br> | |
* | |
* @param aNullPolicy | |
*/ | |
public void setNullPolicy(AbstractNullPolicy aNullPolicy) { | |
nullPolicy = aNullPolicy; | |
} | |
/** | |
* INTERNAL: | |
* Get the AbstractNullPolicy from the Mapping.<br> | |
* The default policy is NullPolicy.<br> | |
* @return | |
*/ | |
public AbstractNullPolicy getNullPolicy() { | |
return nullPolicy; | |
} | |
/** | |
* INTERNAL: | |
*/ | |
public boolean isXMLMapping() { | |
return true; | |
} | |
/** | |
* Set the Mapping field name attribute to the given XPath String | |
* @param xpathString String | |
*/ | |
public void setXPath(String xpathString) { | |
if ((xpathString.indexOf(XMLConstants.ATTRIBUTE) == -1) && (!xpathString.endsWith(XMLConstants.TEXT))) { | |
xpathString += '/' + XMLConstants.TEXT; | |
} | |
setField(new XMLField(xpathString)); | |
} | |
public void initialize(AbstractSession session) throws DescriptorException { | |
super.initialize(session); | |
((XMLField)getField()).setIsCDATA(this.isCDATA()); | |
String xpathString = ((XMLField)getField()).getXPath(); | |
if (this.isAbstractDirectMapping() && (xpathString.indexOf(XMLConstants.ATTRIBUTE) == -1) && (!xpathString.endsWith(XMLConstants.TEXT))) { | |
throw DescriptorException.invalidXpathForXMLDirectMapping(this); | |
} | |
} | |
/** | |
* INTERNAL: | |
* Allows for subclasses to convert the attribute value. | |
*/ | |
public Object getAttributeValue(Object fieldValue, AbstractSession session, AbstractUnmarshalRecord record) { | |
// Unmarshal DOM | |
// If attribute is empty string representing (null) then return the nullValue | |
boolean isNullRepresentedByEmptyNode = nullPolicy.isNullRepresentedByEmptyNode(); | |
boolean isNullRepresentedByXsiNil = nullPolicy.isNullRepresentedByXsiNil(); | |
if (isNullRepresentedByEmptyNode && XMLConstants.EMPTY_STRING.equals(fieldValue)) { | |
fieldValue = null; | |
} else if (null == fieldValue && !isNullRepresentedByEmptyNode) { | |
fieldValue = XMLConstants.EMPTY_STRING; | |
} | |
// PERF: Direct variable access. | |
Object attributeValue = fieldValue; | |
// If attribute is absent check the policy | |
if(attributeValue == XMLRecord.noEntry) { | |
if(!getNullPolicy().getIsSetPerformedForAbsentNode()) { | |
return attributeValue; | |
} else { | |
fieldValue = null; | |
attributeValue = null; | |
} | |
} | |
if (attributeValue == XMLRecord.NIL && isNullRepresentedByXsiNil) { | |
fieldValue = null; | |
attributeValue = null; | |
} | |
// Allow for user defined conversion to the object value. | |
if (converter != null) { | |
attributeValue = convertDataValueToObjectValue(attributeValue, session, (XMLUnmarshaller) record.getUnmarshaller()); | |
} else { | |
// PERF: Avoid conversion check when not required. | |
if ((attributeValue == null) || (attributeValue.getClass() != this.attributeObjectClassification)) { | |
try { | |
attributeValue = session.getDatasourcePlatform().convertObject(attributeValue, this.attributeClassification); | |
} catch (ConversionException e) { | |
throw ConversionException.couldNotBeConverted(this, getDescriptor(), e); | |
} | |
} | |
} | |
if (attributeValue == null) {// Translate default null value, conversion may have produced null. | |
attributeValue = this.nullValue; | |
} | |
return attributeValue; | |
} | |
/** | |
* INTERNAL: | |
* Convert the attribute value to a field value. | |
* Process any converter if defined, and check for null values. | |
*/ | |
public Object getFieldValue(Object attributeValue, CoreAbstractSession session, AbstractMarshalRecord record) { | |
// Marshal | |
// PERF: This method is a major performance code point, | |
// so has been micro optimized and uses direct variable access. | |
Object fieldValue = attributeValue; | |
if ((this.nullValue != null) && (this.nullValue.equals(fieldValue)) && !this.isNullValueMarshalled && !((XMLField)field).isRequired()) { | |
return null; | |
} | |
// Allow for user defined conversion to the object value. | |
fieldValue = convertObjectValueToDataValue(fieldValue, (AbstractSession) session, (XMLMarshaller) record.getMarshaller()); | |
if (fieldValue != null) { | |
Class fieldClassification = getFieldClassification(field); | |
// PERF: Avoid conversion if not required. | |
if(fieldClassification != fieldValue.getClass()){ | |
try { | |
fieldValue = session.getPlatform(descriptor.getJavaClass()).convertObject(fieldValue, fieldClassification); | |
} catch (ConversionException exception) { | |
throw ConversionException.couldNotBeConverted(this, descriptor, exception); | |
} | |
} | |
} | |
return fieldValue; | |
} | |
/** | |
* Get the XPath String | |
* @return String the XPath String associated with this Mapping | |
*/ | |
public String getXPath() { | |
return getFieldName(); | |
} | |
/** | |
* INTERNAL: | |
* Return the mapping's attribute value from the row. | |
* The execution session is passed for the case of building a UnitOfWork clone | |
* directly from a row, the session set in the query will not know which platform to use | |
* for converting the value. Allows the correct session to be passed in. | |
*/ | |
public Object valueFromRow(AbstractRecord row, JoinedAttributeManager joinManager, ObjectBuildingQuery query, CacheKey cacheKey, AbstractSession executionSession, boolean isTargetProtected, Boolean[] wasCacheUsed) { | |
// PERF: Direct variable access. | |
boolean shouldCheckForXsiNil = getNullPolicy().isNullRepresentedByXsiNil(); | |
return getAttributeValue(((DOMRecord)row).getIndicatingNoEntry(this.field, false, shouldCheckForXsiNil), executionSession, (XMLRecord)row); | |
} | |
/** | |
* INTERNAL: | |
* Get a value from the object and set that in the respective field of the row. | |
*/ | |
@Override | |
public void writeFromObjectIntoRow(Object object, AbstractRecord row, AbstractSession session, WriteType writeType) { | |
if (isReadOnly()) { | |
return; | |
} | |
writeSingleValue(getAttributeValueFromObject(object), object, (XMLRecord)row, session); | |
} | |
protected void writeValueIntoRow(AbstractRecord row, DatabaseField aField, Object fieldValue) { | |
row.put(getField(), fieldValue); | |
} | |
public void writeSingleValue(Object value, Object parent, XMLRecord row, AbstractSession session) { | |
Object fieldValue = getFieldValue(value, session, row); | |
if(fieldValue == null && getNullPolicy() != null) { | |
getNullPolicy().directMarshal((Field) this.getField(), row, parent); | |
} else { | |
writeValueIntoRow(row, getField(), fieldValue); | |
} | |
} | |
public void setAttributeValueInObject(Object object, Object value) throws DescriptorException { | |
// PERF: Direct variable access. | |
if(isWriteOnly()) { | |
return; | |
} | |
try { | |
if(value == XMLRecord.noEntry) { | |
return; | |
} | |
if (value != null && value instanceof String) { | |
if(isCollapsingStringValues) { | |
value = XMLConversionManager.getDefaultXMLManager().collapseStringValue((String)value); | |
} else if(isNormalizingStringValues) { | |
value = XMLConversionManager.getDefaultXMLManager().normalizeStringValue((String)value); | |
} | |
} | |
this.attributeAccessor.setAttributeValueInObject(object, value); | |
} catch (DescriptorException exception) { | |
exception.setMapping(this); | |
throw exception; | |
} | |
} | |
public void setIsCDATA(boolean CDATA) { | |
isCDATA = CDATA; | |
} | |
public boolean isCDATA() { | |
return isCDATA; | |
} | |
public boolean isWriteOnly() { | |
return this.isWriteOnly; | |
} | |
public void setIsWriteOnly(boolean b) { | |
this.isWriteOnly = b; | |
} | |
@Override | |
public boolean isCloningRequired() { | |
return false; | |
} | |
public void preInitialize(AbstractSession session) throws DescriptorException { | |
getAttributeAccessor().setIsWriteOnly(this.isWriteOnly()); | |
getAttributeAccessor().setIsReadOnly(this.isReadOnly()); | |
super.preInitialize(session); | |
} | |
/** | |
* PUBLIC: | |
* Returns true if this mapping is normalizing string values on unmarshal before setting | |
* them in the object. Normalize replaces any CR, LF or Tab characters with a | |
* single space character. | |
*/ | |
public boolean isNormalizingStringValues() { | |
return this.isNormalizingStringValues; | |
} | |
/** | |
* PUBLIC: | |
* Indicates that this mapping should normalize all string values before setting them | |
* in the object on unmarshal. Normalize replaces any CR, LF or Tab characters with a | |
* single space character. | |
* @param normalize | |
*/ | |
public void setNormalizingStringValues(boolean normalize) { | |
this.isNormalizingStringValues = normalize; | |
} | |
/** | |
* PUBLIC: | |
* Indicates that this mapping should collapse all string values before setting them | |
* in the object on unmarshal. Collapse removes leading and trailing whitespaces, and replaces | |
* any sequence of whitepsace characters with a single space. | |
* @param collapse | |
*/ | |
public void setCollapsingStringValues(boolean collapse) { | |
this.isCollapsingStringValues = collapse; | |
} | |
/** | |
* PUBLIC: | |
* Returns true if this mapping should collapse all string values before setting them | |
* in the object on unmarshal. Collapse removes leading and trailing whitespaces, and replaces | |
* any sequence of whitepsace characters with a single space. | |
*/ | |
public boolean isCollapsingStringValues() { | |
return this.isCollapsingStringValues; | |
} | |
/** | |
* PUBLIC: | |
* Returns true if this mapping's value should be marshalled, in the case that | |
* it is equal to the default null value. | |
*/ | |
public boolean isNullValueMarshalled() { | |
return this.isNullValueMarshalled; | |
} | |
/** | |
* PUBLIC: | |
* Set whether this mapping's value should be marshalled, in the case that | |
* it is equal to the default null value. | |
*/ | |
public void setNullValueMarshalled(boolean value) { | |
this.isNullValueMarshalled = value; | |
} | |
/** | |
* INTERNAL | |
* @since EclipseLink 2.5.0 | |
*/ | |
public Object convertObjectValueToDataValue(Object value, Session session, XMLMarshaller marshaller) { | |
if (hasConverter()) { | |
if (converter instanceof XMLConverter) { | |
return ((XMLConverter)converter).convertObjectValueToDataValue(value, session, marshaller); | |
} else { | |
return converter.convertObjectValueToDataValue(value, session); | |
} | |
} | |
return value; | |
} | |
/** | |
* INTERNAL | |
* @since EclipseLink 2.5.0 | |
*/ | |
public Object convertDataValueToObjectValue(Object fieldValue, Session session, XMLUnmarshaller unmarshaller) { | |
if (hasConverter()) { | |
if (converter instanceof XMLConverter) { | |
return ((XMLConverter)converter).convertDataValueToObjectValue(fieldValue, session, unmarshaller); | |
} else { | |
return converter.convertDataValueToObjectValue(fieldValue, session); | |
} | |
} | |
return fieldValue; | |
} | |
} |