blob: aaa788014bfb73d65c6275362d04c49f5b3bccf1 [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.descriptors;
import java.util.*;
import org.eclipse.persistence.internal.descriptors.OptimisticLockingPolicy;
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.internal.helper.*;
import org.eclipse.persistence.internal.identitymaps.CacheKey;
import org.eclipse.persistence.queries.*;
import org.eclipse.persistence.expressions.*;
import org.eclipse.persistence.exceptions.*;
/**
* <p><b>Purpose</b>: An abstract superclass of some implementations of the OptimisticLockingPolicy
* interface. All of the subclasses of this class implement OptimisticLocking
* based on mapped fields in the object. These fields are only compared and not modified.
* Any modification (incrementing etc..) must be handled by the application.
*
* @see AllFieldsLockingPolicy
* @see ChangedFieldsLockingPolicy
* @see SelectedFieldsLockingPolicy
* @since TopLink 2.1
* @author Peter Krogh
*/
public abstract class FieldsLockingPolicy implements OptimisticLockingPolicy {
protected ClassDescriptor descriptor;
protected List<DatabaseField> allNonPrimaryKeyFields;
/**
* PUBLIC:
* Create a new field locking policy.
* A field locking policy is based on locking on a subset of fields by comparing with their previous values to detect field-level collisions.
* Note: the unit of work must be used for all updates when using field locking.
*/
protected FieldsLockingPolicy() {
super();
}
/**
* INTERNAL:
* Add update fields for template row.
* These are any unmapped fields required to write in an update.
* Since all fields are mapped, there is nothing required.
*/
@Override
public void addLockFieldsToUpdateRow(AbstractRecord Record, AbstractSession session) {
// Nothing required.
}
/**
* INTERNAL:
* Values to be included in the locking mechanism are added
* to the translation row. Set the translation row to all the original field values.
*/
@Override
public abstract void addLockValuesToTranslationRow(ObjectLevelModifyQuery query);
/**
* INTERNAL:
* Returns the fields that should be compared in the where clause.
* In this case, it is all the fields, except for the primary key
* and class indicator fields.
* This is called during lazy initialization.
*/
protected List<DatabaseField> buildAllNonPrimaryKeyFields() {
List<DatabaseField> fields = new ArrayList<>();
for (DatabaseField dbField : descriptor.getSelectionFields()) {
if (!isPrimaryKey(dbField)) {
if (descriptor.hasInheritance()) {
DatabaseField classField = descriptor.getInheritancePolicy().getClassIndicatorField();
if (!((classField == null) || dbField.equals(classField))) {
fields.add(dbField);
}
} else {
fields.add(dbField);
}
}
}
/* CR#... nullpoint occurs if null is returned, not sure why this was here.
if (fields.isEmpty()) {
return null;
}*/
return fields;
}
/**
* INTERNAL:
* When given an expression, this method will return a new expression with the optimistic
* locking values included. The values are taken from the passed in database row.
* This expression will be used in a delete call.
*/
@Override
public Expression buildDeleteExpression(DatabaseTable table, Expression mainExpression, AbstractRecord row) {
return mainExpression.and(buildExpression(table, row, null, mainExpression.getBuilder()));
}
/**
* INTERNAL:
* returns the expression to be used in both the delete and update where clause.
*/
protected Expression buildExpression(DatabaseTable table, AbstractRecord transRow, AbstractRecord modifyRow, ExpressionBuilder builder) {
Expression exp = null;
DatabaseField field;
Iterator<DatabaseField> iterator = getFieldsToCompare(table, transRow, modifyRow).iterator();
if (iterator.hasNext()) {
field = iterator.next();//First element
exp = builder.getField(field).equal(builder.getParameter(field));
}
while (iterator.hasNext()) {
field = iterator.next();
exp = exp.and(builder.getField(field).equal(builder.getParameter(field)));
}
return exp;
}
/**
* INTERNAL:
* This method must be included in any locking policy. When given an
* expression, this method will return a new expression with the optimistic
* locking values included. The values are taken from the passed in database row.
* This expression will be used in a delete call.
*/
@Override
public Expression buildUpdateExpression(DatabaseTable table, Expression mainExpression, AbstractRecord transRow, AbstractRecord modifyRow) {
return mainExpression.and(buildExpression(table, transRow, modifyRow, mainExpression.getBuilder()));
}
/**
* INTERNAL:
* Clone the policy
*/
@Override
public Object clone() {
try {
return super.clone();
} catch (CloneNotSupportedException e) {
return null;
}
}
/**
* INTERNAL:
* Indicates whether compareWriteLockValues method is supported by the policy.
* Numeric or timestamp lock values could be compared:
* for every pair of values v1 and v2 - either {@literal v1<v2; or v1==v2; or v1>v2}.
* However it's impossible to compare values for FieldsLockingPolicy for two reasons:
* 1. there is no "linear order": {@literal v1<v2 and v>v2} is not defined: either v1==v2 or v1!=v2;
* 2. locking value is not a single field which is not part of mapped object value
* but rather a set of object's mapped fields. That means any object's mapped attribute change
* is potentially a change of the locking value.
* For ChangedFieldsLockingPolicy every mapped attribute's change is a change of locking value.
* The pattern used by versioning: "if the original locking value is unchanged
* then the object hasn't been changed outside of the application", which allows
* to distinguish between the change made inside and outside the application,
* doesn't work for fields locking.
* It degenerates into useless pattern: "if the original locking value is unchanged
* then the object hasn't been changed".
*
* Use compareWriteLockValues method only if this method returns true.
*/
@Override
public boolean supportsWriteLockValuesComparison() {
return false;
}
/**
* INTERNAL:
* This method shouldn't be called if supportsWriteLockValuesComparison() returns false.
* This method compares two writeLockValues.
* The writeLockValues should be non-null and of the correct type.
* Returns:
* -1 if value1 is less (older) than value2;
* 0 if value1 equals value2;
* 1 if value1 is greater (newer) than value2.
* Throws:
* NullPointerException if the passed value is null;
* ClassCastException if the passed value is of a wrong type.
*/
@Override
public int compareWriteLockValues(Object value1, Object value2){
// should never be called because supportsWriteLockValuesComparison() returns false.
return -1;
}
/**
* INTERNAL:
* Returns the fields that should be compared in the where clause.
* In this case, it is all the fields, except for the primary key
* and class indicator field.
*/
protected List<DatabaseField> getAllNonPrimaryKeyFields() {
if (allNonPrimaryKeyFields == null) {
allNonPrimaryKeyFields = buildAllNonPrimaryKeyFields();
}
return allNonPrimaryKeyFields;
}
/**
* INTERNAL:
* filter the fields based on the passed in table. Only return fields of this table.
*/
protected List<DatabaseField> getAllNonPrimaryKeyFields(DatabaseTable table) {
List<DatabaseField> filteredFields = new ArrayList<>();
for (DatabaseField dbField : getAllNonPrimaryKeyFields()) {
if (dbField.getTableName().equals(table.getName())) {
filteredFields.add(dbField);
}
}
return filteredFields;
}
/**
* INTERNAL:
* This is the base value that is older than all other values, it is used in the place of
* null in some situations.
*/
@Override
public <T> T getBaseValue(){
return null; // this locking type does not store values in the cache
}
/**
* INTERNAL:
* Returns the fields that should be compared in the where clause.
* This method must be implemented by the subclass
*/
protected abstract List<DatabaseField> getFieldsToCompare(DatabaseTable table, AbstractRecord transRow, AbstractRecord modifyRow);
/**
* INTERNAL:
* Return the write lock field.
*/
@Override
public DatabaseField getWriteLockField() {
// Does not apply to any field locking policy, so return null
return null;
}
/**
* INTERNAL:
*/
@Override
public Expression getWriteLockUpdateExpression(ExpressionBuilder builder, AbstractSession session) {
// Does not apply to any field locking policy, so return null
return null;
}
/**
* ADVANCED:
* returns the LockOnChange mode for this policy. This mode specifies if a
* Optimistic Write lock should be enforced on this entity when a set of mappings are changed.
* Unfortunately this locking policy can not enforce an optimistic write lock unless a FK or DTF field
* has changed so this type returns LockOnChange.NONE
*/
@Override
public LockOnChange getLockOnChangeMode(){
return LockOnChange.NONE;
}
/**
* INTERNAL:
* Return the value that should be stored in the identity map. If the value
* is stored in the object, then return a null.
*/
@Override
public <T> T getValueToPutInCache(AbstractRecord row, AbstractSession session) {
return null;
}
/**
* INTERNAL:
* Return the number of version difference between the two states of the object.
*/
@Override
public int getVersionDifference(Object currentValue, Object domainObject, Object primaryKeys, AbstractSession session) {
// There is no way of knowing what the difference is so return 0
// This should never be called for field locking.
return 0;
}
/**
* INTERNAL:
* This method will return the optimistic lock value for the object
*/
@Override
public <T> T getWriteLockValue(Object domainObject, Object primaryKey, AbstractSession session) {
//There is no way of knowing if this value is newer or not, so always return true.
return null;
}
/**
* INTERNAL:
* It is responsible for initializing the policy;
*/
@Override
public void initialize(AbstractSession session) {
// If the version field is not in the primary table, then they cannot be batched together.
if (this.descriptor.getTables().size() > 0) {
this.descriptor.setHasMultipleTableConstraintDependecy(true);
}
}
/**
* INTERNAL:
* It is responsible for initializing the policy;
*/
@Override
public void initializeProperties() {
//nothing to do
}
/**
* PUBLIC:
* Return true if the lock value is stored in the cache.
*/
@Override
public boolean isStoredInCache() {
return false;
}
/**
* PUBLIC:
* Return true if the policy uses cascade locking. Currently, not supported
* on this policy at this time.
*/
@Override
public boolean isCascaded() {
return false;
}
/**
* INTERNAL:
* Compares the value and the value from the object
* (or cache). Will return true if the object is newer
* than the row.
*/
@Override
public boolean isNewerVersion(Object currentValue, Object domainObject, Object primaryKey, AbstractSession session) {
//There is no way of knowing if this value is newer or not, so always return true.
return true;
}
/**
* INTERNAL:
* Compares the value from the row and from the object
* (or cache). Will return true if the object is newer
* than the row.
*/
@Override
public boolean isNewerVersion(AbstractRecord Record, Object domainObject, Object primaryKey, AbstractSession session) {
//There is no way of knowing if this value is newer or not, so always return true.
return true;
}
/**
* INTERNAL:
* Returns whether or not this field is a primary key.
* This method will also return true for secondary table primarykeys
*/
protected boolean isPrimaryKey(DatabaseField dbField) {
if (descriptor.getPrimaryKeyFields().contains(dbField)) {
return true;
} else {
if (descriptor.isMultipleTableDescriptor()) {
for (Iterator<Map<DatabaseField, DatabaseField>> enumtr = descriptor.getAdditionalTablePrimaryKeyFields().values().iterator();
enumtr.hasNext();) {
if (enumtr.next().containsKey(dbField)) {
return true;
}
}
}
}
return false;
}
/**
* INTERNAL:
* Only applicable when the value is stored in the cache.
*/
@Override
public void mergeIntoParentCache(UnitOfWorkImpl uow, Object primaryKey, Object object) {
// nothing to do
}
/**
* INTERNAL:
* This method should merge changes from the parent into the child.
*
* #see this method in VersionLockingPolicy
*/
@Override
public void mergeIntoParentCache(CacheKey unitOfWorkCacheKey, CacheKey parentSessionCacheKey){
// nothing to do
}
/**
* INTERNAL: Set method for all the primary keys
*/
protected void setAllNonPrimaryKeyFields(List<DatabaseField> allNonPrimaryKeyFields) {
this.allNonPrimaryKeyFields = allNonPrimaryKeyFields;
}
/**
* INTERNAL: Set method for the descriptor
*/
@Override
public void setDescriptor(ClassDescriptor descriptor) {
this.descriptor = descriptor;
}
/**
* ADVANCED:
* Sets the LockOnChange mode for this policy. This mode specifies if a
* Optimistic Write lock should be enforced on this entity when set of mappings are changed.
* Unfortunately this locking policy can not always force an optimistic lock unless the core fields have changed
*/
@Override
public void setLockOnChangeMode(LockOnChange lockOnChangeMode){
//no-op for this type
}
/**
* INTERNAL:
* Put the initial writelock value into the modifyRow.
* There is nothing to do because all the lock values are in the mappings.
*/
@Override
public void setupWriteFieldsForInsert(ObjectLevelModifyQuery query) {
//nothing to do.
}
/**
* INTERNAL:
* Nothing to do because all updates are handled by the application
*/
@Override
public void updateRowAndObjectForUpdate(ObjectLevelModifyQuery query, Object domainObject) {
//nothing to do
}
/**
* INTERNAL:
* Returns true if the policy has been set to set an optimistic read lock when a owning mapping changes.
* Unfortunately this locking policy can not always force an optimistic lock unless the core fields have changed
*/
@Override
public boolean shouldUpdateVersionOnOwnedMappingChange(){
return false;
}
/**
* INTERNAL:
* Returns true if the policy has been set to set an optimistic read lock when any mapping changes.
* Unfortunately this locking policy can not always force an optimistic lock unless the core fields have changed
*/
@Override
public boolean shouldUpdateVersionOnMappingChange(){
return false;
}
/**
* INTERNAL:
* Check the row count for lock failure.
*/
@Override
public void validateDelete(int rowCount, Object object, DeleteObjectQuery query) {
if (rowCount <= 0) {
// Mark the object as invalid in the session cache.
query.getSession().getParentIdentityMapSession(query, true, true).getIdentityMapAccessor().invalidateObject(object);
throw OptimisticLockException.objectChangedSinceLastReadWhenDeleting(object, query);
}
}
/**
* INTERNAL:
* Check the row count for lock failure.
*/
@Override
public void validateUpdate(int rowCount, Object object, WriteObjectQuery query) {
if (rowCount <= 0) {
// Mark the object as invalid in the session cache.
query.getSession().getParentIdentityMapSession(query, true, true).getIdentityMapAccessor().invalidateObject(object);
throw OptimisticLockException.objectChangedSinceLastReadWhenUpdating(object, query);
}
}
/**
* INTERNAL:
* throw an exception if not inside a unit of work at this point
*/
protected void verifyUsage(AbstractSession session) {
if (!session.isUnitOfWork()) {
throw ValidationException.fieldLevelLockingNotSupportedWithoutUnitOfWork();
}
}
}