/*
 * Copyright (c) 1998, 2018 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.Arrays;
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 java.util.Vector;

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.descriptors.OptimisticLockingPolicy;
import org.eclipse.persistence.internal.helper.ClassConstants;
import org.eclipse.persistence.internal.identitymaps.CacheId;
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;

/**
 * <p>
 * <b>Purpose</b>: Hold the Records of change for a particular instance of an object.
 * <p>
 * <b>Description</b>: This class uses the Primary Keys of the Object it represents,
 * and the class.
 * <p>
 */
public class ObjectChangeSet implements Serializable, Comparable<ObjectChangeSet>, 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<org.eclipse.persistence.sessions.changesets.ChangeRecord> changes;
    protected transient Map<String, ChangeRecord> 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<String> 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<String> getChangedAttributeNames() {
        List<String> names = new ArrayList<String>();
        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<org.eclipse.persistence.sessions.changesets.ChangeRecord> getChanges() {
        if (this.changes == null) {
            this.changes = new ArrayList<org.eclipse.persistence.sessions.changesets.ChangeRecord>();
        }
        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 = (Class)((AbstractSession)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 keys for the object that this change set represents.
     */
    @Override
    @Deprecated
    public Vector getPrimaryKeys() {
        if (this.id instanceof CacheId) {
            return new Vector(Arrays.asList(((CacheId)this.id).getPrimaryKey()));
        }
        Vector primaryKey = new Vector(1);
        primaryKey.add(this.id);
        return primaryKey;
    }

    /**
     * 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.booleanValue()) {
            // 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<ChangeRecord>)(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<ChangeRecord>)(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<String> getDeferredSet() {
        if (deferredSet == null){
            this.deferredSet = new HashSet<String>();
        }
        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, ClassConstants.JavaSqlTimestamp_Class);
            this.initialWriteLockValue = session.getPlatform(descriptor.getJavaClass()).getConversionManager().convertObject(this.initialWriteLockValue, ClassConstants.JavaSqlTimestamp_Class);
        } 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
     * @return
     */
    @Override
    public boolean shouldRecalculateAfterUpdateEvent() {
        return shouldRecalculateAfterUpdateEvent;
    }

    /**
     * ADVANCED
     * Set whether this ObjectChangeSet should be recalculated after changes in event
     * @return
     */
    @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);
    }

}

