blob: e82aaf2d6f190f808865e11f4f954d4135abf17a [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
// // 30/05/2012-2.4 Guy Pelletier
// - 354678: Temp classloader is still being used during metadata processing
package org.eclipse.persistence.mappings.foundation;
import java.util.*;
import org.eclipse.persistence.descriptors.ClassDescriptor;
import org.eclipse.persistence.exceptions.*;
import org.eclipse.persistence.expressions.Expression;
import org.eclipse.persistence.internal.descriptors.*;
import org.eclipse.persistence.internal.helper.*;
import org.eclipse.persistence.internal.identitymaps.CacheKey;
import org.eclipse.persistence.internal.queries.JoinedAttributeManager;
import org.eclipse.persistence.internal.sessions.*;
import org.eclipse.persistence.mappings.*;
import org.eclipse.persistence.mappings.converters.Converter;
import org.eclipse.persistence.queries.*;
/**
* Chunks of data from non-relational data sources can have an
* embedded component objects. These can be
* mapped using this mapping. The format of the embedded
* data is determined by the reference descriptor.
*
* @author Big Country
* @since TOPLink/Java 3.0
*/
public abstract class AbstractCompositeObjectMapping extends AggregateMapping {
/** The aggregate object is stored in a single field. */
protected DatabaseField field;
/** Allows user defined conversion between the object attribute value and the database value. */
protected Converter converter;
/**
* Default constructor.
*/
protected AbstractCompositeObjectMapping() {
super();
}
/**
* 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 discover and persist new objects during commit.
*/
@Override
public void cascadeDiscoverAndPersistUnregisteredNewObjects(Object object, Map newObjects, Map unregisteredExistingObjects, Map visitedObjects, UnitOfWorkImpl uow, Set cascadeErrors) {
Object objectReferenced = getRealAttributeValueFromObject(object, uow);
if (objectReferenced != null) {
ObjectBuilder builder = getReferenceDescriptor(objectReferenced.getClass(), uow).getObjectBuilder();
builder.cascadeRegisterNewForCreate(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);
}
}
/**
* Return the fields mapped by the mapping.
*/
@Override
protected Vector collectFields() {
Vector fields = new Vector(1);
fields.addElement(this.getField());
return fields;
}
/**
* PUBLIC:
* Return the converter on the mapping.
* A converter can be used to convert between the object's value and database value of the attribute.
*/
public Converter getConverter() {
return converter;
}
/**
* INTERNAL:
* The aggregate object is held in a single field.
*/
@Override
public DatabaseField getField() {
return field;
}
/**
* PUBLIC:
* Indicates if there is a converter on the mapping.
*/
public boolean hasConverter() {
return getConverter() != null;
}
/**
* INTERNAL:
*/
@Override
public boolean isAbstractCompositeObjectMapping() {
return true;
}
/**
* 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());
// initialize the converter - if necessary
if (hasConverter()) {
getConverter().initialize(this, session);
}
}
/**
* INTERNAL:
* Set the value of the attribute mapped by this mapping.
*/
@Override
public void setAttributeValueInObject(Object object, Object value) throws DescriptorException {
// PERF: Direct variable access.
try {
this.attributeAccessor.setAttributeValueInObject(object, value);
} catch (DescriptorException exception) {
exception.setMapping(this);
throw exception;
}
}
/**
* PUBLIC:
* Set the converter on the mapping.
* A converter can be used to convert between the object's value and database value of the attribute.
*/
public void setConverter(Converter converter) {
this.converter = converter;
}
/**
* The aggregate object is held in a single field.
*/
public void setField(DatabaseField field) {
this.field = field;
}
/**
* INTERNAL:
* Extract and return value of the field from the object
*/
@Override
public Object valueFromObject(Object object, DatabaseField field, AbstractSession session) throws DescriptorException {
Object attributeValue = this.getAttributeValueFromObject(object);
if(this.getConverter() != null) {
this.getConverter().convertObjectValueToDataValue(attributeValue, session);
}
if (attributeValue == null) {
return null;
} else {
return this.getObjectBuilder(attributeValue, session).extractValueFromObjectForField(attributeValue, field, session);
}
}
/**
* INTERNAL:
* Extract and return the aggregate object from
* the specified 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);
Integer refreshCascade = null;
if (sourceQuery != null && sourceQuery.isObjectBuildingQuery() && sourceQuery.shouldRefreshIdentityMapResult()){
refreshCascade = sourceQuery.getCascadePolicy();
}
//get the clone root.
return buildClonePart(cached, executionSession.getIdentityMapAccessor().getFromIdentityMap(cacheKey.getKey(), referenceClass), cacheKey, attributeValue, refreshCascade, executionSession);
}
return result;
} else if (!this.isCacheable && !isTargetProtected && (cacheKey != null)) {
return null;
}
}
if (row.hasSopObject()) {
return getAttributeValueFromObject(row.getSopObject());
}
Object fieldValue = row.get(this.field);
// BUG#2667762 there could be whitespace in the row instead of null
if ((fieldValue == null) || (fieldValue instanceof String)) {
return null;
}
// pretty sure we can ignore inheritance here:
AbstractRecord nestedRow = this.referenceDescriptor.buildNestedRowFromFieldValue(fieldValue);
ClassDescriptor descriptor = this.referenceDescriptor;
if (descriptor.hasInheritance()) {
Class<?> nestedElementClass = descriptor.getInheritancePolicy().classFromRow(nestedRow, executionSession);
descriptor = getReferenceDescriptor(nestedElementClass, executionSession);
}
ObjectBuilder objectBuilder = descriptor.getObjectBuilder();
Object toReturn = buildCompositeObject(objectBuilder, nestedRow, sourceQuery, cacheKey, joinManager, executionSession);
if (this.converter != null) {
toReturn = this.converter.convertDataValueToObjectValue(toReturn, executionSession);
}
return toReturn;
}
/**
* INTERNAL:
* Builds a shallow original object. Only direct attributes and primary
* keys are populated. In this way the minimum original required for
* instantiating a working copy clone can be built without placing it in
* the shared cache (no concern over cycles).
*/
@Override
public void buildShallowOriginalFromRow(AbstractRecord row, Object original, JoinedAttributeManager joinManager, ObjectBuildingQuery sourceQuery, AbstractSession executionSession) {
Object fieldValue = row.get(this.getField());
// BUG#2667762 there could be whitespace in the row instead of null
if ((fieldValue == null) || (fieldValue instanceof String)) {
return;
}
// pretty sure we can ignore inheritance here:
AbstractRecord nestedRow = this.getReferenceDescriptor().buildNestedRowFromFieldValue(fieldValue);
ClassDescriptor descriptor = this.getReferenceDescriptor();
if (descriptor.hasInheritance()) {
Class<?> nestedElementClass = descriptor.getInheritancePolicy().classFromRow(nestedRow, executionSession);
descriptor = this.getReferenceDescriptor(nestedElementClass, executionSession);
}
ObjectBuilder objectBuilder = descriptor.getObjectBuilder();
// instead of calling buildCompositeObject, which calls either objectBuilder.
// buildObject or buildNewInstance and buildAttributesIntoObject, do the
// following always. Since shallow original no concern over cycles or caching.
Object element = objectBuilder.buildNewInstance();
objectBuilder.buildAttributesIntoShallowObject(element, nestedRow, sourceQuery);
setAttributeValueInObject(original, element);
}
protected abstract Object buildCompositeObject(ObjectBuilder objectBuilder, AbstractRecord nestedRow, ObjectBuildingQuery query, CacheKey parentCacheKey, JoinedAttributeManager joinManger, AbstractSession targetSession);
/**
* INTERNAL:
* Build the value for the database field and put it in the
* specified database row.
*/
@Override
public void writeFromObjectIntoRow(Object object, AbstractRecord record, AbstractSession session, WriteType writeType) throws DescriptorException {
if (this.isReadOnly()) {
return;
}
Object attributeValue = this.getAttributeValueFromObject(object);
if(getConverter() != null) {
getConverter().convertObjectValueToDataValue(attributeValue, session);
}
if (attributeValue == null) {
record.put(this.getField(), null);
} else {
Object fieldValue = buildCompositeRow(attributeValue, session, record, writeType);
record.put(this.getField(), fieldValue);
}
}
protected abstract Object buildCompositeRow(Object attributeValue, AbstractSession session, AbstractRecord record, WriteType writeType);
/**
* INTERNAL:
* In case Query By Example is used, this method builds and returns an expression that
* corresponds to a single attribute and it's value.
*/
@Override
public Expression buildExpression(Object queryObject, QueryByExamplePolicy policy, Expression expressionBuilder, Map processedObjects, AbstractSession session) {
if (policy.shouldValidateExample()){
throw QueryException.unsupportedMappingQueryByExample(queryObject.getClass().getName(), this);
}
return null;
}
/**
* INTERNAL:
* If it has changed, build the value for the database field and put it in the
* specified database row.
* If any part of the aggregate object has changed, the entire object is
* written to the database row (i.e. partial updates are not supported).
*/
@Override
public void writeFromObjectIntoRowForUpdate(WriteObjectQuery query, AbstractRecord row) throws DescriptorException {
if (query.getSession().isUnitOfWork()) {
if (this.compareObjects(query.getObject(), query.getBackupClone(), query.getSession())) {
return;// nothing has changed
}
}
this.writeFromObjectIntoRow(query.getObject(), row, query.getSession(), WriteType.UPDATE);
}
/**
* INTERNAL:
* Get the attribute value from the object and add the appropriate
* values to the specified database row.
*/
@Override
public void writeFromObjectIntoRowWithChangeRecord(ChangeRecord changeRecord, AbstractRecord row, AbstractSession session, WriteType writeType) throws DescriptorException {
Object object = ((ObjectChangeSet)changeRecord.getOwner()).getUnitOfWorkClone();
this.writeFromObjectIntoRow(object, row, session, writeType);
}
/**
* INTERNAL:
* Write fields needed for insert into the template for with null values.
*/
@Override
public void writeInsertFieldsIntoRow(AbstractRecord record, AbstractSession session) {
if (this.isReadOnly()) {
return;
}
record.put(this.getField(), null);
}
}