blob: 17abadf89e11191d480a49d151a7c5e36d03e78d [file] [log] [blame]
/*
* 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.mappings;
import java.util.*;
import javax.xml.namespace.QName;
import org.eclipse.persistence.descriptors.ClassDescriptor;
import org.eclipse.persistence.exceptions.ConversionException;
import org.eclipse.persistence.exceptions.DatabaseException;
import org.eclipse.persistence.exceptions.DescriptorException;
import org.eclipse.persistence.internal.descriptors.ObjectBuilder;
import org.eclipse.persistence.internal.helper.ClassConstants;
import org.eclipse.persistence.internal.helper.DatabaseField;
import org.eclipse.persistence.internal.identitymaps.CacheId;
import org.eclipse.persistence.internal.identitymaps.CacheKey;
import org.eclipse.persistence.internal.oxm.ConversionManager;
import org.eclipse.persistence.internal.oxm.Reference;
import org.eclipse.persistence.internal.oxm.ReferenceResolver;
import org.eclipse.persistence.internal.oxm.XMLConversionManager;
import org.eclipse.persistence.internal.oxm.mappings.ObjectReferenceMapping;
import org.eclipse.persistence.internal.oxm.record.UnmarshalRecord;
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.internal.sessions.UnitOfWorkImpl;
import org.eclipse.persistence.mappings.AggregateMapping;
import org.eclipse.persistence.mappings.AttributeAccessor;
import org.eclipse.persistence.oxm.XMLDescriptor;
import org.eclipse.persistence.oxm.XMLField;
import org.eclipse.persistence.oxm.XMLUnionField;
import org.eclipse.persistence.oxm.record.DOMRecord;
import org.eclipse.persistence.oxm.record.XMLRecord;
import org.eclipse.persistence.queries.ObjectBuildingQuery;
/**
* TopLink OXM version of a 1-1 mapping. A list of source-target key field
* associations is used to link the source xpaths to their related target xpaths,
* and hence their primary key (unique identifier) values used when (un)marshalling.
* This mapping has a Vector of XMLFields as opposed to a single XMLField.
*
* It is important to note that each target xpath is assumed to be set as a primary
* key field on the target (reference) class descriptor - this is necessary in order
* to locate the correct target object instance in the session cache when resolving
* mapping references.
*/
public class XMLObjectReferenceMapping extends AggregateMapping implements ObjectReferenceMapping<AbstractSession, AttributeAccessor, ContainerPolicy, ClassDescriptor, DatabaseField, UnmarshalRecord, XMLField, XMLRecord>, XMLMapping {
protected HashMap sourceToTargetKeyFieldAssociations;
protected Vector sourceToTargetKeys; // maintain the order of the keys
private boolean isWriteOnly;
private XMLInverseReferenceMapping inverseReferenceMapping;
/**
* PUBLIC:
* The default constructor initializes the sourceToTargetKeyFieldAssociations
* and sourceToTargetKeys data structures.
*/
public XMLObjectReferenceMapping() {
sourceToTargetKeyFieldAssociations = new HashMap();
sourceToTargetKeys = new Vector();
}
/**
* PUBLIC:
* Add a source-target xpath pair to the map.
*
*/
@Override
public void addSourceToTargetKeyFieldAssociation(String srcXPath, String tgtXPath) {
XMLField srcFld = new XMLField(srcXPath);
sourceToTargetKeys.add(srcFld);
if(null == tgtXPath) {
sourceToTargetKeyFieldAssociations.put(srcFld, null);
} else {
sourceToTargetKeyFieldAssociations.put(srcFld, new XMLField(tgtXPath));
}
}
public void addSourceToTargetKeyFieldAssociation(XMLField srcField, XMLField tgtField) {
sourceToTargetKeys.add(srcField);
sourceToTargetKeyFieldAssociations.put(srcField, tgtField);
}
/**
* INTERNAL:
* Retrieve the target object's primary key value that is mapped to a given
* source xpath (in the source-target key field association list).
*
* @return null if the target object is null, the reference class is null, or
* a primary key field name does not exist on the reference descriptor that
* matches the target field name - otherwise, return the associated primary
* key value
*/
@Override
public Object buildFieldValue(Object targetObject, XMLField xmlFld, AbstractSession session) {
if (targetObject == null || getReferenceClass() == null) {
return null;
}
ClassDescriptor descriptor = referenceDescriptor;
if(null == descriptor) {
descriptor = session.getClassDescriptor(targetObject);
}
ObjectBuilder objectBuilder = descriptor.getObjectBuilder();
Object primaryKey = objectBuilder.extractPrimaryKeyFromObject(targetObject, session);
int idx = 0;
if(!(null == referenceClass || ClassConstants.OBJECT == getReferenceClass())) {
idx = descriptor.getPrimaryKeyFields().indexOf(getSourceToTargetKeyFieldAssociations().get(xmlFld));
if (idx == -1) {
return null;
}
}
if (primaryKey instanceof CacheId) {
return ((CacheId)primaryKey).getPrimaryKey()[idx];
} else {
return primaryKey;
}
}
/**
* INTERNAL:
* Create (if necessary) and populate a reference object that will be used
* during the mapping reference resolution phase after unmarshalling is
* complete.
*
*/
@Override
public void buildReference(UnmarshalRecord record, XMLField xmlField, Object object, AbstractSession session) {
ReferenceResolver resolver = record.getReferenceResolver();
if (resolver == null) {
return;
}
Object srcObject = record.getCurrentObject();
// the order in which the primary keys are added to the vector is
// relevant for cache lookup - it must match the ordering of the
// reference descriptor's primary key entries
Reference reference = resolver.getReference(this, srcObject);
CacheId primaryKeys;
if(null == referenceClass || ClassConstants.OBJECT == referenceClass) {
if (reference == null) {
// if reference is null, create a new instance and set it on the resolver
primaryKeys = new CacheId(new Object[1]);
reference = new Reference(this, srcObject, referenceClass, primaryKeys);
resolver.addReference(reference);
record.reference(reference);
} else {
primaryKeys = (CacheId) reference.getPrimaryKey();
}
primaryKeys.set(0, object);
} else {
Vector pkFieldNames = referenceDescriptor.getPrimaryKeyFieldNames();
// if reference is null, create a new instance and set it on the resolver
if (reference == null) {
primaryKeys = new CacheId(new Object[pkFieldNames.size()]);
reference = new Reference(this, srcObject, referenceClass, primaryKeys);
resolver.addReference(reference);
record.reference(reference);
} else {
primaryKeys = (CacheId) reference.getPrimaryKey();
}
XMLField tgtFld = (XMLField) getSourceToTargetKeyFieldAssociations().get(xmlField);
int idx = pkFieldNames.indexOf(tgtFld.getQualifiedName());
// fix for bug# 5687430
// need to get the actual type of the target (i.e. int, String, etc.)
// and use the converted value when checking the cache.
Object value = session.getDatasourcePlatform().getConversionManager().convertObject(object, referenceDescriptor.getTypedField(tgtFld).getType());
if (value != null) {
primaryKeys.set(idx, value);
}
}
}
/**
* INTERNAL:
* Cascade perform delete through mappings that require the cascade
*/
@Override
public void cascadePerformRemoveIfRequired(Object object, UnitOfWorkImpl uow, Map visitedObjects) {
// objects referenced by this mapping are not registered as they have
// no identity, however mappings from the referenced object may need cascading.
Object objectReferenced = getRealAttributeValueFromObject(object, uow);
if (objectReferenced == null) {
return;
}
if (!visitedObjects.containsKey(objectReferenced)) {
visitedObjects.put(objectReferenced, objectReferenced);
ObjectBuilder builder = getReferenceDescriptor(objectReferenced.getClass(), uow).getObjectBuilder();
builder.cascadePerformRemove(objectReferenced, uow, visitedObjects);
}
}
/**
* INTERNAL:
* Cascade registerNew for Create through mappings that require the cascade
*/
@Override
public void cascadeRegisterNewIfRequired(Object object, UnitOfWorkImpl uow, Map visitedObjects) {
// Aggregate objects are not registered but their mappings should be.
Object objectReferenced = getRealAttributeValueFromObject(object, uow);
if (objectReferenced == null) {
return;
}
if (!visitedObjects.containsKey(objectReferenced)) {
visitedObjects.put(objectReferenced, objectReferenced);
ObjectBuilder builder = getReferenceDescriptor(objectReferenced.getClass(), uow).getObjectBuilder();
builder.cascadeRegisterNewForCreate(objectReferenced, uow, visitedObjects);
}
}
/**
* INTERNAL:
* Return a list of XMLFields based on the source XPath values
* in the source-target key field associations list.
*/
@Override
public Vector getFields() {
return sourceToTargetKeys;
}
/**
* Return a QName representation the schema type for a given XMLField, if
* applicable.
*
* Note: This method performs the same functionality as 'getSchemaType' in
* org.eclipse.persistence.internal.oxm.XMLSimpleMappingNodeValue.
*
*/
protected QName getSchemaType(XMLField xmlField, Object value, AbstractSession session) {
QName schemaType = null;
if (xmlField.isTypedTextField()) {
ConversionManager conversionManager = (ConversionManager) session.getDatasourcePlatform().getConversionManager();
schemaType = xmlField.getXMLType(value.getClass(), conversionManager);
} else if (xmlField.isUnionField()) {
return getSingleValueToWriteForUnion((XMLUnionField) xmlField, value, session);
} else if (xmlField.getSchemaType() != null) {
schemaType = xmlField.getSchemaType();
}
return schemaType;
}
/**
* Return a single QName representation for a given XMLUnionField, if applicable.
*
* Note: This method performs the same functionality as 'getSingleValueToWriteForUnion'
* in org.eclipse.persistence.internal.oxm.XMLSimpleMappingNodeValue.
*
*/
protected QName getSingleValueToWriteForUnion(XMLUnionField xmlField, Object value, AbstractSession session) {
ArrayList schemaTypes = xmlField.getSchemaTypes();
QName schemaType = null;
QName nextQName;
Class javaClass;
for (int i = 0; i < schemaTypes.size(); i++) {
nextQName = (QName) (xmlField).getSchemaTypes().get(i);
try {
if (nextQName != null) {
ConversionManager conversionManager = (ConversionManager) session.getDatasourcePlatform().getConversionManager();
javaClass = xmlField.getJavaClass(nextQName, conversionManager);
conversionManager.convertObject(value, javaClass, nextQName);
schemaType = nextQName;
break;
}
} catch (ConversionException ce) {
if (i == (schemaTypes.size() - 1)) {
schemaType = nextQName;
}
}
}
return schemaType;
}
/**
* INTERNAL:
* Return a list of source-target xmlfield pairs.
*
*/
@Override
public HashMap getSourceToTargetKeyFieldAssociations() {
return sourceToTargetKeyFieldAssociations;
}
/**
* Return a string representation of a given value, based on a given schema type.
*
* Note: This method performs the same functionality as 'getValueToWrite'
* in org.eclipse.persistence.internal.oxm.XMLSimpleMappingNodeValue.
*
*/
protected String getValueToWrite(QName schemaType, Object value, AbstractSession session) {
return (String) ((XMLConversionManager) session.getDatasourcePlatform().getConversionManager()).convertObject(value, ClassConstants.STRING, schemaType);
}
/**
* INTERNAL:
* Register a ReferenceResolver as an event listener on the session,
* if one doesn't already exist. Each source/target field will have
* a namespace resolver set as well.
*
* @see org.eclipse.persistence.internal.oxm.ReferenceResolver
* @see org.eclipse.persistence.oxm.NamespaceResolver
*/
@Override
public void initialize(AbstractSession session) throws DescriptorException {
if (null == referenceClass) {
if(referenceClassName != null){
setReferenceClass(session.getDatasourcePlatform().getConversionManager().convertClassNameToClass(referenceClassName));
}
}
if(!(null == referenceClass || referenceClass == ClassConstants.OBJECT)) {
super.initialize(session);
}
// iterate over each source & target XMLField and set the
// appropriate namespace resolver
XMLDescriptor descriptor = (XMLDescriptor) this.getDescriptor();
XMLDescriptor targetDescriptor = (XMLDescriptor) getReferenceDescriptor();
for (int index = 0; index < sourceToTargetKeys.size(); index++) {
XMLField sourceField = (XMLField) sourceToTargetKeys.get(index);
XMLField targetField = (XMLField) sourceToTargetKeyFieldAssociations.remove(sourceField);
sourceField = (XMLField) descriptor.buildField(sourceField);
sourceToTargetKeys.set(index, sourceField);
if(null != targetField) {
if(null == targetDescriptor) {
throw DescriptorException.referenceClassNotSpecified(this);
}
//primary key field from ref desc
List<DatabaseField > pkFields = targetDescriptor.getPrimaryKeyFields();
for(int i=0; i<pkFields.size(); i++){
XMLField nextPKField = (XMLField)pkFields.get(i);
if(targetField.equals(nextPKField)){
targetField = (XMLField) targetDescriptor.buildField(nextPKField);
sourceField.setSchemaType(targetField.getSchemaType());
break;
}
}
}
sourceToTargetKeyFieldAssociations.put(sourceField, targetField);
}
if (this.inverseReferenceMapping != null) {
if (null != this.inverseReferenceMapping.getAttributeAccessor()) {
this.inverseReferenceMapping.getAttributeAccessor().initializeAttributes(this.referenceClass);
}
}
}
@Override
public void preInitialize(AbstractSession session) throws DescriptorException {
getAttributeAccessor().setIsWriteOnly(this.isWriteOnly());
getAttributeAccessor().setIsReadOnly(this.isReadOnly());
super.preInitialize(session);
}
/**
* INTERNAL:
* Indicates that this is an XML mapping.
*/
@Override
public boolean isXMLMapping() {
return true;
}
/**
* INTERNAL:
* Extract the primary key values from the row, then create an
* org.eclipse.persistence.internal.oxm.Reference instance and store it
* on the session's org.eclipse.persistence.internal.oxm.ReferenceResolver.
*/
@Override
public Object readFromRowIntoObject(AbstractRecord databaseRow, JoinedAttributeManager joinManager, Object targetObject, CacheKey parentCacheKey, ObjectBuildingQuery sourceQuery, AbstractSession executionSession, boolean isTargetProtected) throws DatabaseException {
// the order in which the primary keys are added to the vector is
// relevant for cache lookup - it must match the ordering of the
// reference descriptor's primary key entries
CacheId primaryKeys;
ClassDescriptor descriptor = sourceQuery.getSession().getClassDescriptor(referenceClass);
Vector pkFieldNames = null;
if(null == descriptor) {
primaryKeys = new CacheId(new Object[1]);
} else {
pkFieldNames = descriptor.getPrimaryKeyFieldNames();
primaryKeys = new CacheId(new Object[pkFieldNames.size()]);
}
Iterator keyIt = sourceToTargetKeys.iterator();
while (keyIt.hasNext()) {
XMLField keyFld = (XMLField) keyIt.next();
XMLField tgtFld = (XMLField) getSourceToTargetKeyFieldAssociations().get(keyFld);
Object value;
int idx = 0;
if(null == tgtFld) {
value = databaseRow.get(keyFld);
} else {
idx = pkFieldNames.indexOf(tgtFld.getXPath());
if (idx == -1) {
continue;
}
// fix for bug# 5687430
// need to get the actual type of the target (i.e. int, String, etc.)
// and use the converted value when checking the cache.
value = executionSession.getDatasourcePlatform().getConversionManager().convertObject(databaseRow.get(keyFld), descriptor.getTypedField(tgtFld).getType());
}
if (value != null) {
primaryKeys.set(idx, value);
}
}
// store the Reference instance on the resolver for use during mapping
// resolution phase
ReferenceResolver resolver = ((DOMRecord) databaseRow).getReferenceResolver();
if (resolver != null) {
resolver.addReference(new Reference(this, targetObject, referenceClass, primaryKeys));
}
return null;
}
/**
*/
public void setField(DatabaseField field) {
// do nothing.
}
/**
* INTERNAL:
* Set the list of source-target xmlfield pairs.
*/
public void setSourceToTargetKeyFieldAssociations(HashMap sourceToTargetKeyFieldAssociations) {
this.sourceToTargetKeyFieldAssociations = sourceToTargetKeyFieldAssociations;
}
/**
* INTERNAL:
* Write the attribute value from the object to the row.
*/
@Override
public void writeFromObjectIntoRow(Object object, AbstractRecord row, AbstractSession session, WriteType writeType) {
// for each xmlField on this mapping
Object targetObject = getAttributeValueFromObject(object);
writeSingleValue(targetObject, object, (XMLRecord) row, session);
}
@Override
public void writeSingleValue(Object value, Object parent, XMLRecord row, AbstractSession session) {
for (Iterator fieldIt = getFields().iterator(); fieldIt.hasNext();) {
XMLField xmlField = (XMLField) fieldIt.next();
Object fieldValue = buildFieldValue(value, xmlField, session);
if (fieldValue != null) {
QName schemaType = getSchemaType(xmlField, fieldValue, session);
String stringValue = getValueToWrite(schemaType, fieldValue, session);
row.put(xmlField, stringValue);
}
}
}
@Override
public void setIsWriteOnly(boolean b) {
this.isWriteOnly = b;
}
@Override
public boolean isWriteOnly() {
return this.isWriteOnly;
}
@Override
public void setAttributeValueInObject(Object object, Object value) throws DescriptorException {
if(isWriteOnly()) {
return;
}
super.setAttributeValueInObject(object, value);
}
@Override
public XMLInverseReferenceMapping getInverseReferenceMapping() {
return inverseReferenceMapping;
}
void setInverseReferenceMapping(XMLInverseReferenceMapping inverseReferenceMapping) {
this.inverseReferenceMapping = inverseReferenceMapping;
}
@Override
public boolean isObjectReferenceMapping() {
return true;
}
}