blob: 585383a617448bbead3d50b0a793883bc779c17c [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
// 06/03/2013-2.5.1 Guy Pelletier
// - 402380: 3 jpa21/advanced tests failed on server with
// "java.lang.NoClassDefFoundError: org/eclipse/persistence/testing/models/jpa21/advanced/enums/Gender"
package org.eclipse.persistence.mappings.foundation;
import java.util.*;
import org.eclipse.persistence.exceptions.*;
import org.eclipse.persistence.internal.descriptors.*;
import org.eclipse.persistence.internal.helper.*;
import org.eclipse.persistence.internal.identitymaps.CacheKey;
import org.eclipse.persistence.internal.oxm.mappings.Field;
import org.eclipse.persistence.internal.queries.*;
import org.eclipse.persistence.internal.sessions.*;
import org.eclipse.persistence.internal.sessions.remote.ObjectDescriptor;
import org.eclipse.persistence.mappings.*;
import org.eclipse.persistence.mappings.converters.*;
import org.eclipse.persistence.mappings.structures.ArrayCollectionMapping;
import org.eclipse.persistence.mappings.structures.ArrayCollectionMappingHelper;
import org.eclipse.persistence.queries.*;
import org.eclipse.persistence.sessions.remote.*;
import org.eclipse.persistence.sessions.CopyGroup;
/**
* <code>AbstractCompositeDirectCollectionMapping</code> consolidates the behavior of mappings that
* map collections of "native" data objects (e.g. <code>String</code>s).
* These are objects that do not have their own descriptor and repeat within the XML record
* for the containing object.
*
* @author Big Country
* @since TOPLink/Java 3.0
*/
public abstract class AbstractCompositeDirectCollectionMapping extends DatabaseMapping implements ContainerMapping, ArrayCollectionMapping {
/** This is the field holding the nested collection. */
protected DatabaseField field;
/** This is the "data type" associated with each element in the nested collection.
Depending on the data store, this could be optional. */
protected String elementDataTypeName;
/** Allows user defined conversion between the object value and the database value. */
protected Converter valueConverter;
/** This determines the type of container used to hold the nested collection
in the object. */
private ContainerPolicy containerPolicy;
/**
* Default constructor.
*/
protected AbstractCompositeDirectCollectionMapping() {
super();
this.containerPolicy = ContainerPolicy.buildDefaultPolicy();
this.elementDataTypeName = "";
this.setWeight(WEIGHT_AGGREGATE);
}
/**
* PUBLIC:
* Return the converter on the mapping.
* A converter can be used to convert between the direct collection's object value and database value.
*/
public Converter getValueConverter() {
return valueConverter;
}
/**
* PUBLIC:
* Indicates if there is a converter on the mapping.
*/
public boolean hasValueConverter() {
return getValueConverter() != null;
}
/**
* PUBLIC:
* Set the converter on the mapping.
* A converter can be used to convert between the direct collection's object value and database value.
*/
public void setValueConverter(Converter valueConverter) {
this.valueConverter = valueConverter;
}
/**
* INTERNAL:
* Build and return a new element based on the change set.
*/
@Override
public Object buildAddedElementFromChangeSet(Object changeSet, MergeManager mergeManager, AbstractSession targetSession) {
return this.buildElementFromChangeSet(changeSet, mergeManager, targetSession);
}
/**
* INTERNAL:
* Clone the attribute from the clone and assign it to the backup.
* For these mappings, this is the same as building the first clone.
*/
@Override
public void buildBackupClone(Object clone, Object backup, UnitOfWorkImpl unitOfWork) {
this.buildClone(clone, null, backup, null, unitOfWork);
}
/**
* INTERNAL:
* Build and return a change set for the specified element.
* Direct collections simply store the element itself, since it is immutable.
*/
@Override
public Object buildChangeSet(Object element, ObjectChangeSet owner, AbstractSession session) {
return element;
}
/**
* INTERNAL:
* Clone the attribute from the original and assign it to the clone.
*/
@Override
public void buildClone(Object original, CacheKey cacheKey, Object clone, Integer refreshCascade, AbstractSession cloningSession) {
Object attributeValue = this.getAttributeValueFromObject(original);
this.setAttributeValueInObject(clone, this.buildClonePart(attributeValue, cacheKey, cloningSession));
}
/**
* INTERNAL:
* Extract value from the row and set the attribute to this value in the
* working copy clone.
* In order to bypass the shared cache when in transaction a UnitOfWork must
* be able to populate working copies directly from the row.
*/
@Override
public void buildCloneFromRow(AbstractRecord row, JoinedAttributeManager joinManager, Object clone, CacheKey sharedCacheKey, ObjectBuildingQuery sourceQuery, UnitOfWorkImpl unitOfWork, AbstractSession executionSession) {
// for direct collection a cloned value is no different from an original value
Object cloneAttributeValue = valueFromRow(row, joinManager, sourceQuery, sharedCacheKey, executionSession, true, new Boolean[1]);
setAttributeValueInObject(clone, cloneAttributeValue);
}
/**
* Build and return a clone of the specified attribute value.
*/
protected Object buildClonePart(Object attributeValue, CacheKey parentCacheKey, AbstractSession cloningSession) {
if (attributeValue == null) {
return this.getContainerPolicy().containerInstance();
} else {
if ((getValueConverter() == null) || (!getValueConverter().isMutable())) {
return this.getContainerPolicy().cloneFor(attributeValue);
}
// Clone the values of the collection as well.
Object cloneContainer = this.getContainerPolicy().containerInstance();
Object iterator = this.getContainerPolicy().iteratorFor(attributeValue);
while (this.getContainerPolicy().hasNext(iterator)) {
Object originalValue = this.getContainerPolicy().next(iterator, cloningSession);
// Bug 4182377 - there was a typo in the conversion logic
Object cloneValue = getValueConverter().convertDataValueToObjectValue(getValueConverter().convertObjectValueToDataValue(originalValue, cloningSession), cloningSession);
this.getContainerPolicy().addInto(cloneValue, cloneContainer, cloningSession);
}
return cloneContainer;
}
}
/**
* INTERNAL:
* Copy of the attribute of the object.
* This is NOT used for unit of work but for templatizing an object.
*/
@Override
public void buildCopy(Object copy, Object original, CopyGroup group) {
Object attributeValue = getAttributeValueFromObject(original);
if (attributeValue == null) {
attributeValue = getContainerPolicy().containerInstance();
} else {
attributeValue = getContainerPolicy().cloneFor(attributeValue);
}
setAttributeValueInObject(copy, attributeValue);
}
/**
* Build and return a new element based on the change set.
* Direct collections simply store the element itself, since it is immutable.
*/
protected Object buildElementFromChangeSet(Object changeSet, MergeManager mergeManager, AbstractSession targetSession) {
return changeSet;
}
/**
* INTERNAL:
* Build and return a new element based on the specified element.
* Direct collections simply return the element itself, since it is immutable.
*/
@Override
public Object buildElementFromElement(Object object, MergeManager mergeManager, AbstractSession targetSession) {
return object;
}
/**
* INTERNAL:
* Build and return a new element based on the change set.
*/
@Override
public Object buildRemovedElementFromChangeSet(Object changeSet, MergeManager mergeManager, AbstractSession targetSession) {
return this.buildElementFromChangeSet(changeSet, mergeManager, targetSession);
}
/**
* 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, this is a no-op.
}
/**
* INTERNAL:
* Cascade registerNew for Create through mappings that require the cascade
*/
@Override
public void cascadeRegisterNewIfRequired(Object object, UnitOfWorkImpl uow, Map visitedObjects) {
//objects referenced by this mapping are not registered as they have
// no identity, this is a no-op.
}
/**
* Return the fields handled by the mapping.
*/
@Override
protected Vector collectFields() {
Vector fields = new Vector(1);
fields.addElement(this.getField());
return fields;
}
/**
* INTERNAL:
* Compare the non-null elements. Return true if they are alike.
* Use #equals() to determine if two elements are the same.
*/
@Override
public boolean compareElements(Object element1, Object element2, AbstractSession session) {
return element1.equals(element2);
}
/**
* INTERNAL:
* Compare the non-null elements and return true if they are alike.
*/
@Override
public boolean compareElementsForChange(Object element1, Object element2, AbstractSession session) {
return this.compareElements(element1, element2, session);
}
protected ChangeRecord convertToChangeRecord(Object cloneCollection, ObjectChangeSet owner, AbstractSession session) {
//since a minimal update for composites can't be done, we are only recording
//an all-or-none change. Therefore, this can be treated as a simple direct
//value.
ContainerPolicy cp = this.getContainerPolicy();
Object container = cp.containerInstance();
Object iter = cp.iteratorFor(cloneCollection);
while (cp.hasNext(iter)) {
cp.addInto(cp.next(iter, session), container, session);
}
DirectToFieldChangeRecord changeRecord = new DirectToFieldChangeRecord(owner);
changeRecord.setAttribute(getAttributeName());
changeRecord.setMapping(this);
changeRecord.setNewValue(container);
return changeRecord;
}
/**
* INTERNAL:
* An object has been serialized from the server to the client.
* Replace the transient attributes of the remote value holders
* with client-side objects.
*/
@Override
public void fixObjectReferences(Object object, Map<Object, ObjectDescriptor> objectDescriptors, Map<Object, Object> processedObjects, ObjectLevelReadQuery query, DistributedSession session) {
// Do nothing....
// The nested collection should de-serialize without need for any further manipulation.
}
/**
* PUBLIC:
* Return the class each element in the object's
* collection should be converted to, before the collection
* is inserted into the object.
* This is optional - if left null, the elements will be added
* to the object's collection unconverted.
*/
public Class<?> getAttributeElementClass() {
if (!(getValueConverter() instanceof TypeConversionConverter)) {
return null;
}
return ((TypeConversionConverter)getValueConverter()).getObjectClass();
}
/**
* INTERNAL:
* Return the mapping's containerPolicy.
*/
@Override
public ContainerPolicy getContainerPolicy() {
return containerPolicy;
}
/**
* INTERNAL:
* Return the field that holds the nested collection.
*/
@Override
public DatabaseField getField() {
return field;
}
/**
* INTERNAL:
*/
@Override
public boolean isAbstractCompositeDirectCollectionMapping() {
return true;
}
/**
* PUBLIC:
* Return the class each element in the database row's
* collection should be converted to, before the collection
* is inserted into the database.
* This is optional - if left null, the elements will be added
* to the database row's collection unconverted.
*/
public Class<?> getFieldElementClass() {
if (!(getValueConverter() instanceof TypeConversionConverter)) {
return null;
}
return ((TypeConversionConverter)getValueConverter()).getDataClass();
}
/**
* PUBLIC:
* Return the name of the field that holds the nested collection.
*/
public String getFieldName() {
return this.getField().getName();
}
/**
* INTERNAL:
* Convenience method.
* Return the value of an attribute, unwrapping value holders if necessary.
* If the value is null, build a new container.
*/
@Override
public Object getRealCollectionAttributeValueFromObject(Object object, AbstractSession session) throws DescriptorException {
Object value = this.getRealAttributeValueFromObject(object, session);
if (value == null) {
value = this.getContainerPolicy().containerInstance(1);
}
return value;
}
/**
* INTERNAL:
* Initialize the mapping.
*/
@Override
public void initialize(AbstractSession session) throws DescriptorException {
super.initialize(session);
if (getField() == null) {
throw DescriptorException.fieldNameNotSetInMapping(this);
}
setField(getDescriptor().buildField(getField()));
setFields(collectFields());
if (getValueConverter() != null) {
getValueConverter().initialize(this, session);
}
}
/**
* INTERNAL:
* Iterate on the appropriate attribute value.
*/
@Override
public void iterate(DescriptorIterator iterator) {
// PERF: Only iterate when required.
if (iterator.shouldIterateOnPrimitives()) {
Object attributeValue = this.getAttributeValueFromObject(iterator.getVisitedParent());
if (attributeValue == null) {
return;
}
ContainerPolicy cp = this.getContainerPolicy();
for (Object iter = cp.iteratorFor(attributeValue); cp.hasNext(iter);) {
iterator.iteratePrimitiveForMapping(cp.next(iter, iterator.getSession()), this);
}
}
}
/**
* INTERNAL:
* Return whether the element's user-defined Map key has changed
* since it was cloned from the original version.
* Direct elements are not allowed to have keys.
*/
@Override
public boolean mapKeyHasChanged(Object element, AbstractSession session) {
return false;
}
/**
* PUBLIC:
* Set the class each element in the object's
* collection should be converted to, before the collection
* is inserted into the object.
* This is optional - if left null, the elements will be added
* to the object's collection unconverted.
*/
public void setAttributeElementClass(Class<?> attributeElementClass) {
TypeConversionConverter converter;
if (getValueConverter() instanceof TypeConversionConverter) {
converter = (TypeConversionConverter)getValueConverter();
} else {
converter = new TypeConversionConverter();
setValueConverter(converter);
}
converter.setObjectClass(attributeElementClass);
}
/**
* PUBLIC:
* Set the class each element in the object's
* collection should be converted to, before the collection
* is inserted into the object.
* This is optional - if left null, the elements will be added
* to the object's collection unconverted.
*/
public void setAttributeElementClassName(String attributeElementClass) {
TypeConversionConverter converter;
if (getValueConverter() instanceof TypeConversionConverter) {
converter = (TypeConversionConverter)getValueConverter();
} else {
converter = new TypeConversionConverter();
setValueConverter(converter);
}
converter.setObjectClassName(attributeElementClass);
}
/**
* ADVANCED:
* Set the mapping's containerPolicy.
*/
@Override
public void setContainerPolicy(ContainerPolicy containerPolicy) {
this.containerPolicy = containerPolicy;
}
/**
* Set the field that holds the nested collection.
*/
public void setField(DatabaseField field) {
this.field = field;
}
/**
* PUBLIC:
* Set the class each element in the database row's
* collection should be converted to, before the collection
* is inserted into the database.
* This is optional - if left null, the elements will be added
* to the database row's collection unconverted.
*/
public void setFieldElementClass(Class<?> fieldElementClass) {
TypeConversionConverter converter;
if (getValueConverter() instanceof TypeConversionConverter) {
converter = (TypeConversionConverter)getValueConverter();
} else {
converter = new TypeConversionConverter();
setValueConverter(converter);
}
converter.setDataClass(fieldElementClass);
}
/**
* PUBLIC:
* Configure the mapping to use an instance of the specified container class
* to hold the nested 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<?> concreteClass) {
this.setContainerPolicy(ContainerPolicy.buildPolicyFor(concreteClass));
}
/**
* INTERNAL:
* Used to set the collection class by name.
* This is required when building from metadata to allow the correct class loader to be used.
*/
@Override
public void useCollectionClassName(String concreteClassName) {
setContainerPolicy(new CollectionContainerPolicy(concreteClassName));
}
/**
* INTERNAL:
* Used to set the collection class by name.
* This is required when building from metadata to allow the correct class loader to be used.
*/
@Override
public void useListClassName(String concreteClassName) {
setContainerPolicy(new ListContainerPolicy(concreteClassName));
}
/**
* PUBLIC:
* Mapping does not support Map containers.
* It supports only Collection containers.
*/
@Override
public void useMapClass(Class<?> concreteClass, String methodName) {
throw new UnsupportedOperationException(this.getClass().getName() + ".useMapClass(Class, String)");
}
@Override
public void useMapClassName(String concreteContainerClassName, String methodName) {
throw new UnsupportedOperationException(this.getClass().getName() + ".useMapClass(String, String)");
}
/**
* PUBLIC:
* Sets whether the mapping uses a single node.
* @param usesSingleNode true if the items in the collection are in a single node or false if each of the items in the collection is in its own node
*/
public void setUsesSingleNode(boolean usesSingleNode) {
if (getField() instanceof Field) {
((Field)getField()).setUsesSingleNode(usesSingleNode);
}
}
/**
* PUBLIC:
* Checks whether the mapping uses a single node.
*
* @return True if the items in the collection are in a single node or false if each of the items in the collection is in its own node.
*/
public boolean usesSingleNode() {
if (getField() instanceof Field) {
return ((Field)getField()).usesSingleNode();
}
return false;
}
/**
* INTERNAL:
* Build the nested collection from the database row.
*/
@Override
public Object valueFromRow(AbstractRecord row, JoinedAttributeManager joinManager, ObjectBuildingQuery sourceQuery, CacheKey cacheKey, AbstractSession executionSession, boolean isTargetProtected, Boolean[] wasCacheUsed) throws DatabaseException {
if (this.descriptor.getCachePolicy().isProtectedIsolation()){
if (this.isCacheable && isTargetProtected && cacheKey != null){
//cachekey will be null when isolating to uow
//used cached collection
Object result = null;
Object cached = cacheKey.getObject();
if (cached != null){
if (wasCacheUsed != null){
wasCacheUsed[0] = Boolean.TRUE;
}
Object attributeValue = this.getAttributeValueFromObject(cached);
return buildClonePart(attributeValue, cacheKey, executionSession);
}
return result;
}else if (!this.isCacheable && !isTargetProtected && cacheKey != null){
return null;
}
}
if (row.hasSopObject()) {
return getAttributeValueFromObject(row.getSopObject());
}
ContainerPolicy cp = this.getContainerPolicy();
Object fieldValue = row.getValues(this.getField());
if (fieldValue == null) {
return cp.containerInstance();
}
Vector fieldValues = this.getDescriptor().buildDirectValuesFromFieldValue(fieldValue);
if (fieldValues == null) {
return cp.containerInstance();
}
Object result = cp.containerInstance(fieldValues.size());
for (Enumeration stream = fieldValues.elements(); stream.hasMoreElements();) {
Object element = stream.nextElement();
if (this.getValueConverter() != null) {
element = getValueConverter().convertDataValueToObjectValue(element, executionSession);
}
cp.addInto(element, result, sourceQuery.getSession());
}
return result;
}
/**
* INTERNAL:
* Get the attribute value from the object and
* store it in the appropriate field of the row.
*/
@Override
public void writeFromObjectIntoRow(Object object, AbstractRecord row, AbstractSession session, WriteType writeType) {
if (this.isReadOnly()) {
return;
}
Object attributeValue = this.getAttributeValueFromObject(object);
if (attributeValue == null) {
row.put(this.getField(), null);
return;
}
ContainerPolicy cp = this.getContainerPolicy();
Vector elements = new Vector(cp.sizeFor(attributeValue));
for (Object iter = cp.iteratorFor(attributeValue); cp.hasNext(iter);) {
Object element = cp.next(iter, session);
if (this.getValueConverter() != null) {
element = getValueConverter().convertObjectValueToDataValue(element, session);
}
if (element != null) {
elements.addElement(element);
}
}
Object fieldValue = null;
if (!elements.isEmpty()) {
fieldValue = this.getDescriptor().buildFieldValueFromDirectValues(elements, elementDataTypeName, session);
}
row.put(this.getField(), fieldValue);
}
/**
* INTERNAL:
* If any part of the nested collection has changed, the whole thing is written.
*/
@Override
public void writeFromObjectIntoRowForUpdate(WriteObjectQuery writeQuery, AbstractRecord row) throws DescriptorException {
AbstractSession session = writeQuery.getSession();
if (session.isUnitOfWork()) {
if (this.compareObjects(writeQuery.getObject(), writeQuery.getBackupClone(), session)) {
return;// nothing is changed, no work required
}
}
this.writeFromObjectIntoRow(writeQuery.getObject(), row, session, WriteType.UPDATE);
}
/**
* INTERNAL:
* Get the appropriate attribute value from the object
* and put it in the appropriate field of the database row.
* Loop through the reference objects and extract the
* primary keys and put them in the vector of "nested" rows.
*/
@Override
public void writeFromObjectIntoRowWithChangeRecord(ChangeRecord changeRecord, AbstractRecord row, AbstractSession session, WriteType writeType) {
Object object = ((ObjectChangeSet)changeRecord.getOwner()).getUnitOfWorkClone();
this.writeFromObjectIntoRow(object, row, session, writeType);
}
/**
* INTERNAL:
* Write the fields needed for insert into the template with null values.
*/
@Override
public void writeInsertFieldsIntoRow(AbstractRecord row, AbstractSession session) {
if (this.isReadOnly()) {
return;
}
row.put(this.getField(), null);
}
/**
* INTERNAL:
* Return the classifiction for the field contained in the mapping.
* This is used to convert the row value to a consistent java value.
* By default this is unknown.
*/
@Override
public Class<?> getFieldClassification(DatabaseField fieldToClassify) {
return getAttributeElementClass();
}
@Override
public boolean isCollectionMapping() {
return true;
}
@Override
public void convertClassNamesToClasses(ClassLoader classLoader){
super.convertClassNamesToClasses(classLoader);
this.containerPolicy.convertClassNamesToClasses(classLoader);
// Convert and any Converter class names.
convertConverterClassNamesToClasses(valueConverter, classLoader);
}
/**
* INTERNAL:
* Build and return the change record that results
* from comparing the two direct collection attributes.
*/
@Override
public ChangeRecord compareForChange(Object clone, Object backup, ObjectChangeSet owner, AbstractSession session) {
return (new ArrayCollectionMappingHelper(this)).compareForChange(clone, backup, owner, session);
}
/**
* INTERNAL:
* Compare the attributes belonging to this mapping for the objects.
*/
@Override
public boolean compareObjects(Object object1, Object object2, AbstractSession session) {
return (new ArrayCollectionMappingHelper(this)).compareObjects(object1, object2, session);
}
/**
* INTERNAL:
* Merge changes from the source to the target object.
*/
@Override
public void mergeChangesIntoObject(Object target, ChangeRecord changeRecord, Object source, MergeManager mergeManager, AbstractSession targetSession) {
(new ArrayCollectionMappingHelper(this)).mergeChangesIntoObject(target, changeRecord, source, mergeManager, targetSession);
}
/**
* INTERNAL:
* Merge changes from the source to the target object.
* Simply replace the entire target collection.
*/
@Override
public void mergeIntoObject(Object target, boolean isTargetUnInitialized, Object source, MergeManager mergeManager, AbstractSession targetSession) {
(new ArrayCollectionMappingHelper(this)).mergeIntoObject(target, isTargetUnInitialized, source, mergeManager, targetSession);
}
/**
* ADVANCED:
* This method is used to have an object add to a collection once the changeSet is applied
* The referenceKey parameter should only be used for direct Maps.
*/
@Override
public void simpleAddToCollectionChangeRecord(Object referenceKey, Object changeSetToAdd, ObjectChangeSet changeSet, AbstractSession session) {
(new ArrayCollectionMappingHelper(this)).simpleAddToCollectionChangeRecord(referenceKey, changeSetToAdd, changeSet, session);
}
/**
* ADVANCED:
* This method is used to have an object removed from a collection once the changeSet is applied
* The referenceKey parameter should only be used for direct Maps.
*/
@Override
public void simpleRemoveFromCollectionChangeRecord(Object referenceKey, Object changeSetToRemove, ObjectChangeSet changeSet, AbstractSession session) {
(new ArrayCollectionMappingHelper(this)).simpleRemoveFromCollectionChangeRecord(referenceKey, changeSetToRemove, changeSet, session);
}
/**
* INTERNAL
* Called when a DatabaseMapping is used to map the key in a collection. Returns the key.
*/
public Object createMapComponentFromRow(AbstractRecord dbRow, ObjectBuildingQuery query, CacheKey parentCacheKey, AbstractSession session, boolean isTargetProtected){
Object key = dbRow.get(getField());
if (getValueConverter() != null){
key = getValueConverter().convertDataValueToObjectValue(key, session);
}
return key;
}
}