| /* |
| * 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<String> 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 ((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; |
| } |
| |
| } |