org.eclipse.persistence.oxm.mappings.XMLCompositeObjectMapping Maven / Gradle / Ivy
Show all versions of eclipselink Show documentation
/*
* Copyright (c) 1998, 2024 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.mappings;
import org.eclipse.persistence.descriptors.ClassDescriptor;
import org.eclipse.persistence.exceptions.DatabaseException;
import org.eclipse.persistence.exceptions.DescriptorException;
import org.eclipse.persistence.exceptions.XMLMarshalException;
import org.eclipse.persistence.internal.descriptors.ObjectBuilder;
import org.eclipse.persistence.internal.helper.DatabaseField;
import org.eclipse.persistence.internal.identitymaps.CacheKey;
import org.eclipse.persistence.internal.oxm.ConversionManager;
import org.eclipse.persistence.internal.oxm.XMLObjectBuilder;
import org.eclipse.persistence.internal.oxm.XPathEngine;
import org.eclipse.persistence.internal.oxm.XPathFragment;
import org.eclipse.persistence.internal.oxm.XPathQName;
import org.eclipse.persistence.internal.oxm.mappings.CompositeObjectMapping;
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.mappings.AttributeAccessor;
import org.eclipse.persistence.mappings.converters.Converter;
import org.eclipse.persistence.mappings.foundation.AbstractCompositeObjectMapping;
import org.eclipse.persistence.oxm.XMLConstants;
import org.eclipse.persistence.oxm.XMLContext;
import org.eclipse.persistence.oxm.XMLDescriptor;
import org.eclipse.persistence.oxm.XMLField;
import org.eclipse.persistence.oxm.XMLMarshaller;
import org.eclipse.persistence.oxm.XMLUnmarshaller;
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.platform.xml.XMLPlatformFactory;
import org.eclipse.persistence.queries.ObjectBuildingQuery;
import org.eclipse.persistence.sessions.Session;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import javax.xml.namespace.QName;
import java.lang.reflect.Modifier;
/**
* Composite object XML mappings represent a relationship between two classes. In XML, the "owned"
* class may be nested with the element tag representing the "owning" class. This mapping is, by
* definition, privately owned.
*
*
Composite object XML mappings can be used in the following scenarios:
* - Mapping into the Parent Record
* - Mapping to an Element
* - Mapping to Different Elements by Element Name
* - Mapping to Different Elements by Element Position
*
*
* Setting the XPath: 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 path and positional information; the last node in the XPath forms the local
* root node for the composite object. The XPath is specified on the mapping using the setXPath
* method.
*
*
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
*
*
* .
* Indicates "self".
*
*
* phone-number
* The phone-number information is stored in the phone-number element.
*
*
* contact-info/phone-number
* The XPath statement may be used to specify any valid path.
*
*
* phone-number[2]
* The XPath statement may contain positional information. In this case the phone-number
* information is stored in the second occurrence of the phone-number element.
*
*
*
* Mapping into the Parent Record: The composite object may be mapped into the parent
* record in a corresponding XML document.
*
*
*
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="first-name" type="xsd:string"/>
* <xsd:element name="last-name" type="xsd:string"/>
* <xsd:element name="street" type="xsd:string"/>
* <xsd:element name="city" type="xsd:string"/>
* </xsd:sequence>
* </xsd:complexType>
* </xsd:schema>
*
*
*
Code Sample
*
* XMLCompositeObjectMapping addressMapping = new XMLCompositeObjectMapping();
* addressMapping.setAttributeName("address");
* addressMapping.setXPath(".");
* addressMapping.setReferenceClass(Address.class);
*
*
*
Mapping to an Element: The composite object may be mapped to an element in a corresponding
* XML document.
*
*
*
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="first-name" type="xsd:string"/>
* <xsd:element name="last-name" type="xsd:string"/>
* <xsd:element name="address">
* <xsd:complexType>
* <xsd:sequence>
* <xsd:element name="street" type="xsd:string"/>
* <xsd:element name="city" type="xsd:string"/>
* </xsd:sequence>
* </xsd:complexType>
* </xsd:element>
* </xsd:sequence>
* </xsd:complexType>
* </xsd:schema>
*
*
*
Code Sample
*
* XMLCompositeObjectMapping addressMapping = new XMLCompositeObjectMapping();
* addressMapping.setAttributeName("address");
* addressMapping.setXPath("address");
* addressMapping.setReferenceClass(Address.class);
*
*
*
More Information: For more information about using the XML Composite Object Mapping, see the
* "Understanding XML Mappings" chapter of the Oracle TopLink Developer's Guide.
*
* @since Oracle TopLink 10g Release 2 (10.1.3)
*/
public class XMLCompositeObjectMapping extends AbstractCompositeObjectMapping implements XMLMapping, CompositeObjectMapping, XMLNillableMapping {
AbstractNullPolicy nullPolicy;
private XMLInverseReferenceMapping inverseReferenceMapping;
private UnmarshalKeepAsElementPolicy keepAsElementPolicy;
private boolean isWriteOnly;
public XMLCompositeObjectMapping() {
super();
// The default policy is NullPolicy
nullPolicy = new NullPolicy();
}
/**
* Gets the AttributeAccessor that is used to get and set the value of the
* container on the target object.
* @deprecated Replaced by getInverseReferenceMapping().getAttributeAccessor()
*/
@Deprecated
public AttributeAccessor getContainerAccessor() {
if (this.inverseReferenceMapping == null) {
return null;
}
return this.inverseReferenceMapping.getAttributeAccessor();
}
/**
* Sets the AttributeAccessor that is used to get and set the value of the
* container on the target object.
*
* @param anAttributeAccessor - the accessor to be used.
* @deprecated Replaced by getInverseReferenceMapping().setAttributeAccessor()
*/
@Deprecated
public void setContainerAccessor(AttributeAccessor anAttributeAccessor) {
if (this.inverseReferenceMapping == null) {
return;
}
this.inverseReferenceMapping.setAttributeAccessor(anAttributeAccessor);
}
/**
* Sets the name of the backpointer attribute on the target object. Used to
* populate the backpointer. If the specified attribute doesn't exist on
* the reference class of this mapping, a DescriptorException will be thrown
* during initialize.
*
* @param attributeName - the name of the backpointer attribute to be populated
* @deprecated Replaced by getInverseReferenceMapping().setAttributeName()
*/
@Deprecated
public void setContainerAttributeName(String attributeName) {
if (this.inverseReferenceMapping == null) {
return;
}
this.inverseReferenceMapping.setAttributeName(attributeName);
}
/**
* Gets the name of the backpointer attribute on the target object. Used to
* populate the backpointer.
* @deprecated Replaced by getInverseReferenceMapping().getAttributeName()
*/
@Deprecated
public String getContainerAttributeName() {
if (this.inverseReferenceMapping == null) {
return null;
}
return this.inverseReferenceMapping.getAttributeName();
}
/**
* Sets the method name to be used when accessing the value of the back pointer
* on the target object of this mapping. If the specified method doesn't exist
* on the reference class of this mapping, a DescriptorException will be thrown
* during initialize.
*
* @param methodName - the getter method to be used.
* @deprecated Replaced by getInverseReferenceMapping().setGetMethodName()
*/
@Deprecated
public void setContainerGetMethodName(String methodName) {
if (this.inverseReferenceMapping == null) {
return;
}
this.inverseReferenceMapping.setGetMethodName(methodName);
}
/**
* Sets the name of the method to be used when setting the value of the back pointer
* on the target object of this mapping. If the specified method doesn't exist
* on the reference class of this mapping, a DescriptorException will be thrown
* during initialize.
*
* @param methodName - the setter method to be used.
* @deprecated Replaced by getInverseReferenceMapping().setSetMethodName()
*/
@Deprecated
public void setContainerSetMethodName(String methodName) {
if (this.inverseReferenceMapping == null) {
return;
}
this.inverseReferenceMapping.setSetMethodName(methodName);
}
/**
* Gets the name of the method to be used when accessing the value of the
* back pointer on the target object of this mapping.
* @deprecated Replaced by getInverseReferenceMapping().getGetMethodName()
*/
@Deprecated
public String getContainerGetMethodName() {
if (this.inverseReferenceMapping == null) {
return null;
}
return this.inverseReferenceMapping.getGetMethodName();
}
/**
* Gets the name of the method to be used when setting the value of the
* back pointer on the target object of this mapping.
* @deprecated Replaced by getInverseReferenceMapping().getSetMethodName()
*/
@Deprecated
public String getContainerSetMethodName() {
if (this.inverseReferenceMapping == null) {
return null;
}
return this.inverseReferenceMapping.getSetMethodName();
}
@Override
public void convertClassNamesToClasses(ClassLoader classLoader){
if(XMLConstants.UNKNOWN_OR_TRANSIENT_CLASS.equals(referenceClassName)){
return;
}
super.convertClassNamesToClasses(classLoader);
}
/**
* INTERNAL:
* The mapping is initialized with the given session. This mapping is fully initialized
* after this.
*/
@Override
public void initialize(AbstractSession session) throws DescriptorException {
//modified so that reference class on composite mappings is no longer mandatory
String referenceClassName = getReferenceClassName();
if ((this.referenceClass == null) && (referenceClassName != null)) {
if (!referenceClassName.equals(XMLConstants.UNKNOWN_OR_TRANSIENT_CLASS)) {
setReferenceClass(session.getDatasourcePlatform().getConversionManager().convertClassNameToClass(referenceClassName));
}
}
initializeReferenceDescriptorAndField(session);
if(null != getContainerAccessor()) {
getContainerAccessor().initializeAttributes(this.referenceClass);
}
}
protected void initializeReferenceDescriptorAndField(AbstractSession session){
if (this.referenceClass != null) {
super.initialize(session);
} else {
//below should be the same as AbstractCompositeObjectMapping.initialize
if (this.field == null) {
throw DescriptorException.fieldNameNotSetInMapping(this);
}
setField(getDescriptor().buildField(this.field));
setFields(collectFields());
// initialize the converter - if necessary
if (hasConverter()) {
getConverter().initialize(this, session);
}
}
}
/**
* Set the AbstractNullPolicy on the mapping
* The default policy is NullPolicy.
*
*/
@Override
public void setNullPolicy(AbstractNullPolicy aNullPolicy) {
nullPolicy = aNullPolicy;
}
/**
* INTERNAL:
* Get the AbstractNullPolicy from the Mapping.
* The default policy is NullPolicy.
*/
@Override
public AbstractNullPolicy getNullPolicy() {
return nullPolicy;
}
/**
* INTERNAL:
*/
@Override
public boolean isXMLMapping() {
return true;
}
/**
* Get the XPath String
* @return String the XPath String associated with this Mapping
*/
public String getXPath() {
return getField().getName();
}
/**
* Set the Mapping field name attribute to the given XPath String
* @param xpathString String
*/
@Override
public void setXPath(String xpathString) {
this.setField(new XMLField(xpathString));
}
@Override
protected Object buildCompositeRow(Object attributeValue, AbstractSession session, AbstractRecord databaseRow, WriteType writeType) {
XMLDescriptor xmlReferenceDescriptor = null;
try{
xmlReferenceDescriptor = (XMLDescriptor) getReferenceDescriptor(attributeValue, session);
}catch(Exception e){
//do nothing
}
XMLField xmlFld = (XMLField) getField();
if (xmlFld.hasLastXPathFragment() && xmlFld.getLastXPathFragment().hasLeafElementType()) {
XMLRecord xmlRec = (XMLRecord) databaseRow;
xmlRec.setLeafElementType(xmlFld.getLastXPathFragment().getLeafElementType());
}
XMLRecord parent = (XMLRecord) databaseRow;
if (xmlReferenceDescriptor != null) {
return buildCompositeRowForDescriptor(xmlReferenceDescriptor, attributeValue, session, parent, writeType);
} else {
if (attributeValue instanceof Element && getKeepAsElementPolicy() == UnmarshalKeepAsElementPolicy.KEEP_UNKNOWN_AS_ELEMENT) {
return new DOMRecord((Element) attributeValue);
}else{
Node newNode = XPathEngine.getInstance().create((XMLField)getField(), parent.getDOM(), attributeValue, session);
DOMRecord newRow = new DOMRecord(newNode);
return newRow;
}
}
}
protected AbstractRecord buildCompositeRowForDescriptor(ClassDescriptor classDesc, Object attributeValue, AbstractSession session, XMLRecord parentRow, WriteType writeType) {
XMLObjectBuilder objectBuilder = (XMLObjectBuilder) classDesc.getObjectBuilder();
XMLRecord child = (XMLRecord) objectBuilder.createRecordFor(attributeValue, (XMLField) getField(), parentRow, this);
child.setNamespaceResolver(parentRow.getNamespaceResolver());
child.setSession(session);
objectBuilder.buildIntoNestedRow(child, attributeValue, session, (XMLDescriptor)getReferenceDescriptor(), (XMLField) getField());
return child;
}
@Override
protected Object buildCompositeObject(ObjectBuilder objectBuilder, AbstractRecord nestedRow, ObjectBuildingQuery query, CacheKey parentCacheKey, JoinedAttributeManager joinManager, AbstractSession targetSession) {
return objectBuilder.buildObject(query, nestedRow, joinManager);
}
@Override
public Object readFromRowIntoObject(AbstractRecord databaseRow, JoinedAttributeManager joinManager, Object targetObject, CacheKey parentCacheKey, ObjectBuildingQuery sourceQuery, AbstractSession executionSession, boolean isTargetProtected) throws DatabaseException {
Object fieldValue = databaseRow.getIndicatingNoEntry(getField());
// 20071002: noEntry ineffective as a check for an absent node, empty nodes are DOMRecords, absent nodes are null)
// if(fieldValue == AbstractRecord.noEntry && !getNullPolicy().getIsSetPerformedForAbsentNode()) {
// Do not perform a set for an absent node
// return null;
// } else {
// Check for absent nodes based on policy flag
if ((null == fieldValue) || fieldValue instanceof String) {
if (getNullPolicy().getIsSetPerformedForAbsentNode()) {
setAttributeValueInObject(targetObject, null);
} else {
return null;
}
return null;
}
// }
// Empty or xsi:nil nodes (non-absent) will arrive here along with populated nodes
XMLRecord nestedRow = (XMLRecord) this.getDescriptor().buildNestedRowFromFieldValue(fieldValue);
// Check the policy to see if this DOM empty/xsi:nil or filled record represents null
if (getNullPolicy().valueIsNull((Element) nestedRow.getDOM())) {
setAttributeValueInObject(targetObject, null);
return null;
}
Object attributeValue = valueFromRow(fieldValue, nestedRow, joinManager, sourceQuery, executionSession, isTargetProtected);
setAttributeValueInObject(targetObject, attributeValue);
if(null != getContainerAccessor()) {
if(this.inverseReferenceMapping.getContainerPolicy() == null) {
getContainerAccessor().setAttributeValueInObject(attributeValue, targetObject);
} else {
Object backpointerContainer = getContainerAccessor().getAttributeValueFromObject(attributeValue);
if(backpointerContainer == null) {
backpointerContainer = this.inverseReferenceMapping.getContainerPolicy().containerInstance();
getContainerAccessor().setAttributeValueInObject(attributeValue, backpointerContainer);
}
this.inverseReferenceMapping.getContainerPolicy().addInto(targetObject, backpointerContainer, executionSession);
}
}
return attributeValue;
}
public Object valueFromRow(Object fieldValue, XMLRecord nestedRow, JoinedAttributeManager joinManager, ObjectBuildingQuery sourceQuery, AbstractSession executionSession, boolean isTargetProtected) throws DatabaseException {
// pretty sure we can ignore inheritance here:
Object toReturn = null;
// Use local descriptor - not the instance variable on DatabaseMapping
ClassDescriptor aDescriptor = getReferenceDescriptor((DOMRecord) nestedRow);
if (aDescriptor == null) {
if ((getKeepAsElementPolicy() == UnmarshalKeepAsElementPolicy.KEEP_UNKNOWN_AS_ELEMENT) || (getKeepAsElementPolicy() == UnmarshalKeepAsElementPolicy.KEEP_ALL_AS_ELEMENT)) {
XMLPlatformFactory.getInstance().getXMLPlatform().namespaceQualifyFragment((Element) nestedRow.getDOM());
toReturn = nestedRow.getDOM();
toReturn = convertDataValueToObjectValue(toReturn, executionSession, nestedRow.getUnmarshaller());
//try simple case
toReturn = convertToSimpleTypeIfPresent(toReturn, nestedRow, executionSession);
return toReturn;
} else {
NodeList children = nestedRow.getDOM().getChildNodes();
for(int i=0, childrenLength=children.getLength(); i classValue = aDescriptor.getInheritancePolicy().classFromRow(nestedRow, executionSession);
if (classValue == null) {
// no xsi:type attribute - look for type indicator on the field
QName leafElementType = ((XMLField) getField()).getLeafElementType();
if (leafElementType != null) {
XPathQName leafElementXPathQName = new XPathQName(leafElementType, nestedRow.isNamespaceAware());
Object indicator = aDescriptor.getInheritancePolicy().getClassIndicatorMapping().get(leafElementXPathQName);
if(indicator != null) {
classValue = (Class) indicator;
}
}
}
if (classValue != null) {
aDescriptor = this.getReferenceDescriptor(classValue, executionSession);
} else {
// since there is no xsi:type attribute or leaf element type set,
// use the reference descriptor - make sure it is non-abstract
if (Modifier.isAbstract(aDescriptor.getJavaClass().getModifiers())) {
// throw an exception
throw DescriptorException.missingClassIndicatorField((org.eclipse.persistence.internal.oxm.record.XMLRecord) nestedRow, aDescriptor.getInheritancePolicy().getDescriptor());
}
}
}
ObjectBuilder objectBuilder = aDescriptor.getObjectBuilder();
toReturn = buildCompositeObject(objectBuilder, nestedRow, sourceQuery, null, joinManager, executionSession);
}
if (getConverter() != null) {
if (getConverter() instanceof XMLConverter) {
toReturn = ((XMLConverter) getConverter()).convertDataValueToObjectValue(toReturn, executionSession, nestedRow.getUnmarshaller());
} else {
toReturn = getConverter().convertDataValueToObjectValue(toReturn, executionSession);
}
}
return toReturn;
}
private Object convertToSimpleTypeIfPresent(Object toReturn, XMLRecord nestedRow, AbstractSession executionSession){
Node textchild = nestedRow.getDOM().getFirstChild();
String stringValue = null;
if ((textchild != null) && (textchild.getNodeType() == Node.TEXT_NODE)) {
stringValue = textchild.getNodeValue();
if(stringValue != null && getKeepAsElementPolicy() != UnmarshalKeepAsElementPolicy.KEEP_UNKNOWN_AS_ELEMENT && getKeepAsElementPolicy()!=UnmarshalKeepAsElementPolicy.KEEP_ALL_AS_ELEMENT){
toReturn = stringValue;
}
}
if ((stringValue == null) || stringValue.isEmpty()) {
return toReturn;
}
String type = ((Element) nestedRow.getDOM()).getAttributeNS(javax.xml.XMLConstants.W3C_XML_SCHEMA_INSTANCE_NS_URI, XMLConstants.SCHEMA_TYPE_ATTRIBUTE);
if ((null != type) && !type.isEmpty()) {
XPathFragment typeFragment = new XPathFragment(type);
String namespaceURI = nestedRow.resolveNamespacePrefix(typeFragment.getPrefix());
QName schemaTypeQName = new QName(namespaceURI, typeFragment.getLocalName());
ConversionManager conversionManager = (ConversionManager) executionSession.getDatasourcePlatform().getConversionManager();
Class