/*******************************************************************************
 * Copyright (c) 1998, 2013 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
 *     07/15/2011-2.2.1 Guy Pelletier 
 *       - 349424: persists during an preCalculateUnitOfWorkChangeSet event are lost
 ******************************************************************************/  
package org.eclipse.persistence.internal.sessions.remote;

import java.util.*;

import org.eclipse.persistence.config.ReferenceMode;
import org.eclipse.persistence.descriptors.ClassDescriptor;
import org.eclipse.persistence.descriptors.DescriptorQueryManager;
import org.eclipse.persistence.exceptions.*;
import org.eclipse.persistence.internal.sessions.*;
import org.eclipse.persistence.internal.databaseaccess.Platform;
import org.eclipse.persistence.internal.helper.*;
import org.eclipse.persistence.platform.database.DatabasePlatform;
import org.eclipse.persistence.queries.DatabaseQuery;
import org.eclipse.persistence.queries.ObjectLevelReadQuery;
import org.eclipse.persistence.sessions.SessionProfiler;
import org.eclipse.persistence.sessions.remote.*;
import org.eclipse.persistence.logging.SessionLog;

/**
 * Counter part of the unit of work which exists on the client side.
 */
public class RemoteUnitOfWork extends RepeatableWriteUnitOfWork {
    protected List newObjectsCache;
    protected List unregisteredNewObjectsCache;
    protected boolean isOnClient;
    protected transient RemoteSessionController parentSessionController;
    protected boolean isFlush;

    public RemoteUnitOfWork() {
    }
    
    public RemoteUnitOfWork(RemoteUnitOfWork parent) {
        this(parent, null);
    }

    public RemoteUnitOfWork(DistributedSession parent) {
        this(parent, null);
    }
    public RemoteUnitOfWork(RemoteUnitOfWork parent, ReferenceMode referenceMode) {
        super(parent, referenceMode);
        this.isOnClient = true;
        this.discoverUnregisteredNewObjectsWithoutPersist = true;
    }

    public RemoteUnitOfWork(DistributedSession parent, ReferenceMode referenceMode) {
        super(parent, referenceMode);
        this.isOnClient = true;
        this.discoverUnregisteredNewObjectsWithoutPersist = true;
    }

    public boolean isFlush() {
        return isFlush;
    }

    public void setIsFlush(boolean isFlush) {
        this.isFlush = isFlush;
    }

    /**
     * PUBLIC:
     * Tell the unit of work to begin a transaction now.
     * By default the unit of work will begin a transaction at commit time.
     * The default is the recommended approach, however sometimes it is
     * necessary to start the transaction before commit time.  When the
     * unit of work commits, this transaction will be committed.
     *
     * @see #commit()
     * @see #release()
     */
    public void beginEarlyTransaction() throws DatabaseException {
        // Acquire the mutex so session knows it is in a transaction.
        getParent().getTransactionMutex().acquire();
        startOperationProfile(SessionProfiler.Remote, null, SessionProfiler.ALL);
        // This needs a special call for remote, to ensure subsequent queries isolate their data to a unit of work on the server.
        ((DistributedSession)getParent()).getRemoteConnection().beginEarlyTransaction();
        endOperationProfile(SessionProfiler.Remote, null, SessionProfiler.ALL);
        
        setWasTransactionBegunPrematurely(true);
    }
    
    /**
     * The nested unit of work must also be remote.
     */
    @Override
    public UnitOfWorkImpl acquireUnitOfWork() {
        return acquireUnitOfWork(null);
    }

    /**
     * The nested unit of work must also be remote.
     */
    @Override
    public UnitOfWorkImpl acquireUnitOfWork(ReferenceMode referenceMode) {
        log(SessionLog.FINER, SessionLog.TRANSACTION, "acquire_unit_of_work");
        setNumberOfActiveUnitsOfWork(getNumberOfActiveUnitsOfWork() + 1);
        RemoteUnitOfWork ruow = new RemoteUnitOfWork(this, referenceMode);
        ruow.discoverAllUnregisteredNewObjectsInParent();
        return ruow;
    }

    /**
     * This is done to maintain correspondence between local new objects and returned new objects from serialization.
     * Object correspondence is maintained by comparing primary keys but for new objects it is possible that primary
     * key value is null as it is still not inserted. The returned new objects from serialization will have primary
     * key value which will be inserted into corresponding local new objects.
     */
    protected List collectNewObjects() {
        if ((this.newObjectsCloneToOriginal == null) || this.newObjectsCloneToOriginal.isEmpty()) {
            return null;
        }      
        List newObjects = new ArrayList(this.newObjectsCloneToOriginal.size());
        for (Object newObject : this.newObjectsCloneToOriginal.keySet()) {
            newObjects.add(newObject);
        }
        return newObjects;
    }

    /**
     * This is done to maintain correspondence between local unregistered new objects and returned unregistered new
     * objects from serialization. Object correspondence is maintained by comparing primary keys but for unregistered
     * new objects it is possible that primary key value is null as it is still not inserted. The returned unregistered
     * new objects from serialization will have primary key value which will be inserted into corresponding local new
     * objects.
     */
    protected List collectUnregisteredNewObjects() {
        discoverAllUnregisteredNewObjects();
        return new ArrayList(getUnregisteredNewObjects().values());
    }

    /**
     * The remote unit of work returned after its commit on the server is merged with remote unit of work
     * on the remote side.
     */
    protected void commitIntoRemoteUnitOfWork() {
        UnitOfWorkImpl parent = ((UnitOfWorkImpl)getParent());
        // Must merge the transaction flag.
        parent.setWasTransactionBegunPrematurely(wasTransactionBegunPrematurely());
        
        MergeManager manager = new MergeManager(this);
        manager.mergeWorkingCopyIntoRemote();

        // Must clone the clone mapping because entries can be added to it during the merging,
        // and that can lead to concurrency problems.
        Iterator cloneIterator = new IdentityHashMap(getCloneMapping()).keySet().iterator();

        Map clones = new IdentityHashMap(this.cloneMapping.size());
        // Iterate over each clone and let the object build merge to clones into the originals.
        while (cloneIterator.hasNext()) {
            Object remoteClone = cloneIterator.next();
            manager.mergeChanges(remoteClone, null, this);
            Object clone = manager.getTargetVersionOfSourceObject(remoteClone, parent.getDescriptor(remoteClone), parent);
            clones.put(remoteClone, clone);
        }
        
        // Reset the remote change set to be the local one, need to reset all clones to local copy,
        // and reset transient variables.
        parent.setUnitOfWorkChangeSet(this.unitOfWorkChangeSet);
        fixRemoteChangeSet(this.unitOfWorkChangeSet, clones, parent);
        ((RemoteUnitOfWork)parent).setCumulativeUOWChangeSet(this.cumulativeUOWChangeSet);
        fixRemoteChangeSet(this.cumulativeUOWChangeSet, clones, parent);
        
        // Set the deleted objects into the parent.
        if (this.objectsDeletedDuringCommit != null) {
            Map newDeletedObjects = new IdentityHashMap();
            for (Object deletedObject : this.objectsDeletedDuringCommit.keySet()) {
                Object primaryKey = getId(deletedObject);
                Object clone = clones.get(deletedObject);
                if (clone == null) {
                    clone = parent.getIdentityMapAccessor().getFromIdentityMap(primaryKey, deletedObject.getClass());
                    if (clone == null) {
                        clone = deletedObject;
                    }
                }
                newDeletedObjects.put(clone, primaryKey);
                parent.getIdentityMapAccessor().removeFromIdentityMap(primaryKey, clone.getClass());
            }
            parent.setObjectsDeletedDuringCommit(newDeletedObjects);
        }
    }

    /**
     * Simulate a flush, current just begins a transaction and commits.
     */
    @Override
    public void writeChanges() {
        if (!isOnClient()) {
            super.writeChanges();
            return;
        }
        // Check for a nested flush and return early if we are in one
        if (this.isWithinFlush()) {
            log(SessionLog.WARNING, SessionLog.TRANSACTION, "nested_entity_manager_flush_not_executed_pre_query_changes_may_be_pending", getClass().getSimpleName());
            return;
        }
        log(SessionLog.FINER, SessionLog.TRANSACTION, "begin_unit_of_work_flush");
        
        // PERF: If this is an empty unit of work, do nothing (but still may need to commit SQL changes).
        boolean hasChanges = (this.unitOfWorkChangeSet != null) || hasCloneMapping() || hasDeletedObjects() || hasModifyAllQueries() || hasDeferredModifyAllQueries();
        if (hasChanges) {
            // The change set may already exist if using change tracking.
            if (this.unitOfWorkChangeSet == null) {
                this.unitOfWorkChangeSet = new UnitOfWorkChangeSet(this);
            }
            calculateChanges(getCloneMapping(), this.unitOfWorkChangeSet, true, true);
            hasChanges = hasModifications();
        }
        if (!hasChanges) {
            log(SessionLog.FINER, SessionLog.TRANSACTION, "end_unit_of_work_flush");
            return;
        }
        
        if (!wasTransactionBegunPrematurely()) {
            beginEarlyTransaction();
        }

        // New objects cache is created to maintain the correspondence when they are returned back as different copy
        setNewObjectsCache(collectNewObjects());
        // Unregistered new objects cache is created to maintain the correspondence when they are returned back as different copy
        setUnregisteredNewObjectsCache(collectUnregisteredNewObjects());

        // Commit on the server
        RemoteUnitOfWork remoteUnitOfWork;
        try {
            setIsFlush(true);
            startOperationProfile(SessionProfiler.Remote, null, SessionProfiler.ALL);
            remoteUnitOfWork = ((DistributedSession)getParent()).getRemoteConnection().commitRootUnitOfWork(this);
            endOperationProfile(SessionProfiler.Remote, null, SessionProfiler.ALL);
        } finally {
            setIsFlush(false);
        }

        // Make the returned remote unit of work a nested unit of work and merge it with the local remote unit of work
        remoteUnitOfWork.setParent(this);
        remoteUnitOfWork.setProject(getProject());
        remoteUnitOfWork.prepareForMergeIntoRemoteUnitOfWork();
        remoteUnitOfWork.commitIntoRemoteUnitOfWork();

        log(SessionLog.FINER, SessionLog.TRANSACTION, "end_unit_of_work_flush");

        resumeUnitOfWork();
        log(SessionLog.FINER, SessionLog.TRANSACTION, "resume_unit_of_work");
    }

    /**
     * Starts committing the remote unit of work.
     * This must serialize the unit of work across to the server,
     * commit the unit of work on the server,
     * serialize it back and merge any server-side changes (such as sequence numbers) it into itself,
     * then merge into the parent remote session.
     */
    @Override
    public void commitRootUnitOfWork() {
        if (!isOnClient()) {
            if (isSynchronized()) {
                // If we started the JTS transaction then we have to commit it as well.
                if (getParent().wasJTSTransactionInternallyStarted()) {
                    commitInternallyStartedExternalTransaction();
                }
                // Do not commit until the JTS wants to.
                return;
            }
            if (this.eventManager != null) {
                this.eventManager.preCommitUnitOfWork();
            }
            super.commitRootUnitOfWork(); // On the server the normal commit is done.
            if (this.eventManager != null) {
                this.eventManager.postCommitUnitOfWork();
            }
            return;
        }

        // PERF: If this is an empty unit of work, do nothing (but still may need to commit SQL changes).
        boolean hasChanges = (this.unitOfWorkChangeSet != null) || hasCloneMapping() || hasDeletedObjects() || hasModifyAllQueries() || hasDeferredModifyAllQueries();
        if (hasChanges) {
            // The change set may already exist if using change tracking.
            if (this.unitOfWorkChangeSet == null) {
                this.unitOfWorkChangeSet = new UnitOfWorkChangeSet(this);
            }
            calculateChanges(getCloneMapping(), this.unitOfWorkChangeSet, true, true);
            hasChanges = hasModifications();
        }
        if (!hasChanges && (this.cumulativeUOWChangeSet == null) && (this.classesToBeInvalidated == null)) {
            // If no changes, avoid the remote commit, just return.
            // CR#... need to commit the transaction if begun early.
            if (wasTransactionBegunPrematurely()) {
                // Must be set to false for release to know not to rollback.
                setWasTransactionBegunPrematurely(false);
                setWasNonObjectLevelModifyQueryExecuted(false);
                try {
                    commitTransaction();
                } catch (RuntimeException commitFailed) {
                    try {
                        rollbackTransaction();
                    } catch (RuntimeException ignore) {
                        // Ignore
                    }
                    throw commitFailed;
                } catch (Error error) {
                    try {
                        rollbackTransaction();
                    } catch (RuntimeException ignore) {
                        // Ignore
                    }
                    throw error;
                }
            }
            return;
        }
        
        // New objects cache is created to maintain the correspondence when they are returned back as different copy
        setNewObjectsCache(collectNewObjects());
        // Unregistered new objects cache is created to maintain the correspondence when they are returned back as different copy
        setUnregisteredNewObjectsCache(collectUnregisteredNewObjects());

        // Commit on the server
        RemoteUnitOfWork remoteUnitOfWork;
        try {
            startOperationProfile(SessionProfiler.Remote, null, SessionProfiler.ALL);
            remoteUnitOfWork = ((DistributedSession)getParent()).getRemoteConnection().commitRootUnitOfWork(this);
            endOperationProfile(SessionProfiler.Remote, null, SessionProfiler.ALL);
        } catch (RuntimeException exception) {
            // Must ensure remote session transaction mutex is correct.
            if (wasTransactionBegunPrematurely()) {
                getParent().getTransactionMutex().release();
            }
            // If an exception occurred, the unit of work will have rolledback the early transaction
            // so must record this incase the unit of work re-commits.
            setWasTransactionBegunPrematurely(false);
            throw exception;
        }

        // Must ensure remote session transaction mutex is correct.
        if (wasTransactionBegunPrematurely()) {
            setWasTransactionBegunPrematurely(false);
            setWasNonObjectLevelModifyQueryExecuted(false);
            getParent().getTransactionMutex().release();
        }
        // Make the returned remote unit of work a nested unit of work and merge it with the local remote unit of work
        remoteUnitOfWork.setParent(this);
        remoteUnitOfWork.setProject(getProject());
        remoteUnitOfWork.prepareForMergeIntoRemoteUnitOfWork();
        remoteUnitOfWork.commitIntoRemoteUnitOfWork();
        // Now commit this unit of work to the parent remote session
        commitRootUnitOfWorkOnClient();
    }
    
    /**
     * INTERNAL:
     * Changes are calculated on the client, so avoid recalculating them on the server.
     */
    @Override
    public UnitOfWorkChangeSet calculateChanges(Map registeredObjects, UnitOfWorkChangeSet changeSet, boolean assignSequences, boolean shouldCloneMap) {
        if (!this.isOnClient) {
            // The changes are calculated on the client, so don't do them again on the server.
            return changeSet;
        }
        return super.calculateChanges(registeredObjects, changeSet, assignSequences, shouldCloneMap);
    }

    /**
     * INTERNAL:
     * Resume is not required on the server.
     */
    @Override
    public void resumeUnitOfWork() {
        if (!this.isOnClient) {
            // Avoid the resume on the server, only the client should resume.
            return;
        }
        super.resumeUnitOfWork();        
    }
    
    /**
     * Merges remote unit of work to parent remote session.
     */
    protected void commitRootUnitOfWorkOnClient() {
        collectAndPrepareObjectsForNestedMerge();

        //calculate the change set here as we have special behavior for remote
        // in that the new changesets must be updated within the UOWChangeSet as the
        // primary keys have already been assigned.  This was modified for updating
        // new object change set behavior.
        UnitOfWorkChangeSet uowChangeSet = (UnitOfWorkChangeSet)getUnitOfWorkChangeSet();
        if (uowChangeSet == null) {
            //may be using the old commit process usesOldCommit()
            uowChangeSet = new UnitOfWorkChangeSet(this);
            setUnitOfWorkChangeSet(uowChangeSet);
            calculateChanges(getCloneMapping(), uowChangeSet, false, true);
            this.allClones = null;
        }
        for (Map newList : uowChangeSet.getNewObjectChangeSets().values()) {
            Iterator newChangeSets = new IdentityHashMap(newList).keySet().iterator();
            while (newChangeSets.hasNext()) {
                uowChangeSet.putNewObjectInChangesList((ObjectChangeSet)newChangeSets.next(), this);
            }
        }
        
        //add the deleted objects
        if (this.objectsDeletedDuringCommit != null) {
            for (Object deletedObject : this.objectsDeletedDuringCommit.keySet()) {
                uowChangeSet.addDeletedObject(deletedObject, this);
            }
        }

        mergeChangesIntoParent();
    }

    /**
     * PUBLIC:
     * Execute the pre-defined query by name and return the result.
     * Queries can be pre-defined and named to allow for their reuse.
     * The named query can be defined on the remote session or the server-side session.
     *
     * @see #addQuery(String, DatabaseQuery)
     */
    @Override
    public Object executeQuery(String queryName) throws DatabaseException {
        return executeQuery(queryName, new Vector(1));
    }

    /**
     * PUBLIC:
     * Execute the pre-defined query by name and return the result.
     * Queries can be pre-defined and named to allow for their reuse.
     * The class is the descriptor in which the query was pre-defined.
     * The query is executed on the server-side session.
     *
     * @see DescriptorQueryManager#addQuery(String, DatabaseQuery)
     */
    @Override
    public Object executeQuery(String queryName, Class domainClass) throws DatabaseException {
        return executeQuery(queryName, domainClass, new Vector(1));
    }

    /**
     * PUBLIC:
     * Execute the pre-defined query by name and return the result.
     * Queries can be pre-defined and named to allow for their reuse.
     * The class is the descriptor in which the query was pre-defined.
     *
     * @see DescriptorQueryManager#addQuery(String, DatabaseQuery)
     */
    @Override
    public Object executeQuery(String queryName, Class domainClass, Vector argumentValues) throws DatabaseException {
        DistributedSession remoteSession = null;
        if (getParent().isRemoteSession()) {
            remoteSession = (DistributedSession)getParent();
        } else {//must be remote unit of work
            RemoteUnitOfWork uow = (RemoteUnitOfWork)getParent();
            while (uow.getParent().isRemoteUnitOfWork()) {
                uow = (RemoteUnitOfWork)uow.getParent();
            }
            remoteSession = (DistributedSession)uow.getParent();
        }

        startOperationProfile(SessionProfiler.Remote, null, SessionProfiler.ALL);
        Transporter transporter = remoteSession.getRemoteConnection().remoteExecuteNamedQuery(queryName, domainClass, argumentValues);
        endOperationProfile(SessionProfiler.Remote, null, SessionProfiler.ALL);
        transporter.getQuery().setSession(this);
        return transporter.getQuery().extractRemoteResult(transporter);
    }

    /**
     * PUBLIC:
     * Execute the pre-defined query by name and return the result.
     * Queries can be pre-defined and named to allow for their reuse.
     *
     * @see #addQuery(String, DatabaseQuery)
     */
    @Override
    public Object executeQuery(String queryName, Vector argumentValues) throws DatabaseException {
        if (containsQuery(queryName)) {
            return super.executeQuery(queryName, argumentValues);
        }
        return executeQuery(queryName, null, argumentValues);
    }

    /**
     * Return the table descriptor specified for the class.
     */
    @Override
    public ClassDescriptor getDescriptor(Class domainClass) {
        return getParent().getDescriptor(domainClass);
    }

    /**
     * Return the table descriptor specified for the class.
     */
    @Override
    public ClassDescriptor getDescriptorForAlias(String alias) {
        return getParent().getDescriptorForAlias(alias);
    }

    /**
     * Returns a new object cache
     */
    public List getNewObjectsCache() {
        return newObjectsCache;
    }

    /**
     * INTERNAL:
     * Method returns the parent RemoteSessionController for this Remote UnitOfWork
     * Used to retrieve Valueholders that were used on the client
     */
    public RemoteSessionController getParentSessionController() {
        return this.parentSessionController;
    }

    /**
     * INTERNAL:
     * Return the database platform currently connected to.
     * The platform is used for database specific behavior.
     */
    @Override
    public DatabasePlatform getPlatform() {
        return getParent().getPlatform();
    }

    /**
     * INTERNAL:
     * Return the database platform currently connected to.
     * The platform is used for database specific behavior.
     */
    @Override
    public Platform getDatasourcePlatform() {
        return getParent().getDatasourcePlatform();
    }

    /**
     * Returns an unregistered new object cache
     */
    public List getUnregisteredNewObjectsCache() {
        return unregisteredNewObjectsCache;
    }

    /**
     * INTERNAL:
     * Return the results from exeucting the database query.
     * the arguments should be a database row with raw data values.
     */
    @Override
    public Object internalExecuteQuery(DatabaseQuery query, AbstractRecord Record) throws DatabaseException, QueryException {
        if (isOnClient()) {
            //assert !getCommitManager().isActive();
            // This will either throw an exception or do a logic only operation
            // (i.e. mark for later deletion if a deleteObjet query).
            boolean objectLevelRead = (query.isObjectLevelReadQuery() && !query.isReportQuery() && query.shouldMaintainCache());
            if (objectLevelRead) {
                ObjectLevelReadQuery readQuery = (ObjectLevelReadQuery)query;
                if (isAfterWriteChangesButBeforeCommit()) {
                    throw ValidationException.illegalOperationForUnitOfWorkLifecycle(getLifecycle(), "executeQuery(ObjectLevelReadQuery)");
                }
                Object result = readQuery.checkEarlyReturn(this, Record);

                if (result != null) {
                    if (result == InvalidObject.instance) {
                        return null;
                    }
                    return result;
                }

                // Must use the uow connection in these cases.
                // can be certain that commit manager not active as on client.
                if (readQuery.isLockQuery(this) && !wasTransactionBegunPrematurely()) {
                    beginEarlyTransaction();
                }
            } else if (query.isObjectLevelModifyQuery()) {
                // Delete object queries must be processed locally.
                return query.executeInUnitOfWork(this, Record);
            }

            // Starting a transaction early when on a remote UnitOfWork starts
            // a transaction on the server side client session, and all queries
            // when they arrive they will go down the write connection.

            /* Fix to allow executing non-selecting SQL in a UnitOfWork. - RB */
            if ((!getCommitManager().isActive()) && query.isModifyQuery()) {
                if (!wasTransactionBegunPrematurely()) {
                    beginEarlyTransaction();
                }
            }
            Object result = getParent().executeQuery(query, Record);

            if (objectLevelRead) {
                result = ((ObjectLevelReadQuery)query).registerResultInUnitOfWork(result, this, Record, false);
            }
            if (query.isModifyAllQuery()) {
                storeModifyAllQuery(query);
            }
            return result;
        }
        return query.executeInUnitOfWork(this, Record);
    }

    protected boolean isOnClient() {
        return isOnClient;
    }

    /**
     * Return if this session is a unit of work.
     */
    @Override
    public boolean isRemoteUnitOfWork() {
        return true;
    }

    /**
     * The returned remote unit of work from the server is prepared to merge with local remote unit of work.
     */
    protected void prepareForMergeIntoRemoteUnitOfWork() {
        if (this.newObjectsCache == null) {
            return;
        }
        int size = this.newObjectsCache.size();
        if (size == 0) {
            return;
        }
        Map originalToClone = new IdentityHashMap(size);
        Map cloneToOriginal = new IdentityHashMap(size);

        // For new and unregistered objects the clone from the parent remote unit of work is picked and store as original
        // in the remote unit of work. This is done so that changes are merged into the clone of the parent.
        List remoteNewObjects = ((RemoteUnitOfWork)this.parent).getNewObjectsCache();
        for (int index = 0; index < size; index++) {
            Object cloneFromParent = remoteNewObjects.get(index);
            Object cloneFromSelf = this.newObjectsCache.get(index);
            if (cloneFromSelf != null) {
                originalToClone.put(cloneFromParent, cloneFromSelf);
                cloneToOriginal.put(cloneFromSelf, cloneFromParent);
            }
        }

        List remoteUnregisteredObjects = ((RemoteUnitOfWork)this.parent).getUnregisteredNewObjectsCache();
        size = remoteUnregisteredObjects.size();
        for (int index = 0; index < size; index++) {
            Object cloneFromParent = ((RemoteUnitOfWork)getParent()).getUnregisteredNewObjects().get(remoteUnregisteredObjects.get(index));
            Object cloneFromSelf = getUnregisteredNewObjects().get(this.unregisteredNewObjectsCache.get(index));
            originalToClone.put(cloneFromParent, cloneFromSelf);
            cloneToOriginal.put(cloneFromSelf, cloneFromParent);
        }

        this.newObjectsOriginalToClone = originalToClone;
        this.newObjectsCloneToOriginal = cloneToOriginal;
    }

    /**
     * INTERNAL:
     * Re-initialize for the server-side session.
     * This is done when the uow is passed back to the server for committing.
     */
    public void reinitializeForSession(AbstractSession session, RemoteSessionController parentSessionController) {
        // If a server, acquire a client to commit into as client store connection for commit.
        if (session.isServerSession()) {
            session = ((org.eclipse.persistence.sessions.server.ServerSession)session).acquireClientSession();
        }
        setIsOnClient(false);
        setParentSessionController(parentSessionController);
        setParent(session);
        setProject(session.getProject());
        setProfiler(session.getProfiler());
        if (session.hasEventManager()) {
            setEventManager(session.getEventManager().clone(this));
        }
        //	setShouldLogMessages(session.shouldLogMessages());
        setSessionLog(session.getSessionLog());
        setLog(session.getLog());
        // These are transient so must be reset.
        setCommitManager(new CommitManager(this));
        setTransactionMutex(new ConcurrencyManager());
        getCommitManager().setCommitOrder(session.getCommitManager().getCommitOrder());

        if (session.hasExternalTransactionController()) {
            session.getExternalTransactionController().registerSynchronizationListener(this, session);
        }

        if (this.unitOfWorkChangeSet != null) {
            fixRemoteChangeSet(this.unitOfWorkChangeSet, null, this);
        }
        if (this.cumulativeUOWChangeSet != null) {
            fixRemoteChangeSet(this.cumulativeUOWChangeSet, null, this);
        }
    }

    /**
     * INTERNAL:
     * Fix the transient fields in the serialized change set.
     */
    protected void fixRemoteChangeSet(UnitOfWorkChangeSet uowChangeSet, Map cloneMap, AbstractSession session) {
        if (uowChangeSet == null) {
            return;
        }
        uowChangeSet.setSession(session);
        for (Map.Entry<Class, Map<ObjectChangeSet, ObjectChangeSet>> entry : uowChangeSet.getObjectChanges().entrySet()) {
            ClassDescriptor descriptor = getDescriptor(entry.getKey());
            for (ObjectChangeSet changeSet : entry.getValue().values()) {
                changeSet.setDescriptor(descriptor);
                changeSet.setClassType(entry.getKey());
            }
        }
        for (Map.Entry<Class, Map<ObjectChangeSet, ObjectChangeSet>> entry : uowChangeSet.getNewObjectChangeSets().entrySet()) {
            ClassDescriptor descriptor = getDescriptor(entry.getKey());
            for (ObjectChangeSet changeSet : entry.getValue().values()) {
                changeSet.setDescriptor(descriptor);
                changeSet.setClassType(entry.getKey());
            }
        }
        if (cloneMap == null) {
            for (Map.Entry<Object, ObjectChangeSet> entry : uowChangeSet.getCloneToObjectChangeSet().entrySet()) {
                Object clone = entry.getKey();
                ObjectChangeSet changeSet = entry.getValue();
                changeSet.postSerialize(clone, uowChangeSet, session);
            }            
        } else {
            // Also need to reset the remote objects with their local clones.
            int size = uowChangeSet.getCloneToObjectChangeSet().size();
            Map<Object, ObjectChangeSet> newCloneToObjectChangeSet = new IdentityHashMap<Object, ObjectChangeSet>(size);
            Map<ObjectChangeSet, Object> newObjectChangeSetToUOWClone = new IdentityHashMap<ObjectChangeSet, Object>(size);
            for (Map.Entry<Object, ObjectChangeSet> entry : uowChangeSet.getCloneToObjectChangeSet().entrySet()) {
                Object clone = cloneMap.get(entry.getKey());
                // Deleted objects no longer exist, so use remote copy instead.
                if (clone == null) {
                    clone = entry.getKey();
                }
                ObjectChangeSet changeSet = entry.getValue();
                changeSet.postSerialize(clone, uowChangeSet, session);
                newCloneToObjectChangeSet.put(clone, changeSet);
                newObjectChangeSetToUOWClone.put(changeSet, clone);
            }
            uowChangeSet.setCloneToObjectChangeSet(newCloneToObjectChangeSet);
            uowChangeSet.setObjectChangeSetToUOWClone(newObjectChangeSetToUOWClone);
        }
    }

    protected void setIsOnClient(boolean isOnClient) {
        this.isOnClient = isOnClient;
    }

    /**
     * Set a new object cache
     */
    protected void setNewObjectsCache(List newObjectsCache) {
        this.newObjectsCache = newObjectsCache;
    }

    /**
     * INTERNAL:
     * Sets the parent RemoteSessionController for this Remote UnitOfWork
     * Used to retrieve Valueholders that were used on the client
     */
    public void setParentSessionController(RemoteSessionController parentSessionController) {
        this.parentSessionController = parentSessionController;
    }

    /**
     * Set unregistered new object cache
     */
    protected void setUnregisteredNewObjectsCache(List unregisteredNewObjectsCache) {
        this.unregisteredNewObjectsCache = unregisteredNewObjectsCache;
    }

    /**
     * Avoid the toString printing the accessor and platform.
     */
    @Override
    public String toString() {
        return Helper.getShortClassName(getClass()) + "()";
    }

    /**
     * TESTING:
     * This is used by testing code to ensure that a deletion was successful.
     */
    @Override
    public boolean verifyDelete(Object domainObject) {
        return getParent().verifyDelete(domainObject);
    }
}
