/*******************************************************************************
 * Copyright (c) 1998, 2014 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 v1.0 and Eclipse Distribution License v. 1.0 
 * which accompanies this distribution. 
 * The Eclipse Public License is available at http://www.eclipse.org/legal/epl-v10.html
 * and the Eclipse Distribution License is available at 
 * http://www.eclipse.org/org/documents/edl-v10.php.
 *
 * Contributors:
 *     Oracle - initial API and implementation from Oracle TopLink
 ******************************************************************************/  
package org.eclipse.persistence.sdo.helper;

import commonj.sdo.Property;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import org.eclipse.persistence.sdo.DefaultValueStore;
import org.eclipse.persistence.sdo.SDOChangeSummary;
import org.eclipse.persistence.sdo.SDODataObject;
import org.eclipse.persistence.sdo.SDOProperty;
import org.eclipse.persistence.sdo.SDOSequence;
import org.eclipse.persistence.sdo.SDOType;
import org.eclipse.persistence.sdo.ValueStore;
import org.eclipse.persistence.exceptions.SDOException;
import commonj.sdo.ChangeSummary;
import commonj.sdo.DataObject;
import commonj.sdo.helper.CopyHelper;
import commonj.sdo.helper.HelperContext;
import commonj.sdo.impl.HelperProvider;
import org.eclipse.persistence.oxm.XMLRoot;
import org.eclipse.persistence.oxm.sequenced.Setting;

/**
 * <b>Purpose:</b>
 * <ul><li>A helper class for making deep or shallow copies of DataObjects.</li>
 * </ul>
 * <p>
 * <b>Responsibilities:</b>
 * <ul>
 * <li>Perform shallow and deep copy operations on {@link DataObject DataObject}s.</li>
 * </ul>
 *
 * @see org.eclipse.persistence.sdo.SDODataObject
 * @since Oracle TopLink 11.1.1.0.0
 */
public class SDOCopyHelper implements CopyHelper {

    /*
       09/12/06 - Add bidirectional property copy support
       09/16/06 - Use design #1a over #2
                           Modify 1 pass opposite property set algorithm to use 2 passes,
                           the first gathers a list of opposites, the 2nd pass iterates the list after tree completion.
                           Runtime is the same since we still need a hash lookup of the opposite DO
       09/19/06 - Adjust copy algorithm to check for opposites before checking containment
                           cont=false, opp=false -> unidirectional
                           cont=false, opp=true  -> bidirectional
                           cont=true,  opp=false -> normal containment
                           cont=true,  opp=true  -> bidirectional
                           The code for copyPropertyValue() has been moved up to copy()
                           where we use the two cached maps to set non-containment properties
       10/02/06 - Finish isMany 1-n bidirectional/unidirectional support in copy() function
       11/09/06 - Jira#129: implement HelperContext via new HelperProvider.getDefaultContext()
       01/29/07 - #5852525 handle null properties with isSet=true
       02/14/07 - #5878605 do not generate oldSettings during copy of cs with logging=true
       02/15/07 - #5878605 move double iteration of cs list in many case to single iteration
       02/23/07 - #5897730 implement ChangeSummary deep copy
       03/14/07 - p.142 limit scope of while by using for
                        - put isSet() check back into open content part of copyChangeSummary
       04/11/07 - Implement Sequence functionality
       05/01/07 - #6026714: because of enlarged scope, copy of copy sets previous iteration
                          value of unset complex setting
     */

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

    /**
     * INTERNAL:
     * This default constructor must be used in conjunction with the setHelperContext() function.
     * The custom constructor that takes a HelperContext parameter is recommended over this default constructor.
     */
    public SDOCopyHelper() { 
    }

    /**
     * Constructor that takes in a HelperContext instance that contains this copyHelper.<br>
     * This is the recommended constructor.
     * @param aContext
     */
    public SDOCopyHelper(HelperContext aContext) {
        aHelperContext = aContext;
    }

    /**
     * Create a shallow copy of the DataObject dataObject:
     *   Creates a new DataObject copiedDataObject with the same values
     *     as the source dataObject for each property where
     *       property.getType().isDataType() is true.
     *   The value of such a Property property in copiedDataObject is:
     *       dataObject.get(property) for single-valued Properties
     *       (copiedDataObject.get(property) equals() dataObject.get(property)), or
     *       a List where each member is equal to the member at the
     *       same index in dataObject for multi-valued Properties
     *        copiedDataObject.getList(property).get(i) equals() dataObject.getList(property).get(i)
     *   The copied Object is unset for each Property where
     *       property.getType().isDataType() is false
     *       since they are not copied.
     *   Read-only properties are copied.
     *   A copied object shares metadata with the source object
     *     sourceDO.getType() == copiedDO.getType()
     *   If a ChangeSummary is part of the source DataObject
     *     the copy has a new, empty ChangeSummary.
     *     Logging state is the same as the source ChangeSummary.
     *
     * @param dataObject to be copied
     * @return copy of dataObject
     */
    public DataObject copyShallow(DataObject dataObject) {
        if (null == dataObject) {
            return null;
        }
        SDODataObject copy = (SDODataObject)getHelperContext().getDataFactory().create(dataObject.getType().getURI(), dataObject.getType().getName());

        List ocListOriginal = ((SDODataObject)dataObject)._getOpenContentProperties();
        for (Iterator anOCIterator = ocListOriginal.iterator(); anOCIterator.hasNext();) {
            copy.addOpenContentProperty((Property)anOCIterator.next());
        }
        
        List ocAttrsListOriginal = ((SDODataObject)dataObject)._getOpenContentPropertiesAttributes();
        for (Iterator anOCAttrIterator = ocAttrsListOriginal.iterator(); anOCAttrIterator.hasNext();) {
            copy.addOpenContentProperty((Property)anOCAttrIterator.next());
        }

        List allProperties = copy.getInstanceProperties();// start iterating all copy's properties
        Iterator iterProperties = allProperties.iterator();
        while (iterProperties.hasNext()) {
            SDOProperty eachProperty = (SDOProperty)iterProperties.next();
            if (dataObject.isSet(eachProperty)) {
                Object o = getValue((SDODataObject)dataObject, eachProperty, null);
                if (eachProperty.getType().isDataType()) {
                    if (!eachProperty.getType().isChangeSummaryType()) {
                        // we defer sequence updates at this point
                        copy.setInternal(eachProperty, o, false);// make copy if current property is datatype
                    }
                }
            }
        }

        if (dataObject.getType().isSequenced()) {
            List settings = ((SDOSequence)dataObject.getSequence()).getSettings();
            for (int index = 0, size = dataObject.getSequence().size(); index < size; index++) {
                Setting nextSetting = (Setting)settings.get(index);

                Property prop = dataObject.getSequence().getProperty(index);
                if (prop == null || ((SDOType) prop.getType()).isDataType()) {
                    Setting copySetting = nextSetting.copy(copy);
                    copy.getSequence().getSettings().add(copySetting);
                    copy.getSequence().addValueToSettings(copySetting);
                }
            }
        }

        if ((copy != null) && (copy.getChangeSummary() != null) && (copy.getType().getChangeSummaryProperty() != null)) {
            if (((SDODataObject)dataObject).getChangeSummary().isLogging()) {
                copy.getChangeSummary().setLogging(true);
            }
        }

        return copy;
    }

    /**
     * Create a deep copy of the DataObject tree:
     *   Copies the dataObject and all its {@link commonj.sdo.Property#isContainment() contained}
     *     DataObjects recursively.
     *   Values of Properties are copied as in shallow copy,
     *     and values of Properties where
     *       property.getType().isDataType() is false
     *     are copied where each value copied must be a
     *       DataObject contained by the source dataObject.
     *     If a DataObject is outside the DataObject tree and the
     *       property is bidirectional, then the DataObject is skipped.
     *     If a DataObject is outside the DataObject tree and the
     *       property is unidirectional, then the same DataObject is referenced.
     *   Read-only properties are copied.
     *   If any DataObject referenced is not in the containment
     *     tree an IllegalArgumentException is thrown.
     *   If a ChangeSummary is part of the copy tree the new
     *     ChangeSummary refers to objects in the new DataObject tree.
     *     Logging state is the same as the source ChangeSummary.
     *
     * @param dataObject to be copied.
     * @return copy of dataObject
     * @throws IllegalArgumentException if any referenced DataObject
     *   is not part of the containment tree.
     */
    public DataObject copy(DataObject dataObject) throws IllegalArgumentException {
        return copy(dataObject, null);
    }

    /**
     * Create a deep copy of the DataObject tree:
     *   Copies the dataObject and all its {@link commonj.sdo.Property#isContainment() contained}
     *     DataObjects recursively.
     *   <p>For each Property where property.type.dataType is true, the values of Properties are
     *       copied as in shallow copy,  and values of Properties where
     *       property.getType().isDataType() is false
     *     are copied where each value copied must be a
     *       DataObject contained by the source dataObject.
     *     <p>If a DataObject is outside the DataObject tree and the
     *       property is bidirectional, then the DataObject is not copied and
     *       references to the object are also not copied.
     *     <p>If a DataObject is outside the DataObject tree and the
     *       property is unidirectional, then the same DataObject is referenced.
     *   <p>Read-only properties are copied.
     *   <p>If any DataObject referenced is not in the containment
     *     tree an IllegalArgumentException is thrown.
     *   <p>If a ChangeSummary is part of the copy tree the new
     *     ChangeSummary refers to objects in the new DataObject tree.
     *     Logging state is the same as the source ChangeSummary.
     *
     * @param dataObject to be copied.
     * @return copy of dataObject
     * @throws IllegalArgumentException if any referenced DataObject
     *    is not part of the containment tree.
     */
    public DataObject copy(DataObject dataObject, SDOChangeSummary cs) throws IllegalArgumentException {
        if (null == dataObject) {
            return null;
        } else {
            // cache one side of opposite property objects as we iterate to the 2nd side - non-deleted objects only
            //* @param doMap (cache original -> copy DataObject instances to set non-containment properties after tree construction)
            HashMap doMap = new HashMap();

            //* @param propMap (cache original DO:non-containment property values to be set after tree construction)            
            HashMap ncPropMap = new HashMap();

            // build object instances by recursing the tree        	            
            SDODataObject aCopy = copyPrivate((SDODataObject)dataObject, doMap, ncPropMap, cs);

            // After the copy containment tree has been built
            // Iterate the non-containment nodes and copy all uni/bi-directional properties on the copy.
            processNonContainmentNodesPrivate(doMap, ncPropMap);

            // Iterate the map of containment nodes and populate sequenced objects in the copy.
            processContainmentSequencesPrivate(doMap, cs);

            /**
             * ChangeSummary on Root Case:
             * We only need to handle a single root element - isMany=true root elements are not valid
             * Copy changeSummary state from original to copy on all cs-root elements that are also roots.
             * Setting logging to true only after recursively setting internal properties will not create oldSettings in the copy
             * The setLogging call will create oldContainer/oldContainmentProperty entries in the copy.
             */
            if ((aCopy != null) && (aCopy.getChangeSummary() != null) && (aCopy.getType().getChangeSummaryProperty() != null)) {
                // re-reference copy objects in the copy changeSummary
                if (((SDODataObject)dataObject).getChangeSummary().isLogging()) {// switch and use resume logging
                    (aCopy.getChangeSummary()).resumeLogging();//.setLogging(true);
                }
                copyChangeSummary(((SDODataObject)dataObject).getChangeSummary(),//
                                  aCopy.getChangeSummary(), doMap);
            }
            return aCopy;
        }
    }

    /**
     * INTERNAL:
     * Iterate the non-containment nodes and copy all uni/bi-directional properties on the copy.
     * @param doMap
     * @param ncPropMap
     */
    private void processNonContainmentNodesPrivate(HashMap doMap, HashMap ncPropMap) {
        // for the doMap and ncPropMap we can either deep copy them all now or each one during rereferencing later
        // iterate nc property map and set bidirectional/unidirectional properties that are in scope
        for (Iterator ncIterator = ncPropMap.keySet().iterator(); ncIterator.hasNext();) {// p.142 limit scope of while by using for
            // get current source dataobject (the one we will set properties on)
            DataObject sourceDO = (DataObject)ncIterator.next();

            // get the list of properties for the current do
            ArrayList aList = (ArrayList)ncPropMap.get(sourceDO);

            // iterate property list
            for (Iterator propIterator = aList.iterator(); propIterator.hasNext();) {// p.142 limit scope of while by using for
                // get current property
                SDOProperty aProperty = (SDOProperty)propIterator.next();

                /*
                 * Stored in the map we have
                 * doMap: (a=key, a'=value)
                 * ncPropMap (a=key, (list of props)=value
                 * We get the copy from the doMap
                 * We get the copy of the source by doing a get on the current source do
                 */

                // get original object that sourceDO points to via current property
                Object targetDO = sourceDO.get(aProperty);

                // flag whether the property is inside the copy tree
                boolean isPropertyInsideCopyTreeScope = false;

                // get sourceDO copy that we will be setting the property on
                DataObject sourceDOCopy = null;

                // lookup copy of targetDO in map
                Object targetDOCopy = null;

                /*
                 * Handle 1-n many case
                 * For containment=true
                 *   the DO's will be cached previously, and both bidirectional (one) and unidirectional
                 *   properties will set the copy on the copy object
                 * For containment=false
                 *   the DO's will not be cached (outside the tree), only unidirectional properties
                 *   will be set using the original list
                 */
                if (aProperty.isMany()) {
                    // create new list to hold copied list items
                    ListWrapper targetList = (ListWrapper)targetDO;

                    // get source\DO copy that we will be setting the property on
                    sourceDOCopy = (DataObject)doMap.get(sourceDO);

                    // lookup copy of targetDO in map
                    targetDOCopy = new ArrayList();
                    for (int i = 0, size = targetList.size(); i < size; i++) {
                        // get sourceDO key - used as a lookup in our doMap
                        DataObject sourceDOCopyKey = (DataObject)targetList.get(i);
                        DataObject sourceDOCopyValue = (DataObject)doMap.get(sourceDOCopyKey);

                        // add copy to new list
                        if (sourceDOCopyValue != null) {
                            // bidirectional/unidirectional inside copy tree - use copy object
                            ((List)targetDOCopy).add(sourceDOCopyValue);
                        } else {
                            // non-containment properties are not cached - store original for unidirectional
                            //targetDOCopy.add(sourceDOCopyKey);
                        }
                    }

                    // check if the target copies are in our map (inside copy tree scope) 
                    // when containment = false then targetDOCopy is empty
                    // Assume: all items in the list share the same property
                    isPropertyInsideCopyTreeScope = ((List)targetDOCopy).size() > 0;
                } else {
                    // handle 1-1 DataObject
                    // lookup copy of targetDO in map
                    targetDOCopy = doMap.get(targetDO);

                    // get sourceDO copy that we will be setting the property on
                    sourceDOCopy = (DataObject)doMap.get(sourceDO);

                    // check if the target copy is in our map (inside copy tree scope)
                    isPropertyInsideCopyTreeScope = targetDOCopy != null;
                }

                // set nc property if we are in the copy tree
                // check if the target copy is in our map (inside copy tree scope)
                if (isPropertyInsideCopyTreeScope) {
                    ((SDODataObject)sourceDOCopy).set(aProperty, targetDOCopy, false);
                } else {
                    // only set unidirectional properties
                    if (null == aProperty.getOpposite()) {
                        // spec 3.9.4 set property to original object when unidirectional
                        ((SDODataObject)sourceDOCopy).set(aProperty, targetDO, false);
                    }
                }
            }
        }
    }

    /**
     * INTERNAL:
     * Make a copy of all settings on the original sequence onto the copy sequence while using a
     * cross-reference doMap that relates the original DataObject key to the copy DataObject key.
     * This function is used during deep copy and changeSummary copy.
     * @param origSequence
     * @param copySequence
     * @param doMap
     */
    private void replicateAndRereferenceSequenceCopyPrivate(SDOSequence origSequence, SDOSequence copySequence, DataObject dataObject, DataObject copy, Map doMap, SDOChangeSummary cs) {
        if (cs != null && cs.isDeleted(dataObject)) {
            origSequence = cs.getOldSequence(dataObject);
        }
        
        SDOProperty seqProperty = null;
        try {
            List settings = origSequence.getSettings();
            for (int index = 0, size = origSequence.size(); index < size; index++) {
                Setting nextSetting = (Setting)settings.get(index);                
                seqProperty = origSequence.getProperty(nextSetting);
                
                if ((null == seqProperty) || seqProperty.getType().isDataType()) {
                    Setting copySetting = nextSetting.copy(copy);
                    copySequence.getSettings().add(copySetting);
                    copySequence.addValueToSettings(copySetting);
                } else {
                    Object copySeqValue = null;
                    Object origSeqValue = origSequence.getValue(index);
                    
                    if (cs != null) {
                        Object orig = cs.getReverseDeletedMap().get(origSeqValue);
                        if (orig != null) {
                            origSeqValue = orig;
                        }
                    }
                    
                    if (origSeqValue instanceof XMLRoot) {
                        origSeqValue = ((XMLRoot)origSeqValue).getObject();
                    }

                    // lookup copy if not null, if null then the copySeqValue will be null in the reduced scope assignment above
                    if (null != origSeqValue) {
                        copySeqValue = doMap.get(origSeqValue);
                    } else {
                        // as a secondary verification to the assignment above - make sure the copy is null as well
                        copySeqValue = null;// this assignment is however redundant in our reduced scope assignment above
                    }

                    //now we have the new value
                    Setting copySetting = nextSetting.copy(copy, copySeqValue);
                    copySequence.getSettings().add(copySetting);
                    copySequence.addValueToSettings(copySetting);
                }

                /**
                 * Move assignment inside the loop to minimize scope and to
                 * initialize the variable to null to handle the case where the original is null and
                 * no lookup is performed.
                 * Do not move the scope of this variable outside the for loop or we will end up
                 * with the previous iterations' value set for a complex object that is unset(null).
                 * see #6026714
                 */
            }
        } catch (ClassCastException cce) {// catch any failure of a DataObject cast above          
            throw SDOException.foundSimpleValueForNonDataTypeProperty(seqProperty.getName());
        }
    }

    /**
     * INTERNAL:
     * Iterate the map of containment nodes and populate sequenced objects in the copy.
     * @param doMap
     */
    private void processContainmentSequencesPrivate(Map doMap, SDOChangeSummary cs) {

        /**
         * Prerequisites: - the copy tree has been built and the doMap has been
         * fully populated with containment nodes.<br/> Iterate the doMap and
         * for each containment dataObject populate the sequence
         */

        // iterate containment nodes		
        for (Iterator cIterator = doMap.keySet().iterator(); cIterator.hasNext();) {
            DataObject cObject = (DataObject)cIterator.next();
            DataObject copyDO = (DataObject)doMap.get(cObject);

            // process sequences
            if (cObject.getType().isSequenced()) {
                SDOSequence origSequence = (SDOSequence)cObject.getSequence();

                // iterate the original object sequence - in sequence and create settings on the copy
                replicateAndRereferenceSequenceCopyPrivate(origSequence, (SDOSequence)copyDO.getSequence(), cObject, copyDO, doMap, cs);
            }
        }
    }

    /**
     * INTERNAL: Create a new uninitialized ValueStore.
     *
     * @return
     */
    private ValueStore createValueStore() {
        return new DefaultValueStore();
    }

    /**
     * INTERNAL:
     * Implement ChangeSummary deep copy
     *      Note: a copy with a CS requires the DefautlValueStore implementation
     *      because of changes outside the ValueStore interface for the dataObject field
     * DeepCopy the original changeSummary into the copy dataObject.
     * All deleted keys in originalValueStore, deletedMap, deepCopies are the same original object.
     * We require the following relationships in order to build copies of originals and copies of copies of originals.
     *     origDOtoCopyDOMap
     *         original object (deleted + current) - in original DO : copy of original object - in copy DO
     *     copyDOtoCopyOfDOMap
     *
     *     Assumptions:
     *        Property objects instances are copied only by reference - metadata is the same in the copy.
     *        Deleted objects never exist in the currentValueStore (and are therefore not in the doMap).
     *        Created and modified objects are always in the currentValueStore (and are in the doMap).
     *
     * @param anOriginalCS
     * @param aCopyCS
     * @param doMap (map of original do's (CS1) to their copy do's in (CS2))
     */
    private void copyChangeSummary(ChangeSummary anOriginalCS, ChangeSummary aCopyCS,//
                                   Map origDOCS1toCopyDOCS2Map) {
        // cast interfaces to concrete classes in one place
        SDOChangeSummary originalCS = (SDOChangeSummary)anOriginalCS;
        SDOChangeSummary copyCS = (SDOChangeSummary)aCopyCS;

        // handled by copy constructor
        // map of copy of original ListWrapper (CS2) to its new copy of a copy (CS2) - link ValueStores to Elements 
        HashMap copyListWrapperCS2toCopyOfListCS2Map = new HashMap();

        // in the absence of a ListWrapper.getProperty() we keep a map
        HashMap propertyToOriginalListMap = new HashMap();

        /**
         * In 3 parts we add deleted objects to the global doMap and copy modified, created nodes
         **/

        // fields that need re-referencing from original to copy
        DataObject anOriginalObject = null;
        DataObject aCopyOfOriginalObject = null;

        // iterate deleted objects
        for (Iterator anIterator = originalCS.getDeleted().iterator(); anIterator.hasNext();) {
            anOriginalObject = (DataObject)anIterator.next();
            aCopyOfOriginalObject = copy(anOriginalObject, null);
            // fix deletedList
            copyCS.getDeleted().add(aCopyOfOriginalObject);
            // Assumption check do map before a possible re-add - reset()
            if (null == origDOCS1toCopyDOCS2Map.get(anOriginalObject)) {
                // add temp map of original  : copy of original            
                origDOCS1toCopyDOCS2Map.put(anOriginalObject, aCopyOfOriginalObject);
            } 
        }

        // iterate created objects    	
        for (Iterator aIterator = originalCS.getCreated().iterator(); aIterator.hasNext();) {
            copyCS.getCreated().add(origDOCS1toCopyDOCS2Map.get(aIterator.next()));
        }

        // add modified objects    	
        for (Iterator anIterator = originalCS.getModified().iterator(); anIterator.hasNext();) {
            copyCS.getModified().add(origDOCS1toCopyDOCS2Map.get(anIterator.next()));
        }

        /**
         * Fix originalValueStores by deep copying the original dataObject:key and the original valueStore:value
         * key is original deleted object in [deepCopies] - value is copy of the ValueStore
         */
        ValueStore aVSCopy = null;
        ValueStore aVSOriginal = null;
        for (Iterator anIterator = originalCS.getOriginalValueStores().keySet().iterator();
                 anIterator.hasNext();) {
            anOriginalObject = (DataObject)anIterator.next();
            // deep copy to get corresponding copy DataObject (deleted objects were added to doMap)
            aCopyOfOriginalObject = (DataObject)origDOCS1toCopyDOCS2Map.get(anOriginalObject);

            /**
             * Recursively shallow-copy elements (by iterating the ovs map and iterating the properties of each item)
             * Fix the dataObject pointer 
             */
            aVSCopy = createValueStore();
            aVSOriginal = (ValueStore)originalCS.getOriginalValueStores().get(anOriginalObject);
            // changes made to the copy VS must not affect the original -hence the dataObject field must be a copy of the original
            aVSCopy.initialize(aCopyOfOriginalObject);
            Object aVSPropertyItem = null;

            // get the # of non-opencontent properties for the object holding the CS - do not use DVS.getTypePropertyValues()
            for (int size = ((SDOType) anOriginalObject.getType()).getDeclaredProperties().size(), i = 0; 
                     i < size; i++) {
                aVSPropertyItem = aVSOriginal.getDeclaredProperty(i);
                // only iterate set properties
                if (aVSOriginal.isSetDeclaredProperty(i)) {
                    // shallow copy the object values
                    // handle single case
                    SDOProperty currentProperty = (SDOProperty)((SDOType)anOriginalObject.getType()).getDeclaredProperties().get(i);
                    if (currentProperty.isMany()) {
                        propertyToOriginalListMap.put(aVSPropertyItem, currentProperty);

                        // handle many case - handled by originalElements
                        // container DO must be in our reference map
                        SDODataObject copyContainer = (SDODataObject)origDOCS1toCopyDOCS2Map.get(anOriginalObject);
                        ListWrapper aCopyOfListCopy = (ListWrapper)((DataObject)copyContainer).getList(currentProperty);

                        // add reference of new copy of original List keyed on original List
                        copyListWrapperCS2toCopyOfListCS2Map.put((anOriginalObject).getList(currentProperty), aCopyOfListCopy);
                        aVSCopy.setDeclaredProperty(i, aCopyOfListCopy);
                    } else {
                        // COMPLEX SINGLE
                        if (!currentProperty.getType().isDataType()) {
                            // are we using the cast to DataObject as a sort of instance check that would throw a CCE?
                            aVSCopy.setDeclaredProperty(i, origDOCS1toCopyDOCS2Map.get(aVSPropertyItem));
                        } else {
                            // SIMPLE SINGLE
                            // skip changeSummary property
                            if (!currentProperty.getType().isChangeSummaryType()) {
                                // simple singles set
                                aVSCopy.setDeclaredProperty(i, aVSPropertyItem);
                            }
                        }
                    }
                }
            }

            // create list of unset and current open content properties
            List ocPropertiesList = new ArrayList();
            ocPropertiesList.addAll(originalCS.getUnsetOCProperties(anOriginalObject));
            // add existing properties
            ocPropertiesList.addAll(((SDODataObject)anOriginalObject)._getOpenContentProperties());
            ocPropertiesList.addAll(((SDODataObject)anOriginalObject)._getOpenContentPropertiesAttributes());
            // iterate existing open content properties            
            for (Iterator i = ocPropertiesList.iterator(); i.hasNext();) {
                SDOProperty ocProperty = (SDOProperty)i.next();
                if (aVSOriginal.isSetOpenContentProperty(ocProperty)) {
                    // get oc value              
                    Object anOCPropertyItem = aVSOriginal.getOpenContentProperty(ocProperty);

                    // get oc copy - shallow copy the object values
                    if (ocProperty.isMany()) {
                        // handle many case - handled by originalElements
                        // container DO must be in our reference map
                        SDODataObject copyContainer = (SDODataObject)origDOCS1toCopyDOCS2Map.get(anOriginalObject);
                        ListWrapper aCopyOfListCopy = (ListWrapper)((DataObject)copyContainer).getList(ocProperty);

                        // add reference of new copy of original List keyed on original List
                        copyListWrapperCS2toCopyOfListCS2Map.put((anOriginalObject).getList(ocProperty), aCopyOfListCopy);
                        aVSCopy.setOpenContentProperty(ocProperty, aCopyOfListCopy);
                    } else {
                        // handle complex single case
                        if (!ocProperty.getType().isDataType()) {
                            aVSCopy.setOpenContentProperty(ocProperty, origDOCS1toCopyDOCS2Map.get(aVSPropertyItem));
                        } else {
                            // simple singles set
                            aVSCopy.setOpenContentProperty(ocProperty, anOCPropertyItem);
                        }
                    }
                }
            }

            // set the copy map entry keyed on copy with value a deep copy of the copy
            copyCS.getOriginalValueStores().put(aCopyOfOriginalObject, aVSCopy);
        }

        // end originalValueStore iteration

        /**
         * Fix originalElements by deep copying the original dataObject:key and the original List:value
         * key is original deleted object in [deepCopies] - value is copy of the elements
         * The instances of ListWrapper inside the valueStores must be the same ones used in the originalElements
         */
        ListWrapper anOriginalListKey = null;
        ListWrapper aCopyListWrapper = null;
        List aCopyList = null;
        for (Iterator anIterator = originalCS.getOriginalElements().keySet().iterator();
                 anIterator.hasNext();) {
            anOriginalListKey = (ListWrapper)anIterator.next();
            // create a new ListWrapper
            SDOProperty aProperty = (SDOProperty) propertyToOriginalListMap.get(anOriginalListKey);
            aCopyListWrapper = (ListWrapper)copyListWrapperCS2toCopyOfListCS2Map.get(anOriginalListKey);
            aCopyList = new ArrayList();

            /**
             * For each key:ListWrapper
             *     - shallow copy all the items in the currentElements list
             *     - replace the dataObject with its copy of the copy
             *     - leave the property as is
             * For each value:ArrayList
             *     - replace all values with their copy
             */
            Object aListItem = null;
            Object aListItemCopy = null;
            for (Iterator anItemIterator = anOriginalListKey.iterator(); anItemIterator.hasNext();) {
                aListItem = anItemIterator.next();
                // for simple many types we use the original in the copy
                if (!aProperty.getType().isDataType()) {
                    // get the copy of the original (in the current valuestore) - we need do not make a copy of this copy
                    // we should have a copy of the copy for List items - ListWrapper.add(item) will remove the item from its original wrapper
                    aListItemCopy = origDOCS1toCopyDOCS2Map.get(aListItem);
                } else { 
                    aListItemCopy = aListItem;
                }
                aCopyList.add(aListItemCopy);
            }

            // add element list directly to the ListWrapper and bypass the cs element copy and containment updates
            aCopyListWrapper.setCurrentElements(aCopyList);
            List listValueCopy = new ArrayList();

            // fix ArrayList value
            List listValue = (List)originalCS.getOriginalElements().get(anOriginalListKey);
            aListItem = null;
            aListItemCopy = null;
            for (Iterator aListIterator = listValue.iterator(); aListIterator.hasNext();) {
                aListItem = aListIterator.next();
                // for simple many types we use the original in the copy
                if (!aProperty.getType().isDataType()) {
                    aListItemCopy = origDOCS1toCopyDOCS2Map.get(aListItem);                    
                } else {
                    aListItemCopy = aListItem;
                }

                // don't add nulls to the listWrapper so an undoChanges will encounter an NPE later
                if (aListItemCopy != null) {
                    listValueCopy.add(aListItemCopy);
                } 
            }

            // set the copy map entry keyed on copy with value a deep copy of the copy
            copyCS.getOriginalElements().put(aCopyListWrapper, listValueCopy);
        }

        // end originalist Iteration

        /**
         * fields that are already set when logging is turned on but need to be fixed (deleted objects need references)
         */
        Map oldContainersMap = originalCS.getOldContainers();
        Map copyContainersMap = copyCS.getOldContainers();
        DataObject oldContainerKey = null;
        DataObject copyContainerKey = null;

        // convert any existing entries in the Map - should normally be 0 - unless any OC properties were unset
        for (Iterator anIterator = oldContainersMap.keySet().iterator(); anIterator.hasNext();) {
            oldContainerKey = (DataObject)anIterator.next();
            // get corresponding copy
            copyContainerKey = (SDODataObject)origDOCS1toCopyDOCS2Map.get(oldContainerKey);
            // check existing copyContainers for existing objects - should be 0 - add all objects when pauseLogging() used
            DataObject oldContainerValue = null;
            if (null == copyContainersMap.get(copyContainerKey)) {
                oldContainerValue = (DataObject)oldContainersMap.get(oldContainerKey);
                // set copy key:value pair on copy map directly
                copyContainersMap.put(copyContainerKey, origDOCS1toCopyDOCS2Map.get(oldContainerValue));
            }
        }

        Map oldContainmentPropertyMap = originalCS.getOldContainmentProperty();
        Map copyContainmentPropertyMap = copyCS.getOldContainmentProperty();
        DataObject oldContainmentPropertyKey = null;
        DataObject copyContainmentPropertyKey = null;

        // convert any existing entries in the Map - should normally be 0 - unless any OC properties were unset
        for (Iterator iterContProp = oldContainmentPropertyMap.keySet().iterator();
                 iterContProp.hasNext();) {
            oldContainmentPropertyKey = (DataObject)iterContProp.next();
            // get corresponding copy
            copyContainmentPropertyKey = (SDODataObject)origDOCS1toCopyDOCS2Map.get(oldContainmentPropertyKey);
            // check existing copyContainers for existing objects - should be 0 - add all objects when pauseLogging() used
            if (null == copyContainmentPropertyMap.get(copyContainmentPropertyKey)) {
                // set copy key:value pair on copy map directly
                copyContainmentPropertyMap.put(copyContainmentPropertyKey, oldContainmentPropertyMap.get(oldContainmentPropertyKey));
            }
        }

        Map oldUnsetOCPropertyMap = originalCS.getUnsetOCPropertiesMap();
        SDODataObject oldOCPropertyContainer = null;

        // convert any existing entries in the Map - should normally be 0
        for (Iterator iterContainer = oldUnsetOCPropertyMap.keySet().iterator();
                 iterContainer.hasNext();) {
            // DataObject will be non-Null
            oldOCPropertyContainer = (SDODataObject)iterContainer.next();
            // check existing copyContainers for existing objects - should be 0 - add all objects when pauseLogging() used
            for (Iterator iterUnset = ((List)oldUnsetOCPropertyMap.get(oldOCPropertyContainer)).iterator();
                     iterUnset.hasNext();) {
                // set/create new list on copy Map with corresponding copy of container
                copyCS.setUnsetOCProperty((SDODataObject)origDOCS1toCopyDOCS2Map.get(//
                oldOCPropertyContainer), (Property)iterUnset.next());
            }
        }

        // process sequences

        /**
         * Fix originalSequences by deep copying the original dataObject:key and the original Sequence:value
         * key is original deleted object in [deepCopies] - value is copy of the settings in the sequence.
         * The instances of Sequence inside the originalSequences must be the same ones used in the originalElements
         */

        // iterate the map of <DataObject, Sequence>
        for (Iterator aMapIterator = originalCS.getOriginalSequences().keySet().iterator();
                 aMapIterator.hasNext();) {
            SDODataObject sequenceDataObjectKey = (SDODataObject)aMapIterator.next();
            SDOSequence originalSequence = (SDOSequence)originalCS.getOriginalSequences().get(sequenceDataObjectKey);

            // create a new Sequence with a pointer to the copy of the original DataObject backpointer
            // assume that all dataObject backpointers are containment objects.
            SDODataObject copyOriginalSequenceDataObject = (SDODataObject)origDOCS1toCopyDOCS2Map.get(originalSequence.getDataObject());
            SDOSequence copySequence = new SDOSequence(copyOriginalSequenceDataObject);

            replicateAndRereferenceSequenceCopyPrivate(originalSequence, copySequence, originalSequence.getDataObject(), copyOriginalSequenceDataObject, origDOCS1toCopyDOCS2Map, originalCS);

            // set the copy map entry keyed on copy with value a deep copy of the copy
            copyCS.getOriginalSequences().put(copyOriginalSequenceDataObject, copySequence);
        }

        /**
         * fields to ignore
        // aHelperContext    SDOHelperContext  (id=42)
        // dataGraph    null
        // logging    true
        // loggingMapping    true
        // unsetPropsMap    HashMap<K,V>  (id=117)
        // createdXPaths    null
        // deletedXPaths    null
        // modifiedDoms    null
         */
        /**
         * fields to ignore that are on-demand
         *     oldSettings    HashMap<K,V>  (id=110)
         * reverseDeletedMap    HashMap<K,V>  (id=116)
         * oldSequences    HashMap<K,V>  (id=88)
         */
    }

    /**
     * INTERNAL:
     * Build the copy tree and cache all reachable DataObjects with their copy<br>
     * Cache all non-containment properties - to be set after tree construction<br>
     * Recurse the tree in preorder traversal (root, child1-n)
     * Scope: We do not have to check the copyTree scope when iterating opposites since
     * we will not enter any opposite property dataTree that is outside of the copyTree scope
     * @param doMap (cache original -> copy DataObject instances to set non-containment properties after tree construction)
     * @param ncPropMap (cache original DO:non-containment property values to be set after tree construction)
     */
    private SDODataObject copyPrivate(SDODataObject dataObject, HashMap doMap,//
                                   HashMap ncPropMap, SDOChangeSummary cs) throws IllegalArgumentException {
        // check for null DataObject
        if (null == dataObject) {
            return null;// this is acceptable behavior
        }

        SDODataObject copy = (SDODataObject)getHelperContext()// 
        .getDataFactory().create(dataObject.getType().getURI(), dataObject.getType().getName());

        // store current object for reference by the non-containment map
        doMap.put(dataObject, copy);

        List ocListOriginal = dataObject._getOpenContentProperties();
        for (Iterator anOCIterator = ocListOriginal.iterator(); anOCIterator.hasNext();) {
            copy.addOpenContentProperty((Property)anOCIterator.next());
        }
        
        List ocAttrsListOriginal = dataObject._getOpenContentPropertiesAttributes();
        for (Iterator anOCAttrIterator = ocAttrsListOriginal.iterator(); anOCAttrIterator.hasNext();) {
            copy.addOpenContentProperty((Property)anOCAttrIterator.next());
        }

        //  start iterating all copy's properties
        for (Iterator iterInstanceProperties = copy.getInstanceProperties().iterator();
                 iterInstanceProperties.hasNext();) {
            SDOProperty eachProperty = (SDOProperty)iterInstanceProperties.next();
            boolean isSet = isSet(dataObject, eachProperty, cs);
            if (isSet) {
                Object o = getValue(dataObject, eachProperty, cs);
                if (eachProperty.getType().isDataType()) {
                    if (!eachProperty.getType().isChangeSummaryType()) {

                        /**
                         * ChangeSummaries must be cleared with logging set to the original state without creating oldSettings.
                         * The logging flag from the original will be set on the copy after this copy call completes
                         * and gets set on its container.
                         * The cs is off by default in the copy object.
                         * updateSequence flag is false - we will populate the sequence in order after subtree creation
                         */
                        copy.setInternal(eachProperty, o, false);
                    }
                } else {
                    // case matrix for containment and opposite combinations
                    // cont=false, opp=false -> unidirectional
                    // cont=false, opp=true  -> bidirectional
                    // cont=true,  opp=false -> normal containment
                    // cont=true,  opp=true  -> bidirectional
                    if (eachProperty.isContainment()) {
                        // process containment properties (normal, half of bidirectionals)                        
                        copyContainmentPropertyValue(copy, eachProperty, o, doMap, ncPropMap, cs);
                    } else {
                        // copy non-containment do (not properties (unidirectional, half of bidirectionals))
                        //copyPropertyValue(dataObject, copy, eachProperty, o, copyRoot, doMap);
                        //cacheNContainmentPropertyValue(copy, eachProperty, o, doMap, ncPropMap);
                        // store non-containment properties
                        ArrayList anArray = (ArrayList)ncPropMap.get(dataObject);
                        if (anArray == null) {
                            anArray = new ArrayList();
                            anArray.add(eachProperty);
                            // store property array into the map for the first time
                            ncPropMap.put(dataObject, anArray);
                        } else {
                            // add to existing array of nc properties in the map
                            anArray.add(eachProperty);
                        }
                    }
                }
            }            
        }

        // sequences will not be processed until the entire tree is copied so we can resolve any reference relationships
        return copy;
    }

    /**
     * INTERNAL:
     * Recursive function deep copies all contained properties.
     * Requirements: The value object has isSet=true for all callers.
     * @param copy
     * @param property
     * @param value
     * @param doMap (cache original -> copy DataObject instances to set non-containment properties after tree construction)
     * @param propMap (cache original DO:non-containment property values to be set after tree construction)
     */
    private void copyContainmentPropertyValue(SDODataObject copy, SDOProperty property, Object value,//
                                              HashMap doMap, HashMap ncPropMap, SDOChangeSummary cs) {
        if (property.isMany()) {
            List copyValue = new ArrayList();

            // set the copy to an empty list and add each items from the original in sequence
            // updateSequence flag is false - we will populate the sequence in order after subtree creation
            copy.setInternal(property, copyValue, false);
            for (Iterator iterValues = ((List)value).iterator(); iterValues.hasNext();) {
                SDODataObject o = (SDODataObject)iterValues.next();
                SDODataObject copyO = copyPrivate(o, doMap, ncPropMap, cs);
                ((ListWrapper)copy.getList(property)).add(copyO, false);
                // set changeSummary on all cs-root elements in the list after they are added to the containment tree
                if ((copyO != null) && (copyO.getChangeSummary() != null) && (copyO.getType().getChangeSummaryProperty() != null)) {
                    // re-reference copy objects in the copy changeSummary
                    if (o.getChangeSummary().isLogging()) {
                        (copyO.getChangeSummary()).setLogging(true);
                    }
                    copyChangeSummary(o.getChangeSummary(),//
                                      copyO.getChangeSummary(), doMap);
                }
            }
        } else {// handle non-many case
            // implementers of this function will always pass in a DataObject that may be null
            SDODataObject copyO = copyPrivate((SDODataObject)value, doMap, ncPropMap, cs);

            //  #5852525 handle null properties with isSet=true - fixed 20070130
            // we will set the isSet index in the ValueStore to true for all isSet=true objects, even NULL ones.
            // updateSequence flag is false - we will populate the sequence in order after subtree creation
            copy.setInternal(property, copyO, false);

            // set changeSummary on all cs-root elements in the list after they are added to the containment tree using the original logging value
            if ((copyO != null) && (copyO.getChangeSummary() != null) && (copyO.getType().getChangeSummaryProperty() != null)) {
                // re-reference copy objects in the copy changeSummary
                if (((SDODataObject)value).getChangeSummary().isLogging()) {
                    copyO.getChangeSummary().setLogging(true);
                }
                copyChangeSummary(((SDODataObject)value).getChangeSummary(),//
                                  copyO.getChangeSummary(), doMap);
            }
        }
    }

    /**
     * INTERNAL:
     * Return the helperContext containing this copyHelper.
     * @return
     */
    public HelperContext getHelperContext() {
        if(null == aHelperContext) {
            aHelperContext = HelperProvider.getDefaultContext();
        }
        return aHelperContext;
    }

    /**
     * INTERNAL:
     * Set the helperContext if this copyHelper was created using the default constructor.
     * @param helperContext
     */
    public void setHelperContext(HelperContext helperContext) {
        aHelperContext = helperContext;
    }

    /**
     * INTERNAL:
     * Used during XML Unmarshal.
     * @param dataObject
     * @param property
     * @param cs
     * @return
     */
    private boolean isSet(SDODataObject dataObject, Property property, SDOChangeSummary cs) {
        if (cs != null) {
            return cs.wasSet(dataObject, property);
        }
        return dataObject.isSetInternal(property);
    }

    /**
     * INTERNAL:
     * Used during XML Unmarshal.
     * @param dataObject
     * @param property
     * @param cs
     * @return
     */
    private Object getValue(SDODataObject dataObject, Property property, SDOChangeSummary cs) {
        if (cs != null) {
            Object returnValue = cs.getPropertyInternal(dataObject, property);
            if (property.isMany()) {
                if (cs.getOriginalElements().containsKey(returnValue)) {
                    // are we using the cast to List as a sort of instance check that would throw a CCE?
                    return cs.getOriginalElements().get(returnValue);
                }
            }
            return returnValue;
        }
        return dataObject.get(property);// get the value of current property                      
    }
}
