/*
 * 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.sdo;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import commonj.sdo.ChangeSummary;
import commonj.sdo.DataGraph;
import commonj.sdo.DataObject;
import commonj.sdo.Property;
import commonj.sdo.Sequence;
import commonj.sdo.helper.HelperContext;
import org.eclipse.persistence.sdo.helper.ListWrapper;
import org.eclipse.persistence.sdo.helper.SDOCopyHelper;

/**
 * <p><b>Purpose</b>:A change summary is used to record changes to DataObjects.
 * <p><b>Responsibilities</b>:<ul>
 * <li> Track changes to DataObjects that are within the scope of this ChangeSummary (based on the root object of the Changesummary).
 * <li> Track if those DataObjects are created, modified or deleted.
 * <li> Return the values at the time that ChangeSummary logging was turned on for DataObjects in scope.
 * </ul>
 * <p>This class is implemented as a Memento (283) [GOF - Gamma, Helm, Johnson, Vlissides] Design Pattern.<br>
 * (Without violating encapsulation, capture and externalize an object's internal state so that the object can be restored to this state later.)<br>
 * <p>The class also realizes some aspects of the Command (233) Pattern - the undo-able operation part (from the first change).<br>
 * (Encapsulate a request as an object, thereby letting you parameterize clients with different requests,
 * queue or log requests, and support undo-able operations.)
 */
public class SDOChangeSummary implements ChangeSummary {
    private SDODataObject rootDataObject;
    private boolean logging;

    //loggingMapping is the boolean that's mapped in OX, we don't really want to turn
    //on logging until after the XML has all been processed at which time we set the
    //real logging boolean
    private boolean loggingMapping;
    private DataGraph dataGraph;
    private List createdList;
    private List deletedList;
    private Map deepCopies;
    private List createdXPaths;//for ox mapping

    /** The deletedXPaths field is picked up reflectively during marshal/unmarshal operations. */
    @SuppressWarnings("unused") private List deletedXPaths; //for ox mapping
    private List modifiedDoms;
    private Map unsetPropsMap;

    /** To implement ChangeSummary undo we require a copy of the original state of our model.
     * The originalValueStores will be populated when we start logging - it will then point to the
     * currentValueStore while the currentValueStore will be shallow copied (its child ValueStores
     * are shared between both ValueStores)
     */
    private Map originalValueStores;// HashMap<DataObject, ValueStore>
    private Map originalElements;

    // old values
    private Map oldContainer;// Key is DataObject, value is DataObject
    private Map oldContainmentProperty;// Key is DataObject, value is Property

    /** Cache map of originalSequences Map<DataObject, Sequence> */
    private Map oldSequences;// Key is DataObject, value is Sequence

    /** Map of originalSequence Map<DataObject, Sequence> */
    private Map originalSequences;// Key is DataObject, value is Sequence
    private Map unsetOCPropsMap;

    /** A HashMap of List objects keyed off SDODataObject, null key:value permitted */
    private Map oldSettings;// List of SDOSettings

    /** */
    private Map reverseDeletedMap;

    /** Hold the context containing all helpers so that we can preserve inter-helper relationships */
    private HelperContext aHelperContext;

    public SDOChangeSummary() {
        // HelperContext is set during unmarshalling in SDOUnmarshalListener
        createdList = new ArrayList();
        deepCopies = new HashMap();
        deletedList = new ArrayList();
        oldSettings = new HashMap();
        oldContainer = new HashMap();
        oldContainmentProperty = new HashMap();
        unsetPropsMap = new HashMap();
        unsetOCPropsMap = new HashMap();
        originalValueStores = new HashMap();
        originalElements = new HashMap();
        reverseDeletedMap = new HashMap();
    }

    public SDOChangeSummary(SDODataObject dataObject, HelperContext aContext) {
        this();
        aHelperContext = aContext;
        rootDataObject = dataObject;
    }

    public SDOChangeSummary(SDODataGraph dataGraph, HelperContext aContext) {
        this();
        aHelperContext = aContext;
        this.dataGraph = dataGraph;
    }

    /**
     * Indicates whether change logging is on (<code>true</code>) or off (<code>false</code>).
     * @return <code>true</code> if change logging is on.
     * @see #beginLogging
     * @see #endLogging
     */
    public boolean isLogging() {
        return logging;
    }

    /**
     * INTERNAL:
    * Set flag created value.
    * @param created   flag created's new value.
    */
    public void setCreated(DataObject anObject, boolean created) {
        if (getRootObject() == anObject) {
            return;
        }

        // Explicitly clear the flag
        if (isLogging() && !created) {
            createdList.remove(anObject);
        }

        if (isLogging() && !isCreated(anObject)) {
            if (created) {
                // remove from other sets
                deletedList.remove(anObject);
                // add to set
                createdList.add(anObject);
            }
        }
    }

    /**
     * INTERNAL:
    * Set flag modified value.
    * @param deleted   flag modified's new value.
    */
   public boolean setDeleted(DataObject anObject, boolean deleted) {
        if (getRootObject() == anObject) {
            return false;
        }

        // Explicitly clear the flag
        if (isLogging() && !deleted) {
            deletedList.remove(anObject);
        }

        if (isLogging() && !this.isDeleted(anObject)) {
            if (deleted) {
                // remove from other sets
                if(isCreated(anObject)){
                  createdList.remove(anObject);

                  oldSettings.remove(anObject);
                  originalValueStores.remove(anObject);
                  originalElements.remove(anObject);
                  return false;
                }else {
                  pauseLogging();
                  deletedList.add(anObject);
                  resumeLogging();
                }
            }
        }
        return true;
    }

    /**
      * INTERNAL:
     * @param aKey
     * @param aValue
     * void
     */
    public void setOldContainer(SDODataObject aKey, DataObject aValue) {
        oldContainer.put(aKey, aValue);
    }

    /**
     * INTERNAL:
     *
     * @return
     */
    public Map getOldContainers() {
        return oldContainer;
    }

    /**
     * INTERNAL:
     * @param aKey DataObject
     * @param aValue Property
     * void
     *
     */
    public void setOldContainmentProperty(SDODataObject aKey, Property aValue) {
        oldContainmentProperty.put(aKey, aValue);
    }

    /**
     * INTERNAL:
     * @param aKey DataObject
     * @param aValue Property
     * void
     *
     */
    public void setOldSequence(SDODataObject aKey, Sequence aValue) {
        getOldSequences().put(aKey, aValue);
    }

    /**
     * Returns the {@link DataGraph data graph} associated with this change summary or null.
     * @return the data graph.
     * @see DataGraph#getChangeSummary
     */
    public DataGraph getDataGraph() {
        return dataGraph;
    }

    /**
     * Returns a list consisting of all the {@link DataObject data objects} that have been changed while {@link #isLogging logging}.
     * <p>
     * The {@link #isCreated new} and {@link #isModified modified} objects in the List are references to objects
     * associated with this ChangeSummary.
     * The {@link #isDeleted deleted} objects in the List are references to objects
     * at the time that event logging was enabled;
     * <p> Each changed object must have exactly one of the following methods return true:
     *   {@link #isCreated isCreated},
     *   {@link #isDeleted isDeleted}, or
     *   {@link #isModified isModified}.
     * @return a list of changed data objects.
     * @see #isCreated(DataObject)
     * @see #isDeleted(DataObject)
     * @see #isModified(DataObject)
     */
    public List getChangedDataObjects() {
        // merge all the sets
        ArrayList aList = new ArrayList();
        aList.addAll(getModified());
        if (deletedList != null) {
            aList.addAll(deletedList);
        }
        if (createdList != null) {
            aList.addAll(createdList);
        }
        return aList;
    }

    /**
     * INTERNAL:
     * Return all modified objects
     * @return
     * Set
     */
    public List getModified() {
        ArrayList modifiedList = new ArrayList();
        getModified(rootDataObject, modifiedList);
        return modifiedList;
    }

    private void getModified(SDODataObject sdoDataObject, List modifiedList) {
        if(null == sdoDataObject) {
            return;
        }

        if(isModified(sdoDataObject)) {
            modifiedList.add(sdoDataObject);
        }
        List<Property> properties = sdoDataObject.getInstanceProperties();
        for(int x=0; x<properties.size(); x++) {
            Property property = properties.get(x);
            if(property.isContainment()) {
                if(property.isMany()) {
                    List<SDODataObject> dataObjects = sdoDataObject.getList(property);
                    for(int y=0; y<dataObjects.size(); y++) {
                        getModified(dataObjects.get(y), modifiedList);
                    }
                } else {
                    if ((property.getType() != null) && !(((SDOType)property.getType()).isChangeSummaryType())) {
                        getModified(sdoDataObject.getDataObject(property), modifiedList);
                    }
                }
            }
        }
    }

    /**
     * INTERNAL:
     * Return all deleted objects
     * @return
     * Set
     */
    public List getDeleted() {
        return deletedList;
    }

    /**
     * INTERNAL:
     * Return all created objects
     * @return
     * Set
     */
    public List getCreated() {
        return createdList;
    }

    /**
     * Returns whether or not the specified data object was created while {@link #isLogging logging}.
     * Any object that was added to the scope
     * but was not in the scope when logging began,
     * will be considered created.
     * @param dataObject the data object in question.
     * @return <code>true</code> if the specified data object was created.
     * @see #getChangedDataObjects
     */
    public boolean isCreated(DataObject dataObject) {
        return (createdList != null) && createdList.contains(dataObject);
    }

    /**
     * Returns whether or not the specified data object was deleted while {@link #isLogging logging}.
     * Any object that is not in scope but was in scope when logging began
     * will be considered deleted.
     * @param dataObject the data object in question.
     * @return <code>true</code> if the specified data object was deleted.
     * @see #getChangedDataObjects
     */
    public boolean isDeleted(DataObject dataObject) {
        return (deletedList != null) && deletedList.contains(dataObject);
    }

    /**
     * Returns whether or not the specified data object was updated while {@link #isLogging logging}.
     * An object that was contained in the scope when logging began
     * and remains in the scope when logging ends will be considered potentially modified.
     * <p> An object considered modified must have at least one old value setting.
     * @param dataObject the data object in question.
     * @return <code>true</code> if the specified data object was modified.
     * @see #getChangedDataObjects
     */
    public boolean isModified(DataObject dataObject) {
        // a modified data object is present in the original value
        // stores list and has not been deleted
        if (this.originalValueStores.get(dataObject) == null || isDeleted(dataObject)) {
            return false;
        }
        return true;
    }

    /**
     * Returns a list of {@link ChangeSummary.Setting settings}
     * that represent the property values of the given <code>dataObject</code>
     * at the point when logging {@link #beginLogging() began}.
     * <p>In the case of a {@link #isDeleted(DataObject) deleted} object,
     * the List will include settings for all the Properties.
     * <p> An old value setting indicates the value at the
     * point logging begins.  A setting is only produced for
     * {@link #isModified modified} objects if
     * either the old value differs from the current value or
     * if the isSet differs from the current value.
     * <p> No settings are produced for {@link #isCreated created} objects.
     * @param dataObject the object in question.
     * @return a list of settings.
     * @see #getChangedDataObjects
     */
    public List getOldValues(DataObject dataObject) {
        if ((dataObject == null) || (!isDeleted(dataObject) && ((((SDODataObject)dataObject).getChangeSummary() != null) && (((SDODataObject)dataObject).getChangeSummary() != this)))) {
            return new ArrayList();
        }
        if (!isCreated(dataObject) && isDirty(dataObject)) {
            List oldSettingsList = new ArrayList();
            for (int i = 0; i < dataObject.getInstanceProperties().size(); i++) {
                SDOProperty nextProp = (SDOProperty)dataObject.getInstanceProperties().get(i);
                Setting setting = getOldValueForChangedDataObject(dataObject, nextProp);
                if (setting != null) {
                    oldSettingsList.add(setting);
                }
            }

            List openProps = (List)getUnsetOCPropertiesMap().get(dataObject);
            if(openProps != null){
              for(int i=0; i< openProps.size(); i++){
                SDOProperty nextProp = (SDOProperty)openProps.get(i);
                Setting setting = getOldValueForChangedDataObject(dataObject, nextProp);
                  if (setting != null) {
                      oldSettingsList.add(setting);
                  }
              }
            }

            return oldSettingsList;
        }

        return new ArrayList();// Note: spec did not mention null value case.
    }

    /**
     * INTERNAL:
     *
     * @param dataObject
     * @return
     */
    public List getUnsetProps(DataObject dataObject) {
        //call getOldValues to populate oldSettings which will populate unsetPropsMap
        getOldValues(dataObject);
        Object value = unsetPropsMap.get(dataObject);
        if (value == null) {
            return new ArrayList();
        }
        return (List)value;

    }

    /**
     * INTERNAL:
     * Return the entire HashMap of lists of open content properties that were unset
     * keyed on dataObject
     * @return
     */
    public Map getUnsetOCPropertiesMap() {
        return unsetOCPropsMap;
    }

    /**
     * INTERNAL:
     * Return a List containing all open content properties that were unset
     * @param dataObject
     * @return
     */
    public List getUnsetOCProperties(DataObject dataObject) {
        Object value = unsetOCPropsMap.get(dataObject);
        if (null == value) {
            return new ArrayList();
        }
        return (List)value;
    }

    /**
     * INTERNAL:
     * Add an open content property that has been unset to the list keyed on dataObject
     * @param dataObject
     * @param ocKey
     */
    public void setUnsetOCProperty(DataObject dataObject, Property ocKey) {
        Object value = unsetOCPropsMap.get(dataObject);
        if (null == value) {
            // create a new list and populate
            List aList = new ArrayList();
            aList.add(ocKey);
            unsetOCPropsMap.put(dataObject, aList);
        } else {
            // dont replace existing key (ie we readd a previously unset oc property)
            if (!((List)value).contains(ocKey)) {
                ((List)value).add(ocKey);
            }
        }
    }

    /**
     * INTERNAL:
     * Delete an open content property from the list of unset oc properties keyed on dataObject
     * @param dataObject
     * @param ocKey
     */
    public void removeUnsetOCProperty(DataObject dataObject, Property ocKey) {
        Object value = unsetOCPropsMap.get(dataObject);
        if (value != null) {
            // if we referenced the property - check if it is the only one left in the map value
            if (((List)value).remove(ocKey)) {
                if(!(((List)value).size() > 0)) {
                    unsetOCPropsMap.remove(dataObject);
                }
            }
        }
    }

    /**
     * Clears the List of {@link #getChangedDataObjects changes} and turns change logging on.
     * No operation occurs if logging is already on.
     * @see #endLogging
     * @see #isLogging
     */
    public void beginLogging() {
        if (!logging) {
            logging = true;
            loggingMapping = true;
            resetChanges();
            rootDataObject.resetChanges();
        }
    }

    /**
     * INTERNAL:
     * Turn both logging flags back on.
     */
    public void resumeLogging() {
        if (!logging) {
            logging = true;
            loggingMapping = true;
        }
    }

    /**
     * An implementation that requires logging may throw an UnsupportedOperationException.
     * Turns change logging off.  No operation occurs if logging is already off.
     * @see #beginLogging
     * @see #isLogging
     */
    public void endLogging() {
        logging = false;
        loggingMapping = false;
        //October 12, 1007 - as per the spec do not clear anything on Changesummary on endLogging
    }

    /**
     * INTERNAL:
     * Turn both logging flags on.
     */
    public void pauseLogging() {
        logging = false;
        loggingMapping = false;
    }

    /**
     * INTERNAL:
     * Called from beginLogging and undoChanges
     */
    private void resetChanges() {
        createdList.clear();
        deletedList.clear();
        // See spec. p.30 "List of changed DataObjects cleared"
        oldSettings.clear();
        deepCopies.clear();
        oldContainer.clear();
        oldContainmentProperty.clear();
        unsetPropsMap.clear();
        unsetOCPropsMap.clear();
        originalValueStores.clear();
        originalElements.clear();
        reverseDeletedMap.clear();
        getOldSequences().clear();
        getOriginalSequences().clear();
    }

    /**
     * Returns the ChangeSummary root DataObject - the object from which
     * changes are tracked.
     * When a DataGraph is used, this is the same as getDataGraph().getRootObject().
     * @return the ChangeSummary root DataObject
     */
    public SDODataObject getRootObject() {
        return rootDataObject;
    }

    /**
     * Returns a {@link ChangeSummary.Setting setting} for the specified property
     * representing the property value of the given <code>dataObject</code>
     * at the point when logging {@link #beginLogging() began}.
     * <p>Returns null if the property was not modified and
     * has not been {@link #isDeleted(DataObject) deleted}.
     * @param dataObject the object in question.
     * @param property the property of the object.
     * @return the Setting for the specified property.
     * @see #getChangedDataObjects
     */
    public SDOChangeSummary.Setting getOldValue(DataObject dataObject, Property property) {
        if ((dataObject == null) || (!isDeleted(dataObject) && ((((SDODataObject)dataObject).getChangeSummary() != null) && (((SDODataObject)dataObject).getChangeSummary() != this)))) {
            return null;
        }

        if (!isCreated(dataObject) && isDirty(dataObject)) {
            return getOldValueForChangedDataObject(dataObject, (SDOProperty) property);
        }
        return null;
    }

    /**
     * INTERNAL:
    * @param dataObject which is not null and not created and is dirty in the scope of this changesummary
    * @param property
    * @return new or already existing Setting
    */
    private ChangeSummary.Setting getOldValueForChangedDataObject(DataObject dataObject, SDOProperty property) {
        if ((null == property) || property.getType().isChangeSummaryType()) {
            return null;
        }
        Setting setting = getPropertyInOldSettings(dataObject, property);
        if (setting == null) {
            SDODataObject sdoDataObject = ((SDODataObject)dataObject);
            boolean isDeleted = isDeleted(dataObject);
            Object oldValue = getPropertyInternal(sdoDataObject, property);
            Object currentValue = sdoDataObject.getPropertyInternal(property);
            boolean isSet = sdoDataObject.isSetInternal(property);
            boolean wasSet = wasSet(sdoDataObject, property);

            if (property.isMany()) {
                currentValue = ((ListWrapper)currentValue).getCurrentElements();
                if (isDirty(((ListWrapper)oldValue))) {
                    List elements = (List)getOriginalElements().get(oldValue);
                    oldValue = new ArrayList(elements);

                    if (!property.getType().isDataType()) {
                        for (int i = 0; i < ((List)oldValue).size(); i++) {
                            Object next = ((List)oldValue).get(i);

                            Object deepCopy = getOrCreateDeepCopy((DataObject)next);
                            ((List)oldValue).set(i, deepCopy);
                        }
                    }
                } else {
                    oldValue = currentValue;
                }
            }
            if (isDeleted || ((wasSet != isSet) || (oldValue != currentValue))) {
                if ((oldValue != null) && !property.getType().isDataType()) {
                    if (oldValue instanceof DataObject) {
                        oldValue = getOrCreateDeepCopy((DataObject)oldValue);
                    }
                }
                setting = buildAndAddOldSetting((SDODataObject)dataObject, property, oldValue, wasSet);
            }
        }
        return setting;
    }

    /**
     * INTERNAL:
     *
     * @param dataObject
     * @param property
     * @param value
     * @param isSet
     * @return
     */
    private Setting buildAndAddOldSetting(SDODataObject dataObject, Property property, Object value, boolean isSet) {
        SDOSetting setting = new SDOSetting(property, value);
        setting.setIsSet(isSet);

        if (oldSettings.get(dataObject) == null) {
            List aList = new ArrayList();
            aList.add(setting);
            oldSettings.put(dataObject, aList);
        } else {
            List theList = (List)oldSettings.get(dataObject);
            theList.add(setting);
        }

        if (!setting.isSet()) {
            List theList = (List)unsetPropsMap.get(dataObject);
            if (theList == null) {
                List aList = new ArrayList();
                aList.add(setting.getProperty().getName());
                unsetPropsMap.put(dataObject, aList);
            } else {
                if (!theList.contains(setting.getProperty().getName())) {
                    theList.add(setting.getProperty().getName());
                }
            }
        }

        return setting;
    }

    /**
     * Returns the value of the {@link DataObject#getContainer container} data object
     * at the point when logging {@link #beginLogging() began}.
     * @param dataObject the object in question.
     * @return the old container data object.
     */
    public SDODataObject getOldContainer(DataObject dataObject) {
        return (SDODataObject) oldContainer.get(dataObject);
    }

    /**
     * Returns the value of the {@link DataObject#getContainmentProperty containment property} data object property
     * at the point when logging {@link #beginLogging() began}.
     * @param dataObject the object in question.
     * @return the old containment property.
     */
    public SDOProperty getOldContainmentProperty(DataObject dataObject) {
        return (SDOProperty) oldContainmentProperty.get(dataObject);
    }

    /**
     * Returns the value of the {@link DataObject#getSequence sequence} for the data object
     * at the point when logging {@link #beginLogging() began}.
     * @param dataObject the object in question.
     * @return the old containment property.
     */
    public SDOSequence getOldSequence(DataObject dataObject) {
        if ((dataObject == null) || (!isDeleted(dataObject) && ((((SDODataObject)dataObject).getChangeSummary() != null) && (((SDODataObject)dataObject).getChangeSummary() != this)))) {
            return null;
        }
        if (!isCreated(dataObject) && dataObject.getType().isSequenced()) {
            // check cache first
            if (getOldSequences().containsKey(dataObject)) {
                return (SDOSequence) getOldSequences().get(dataObject);
            }

            // no sequence - get from the original sequence map
            SDOSequence originalSeq = (SDOSequence) getOriginalSequences().get(dataObject);
            if (originalSeq == null) {
                originalSeq = (SDOSequence) dataObject.getSequence();
            }

            SDOSequence seqWithDeepCopies = new SDOSequence((SDODataObject) dataObject);
            for (int i = 0; i < originalSeq.size(); i++) {
                // setting/value may be null in some cases
                Object nextOriginalSettingValue = originalSeq.getValue(i);
                if (nextOriginalSettingValue == null) {
                    continue;
                }

                // property may be null if the setting contains unstructured text
                SDOProperty nextOriginalSettingProp = originalSeq.getProperty(i);
                if (nextOriginalSettingProp == null) {
                    // handle unstructured text
                    seqWithDeepCopies.addText(nextOriginalSettingValue.toString());
                } else if (nextOriginalSettingProp.getType().isDataType()) {
                    // handle simple types
                    seqWithDeepCopies.addSettingWithoutModifyingDataObject(nextOriginalSettingProp, nextOriginalSettingValue, false);
                } else {
                    // handle complex types
                    seqWithDeepCopies.addSettingWithoutModifyingDataObject(nextOriginalSettingProp, getOrCreateDeepCopy((DataObject) nextOriginalSettingValue), false);
                }
            }
            // store deep copy of old sequence in cache
            getOldSequences().put(dataObject, seqWithDeepCopies);
            return seqWithDeepCopies;
        }
        return null;
    }

    /**
     * This method is intended for use by service implementations only.
     * Undoes all changes in the log to restore the tree of
     * DataObjects to its original state when logging began.
     * isLogging() is unchanged.  The log is cleared.
     * @see #beginLogging
     * @see #endLogging
     * @see #isLogging
     */
    public void undoChanges() {

        /**
         * See Jira SDO-109/107 and 125/225 for open issues with move optimization
         * See bug#5882923 for smart local undo via set()/unset()
         */
        Property oldProp = getOldContainmentProperty(rootDataObject);
        String oldName = null;
        if (oldProp != null) {
            oldName = oldProp.getName();
        }
        rootDataObject.undoChanges(true, this, getOldContainer(rootDataObject), oldName);
        resetChanges();
        rootDataObject.resetChanges();
    }

    /**
     * INTERNAL:
     * Set the root DataObject for this ChangeSummary.
     * @param dataObject  the root of DataObject tree this ChangeSummary belongs to
     */
    public void setRootDataObject(DataObject dataObject) {
        rootDataObject = (SDODataObject)dataObject;
    }

    /**
     * INTERNAL:
    * Used by CopyHelper to set logging when creating a copy of a changesummary
    * @param logging      logging status
    */
    public void setLogging(boolean logging) {
        if (logging) {
            beginLogging();
        } else {
            endLogging();
        }

        this.loggingMapping = logging;
    }

    /**
     * INTERNAL:
     * Check if a property is in its DataObject's oldsetting list
     * @param dataObject      property's owner
     * @param property        property to be checked
     * @return                property's Setting if this property is in list
     */
    private ChangeSummary.Setting getPropertyInOldSettings(DataObject dataObject, Property property) {
        Iterator iterOldSettings = null;
        List aList = (List)oldSettings.get(dataObject);
        if (aList != null) {
            iterOldSettings = aList.iterator();
            ChangeSummary.Setting curSetting;
            while (iterOldSettings.hasNext()) {
                curSetting = (ChangeSummary.Setting)iterOldSettings.next();
                if (curSetting.getProperty().equals(property)) {
                    return curSetting;
                }
            }
        }
        return null;
    }

    /**
     * INTERNAL:
     * Set the helperContext if the default SDOChangeSummary constructor was used
     * @param helperContext
     */
    public void setHelperContext(HelperContext helperContext) {
        aHelperContext = helperContext;
    }

    /**
     * INTERNAL:
     *
     * @param createdXPathsList
     */
    public void setCreatedXPaths(List createdXPathsList) {
        createdXPaths = createdXPathsList;
    }

    /**
     * INTERNAL:
     *
     * @return
     */
    public List getCreatedXPaths() {
        return createdXPaths;
    }

    /**
     * INTERNAL:
     * Return the logging state during mapping operations
     * @return logging state
     */
    public boolean isLoggingMapping() {
        return loggingMapping;
    }

    /**
     * INTERNAL:
     *
     * @param modifiedDomsList
     */
    public void setModifiedDoms(List modifiedDomsList) {
        modifiedDoms = modifiedDomsList;
    }

    /**
     * INTERNAL:
     *
     * @return
     */
    public List getModifiedDoms() {
        return modifiedDoms;
    }

    /**
     * INTERNAL:
     * The deletedXPaths field is picked up reflectively during marshal/unmarshal operations.
     * @param deletedXPathsList
     */
    public void setDeletedXPaths(List deletedXPathsList) {
        deletedXPaths = deletedXPathsList;
    }

    public Map getOldContainmentProperty() {
        return oldContainmentProperty;
    }

    public Map getOldContainer() {
        return oldContainer;
    }

    /**
     * INTERNAL:
     *
     * @param dataObject
     * @param property
     * @return
     */
    public boolean wasSet(DataObject dataObject, Property property) {
        ValueStore vs = (ValueStore)originalValueStores.get(dataObject);
        if (null == vs) {
            vs = ((SDODataObject)dataObject)._getCurrentValueStore();
        }
        if (property.isOpenContent()) {
            return vs.isSetOpenContentProperty(property);
        } else {
            return vs.isSetDeclaredProperty(((SDOProperty)property).getIndexInType());
        }
    }

    /**
     * INTERNAL:
     *
     * @param dataObject
     * @param property
     * @return
     */
    public Object getPropertyInternal(DataObject dataObject, Property property) {
        ValueStore vs = (ValueStore)originalValueStores.get(dataObject);

        if (null == vs) {
            vs = ((SDODataObject)dataObject)._getCurrentValueStore();
        }

        if (property.isOpenContent()) {
            return vs.getOpenContentProperty(property);
        } else {
            return vs.getDeclaredProperty(((SDOProperty)property).getIndexInType());
        }
    }

    /**
     * INTERNAL:
     *
     * @param dataObject
     * @param property
     * @param value
     */
    public void setPropertyInternal(DataObject dataObject, Property property, Object value) {
        ValueStore vs = (ValueStore)originalValueStores.get(dataObject);
        if (property.isOpenContent()) {
            vs.setOpenContentProperty(property, value);
        } else {
            vs.setDeclaredProperty(((SDOProperty)property).getIndexInType(), value);
        }
    }

    /**
     * INTERNAL:
     * Return the map of original ValueStores keyed on
     * @return
     */
    public Map getOriginalValueStores() {
        return originalValueStores;
    }

    /**
     * INTERNAL:
     * Return whether the <code>dataObject</code> has been modified.
     * @param dataObject
     * @return
     */
    public boolean isDirty(DataObject dataObject) {
        ValueStore vs = (ValueStore)originalValueStores.get(dataObject);
        return vs != null;
    }

    /**
     * INTERNAL:
     * Return whether the <code>aListWrapper</code> has been modified.
     * @param aListWrapper
     * @return
     */
    public boolean isDirty(ListWrapper aListWrapper) {
        Object originalList = getOriginalElements().get(aListWrapper);
        return originalList != null;
    }

    /**
     * INTERNAL:
     * Return whether the <code>aSequence</code> has been modified.
     * @param aSequence
     * @return
     */
    public boolean isDirty(SDOSequence aSequence) {
        Object originalSequence = getOriginalSequences().get(aSequence.getDataObject());
        return originalSequence != null;
    }

    /**
     * INTERNAL:
     *
     * @param dataObject
     * @param property
     */
  public void unsetPropertyInternal(DataObject dataObject, Property property) {
        ValueStore vs = (ValueStore)originalValueStores.get(dataObject);
        if (property.isMany()) {
            ListWrapper currentValue = (ListWrapper)dataObject.getList(property);
            originalElements.put(currentValue, new ArrayList());
            if (property.isOpenContent()) {
                vs.unsetOpenContentProperty(property);
            } else {
                vs.unsetDeclaredProperty(((SDOProperty)property).getIndexInType());
            }
        } else {
            if (property.isOpenContent()) {
                vs.unsetOpenContentProperty(property);
            } else {
                vs.unsetDeclaredProperty(((SDOProperty)property).getIndexInType());
            }
        }
    }

    /**
    * INTERNAL:
    * @return Map of original elements, key and value are both listwrappers
    */
    public Map getOriginalElements() {
        return originalElements;
    }

    /**
     * INTERNAL:
     * @param original
     * @return
     */
    private DataObject getOrCreateDeepCopy(DataObject original) {
        DataObject value = (DataObject)getDeepCopies().get(original);
        if (null == value) {
            DataObject undoneCopy = ((SDOCopyHelper)aHelperContext.getCopyHelper()).copy(original, this);

            // if original is null we will create only 1 key:value null:null pair
            getDeepCopies().put(original, undoneCopy);
            getReverseDeletedMap().put(undoneCopy, original);
            return undoneCopy;
        } else {
            return value;
        }
    }

    /**
     * INTERNAL:
     * @return Map of deep copies of DataObjects key is original dataobject
     */
    public Map getDeepCopies() {
        return deepCopies;
    }

    /**
     * INTERNAL:
     * @return Map of deep copies of DataObjects key is copy of dataobject
     */
    public Map getReverseDeletedMap() {
        return reverseDeletedMap;
    }

    /**
      * INTERNAL:
      * Return a map of original sequences keyed on DataObject.
      * @return Map of old Sequences
      */
    public Map getOriginalSequences() {
        if (null == originalSequences) {
            originalSequences = new HashMap();
        }
        return originalSequences;
    }

    /**
    * INTERNAL:
    * Return a map of original sequences keyed on DataObject (cached values).
    * @return Map of old Sequences that have deep copies of all DataObjects
    */
    public Map getOldSequences() {
        if (null == oldSequences) {
            oldSequences = new HashMap();
        }
        return oldSequences;
    }

    /**
     * INTERNAL:
     * Return the string representation of the receiver.
     */
    public String toString() {
        StringBuffer aBuffer = new StringBuffer();
        aBuffer.append("ChangeSummary@");
        aBuffer.append(getClass().hashCode());
        aBuffer.append(" [logging: ");
        aBuffer.append(logging);
        aBuffer.append(", root: ");
        aBuffer.append(rootDataObject);
        List aList = getChangedDataObjects();
        if (aList != null) {
            aBuffer.append(", ");
            aBuffer.append(aList.size());
            aBuffer.append(" changes: <");
            boolean first = true;
            for (Iterator i = aList.iterator(); i.hasNext();) {
                if (first) {
                    first = false;
                } else {
                    aBuffer.append(", ");
                }
                aBuffer.append(i.next());// null values are handled
            }
        }
        aBuffer.append(">]");
        return aBuffer.toString();
    }

}
