org.eclipse.persistence.internal.sessions.ObjectChangeSet Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of eclipselink Show documentation
Show all versions of eclipselink Show documentation
EclipseLink build based upon Git transaction f2b9fc5
/*
* 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.internal.sessions;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.eclipse.persistence.descriptors.ClassDescriptor;
import org.eclipse.persistence.descriptors.FetchGroupManager;
import org.eclipse.persistence.descriptors.TimestampLockingPolicy;
import org.eclipse.persistence.descriptors.VersionLockingPolicy;
import org.eclipse.persistence.internal.core.helper.CoreClassConstants;
import org.eclipse.persistence.internal.descriptors.OptimisticLockingPolicy;
import org.eclipse.persistence.internal.helper.ClassConstants;
import org.eclipse.persistence.internal.identitymaps.CacheKey;
import org.eclipse.persistence.logging.SessionLog;
import org.eclipse.persistence.mappings.DatabaseMapping;
import org.eclipse.persistence.mappings.foundation.AbstractDirectMapping;
import org.eclipse.persistence.queries.FetchGroup;
import org.eclipse.persistence.queries.ReadObjectQuery;
/**
*
* Purpose: Hold the Records of change for a particular instance of an object.
*
* Description: This class uses the Primary Keys of the Object it represents,
* and the class.
*
*/
public class ObjectChangeSet implements Serializable, Comparable, org.eclipse.persistence.sessions.changesets.ObjectChangeSet {
/** Allow change sets to be compared by changes for batching. */
public static class ObjectChangeSetComparator implements Comparator, Serializable {
private static final long serialVersionUID = -7902750710186726851L;
/**
* Determine if the receiver is greater or less than the change set.
*/
@Override
public int compare(Object object1, Object object2) {
if (object1 == object2) {
return 0;
}
ObjectChangeSet left = (ObjectChangeSet)object1;
ObjectChangeSet right = (ObjectChangeSet)object2;
// Sort by changes to keep same SQL together for batching.
if ((left.changes != null) && right.changes != null) {
int size = left.changes.size();
List otherChanges = right.changes;
int otherSize = otherChanges.size();
if (size > otherSize) {
return 1;
} else if (size < otherSize) {
return -1;
}
for (int index = 0; index < size; index++) {
ChangeRecord record = (ChangeRecord)left.changes.get(index);
ChangeRecord otherRecord = (ChangeRecord)otherChanges.get(index);
int compare = record.getAttribute().compareTo(otherRecord.getAttribute());
if (compare != 0) {
return compare;
}
}
}
return left.compareTo(right);
}
}
/** This is the collection of changes */
protected List changes;
protected transient Map attributesToChanges;
protected boolean shouldBeDeleted;
protected Object id;
protected transient Class> classType;
protected String className;
protected boolean isNew;
protected boolean isAggregate;
protected Object oldKey;
protected Object newKey;
protected AbstractRecord protectedForeignKeys;
/** This member variable holds the reference to the parent UnitOfWork Change Set **/
protected transient UnitOfWorkChangeSet unitOfWorkChangeSet;
/** Used in mergeObjectChanges method for writeLock and initialWriteLock comparison of the merged change sets **/
protected transient OptimisticLockingPolicy optimisticLockingPolicy;
protected Object initialWriteLockValue;
protected Object writeLockValue;
/** Invalid change set shouldn't be merged into object in cache, rather the object should be invalidated **/
protected boolean isInvalid;
protected transient Object cloneObject;
protected boolean hasVersionChange;
/** Contains optimisticReadLockObject corresponding to the clone, non-null indicates forced changes **/
protected Boolean shouldModifyVersionField;
/** For CMP only: indicates that the object should be force updated (whether it has OptimisticLocking or not): getCmpPolicy().getForcedUpdate()==true**/
protected transient boolean hasCmpPolicyForcedUpdate;
protected transient boolean hasChangesFromCascadeLocking;
/**
* This is used during attribute level change tracking when a particular
* change was detected but that change can not be tracked (ie customer set
* entire collection in object).
*/
protected transient Set deferredSet;
/**
* Used to store the type of cache synchronization used for this object
* This variable is set just before the change set is serialized.
*/
protected int cacheSynchronizationType;
/** PERF: Cache the session cacheKey during the merge to avoid duplicate lookups. */
protected transient CacheKey activeCacheKey;
/** Cache the descriptor as it is useful and required in some places. */
protected transient ClassDescriptor descriptor;
/** return whether this change set should be recalculated after an event changes the object */
protected transient boolean shouldRecalculateAfterUpdateEvent = true;
//This controls how long the thread can wait for other thread to put Entity instance in cache
//This is not final to allow a way for the value to be changed without supporting API
public static final int MAX_TRIES = 18000;
/**
* The default constructor.
*/
public ObjectChangeSet() { }
/**
* This constructor is used to create an ObjectChangeSet that represents a regular object.
*/
public ObjectChangeSet(Object primaryKey, ClassDescriptor descriptor, Object cloneObject, UnitOfWorkChangeSet parent, boolean isNew) {
this.cacheSynchronizationType = ClassDescriptor.UNDEFINED_OBJECT_CHANGE_BEHAVIOR;
this.cloneObject = cloneObject;
this.isNew = isNew;
this.shouldBeDeleted = false;
this.id = primaryKey;
this.classType = descriptor.getJavaClass();
this.className = this.classType.getName();
this.descriptor = descriptor;
this.cacheSynchronizationType = descriptor.getCachePolicy().getCacheSynchronizationType();
this.unitOfWorkChangeSet = parent;
this.isAggregate = false;
}
public ClassDescriptor getDescriptor() {
return descriptor;
}
public void setDescriptor(ClassDescriptor descriptor) {
this.descriptor = descriptor;
}
/**
* INTERNAL:
* This method will clear the changerecords from a changeSet
*/
public void clear(boolean clearKeys) {
this.shouldBeDeleted = false;
this.changes = null;
this.attributesToChanges = null;
this.deferredSet = null;
if (clearKeys){
this.setOldKey(null);
this.setNewKey(null);
}
}
/**
* Add the attribute change record.
*/
public void addChange(ChangeRecord changeRecord) {
if (changeRecord == null) {
return;
}
String attributeName = changeRecord.getAttribute();
Map attributeToChanges = getAttributesToChanges();
List changes = getChanges();
ChangeRecord existingChangeRecord = (ChangeRecord)attributeToChanges.get(attributeName);
// change tracking may add a change to an existing attribute fix that here.
if (existingChangeRecord != null) {
changes.remove(existingChangeRecord);
}
changes.add(changeRecord);
attributeToChanges.put(attributeName, changeRecord);
dirtyUOWChangeSet();
// now let's do some house keeping.
DatabaseMapping mapping = changeRecord.getMapping();
OptimisticLockingPolicy olp = getDescriptor().getOptimisticLockingPolicy();
if (olp != null){
if ((olp.shouldUpdateVersionOnOwnedMappingChange() && mapping.isOwned()) || (olp.shouldUpdateVersionOnMappingChange())){
this.shouldModifyVersionField = true; // must update version field when owned mapping changes
}
}
}
/**
* INTERNAL:
* This method is used during attribute level change tracking when a particular
* change was detected but that change can not be tracked (ie customer set
* entire collection in object). In this case flag this attribute for
* deferred change detection at commit time.
*/
public void deferredDetectionRequiredOn(String attributeName){
getDeferredSet().add(attributeName);
}
/**
* INTERNAL:
* Convenience method used to query this change set after it has been sent by
* cache synchronization.
* @return true if this change set should contain all change information, false if only
* the identity information should be available.
*/
public boolean containsChangesFromSynchronization() {
return ((cacheSynchronizationType == ClassDescriptor.SEND_NEW_OBJECTS_WITH_CHANGES) || (cacheSynchronizationType == ClassDescriptor.SEND_OBJECT_CHANGES));
}
/**
* Ensure change sets with the same primary key are equal.
*/
@Override
public boolean equals(Object object) {
if (object instanceof ObjectChangeSet) {
return equals((ObjectChangeSet)object);
}
return false;
}
/**
* Ensure change sets with the same primary key are equal.
*/
public boolean equals(ObjectChangeSet objectChange) {
if (this == objectChange) {
return true;
} else if (this.id == null) {
//new objects are compared based on identity
return false;
}
return (this.id.equals(objectChange.id));
}
/**
* Determine if the receiver is greater or less than the change set.
*/
@Override
public int compareTo(ObjectChangeSet changeSet) {
if (this == changeSet) {
return 0;
}
if (this.id == null) {
if (changeSet.id != null) {
return -1;
} else {
return 0;
}
} else if (changeSet.id == null) {
return 1;
}
try {
return ((Comparable)this.id).compareTo(changeSet.id);
} catch (Exception exception) {
return 0;
}
}
/**
* INTERNAL:
* stores the change records indexed by the attribute names
*/
public Map getAttributesToChanges() {
if (this.attributesToChanges == null) {
this.attributesToChanges = new HashMap();
}
return this.attributesToChanges;
}
/**
* INTERNAL:
* returns the change record for the specified attribute name
*/
@Override
public org.eclipse.persistence.sessions.changesets.ChangeRecord getChangesForAttributeNamed(String attributeName) {
return (ChangeRecord)this.getAttributesToChanges().get(attributeName);
}
/**
* ADVANCED:
* This method will return a collection of the attributes changed in the object.
*/
@Override
public List getChangedAttributeNames() {
List names = new ArrayList<>();
for (org.eclipse.persistence.sessions.changesets.ChangeRecord changeRecord : getChanges()) {
names.add(changeRecord.getAttribute());
}
return names;
}
/**
* INTERNAL:
* This method returns a reference to the collection of changes within this changeSet.
*/
@Override
public List getChanges() {
if (this.changes == null) {
this.changes = new ArrayList<>();
}
return changes;
}
/**
* INTERNAL:
* This method returns the class type that this changeSet represents.
* The class type must be initialized, before this method is called.
* @return java.lang.Class or null if the class type isn't initialized.
*/
public Class> getClassType() {
return classType;
}
/**
* ADVANCE:
* This method returns the class type that this changeSet Represents.
* This requires the session to reload the class on serialization.
*/
@Override
public Class> getClassType(org.eclipse.persistence.sessions.Session session) {
if (classType == null) {
classType = session.getDatasourcePlatform().getConversionManager().convertObject(getClassName(), ClassConstants.CLASS);
}
return classType;
}
/**
* ADVANCE:
* This method returns the class type that this changeSet Represents.
* The class type should be used if the class is desired.
*/
@Override
public String getClassName() {
return className;
}
/**
* INTERNAL:
* This method is used to return the initial lock value of the object this changeSet represents.
*/
public Object getInitialWriteLockValue() {
return initialWriteLockValue;
}
/**
* This method returns the key value that this object was stored under in it's
* Respective hashmap.
*/
@Override
public Object getOldKey() {
return this.oldKey;
}
/**
* This method returns the key value that this object will be stored under in it's
* Respective hashmap.
*/
@Override
public Object getNewKey() {
return this.newKey;
}
/**
* ADVANCED:
* This method returns the primary key for the object that this change set represents.
*/
@Override
public Object getId() {
return this.id;
}
public Object getOldValue() {
AbstractSession session = null;
if(this.unitOfWorkChangeSet != null) {
session = this.unitOfWorkChangeSet.getSession();
}
return getOldValue(session);
}
public Object getOldValue(AbstractSession session) {
if (this.isNew) {
return null;
}
if (this.changes == null || this.changes.isEmpty()) {
// object has not changed
return this.cloneObject;
} else {
if(this.cloneObject != null && session != null) {
Object oldValue = this.descriptor.getObjectBuilder().buildNewInstance();
FetchGroup fetchGroup = null;
FetchGroupManager fetchGroupManager = this.descriptor.getFetchGroupManager();
if(fetchGroupManager != null) {
fetchGroup = fetchGroupManager.getObjectFetchGroup(this.cloneObject);
}
for(DatabaseMapping mapping : this.descriptor.getMappings()) {
String attributeName = mapping.getAttributeName();
if(fetchGroup == null || fetchGroup.containsAttributeInternal(attributeName)) {
ChangeRecord changeRecord = (ChangeRecord)getChangesForAttributeNamed(attributeName);
if(changeRecord != null) {
mapping.setRealAttributeValueInObject(oldValue, changeRecord.getOldValue());
} else {
mapping.setAttributeValueInObject(oldValue, mapping.getAttributeValueFromObject(this.cloneObject));
}
}
}
return oldValue;
}
}
return null;
}
public int getSynchronizationType() {
return cacheSynchronizationType;
}
/**
* INTERNAL:
* This method is used to return the complex object specified within the change record.
* The object is collected from the session which, in this case, is the unit of work.
* The object's changed attributes will be merged and added to the identity map.
*/
public Object getTargetVersionOfSourceObject(MergeManager mergeManager, AbstractSession session) {
return getTargetVersionOfSourceObject(mergeManager, session, false);
}
/**
* INTERNAL:
* This method is used to return the complex object specified within the change record.
* The object is collected from the session which, in this case, is the unit of work.
* The object's changed attributes will be merged and added to the identity map
* @param shouldRead boolean if the object can not be found should it be read in from the database.
*/
public Object getTargetVersionOfSourceObject(MergeManager mergeManager, AbstractSession targetSession, boolean shouldRead) {
Object attributeValue = null;
ClassDescriptor descriptor = getDescriptor();
if (descriptor == null) {
descriptor = targetSession.getDescriptor(getClassType(targetSession));
}
if (descriptor != null) {
if (mergeManager.getSession().isUnitOfWork()) {
// The unit of works will have a copy or a new instance must be made
if (((UnitOfWorkImpl)mergeManager.getSession()).getLifecycle() == UnitOfWorkImpl.MergePending) {
// We are merging the unit of work into the original.
attributeValue = getObjectForMerge(mergeManager, targetSession, getId(), descriptor);
if (attributeValue == null){
// Bug 502085
UnitOfWorkImpl uow = (UnitOfWorkImpl)mergeManager.getSession();
if (this.isNew()) {
attributeValue = uow.getOriginalVersionOfObject(getUnitOfWorkClone());
} else {
attributeValue = uow.getOriginalVersionOfObjectOrNull(getUnitOfWorkClone(), this, descriptor, targetSession);
}
}
} else {
// We are merging something else within the unit of work.
// this is most likely because we are updating a backup clone and can retrieve
// the working clone as the result.
attributeValue = getUnitOfWorkClone();
}
} else {
// It is not a unitOfWork so we must be merging into a distributed cache.
attributeValue = getObjectForMerge(mergeManager, targetSession, getId(), descriptor);
}
if ((attributeValue == null) && (shouldRead)) {
// If the cache does not have a copy and I should read it from the database
// Then load the object if possible
ReadObjectQuery query = new ReadObjectQuery();
query.setShouldUseWrapperPolicy(false);
query.setReferenceClass(getClassType(targetSession));
query.setSelectionId(getId());
attributeValue = targetSession.executeQuery(query);
}
}
return attributeValue;
}
/**
* INTERNAL:
* For use within the distributed merge process, this method will get an object from the shared
* cache using a readlock. If a readlock is unavailable then the merge manager will be
* transitioned to deferred locks and a deferred lock will be used.
*/
protected Object getObjectForMerge(MergeManager mergeManager, AbstractSession session, Object primaryKey, ClassDescriptor descriptor) {
Object domainObject = null;
if (primaryKey == null) {
this.activeCacheKey = null;
return null;
}
CacheKey cacheKey = session.getIdentityMapAccessorInstance().getCacheKeyForObject(primaryKey, descriptor.getJavaClass(), descriptor, true);
if (cacheKey != null) {
if (cacheKey.acquireReadLockNoWait()) {
domainObject = cacheKey.getObject();
cacheKey.releaseReadLock();
} else {
if (!mergeManager.isTransitionedToDeferredLocks()) {
session.getIdentityMapAccessorInstance().getWriteLockManager().transitionToDeferredLocks(mergeManager);
}
cacheKey.acquireDeferredLock();
domainObject = cacheKey.getObject();
int tries = 0;
while (domainObject == null) {
++tries;
if (tries > MAX_TRIES){
session.getParent().log(SessionLog.SEVERE, SessionLog.CACHE, "entity_not_available_during_merge", new Object[]{descriptor.getJavaClassName(), cacheKey.getKey(), Thread.currentThread().getName(), cacheKey.getActiveThread()});
break;
}
synchronized (cacheKey) {
if (cacheKey.isAcquired()) {
try {
cacheKey.wait(10);
} catch (InterruptedException e) {
//ignore and return
}
}
domainObject = cacheKey.getObject();
}
}
cacheKey.releaseDeferredLock();
}
} else {
domainObject = mergeManager.registerExistingObjectOfReadOnlyClassInNestedTransaction(getUnitOfWorkClone(), descriptor, session);
// There is no need to get the cache key in this case because UOW is performing
// a nested UOW merge, and no locking occurs.
}
// Set activeCacheKey.
this.activeCacheKey = cacheKey;
return domainObject;
}
/**
* INTERNAL:
* Returns the UnitOfWork Clone that this ChangeSet was built for.
*/
public Object getUnitOfWorkClone() {
return this.cloneObject;
}
/**
* INTERNAL:
* Sets the UnitOfWork Clone that this ChangeSet was built for.
*/
public void setUnitOfWorkClone(Object cloneObject) {
this.cloneObject = cloneObject;
}
/**
* ADVANCED:
* This method is used to return the parent UnitOfWorkChangeSet.
*/
@Override
public org.eclipse.persistence.sessions.changesets.UnitOfWorkChangeSet getUOWChangeSet() {
return unitOfWorkChangeSet;
}
/**
* INTERNAL:
* This method is used to return the lock value of the object this changeSet represents.
*/
@Override
public Object getWriteLockValue() {
return writeLockValue;
}
/**
* ADVANCED:
* This method will return true if the specified attribute has been changed.
* @param attributeName the name of the attribute to search for.
*/
@Override
public boolean hasChangeFor(String attributeName) {
for (org.eclipse.persistence.sessions.changesets.ChangeRecord changeRecord : getChanges()) {
if (changeRecord.getAttribute().equals(attributeName)) {
return true;
}
}
return false;
}
/**
* ADVANCED:
* Returns true if this particular changeSet has changes.
*/
@Override
public boolean hasChanges() {
// a change set must also be considered dirty if only the version number has been updated
// and the version is not a mapped field. This is required to propagate the change
// set via cache sync. to avoid opt. lock exceptions on the remote servers.
return this.isNew || this.hasVersionChange || ((this.changes != null) && (!this.changes.isEmpty()));
}
/**
* INTERNAL:
* Returns true if this particular changeSet has forced SQL changes. This is true whenever
* CMPPolicy.getForceUpdate() == true or if the object has been marked for opt. read
* lock (uow.forceUpdateToVersionField). Kept separate from 'hasChanges' because we don't
* want to merge or cache sync. a change set that has no 'real' changes.
*/
public boolean hasForcedChanges() {
return this.shouldModifyVersionField != null || this.hasCmpPolicyForcedUpdate;
}
/**
* INTERNAL:
* Holds a Boolean indicating whether version field should be modified.
* This Boolean is set by forcedUpdate into uow.getOptimisticReadLockObjects()
* for the clone object and copied here (so don't need to search for it again
* in uow.getOptimisticReadLockObjects()).
*/
public void setShouldModifyVersionField(Boolean shouldModifyVersionField) {
this.shouldModifyVersionField = shouldModifyVersionField;
if(shouldModifyVersionField != null && shouldModifyVersionField) {
// mark the version number as 'dirty'
// Note that at this point there is no newWriteLockValue - it will be set later.
// This flag is set to indicate that the change set WILL have changes.
this.hasVersionChange = true;
}
}
/**
* INTERNAL:
* Holds a Boolean indicating whether version field should be modified.
*/
public Boolean shouldModifyVersionField() {
return this.shouldModifyVersionField;
}
/**
* INTERNAL:
*/
public void setHasCmpPolicyForcedUpdate(boolean hasCmpPolicyForcedUpdate) {
this.hasCmpPolicyForcedUpdate = hasCmpPolicyForcedUpdate;
}
/**
* INTERNAL:
*/
public boolean hasCmpPolicyForcedUpdate() {
return this.hasCmpPolicyForcedUpdate;
}
/**
* INTERNAL:
* Returns true if this particular changeSet has forced SQL changes because
* of a cascade optimistic locking policy.
*/
public boolean hasForcedChangesFromCascadeLocking() {
return this.hasChangesFromCascadeLocking;
}
/**
* INTERNAL:
* Used by calculateChanges to mark this ObjectChangeSet as having to be
* flushed to the db stemming from a cascade optimistic locking policy.
*/
public void setHasForcedChangesFromCascadeLocking(boolean newValue) {
this.setShouldModifyVersionField(Boolean.TRUE);
this.hasChangesFromCascadeLocking = newValue;
}
/**
* This method overrides the hashcode method. If this set has a cacheKey then return the hashcode of the
* cache key, otherwise return the identity hashcode of this object.
*/
@Override
public int hashCode() {
if (getId() == null) {
//new objects are compared based on identity
return System.identityHashCode(this);
}
return getId().hashCode();
}
/**
* INTERNAL:
* Returns true if this particular changeSet has a Key.
*/
public boolean hasKeys() {
return (this.newKey != null) || (this.oldKey != null);
}
/**
* INTERNAL:
* Used to determine if the object change set represents an aggregate object.
*/
public boolean isAggregate() {
return isAggregate;
}
/**
* ADVANCED:
* Returns true if this ObjectChangeSet represents a new object.
*/
@Override
public boolean isNew() {
return isNew;
}
/**
* INTERNAL:
* Indicates whether the change set is invalid.
*/
public boolean isInvalid() {
return isInvalid;
}
/**
* INTERNAL:
* This method will be used to merge changes from a supplied ObjectChangeSet
* into this changeSet.
*/
public void mergeObjectChanges(ObjectChangeSet changeSetToMergeFrom, UnitOfWorkChangeSet mergeToChangeSet, UnitOfWorkChangeSet mergeFromChangeSet) {
if (this == changeSetToMergeFrom || this.isInvalid()) {
return;
}
if(changeSetToMergeFrom.optimisticLockingPolicy != null) {
// optimisticLockingPolicy != null guarantees initialWriteLockValue != null
if(this.optimisticLockingPolicy == null) {
this.optimisticLockingPolicy = changeSetToMergeFrom.optimisticLockingPolicy;
this.initialWriteLockValue = changeSetToMergeFrom.initialWriteLockValue;
this.writeLockValue = changeSetToMergeFrom.writeLockValue;
} else {
// optimisticLockingPolicy != null guarantees initialWriteLockValue != null
Object writeLockValueToCompare = this.writeLockValue;
if(writeLockValueToCompare == null) {
writeLockValueToCompare = this.initialWriteLockValue;
}
// In this merge initialWriteLockValue of this changeSet differs from
// writeLockValue of the changeSetToMergeFrom into which the merge was performed.
// Example:
// Original registered with version 1, the clone changed to version 2, uow.writeChanges is called:
// the corresponding "this" changeSet has initialWriteLockValue = 1 and writeLockValue = 2;
// custom update performed next changing the version of the object in the db to 3;
// the clone is refreshed in the uow - now it's version is 3;
// the cloned is changed to version 4, uow.commit is called:
// the corresponding changeSetToMergeFrom has initialWriteLockValue = 3 and writeLockValue = 4.
// This change set should be invalidated - the custom update would not be reflected after merge,
// therefore no merge into cache should be performed but rather the object in the cache should be invalidated.
if(this.optimisticLockingPolicy.compareWriteLockValues(writeLockValueToCompare, changeSetToMergeFrom.initialWriteLockValue) != 0) {
this.isInvalid = true;
return;
}
// Don't blindly overrite a write lock value with null. A
// consecutive change set may not have caused a version change,
// therefore the write lock value will be null in this case.
// E.g. Attribute change tracking does not discover a change
// across a relational mapping (unless a cascaded optimistic
// locking policy is used).
if (changeSetToMergeFrom.writeLockValue != null) {
this.writeLockValue = changeSetToMergeFrom.writeLockValue;
}
}
}
List changesToMerge = changeSetToMergeFrom.getChanges();
int size = changesToMerge.size();
for (int index = 0; index < size; ++index) {
ChangeRecord record = (ChangeRecord)changesToMerge.get(index);
ChangeRecord thisRecord = (ChangeRecord) getChangesForAttributeNamed(record.getAttribute());
if (thisRecord == null) {
record.updateReferences(mergeToChangeSet, mergeFromChangeSet);
record.setOwner(this);
this.addChange(record);
} else {
thisRecord.mergeRecord(record, mergeToChangeSet, mergeFromChangeSet);
}
}
this.shouldBeDeleted = changeSetToMergeFrom.shouldBeDeleted;
this.setOldKey(changeSetToMergeFrom.oldKey);
this.setNewKey(changeSetToMergeFrom.newKey);
this.hasVersionChange = changeSetToMergeFrom.hasVersionChange;
this.shouldModifyVersionField = changeSetToMergeFrom.shouldModifyVersionField;
this.hasCmpPolicyForcedUpdate = changeSetToMergeFrom.hasCmpPolicyForcedUpdate;
this.hasChangesFromCascadeLocking = changeSetToMergeFrom.hasChangesFromCascadeLocking;
this.deferredSet = changeSetToMergeFrom.deferredSet;
}
/**
* INTERNAL:
* Helper method used by readObject to read a completely serialized change set from
* the stream.
*/
public void readCompleteChangeSet(java.io.ObjectInputStream stream) throws java.io.IOException, ClassNotFoundException {
readIdentityInformation(stream);
// bug 3526981 - avoid side effects of setter methods by directly assigning variables
// still calling setOldKey to avoid duplicating the code in that method
this.changes = (List)stream.readObject();
this.oldKey = stream.readObject();
this.newKey = stream.readObject();
this.protectedForeignKeys = (AbstractRecord)stream.readObject();
}
/**
* INTERNAL:
* Helper method used by readObject to read just the information about object identity
* from a serialized stream.
*/
public void readIdentityInformation(java.io.ObjectInputStream stream) throws java.io.IOException, ClassNotFoundException {
// bug 3526981 - avoid side effects of setter methods by directly assigning variables
this.id = stream.readObject();
this.className = (String)stream.readObject();
this.writeLockValue = stream.readObject();
this.initialWriteLockValue = stream.readObject();
}
/**
* INTERNAL:
* Override the default serialization. Object Change Sets will be serialized differently
* depending on the type of cache synchronization they use.
*/
private void readObject(java.io.ObjectInputStream stream) throws java.io.IOException, ClassNotFoundException {
int cacheSyncType = stream.read();
this.cacheSynchronizationType = cacheSyncType;
// The boolean variables have been assembled into a byte.
// Extract them here
this.shouldBeDeleted = stream.readBoolean();
this.isInvalid = stream.readBoolean();
this.isNew = stream.readBoolean();
this.isAggregate = stream.readBoolean();
this.shouldModifyVersionField = (Boolean)stream.readObject();
this.hasVersionChange = stream.readBoolean();
// Only the identity information is sent with a number of cache synchronization types
// Here we decide what to read.
if (this.shouldBeDeleted || (cacheSyncType == ClassDescriptor.DO_NOT_SEND_CHANGES) || (cacheSyncType == ClassDescriptor.INVALIDATE_CHANGED_OBJECTS)) {
readIdentityInformation(stream);
} else {
readCompleteChangeSet(stream);
}
}
/**
* Set the id of the object for this change set.
*/
public void setId(Object id) {
this.id = id;
}
/**
* Set the changes.
*/
public void setChanges(List changesList) {
this.changes = changesList;
updateUOWChangeSet();
}
/**
* Set the class type.
*/
public void setClassType(Class> newValue) {
this.classType = newValue;
}
/**
* INTERNAL:
* Set the class name. The name is used for serialization with cache coordination.
*/
public void setClassName(String newValue) {
this.className = newValue;
}
/**
* INTERNAL:
* Set if this object change Set represents an aggregate
* @param isAggregate boolean true if the ChangeSet represents an aggregate
*/
public void setIsAggregate(boolean isAggregate) {
this.isAggregate = isAggregate;
}
/**
* INTERNAL:
* Set whether this ObjectChanges represents a new Object
* @param newIsNew boolean true if this ChangeSet represents a new object
*/
public void setIsNew(boolean newIsNew) {
isNew = newIsNew;
}
/**
* This method is used to set the value that this object was stored under in its respected
* map collection
*/
public void setOldKey(Object key) {
//may be merging changeSets lets make sure that we can remove based on the
//old key when we finally merge.
if ((key == null) || (this.oldKey == null)) {
this.oldKey = key;
}
}
/**
* This method is used to set the value that this object will be stored under in its respected
* map collection
*/
public void setNewKey(Object key) {
this.newKey = key;
}
/**
* This method was created in VisualAge.
* @param newValue boolean
*/
public void setShouldBeDeleted(boolean newValue) {
this.shouldBeDeleted = newValue;
}
public void setSynchronizationType(int type) {
cacheSynchronizationType = type;
}
/**
* INTERNAL:
* Used to set the parent change Set.
*/
public void setUOWChangeSet(UnitOfWorkChangeSet newUnitOfWorkChangeSet) {
unitOfWorkChangeSet = newUnitOfWorkChangeSet;
}
/**
* INTERNAL:
* This method should ONLY be used to set the initial writeLock value for
* an ObjectChangeSet when it is first built.
*/
public void setOptimisticLockingPolicyAndInitialWriteLockValue(OptimisticLockingPolicy optimisticLockingPolicy, AbstractSession session) {
// ignore optimistic locking policy if it can't compare lock values (like FieldsLockingPolicy).
if(optimisticLockingPolicy.supportsWriteLockValuesComparison()) {
this.optimisticLockingPolicy = optimisticLockingPolicy;
this.initialWriteLockValue = optimisticLockingPolicy.getWriteLockValue(cloneObject, getId(), session);
}
}
/**
* ADVANCED:
* This method is used to set the writeLock value for an ObjectChangeSet
* Any changes to the write lock value
* should to through setWriteLockValue(Object obj) so that the change set is
* marked as being dirty.
*/
public void setWriteLockValue(java.lang.Object newWriteLockValue) {
this.writeLockValue = newWriteLockValue;
// mark the version number as 'dirty'
this.hasVersionChange = true;
updateUOWChangeSet();
}
/**
* ADVANCED:
* This method is used to set the initial writeLock value for an ObjectChangeSet.
* The initial value will only be set once, and can not be overwritten.
*/
public void setInitialWriteLockValue(Object initialWriteLockValue) {
if (this.initialWriteLockValue == null) {
this.initialWriteLockValue = initialWriteLockValue;
}
}
/**
* Mark change set for a deleted object.
*/
public boolean shouldBeDeleted() {
return shouldBeDeleted;
}
@Override
public String toString() {
return this.getClass().getSimpleName() + "(" + hashCode() + ", " + this.getClassName() + ")" + getChanges().toString();
}
/**
* INTERNAL:
* Used to update a changeRecord that is stored in the CHangeSet with a new value.
*/
public void updateChangeRecordForAttribute(String attributeName, Object value) {
ChangeRecord changeRecord = (ChangeRecord)getChangesForAttributeNamed(attributeName);
if (changeRecord != null) {
changeRecord.updateChangeRecordWithNewValue(value);
}
}
/**
* ADVANCED:
* Used to update a changeRecord that is stored in the CHangeSet with a new value.
* Used when the new value is a mapped object.
*/
public void updateChangeRecordForAttributeWithMappedObject(String attributeName, Object value, AbstractSession session) {
ObjectChangeSet referenceChangeSet = (ObjectChangeSet)this.getUOWChangeSet().getObjectChangeSetForClone(value);
if (referenceChangeSet == null) {
ClassDescriptor descriptor = session.getDescriptor(value.getClass());
if (descriptor != null) {
referenceChangeSet = descriptor.getObjectBuilder().createObjectChangeSet(value, (UnitOfWorkChangeSet)this.getUOWChangeSet(), false, session);
}
}
updateChangeRecordForAttribute(attributeName, referenceChangeSet);
}
/**
* INTERNAL:
* Used to update a changeRecord that is stored in the CHangeSet with a new value.
*/
public void updateChangeRecordForAttribute(DatabaseMapping mapping, Object value, AbstractSession session, Object oldValue) {
String attributeName = mapping.getAttributeName();
ChangeRecord changeRecord = (ChangeRecord)getChangesForAttributeNamed(attributeName);
// bug 2641228 always ensure that we convert the value to the correct type
if (mapping.isDirectToFieldMapping()) {
value = ((AbstractDirectMapping)mapping).getObjectValue(value, session);
}
if (changeRecord != null) {
changeRecord.updateChangeRecordWithNewValue(value);
} else if (mapping.isDirectToFieldMapping()) {
// If it is direct to field then this is most likely the result of a forced update and
// we will need to merge this object.
changeRecord = new DirectToFieldChangeRecord(this);
changeRecord.setAttribute(attributeName);
changeRecord.setMapping(mapping);
((DirectToFieldChangeRecord)changeRecord).setNewValue(value);
((DirectToFieldChangeRecord)changeRecord).setOldValue(oldValue);
this.addChange(changeRecord);
}
}
/**
* INTERNAL:
* This method will be used when merging changesets into other changesets.
* It will fix references within a changeSet so that it's records point to
* changesets within this UOWChangeSet.
*/
public void updateReferences(UnitOfWorkChangeSet localChangeSet, UnitOfWorkChangeSet mergingChangeSet) {
int size = getChanges().size();
for (int index = 0; index < size; ++index) {
ChangeRecord record = (ChangeRecord)getChanges().get(index);
record.updateReferences(localChangeSet, mergingChangeSet);
record.setOwner(this);
}
}
/**
* INTERNAL:
* Override the default serialization since different parts of an ObjectChangeSet will
* be serialized depending on the type of CacheSynchronizationType
*/
private void writeObject(java.io.ObjectOutputStream stream) throws java.io.IOException {
stream.write(this.cacheSynchronizationType);
stream.writeBoolean(this.shouldBeDeleted);
stream.writeBoolean(this.isInvalid);
stream.writeBoolean(this.isNew);
stream.writeBoolean(this.isAggregate);
stream.writeObject(this.shouldModifyVersionField);
stream.writeBoolean(this.hasVersionChange);
if (this.shouldBeDeleted || (this.cacheSynchronizationType == ClassDescriptor.DO_NOT_SEND_CHANGES) || (this.cacheSynchronizationType == ClassDescriptor.INVALIDATE_CHANGED_OBJECTS)) {
writeIdentityInformation(stream);
} else {
writeCompleteChangeSet(stream);
}
}
/**
* INTERNAL:
* Helper method to writeObject. Write only the information necessary to identify this
* ObjectChangeSet to the stream
*/
public void writeIdentityInformation(java.io.ObjectOutputStream stream) throws java.io.IOException {
stream.writeObject(this.id);
stream.writeObject(this.className);
stream.writeObject(this.writeLockValue);
stream.writeObject(this.initialWriteLockValue);
}
/**
* INTERNAL:
* Helper method to readObject. Completely write this ObjectChangeSet to the stream
*/
public void writeCompleteChangeSet(java.io.ObjectOutputStream stream) throws java.io.IOException {
writeIdentityInformation(stream);
stream.writeObject(this.changes);
stream.writeObject(this.oldKey);
stream.writeObject(this.newKey);
stream.writeObject(this.protectedForeignKeys);
}
/**
* INTERNAL:
* Reset the change set's transient variables after serialization.
*/
public void postSerialize(Object clone, UnitOfWorkChangeSet uowChangeSet, AbstractSession session) {
this.unitOfWorkChangeSet = uowChangeSet;
// Clone is null for recursive aggregate call, (clone will be set from root call,
// but descriptor and mapping needs to be set here.
if (clone != null) {
this.cloneObject = clone;
if (this.descriptor == null) {
this.descriptor = session.getDescriptor(clone);
this.classType = clone.getClass();
}
}
if ((this.attributesToChanges == null) && (this.changes != null)) {
for (ChangeRecord change : (List)(List)this.changes) {
getAttributesToChanges().put(change.getAttribute(), change);
}
}
// Aggregates should only be cascaded to, as they need the correct descriptor from the mapping.
if ((this.changes != null) && (this.descriptor != null) && ((clone == null) || !this.descriptor.isAggregateDescriptor())) {
for (ChangeRecord change : (List)(List)this.changes) {
DatabaseMapping mapping = this.descriptor.getObjectBuilder().getMappingForAttributeName(change.getAttribute());
change.setMapping(mapping);
if ((mapping != null) && mapping.isAggregateObjectMapping()) {
AggregateChangeRecord aggregate = (AggregateChangeRecord)change;
ObjectChangeSet aggregateCacheSet = (ObjectChangeSet)aggregate.getChangedObject();
if (aggregateCacheSet != null) {
aggregateCacheSet.setDescriptor(mapping.getReferenceDescriptor());
aggregateCacheSet.postSerialize(null, uowChangeSet, session);
}
}
}
}
}
/**
* This set contains the list of attributes that must be calculated at commit time.
*/
public Set getDeferredSet() {
if (deferredSet == null){
this.deferredSet = new HashSet<>();
}
return deferredSet;
}
/**
* Check to see if there are any attributes that must be calculated at commit time.
*/
public boolean hasDeferredAttributes() {
return ! (deferredSet == null || this.deferredSet.isEmpty());
}
protected void dirtyUOWChangeSet() {
// PERF: Set the unit of work change set to dirty avoid unnecessary message sends.
UnitOfWorkChangeSet unitOfWorkChangeSet = (UnitOfWorkChangeSet)getUOWChangeSet();
if (unitOfWorkChangeSet != null) {
unitOfWorkChangeSet.setHasChanges(true);
}
}
protected void updateUOWChangeSet() {
// needed to explicitly mark parent uow as having changes. This is needed in the
// case of Optimistic read locking and ForceUpdate. In these scenarios, the object
// change set can be modified to contain 'real' changes after the uow change set has
// computed its 'hasChanges' flag. If not done, the change set will not be merged.
if (getUOWChangeSet() != null) {
((org.eclipse.persistence.internal.sessions.UnitOfWorkChangeSet)this.getUOWChangeSet()).setHasChanges(this.hasChanges());
}
}
/**
* Rebuild writeLockValue to the expected type from user format i.e XML change set has all values as String.
*/
protected void rebuildWriteLockValueFromUserFormat(ClassDescriptor descriptor, AbstractSession session) {
if (descriptor.getOptimisticLockingPolicy() instanceof TimestampLockingPolicy) {
this.writeLockValue = session.getPlatform(descriptor.getJavaClass()).getConversionManager().convertObject(this.writeLockValue, CoreClassConstants.TIMESTAMP);
this.initialWriteLockValue = session.getPlatform(descriptor.getJavaClass()).getConversionManager().convertObject(this.initialWriteLockValue, CoreClassConstants.TIMESTAMP);
} else if (descriptor.getOptimisticLockingPolicy() instanceof VersionLockingPolicy) {
this.writeLockValue = session.getPlatform(descriptor.getJavaClass()).getConversionManager().convertObject(this.writeLockValue, ClassConstants.BIGDECIMAL);
this.initialWriteLockValue = session.getPlatform(descriptor.getJavaClass()).getConversionManager().convertObject(this.initialWriteLockValue, ClassConstants.BIGDECIMAL);
}
}
/**
* INTERNAL:
* Remove the change.
* Used by the event mechanism to reset changes after client has updated the object within an event.
*/
public void removeChange(String attributeName){
Object record = getChangesForAttributeNamed(attributeName);
if (record != null) {
getChanges().remove(record);
this.attributesToChanges.remove(attributeName);
}
}
/**
* Remove object represent this change set from identity map. If change set is in XML format, rebuild pk to the correct class type from String
*/
protected void removeFromIdentityMap(AbstractSession session) {
session.getIdentityMapAccessor().removeFromIdentityMap(getId(), getClassType(session));
}
/**
* INTERNAL:
* Indicates whether the object in session cache should be invalidated.
* @param original Object is from session's cache into which the changes are about to be merged, non null.
* @param session AbstractSession into which the changes are about to be merged;
*/
public boolean shouldInvalidateObject(Object original, AbstractSession session) {
// Either no optimistic locking or no version change.
if (optimisticLockingPolicy == null) {
return false;
}
if (session.isRemoteSession()){
//remote unit of work not supported as version values in UOW will be updated
//when the committed UOW is received on the client. That updated value will be
//set in the UOW cache when the changeset is calculated giving the changeset the
//incorrect initialWriteLockValue value
//version number comparison will still be completed later.
return false;
}
if(isInvalid()) {
return true;
}
Object originalWriteLockValue = optimisticLockingPolicy.getWriteLockValue(original, getId(), session);
// initialWriteLockValue and originalWriteLockValue are not equal.
// Example:
// original registered in uow with version 1 (originalWriteLockValue);
// uow.beginEarlyTransaction();
// custom update run through the uow changes the version on the object in the db to 2;
// the clone is refreshed - now it has version 2;
// on uow.commit or uow.writeChanges changeSet is created with initialWriteLockValue = 2;
// The original in the cache should be invalidated - the custom update would not be reflected after merge.
if (this.initialWriteLockValue == null){
if (this.hasChanges()){
return true; // no initial version was available but we will be merging changes with unknown version force invalidation
}else{
return false; // don't invalidate as we are not merging anything anyway
}
}
if (originalWriteLockValue != null && optimisticLockingPolicy.compareWriteLockValues(initialWriteLockValue, originalWriteLockValue) != 0) {
return true;
} else {
return false;
}
}
/**
* INTERNAL:
* PERF: Return the session cache-key, cached during the merge.
*/
public CacheKey getActiveCacheKey() {
return activeCacheKey;
}
/**
* INTERNAL:
* PERF: Set the session cache-key, cached during the merge.
*/
public void setActiveCacheKey(CacheKey activeCacheKey) {
this.activeCacheKey = activeCacheKey;
}
/**
* ADVANCED
* Returns true if this ObjectChangeSet should be recalculated after changes in event
*/
@Override
public boolean shouldRecalculateAfterUpdateEvent() {
return shouldRecalculateAfterUpdateEvent;
}
/**
* ADVANCED
* Set whether this ObjectChangeSet should be recalculated after changes in event
*/
@Override
public void setShouldRecalculateAfterUpdateEvent(boolean shouldRecalculateAfterUpdateEvent) {
this.shouldRecalculateAfterUpdateEvent = shouldRecalculateAfterUpdateEvent;
}
public boolean hasVersionChange() {
return hasVersionChange;
}
public void setHasVersionChange(boolean hasVersionChange) {
this.hasVersionChange = hasVersionChange;
}
public int getCacheSynchronizationType() {
return cacheSynchronizationType;
}
public void setCacheSynchronizationType(int cacheSynchronizationType) {
this.cacheSynchronizationType = cacheSynchronizationType;
}
public void setIsInvalid(boolean isInvalid) {
this.isInvalid = isInvalid;
}
public AbstractRecord getProtectedForeignKeys() {
return this.protectedForeignKeys;
}
public void setProtectedForeignKeys(AbstractRecord protectedForeignKeys) {
this.protectedForeignKeys = protectedForeignKeys;
}
public boolean hasProtectedForeignKeys() {
return (this.protectedForeignKeys != null) && (this.protectedForeignKeys.size() > 0);
}
}