| /* |
| * Copyright (c) 1998, 2020 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.ArrayList; |
| import java.util.HashMap; |
| import java.util.Iterator; |
| import java.util.StringTokenizer; |
| import java.util.Vector; |
| |
| import javax.xml.namespace.QName; |
| |
| import org.eclipse.persistence.descriptors.ClassDescriptor; |
| 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.helper.NonSynchronizedVector; |
| import org.eclipse.persistence.internal.identitymaps.CacheId; |
| import org.eclipse.persistence.internal.identitymaps.CacheKey; |
| 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.CollectionReferenceMapping; |
| import org.eclipse.persistence.internal.oxm.mappings.XMLContainerMapping; |
| import org.eclipse.persistence.internal.oxm.record.UnmarshalRecord; |
| import org.eclipse.persistence.internal.queries.CollectionContainerPolicy; |
| import org.eclipse.persistence.internal.queries.ContainerPolicy; |
| import org.eclipse.persistence.internal.queries.JoinedAttributeManager; |
| import org.eclipse.persistence.internal.queries.ListContainerPolicy; |
| import org.eclipse.persistence.internal.queries.MapContainerPolicy; |
| 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.ContainerMapping; |
| import org.eclipse.persistence.oxm.XMLField; |
| import org.eclipse.persistence.oxm.mappings.nullpolicy.AbstractNullPolicy; |
| 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-M 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. |
| * |
| * 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. |
| * |
| * The usesSingleNode flag should be set to true if the keys are to be written out in space-separated |
| * lists. |
| * |
| * @see XMLObjectReferenceMapping |
| * @see ContainerMapping |
| */ |
| public class XMLCollectionReferenceMapping extends XMLObjectReferenceMapping implements CollectionReferenceMapping<AbstractSession, AttributeAccessor, ContainerPolicy, ClassDescriptor, DatabaseField, UnmarshalRecord, XMLField, XMLRecord>, ContainerMapping { |
| protected ContainerPolicy containerPolicy; // type of container used to hold the aggregate objects |
| private static final String SPACE = " "; |
| private DatabaseField field; |
| private boolean defaultEmptyContainer = XMLContainerMapping.EMPTY_CONTAINER_DEFAULT; |
| private boolean usesSingleNode; |
| private boolean reuseContainer; |
| private AbstractNullPolicy wrapperNullPolicy; |
| |
| /** |
| * PUBLIC: |
| * The default constructor initializes the sourceToTargetKeyFieldAssociations |
| * and sourceToTargetKeys data structures. |
| */ |
| public XMLCollectionReferenceMapping() { |
| sourceToTargetKeyFieldAssociations = new HashMap(); |
| sourceToTargetKeys = new NonSynchronizedVector(); |
| this.containerPolicy = ContainerPolicy.buildDefaultPolicy(); |
| this.usesSingleNode = false; |
| } |
| |
| @Override |
| public DatabaseField getField() { |
| return field; |
| } |
| |
| @Override |
| public void setField(DatabaseField field) { |
| this.field = field; |
| } |
| |
| |
| /** |
| * 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 |
| */ |
| public void setXPath(String xpathString) { |
| this.setField(new XMLField(xpathString)); |
| } |
| |
| /** |
| * 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). |
| * |
| * @param targetObject - the reference class instance that holds the required pk value |
| * @param xmlFld |
| * @param session |
| * @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) { |
| return null; |
| } |
| ClassDescriptor descriptor = referenceDescriptor; |
| if(null == descriptor) { |
| descriptor = session.getClassDescriptor(targetObject); |
| } |
| ObjectBuilder objectBuilder = descriptor.getObjectBuilder(); |
| Object primaryKey = objectBuilder.extractPrimaryKeyFromObject(targetObject, session); |
| XMLField tgtXMLField = (XMLField) getSourceToTargetKeyFieldAssociations().get(xmlFld); |
| int idx = 0; |
| if(!(null == referenceClass || ClassConstants.OBJECT == referenceClass)) { |
| idx = descriptor.getPrimaryKeyFields().indexOf(tgtXMLField); |
| 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, Object container) { |
| buildReference(record.getCurrentObject(), xmlField, object, session, container, record.getReferenceResolver()); |
| } |
| |
| /** |
| * INTERNAL: |
| * Create (if necessary) and populate a reference object that will be used |
| * during the mapping reference resolution phase after unmarshalling is |
| * complete. |
| */ |
| public void buildReference(Object srcObject, XMLField xmlField, Object object, AbstractSession session, Object container, ReferenceResolver resolver) { |
| if (resolver == null) { |
| return; |
| } |
| |
| Reference reference = resolver.getReference(this, srcObject, xmlField); |
| if (reference == null) { |
| // if reference is null, create a new instance and set it on the resolver |
| reference = new Reference(this, srcObject, referenceClass, new HashMap(), container); |
| resolver.addReference(reference); |
| } |
| CacheId primaryKeys; |
| if(null == referenceClass || ClassConstants.OBJECT == referenceClass) { |
| HashMap primaryKeyMap = reference.getPrimaryKeyMap(); |
| CacheId pks = (CacheId) primaryKeyMap.get(null); |
| if (pks == null){ |
| Object[] pkValues = new Object[1]; |
| pks = new CacheId(pkValues); |
| primaryKeyMap.put(null, pks); |
| } |
| if(usesSingleNode) { |
| for (StringTokenizer stok = new StringTokenizer((String) object); stok.hasMoreTokens();) { |
| pks.add(stok.nextToken()); |
| reference = resolver.getReference(this, srcObject, xmlField); |
| if (reference == null) { |
| // if reference is null, create a new instance and set it on the resolver |
| reference = new Reference(this, srcObject, referenceClass, new HashMap(), container); |
| resolver.addReference(reference); |
| } |
| primaryKeyMap = reference.getPrimaryKeyMap(); |
| pks = (CacheId) primaryKeyMap.get(null); |
| if (pks == null){ |
| Object[] pkValues = new Object[1]; |
| pks = new CacheId(pkValues); |
| primaryKeyMap.put(null, pks); |
| } |
| } |
| } else { |
| pks.add(object); |
| } |
| } else { |
| XMLField tgtFld = (XMLField) getSourceToTargetKeyFieldAssociations().get(xmlField); |
| String tgtXPath = tgtFld.getQualifiedName(); |
| HashMap primaryKeyMap = reference.getPrimaryKeyMap(); |
| CacheId pks = (CacheId) primaryKeyMap.get(tgtXPath); |
| ClassDescriptor descriptor = session.getClassDescriptor(referenceClass); |
| if (pks == null){ |
| pks = new CacheId(new Object[0]); |
| primaryKeyMap.put(tgtXPath, pks); |
| } |
| Class type = descriptor.getTypedField(tgtFld).getType(); |
| XMLConversionManager xmlConversionManager = (XMLConversionManager) session.getDatasourcePlatform().getConversionManager(); |
| if(usesSingleNode) { |
| for (StringTokenizer stok = new StringTokenizer((String) object); stok.hasMoreTokens();) { |
| Object value = xmlConversionManager.convertObject(stok.nextToken(), type); |
| if (value != null) { |
| pks.add(value); |
| } |
| reference = resolver.getReference(this, srcObject, xmlField); |
| if (reference == null) { |
| // if reference is null, create a new instance and set it on the resolver |
| reference = new Reference(this, srcObject, referenceClass, new HashMap(), container); |
| resolver.addReference(reference); |
| } |
| primaryKeyMap = reference.getPrimaryKeyMap(); |
| pks = (CacheId) primaryKeyMap.get(null); |
| if (pks == null){ |
| pks = new CacheId(new Object[0]); |
| primaryKeyMap.put(tgtXPath, pks); |
| } |
| } |
| } else { |
| Object value = xmlConversionManager.convertObject(object, type); |
| if (value != null) { |
| pks.add(value); |
| } |
| } |
| } |
| } |
| |
| /** |
| * INTERNAL: |
| * Return the mapping's containerPolicy. |
| */ |
| @Override |
| public ContainerPolicy getContainerPolicy() { |
| return containerPolicy; |
| } |
| |
| /** |
| * INTERNAL: |
| * The mapping is initialized with the given session. This mapping is fully initialized |
| * after this. |
| */ |
| @Override |
| public void initialize(AbstractSession session) throws DescriptorException { |
| super.initialize(session); |
| if(null != getField()) { |
| setField(getDescriptor().buildField(getField())); |
| } |
| ContainerPolicy cp = getContainerPolicy(); |
| if (cp != null) { |
| if (cp.getContainerClass() == null) { |
| Class cls = session.getDatasourcePlatform().getConversionManager().convertClassNameToClass(cp.getContainerClassName()); |
| cp.setContainerClass(cls); |
| } |
| } |
| |
| } |
| |
| /** |
| * INTERNAL: |
| * Extract the primary key values from the row, then create an |
| * org.eclipse.persistence.internal.oxm.Reference instance and stored 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 { |
| ContainerPolicy cp = getContainerPolicy(); |
| |
| Object container = null; |
| |
| if (reuseContainer) { |
| Object currentObject = ((XMLRecord) databaseRow).getCurrentObject(); |
| container = getAttributeAccessor().getAttributeValueFromObject(currentObject); |
| |
| } |
| if(container == null){ |
| container = cp.containerInstance(); |
| } |
| return readFromRowIntoObject(databaseRow, joinManager, targetObject, parentCacheKey, sourceQuery, executionSession, isTargetProtected, container); |
| |
| } |
| |
| /** |
| * INTERNAL: |
| * Extract the primary key values from the row, then create an |
| * org.eclipse.persistence.internal.oxm.Reference instance and stored it |
| * on the session's org.eclipse.persistence.internal.oxm.ReferenceResolver. |
| */ |
| |
| public Object readFromRowIntoObject(AbstractRecord databaseRow, JoinedAttributeManager joinManager, Object targetObject, CacheKey parentCacheKey, ObjectBuildingQuery sourceQuery, AbstractSession executionSession, boolean isTargetProtected, Object container) throws DatabaseException { |
| ClassDescriptor descriptor = sourceQuery.getSession().getClassDescriptor(getReferenceClass()); |
| |
| if(container == null){ |
| readFromRowIntoObject(databaseRow, joinManager, targetObject, parentCacheKey, sourceQuery, executionSession, isTargetProtected); |
| } |
| |
| // for each source xmlField, get the value from the row and store |
| for (Iterator fieldIt = getFields().iterator(); fieldIt.hasNext();) { |
| XMLField fld = (XMLField) fieldIt.next(); |
| // |
| Object fieldValue = databaseRow.getValues(fld); |
| if ((fieldValue == null) || (fieldValue instanceof String) || !(fieldValue instanceof Vector)) { |
| return container; |
| } |
| // 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. |
| for (Iterator valIt = ((Vector) fieldValue).iterator(); valIt.hasNext();) { |
| Object nextValue = valIt.next(); |
| DOMRecord domRecord = (DOMRecord) databaseRow; |
| this.buildReference(domRecord.getCurrentObject(), fld, nextValue, sourceQuery.getSession(), container, domRecord.getReferenceResolver()); |
| } |
| } |
| return null; |
| } |
| |
| |
| /** |
| * ADVANCED: |
| * Set the mapping's containerPolicy. |
| */ |
| @Override |
| public void setContainerPolicy(ContainerPolicy containerPolicy) { |
| // set reference class here if necessary |
| this.containerPolicy = containerPolicy; |
| if (this.containerPolicy instanceof MapContainerPolicy) { |
| ((MapContainerPolicy) this.containerPolicy).setElementClass(getReferenceClass()); |
| } |
| } |
| |
| /** |
| * PUBLIC: |
| * Configure the mapping to use an instance of the specified container class |
| * to hold the target objects. |
| * <p>jdk1.2.x: The container class must implement (directly or indirectly) the Collection interface. |
| * <p>jdk1.1.x: The container class must be a subclass of Vector. |
| */ |
| @Override |
| public void useCollectionClass(Class concreteContainerClass) { |
| this.setContainerPolicy(ContainerPolicy.buildPolicyFor(concreteContainerClass)); |
| } |
| |
| @Override |
| public void useCollectionClassName(String concreteContainerClassName) { |
| this.setContainerPolicy(new CollectionContainerPolicy(concreteContainerClassName)); |
| } |
| |
| @Override |
| public void useListClassName(String concreteContainerClassName) { |
| this.setContainerPolicy(new ListContainerPolicy(concreteContainerClassName)); |
| } |
| |
| /** |
| * PUBLIC: |
| * Configure the mapping to use an instance of the specified container class |
| * to hold the target objects. The key used to index the value in the Map |
| * is the value returned by a call to the specified zero-argument method. |
| * The method must be implemented by the class (or a superclass) of the |
| * value to be inserted into the Map. |
| * <p>jdk1.2.x: The container class must implement (directly or indirectly) the Map interface. |
| * <p>jdk1.1.x: The container class must be a subclass of Hashtable. |
| * <p>The referenceClass must be set before calling this method. |
| */ |
| @Override |
| public void useMapClass(Class concreteContainerClass, String methodName) { |
| // the reference class has to be specified before coming here |
| if (this.getReferenceClass() == null) { |
| throw DescriptorException.referenceClassNotSpecified(this); |
| } |
| ContainerPolicy policy = ContainerPolicy.buildPolicyFor(concreteContainerClass); |
| policy.setKeyName(methodName, getReferenceClass().getName()); |
| this.setContainerPolicy(policy); |
| } |
| |
| /** |
| * PUBLIC: |
| * Configure the mapping to use an instance of the specified container class |
| * to hold the target objects. The key used to index the value in the Map |
| * is the value returned by a call to the specified zero-argument method. |
| * The method must be implemented by the class (or a superclass) of the |
| * value to be inserted into the Map. |
| * <p>jdk1.2.x: The container class must implement (directly or indirectly) the Map interface. |
| * <p>jdk1.1.x: The container class must be a subclass of Hashtable. |
| * <p>The referenceClass must be set before calling this method. |
| */ |
| @Override |
| public void useMapClassName(String concreteContainerClass, String methodName) { |
| // the reference class has to be specified before coming here |
| if (this.getReferenceClass() == null) { |
| throw DescriptorException.referenceClassNotSpecified(this); |
| } |
| MapContainerPolicy policy = new MapContainerPolicy(concreteContainerClass); |
| policy.setKeyName(methodName, getReferenceClass().getName()); |
| this.setContainerPolicy(policy); |
| } |
| |
| /** |
| * INTERNAL: |
| * For the purpose of XMLCollectionReferenceMappings, 'usesSingleNode' |
| * refers to the fact that the source key xpath fields should all be written as |
| * space-separated lists. Would be used for mapping to an IDREFS field in a schema |
| */ |
| @Override |
| public boolean usesSingleNode() { |
| return this.usesSingleNode; |
| } |
| |
| @Override |
| public void setUsesSingleNode(boolean useSingleNode) { |
| this.usesSingleNode = useSingleNode; |
| } |
| |
| /** |
| * 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 |
| if(this.isReadOnly()) { |
| return; |
| } |
| for (Iterator fieldIt = getFields().iterator(); fieldIt.hasNext();) { |
| XMLField xmlField = (XMLField) fieldIt.next(); |
| ContainerPolicy cp = getContainerPolicy(); |
| Object collection = getAttributeAccessor().getAttributeValueFromObject(object); |
| if (collection == null) { |
| return; |
| } |
| |
| Object fieldValue; |
| Object objectValue; |
| StringBuilder stringValueBuilder = new StringBuilder(); |
| QName schemaType; |
| Object iterator = cp.iteratorFor(collection); |
| if (usesSingleNode()) { |
| while (cp.hasNext(iterator)) { |
| objectValue = cp.next(iterator, session); |
| fieldValue = buildFieldValue(objectValue, xmlField, session); |
| if (fieldValue != null) { |
| schemaType = getSchemaType(xmlField, fieldValue, session); |
| String newValue = getValueToWrite(schemaType, fieldValue, session); |
| if (newValue != null) { |
| stringValueBuilder.append(newValue); |
| if (cp.hasNext(iterator)) { |
| stringValueBuilder.append(SPACE); |
| } |
| } |
| } |
| } |
| if (stringValueBuilder.length() > 0) { |
| row.put(xmlField, stringValueBuilder.toString()); |
| } |
| } else { |
| ArrayList keyValues = new ArrayList(); |
| while (cp.hasNext(iterator)) { |
| objectValue = cp.next(iterator, session); |
| fieldValue = buildFieldValue(objectValue, xmlField, session); |
| if (fieldValue != null) { |
| schemaType = getSchemaType(xmlField, fieldValue, session); |
| String stringValue = getValueToWrite(schemaType, fieldValue, session); |
| //row.add(xmlField, stringValue); |
| keyValues.add(stringValue); |
| } |
| } |
| row.put(xmlField, keyValues); |
| } |
| } |
| } |
| |
| @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.add(xmlField, stringValue); |
| } |
| } |
| |
| } |
| |
| @Override |
| public boolean isCollectionMapping() { |
| return true; |
| } |
| |
| /** |
| * Return true if the original container on the object should be used if |
| * present. If it is not present then the container policy will be used to |
| * create the container. |
| */ |
| @Override |
| public boolean getReuseContainer() { |
| return reuseContainer; |
| } |
| |
| /** |
| * Specify whether the original container on the object should be used if |
| * present. If it is not present then the container policy will be used to |
| * create the container. |
| */ |
| @Override |
| public void setReuseContainer(boolean reuseContainer) { |
| this.reuseContainer = reuseContainer; |
| } |
| |
| /** |
| * INTERNAL |
| * Return true if an empty container should be set on the object if there |
| * is no presence of the collection in the XML document. |
| * @since EclipseLink 2.3.3 |
| */ |
| @Override |
| public boolean isDefaultEmptyContainer() { |
| return defaultEmptyContainer; |
| } |
| |
| /** |
| * INTERNAL |
| * Indicate whether by default an empty container should be set on the |
| * field/property if the collection is not present in the XML document. |
| * @since EclipseLink 2.3.3 |
| */ |
| @Override |
| public void setDefaultEmptyContainer(boolean defaultEmptyContainer) { |
| this.defaultEmptyContainer = defaultEmptyContainer; |
| } |
| |
| @Override |
| public AbstractNullPolicy getWrapperNullPolicy() { |
| return this.wrapperNullPolicy; |
| } |
| |
| @Override |
| public void setWrapperNullPolicy(AbstractNullPolicy policy) { |
| this.wrapperNullPolicy = policy; |
| } |
| |
| } |