/*
 * Copyright (c) 1998, 2018 Oracle and/or its affiliates. All rights reserved.
 *
 * This program and the accompanying materials are made available under the
 * terms of the Eclipse Public License v. 2.0 which is available at
 * http://www.eclipse.org/legal/epl-2.0,
 * or the Eclipse Distribution License v. 1.0 which is available at
 * http://www.eclipse.org/org/documents/edl-v10.php.
 *
 * SPDX-License-Identifier: EPL-2.0 OR BSD-3-Clause
 */

// Contributors:
//     Oracle - initial API and implementation from Oracle TopLink
package org.eclipse.persistence.internal.sessions;

import java.util.*;

import org.eclipse.persistence.internal.identitymaps.*;
import org.eclipse.persistence.queries.*;
import org.eclipse.persistence.expressions.*;
import org.eclipse.persistence.exceptions.*;
import org.eclipse.persistence.internal.sessions.AbstractSession;
import org.eclipse.persistence.descriptors.CacheIndex;
import org.eclipse.persistence.descriptors.ClassDescriptor;
import org.eclipse.persistence.sessions.Record;
import org.eclipse.persistence.logging.SessionLog;
import org.eclipse.persistence.internal.descriptors.ObjectBuilder;
import org.eclipse.persistence.internal.descriptors.PersistenceEntity;
import org.eclipse.persistence.internal.helper.WriteLockManager;

/**
 * INTERNAL:
 * Internal subclass that provides access to identity maps through the session.
 * Implements the IdentityMapAccessor interface which provides all publicly available
 * identity map functionality to users.
 * This is the main class that should be used to access identity maps.  In general, any
 * function that accesses the identity map manager should go through this class
 * Any session specific functionality appears in subclasses
 */
public class IsolatedClientSessionIdentityMapAccessor extends org.eclipse.persistence.internal.sessions.IdentityMapAccessor {

    protected Map objectsLockedForClone;

    /**
     * INTERNAL:
     * An IdentityMapAccessor sits between the session and the identityMapManager
     * It needs references in both directions
     */
    public IsolatedClientSessionIdentityMapAccessor(AbstractSession session) {
        super(session);
    }

    /**
     * INTERNAL:
     * Deferred lock the identity map for the object, this is used for avoiding deadlock
     * The return cacheKey should be used to release the deferred lock
     */
    @Override
    public CacheKey acquireDeferredLock(Object primaryKey, Class javaClass, ClassDescriptor descriptor, boolean isCacheCheckComplete) {
        if (!descriptor.getCachePolicy().isSharedIsolation()) {
            return getIdentityMapManager().acquireDeferredLock(primaryKey, javaClass, descriptor, isCacheCheckComplete);
        } else {
            return ((IsolatedClientSession)session).getParent().getIdentityMapAccessorInstance().acquireDeferredLock(primaryKey, javaClass, descriptor, isCacheCheckComplete);
        }
    }

    /**
     * INTERNAL:
     * Provides access for setting a concurrency lock on an object in the IdentityMap.
     * called with true from the merge process, if true then the refresh will not refresh the object.
     */
    @Override
    public CacheKey acquireLock(Object primaryKey, Class domainClass, boolean forMerge, ClassDescriptor descriptor, boolean isCacheCheckComplete) {
        if (!descriptor.getCachePolicy().isSharedIsolation()) {
            return getIdentityMapManager().acquireLock(primaryKey, domainClass, forMerge, descriptor, isCacheCheckComplete);
        } else {
            return ((IsolatedClientSession)session).getParent().getIdentityMapAccessorInstance().acquireLock(primaryKey, domainClass, forMerge, descriptor, isCacheCheckComplete);
        }
    }

    /**
     * INTERNAL:
     * Provides access for setting a concurrency lock on an object in the IdentityMap.
     * called with true from the merge process, if true then the refresh will not refresh the object.
     */
    @Override
    public CacheKey acquireLockNoWait(Object primaryKey, Class domainClass, boolean forMerge, ClassDescriptor descriptor) {
        if (!descriptor.getCachePolicy().isSharedIsolation()) {
            return getIdentityMapManager().acquireLockNoWait(primaryKey, domainClass, forMerge, descriptor);
        } else {
            return ((IsolatedClientSession)session).getParent().getIdentityMapAccessorInstance().acquireLockNoWait(primaryKey, domainClass, forMerge, descriptor);
        }
    }

    /**
     * INTERNAL:
     * Provides access for setting a concurrency lock on an object in the IdentityMap.
     * called with true from the merge process, if true then the refresh will not refresh the object.
     */
    @Override
    public CacheKey acquireLockWithWait(Object primaryKey, Class domainClass, boolean forMerge, ClassDescriptor descriptor, int wait) {
        if (!descriptor.getCachePolicy().isSharedIsolation()) {
            return getIdentityMapManager().acquireLockWithWait(primaryKey, domainClass, forMerge, descriptor, wait);
        } else {
            return ((IsolatedClientSession)session).getParent().getIdentityMapAccessorInstance().acquireLockWithWait(primaryKey, domainClass, forMerge, descriptor, wait);
        }
    }

    /**
     * INTERNAL:
     * Find the cachekey for the provided primary key and place a readlock on it.
     * This will allow multiple users to read the same object but prevent writes to
     * the object while the read lock is held.
     */
    @Override
    public CacheKey acquireReadLockOnCacheKey(Object primaryKey, Class domainClass, ClassDescriptor descriptor) {
        if (!descriptor.getCachePolicy().isSharedIsolation()) {
            return getIdentityMapManager().acquireReadLockOnCacheKey(primaryKey, domainClass, descriptor);
        } else {
            return ((IsolatedClientSession)session).getParent().getIdentityMapAccessorInstance().acquireReadLockOnCacheKey(primaryKey, domainClass, descriptor);
        }
    }

    /**
     * INTERNAL:
     * Find the cachekey for the provided primary key and place a readlock on it.
     * This will allow multiple users to read the same object but prevent writes to
     * the object while the read lock is held.
     * If no readlock can be acquired then do not wait but return null.
     */
    @Override
    public CacheKey acquireReadLockOnCacheKeyNoWait(Object primaryKey, Class domainClass, ClassDescriptor descriptor) {
        if (!descriptor.getCachePolicy().isSharedIsolation()) {
            return getIdentityMapManager().acquireReadLockOnCacheKeyNoWait(primaryKey, domainClass, descriptor);
        } else {
            return ((IsolatedClientSession)session).getParent().getIdentityMapAccessorInstance().acquireReadLockOnCacheKeyNoWait(primaryKey, domainClass, descriptor);
        }
    }

    /**
    * INTERNAL:
    * Lock the entire cache if the cache isolation requires.
    * By default concurrent reads and writes are allowed.
    * By write, unit of work merge is meant.
    */
    @Override
    public boolean acquireWriteLock() {
        getIdentityMapManager().acquireWriteLock();
        // must lock the parents cache as well.
        return ((IsolatedClientSession)session).getParent().getIdentityMapAccessorInstance().acquireWriteLock();
    }

    /**
     * ADVANCED:
     * Return if their is an object for the primary key.
     */
    @Override
    public boolean containsObjectInIdentityMap(Object primaryKey, Class theClass, ClassDescriptor descriptor) {
        if (!descriptor.getCachePolicy().isSharedIsolation()) {
            if (this.identityMapManager == null) {
                return false;
            }
            return getIdentityMapManager().containsKey(primaryKey, theClass, descriptor);
        } else {
            return ((IsolatedClientSession)session).getParent().getIdentityMapAccessorInstance().containsObjectInIdentityMap(primaryKey, theClass, descriptor);
        }
    }

    /**
     * INTERNAL:
     * This method is used to get a list of those classes with IdentityMaps in the Session.
     */
    @Override
    public Vector getClassesRegistered() {
        Vector results = getIdentityMapManager().getClassesRegistered();
        results.addAll(((IsolatedClientSession)session).getParent().getIdentityMapAccessorInstance().getClassesRegistered());
        return results;
    }

    /**
     * ADVANCED:
     * Query the cache in-memory.
     * If the expression is too complex an exception will be thrown.
     * Only return objects that are invalid in the cache if specified.
     */
    @Override
    public Vector getAllFromIdentityMap(Expression selectionCriteria, Class theClass, Record translationRow, int valueHolderPolicy, boolean shouldReturnInvalidatedObjects) throws QueryException {
        if (!session.getDescriptor(theClass).getCachePolicy().isSharedIsolation()) {
            return getIdentityMapManager().getAllFromIdentityMap(selectionCriteria, theClass, translationRow, valueHolderPolicy, shouldReturnInvalidatedObjects);
        } else {
            return ((IsolatedClientSession)session).getParent().getIdentityMapAccessorInstance().getAllFromIdentityMap(selectionCriteria, theClass, translationRow, valueHolderPolicy, shouldReturnInvalidatedObjects);
        }
    }

    /**
     * INTERNAL:
     * Retrieve the cache key for the given identity information.
     * @param primaryKey the primary key of the cache key to be retrieved.
     * @param myClass the class of the cache key to be retrieved.
     */
    @Override
    public CacheKey getCacheKeyForObject(Object primaryKey, Class myClass, ClassDescriptor descriptor, boolean forMerge) {
        if (!descriptor.getCachePolicy().isSharedIsolation()) {
            if (this.identityMapManager == null) {
                return null;
            }
            return getIdentityMapManager().getCacheKeyForObject(primaryKey, myClass, descriptor, forMerge);
        } else {
            return ((IsolatedClientSession)session).getParent().getIdentityMapAccessorInstance().getCacheKeyForObject(primaryKey, myClass, descriptor, forMerge);
        }
    }

    /**
     * INTERNAL:
     * Retrieve the cache key for the given identity information.
     * @param primaryKey the primary key of the cache key to be retrieved.
     * @param myClass the class of the cache key to be retrieved.
     */
    @Override
    public CacheKey getCacheKeyForObjectForLock(Object primaryKey, Class myClass, ClassDescriptor descriptor) {
        if (!descriptor.getCachePolicy().isSharedIsolation()) {
            if (this.identityMapManager == null) {
                return null;
            }
            return getIdentityMapManager().getCacheKeyForObjectForLock(primaryKey, myClass, descriptor);
        } else {
            return ((IsolatedClientSession)session).getParent().getIdentityMapAccessorInstance().getCacheKeyForObjectForLock(primaryKey, myClass, descriptor);
        }
    }

    /**
     * ADVANCED:
     * Return the object from the identity with the primary and class.
     */
    @Override
    public Object getFromIdentityMap(Object primaryKey, Object object, Class theClass, boolean shouldReturnInvalidatedObjects, ClassDescriptor descriptor) {
        if (!descriptor.getCachePolicy().isSharedIsolation()){
            Object cachedObject = null;
            if (this.identityMapManager != null){
                cachedObject = getIdentityMapManager().getFromIdentityMap(primaryKey, theClass, shouldReturnInvalidatedObjects, descriptor);
            }
            if (descriptor.getCachePolicy().isIsolated()) {
                return cachedObject;
            }else{
                return getAndCloneCacheKeyFromParent(primaryKey, object, theClass, shouldReturnInvalidatedObjects, descriptor);
            }
        } else {
            return ((IsolatedClientSession)session).getParent().getIdentityMapAccessorInstance().getFromIdentityMap(primaryKey, object, theClass, shouldReturnInvalidatedObjects, descriptor);
        }
    }

    protected Object getAndCloneCacheKeyFromParent(Object primaryKey, Object objectToClone, Class theClass, boolean shouldReturnInvalidatedObjects, ClassDescriptor descriptor) {
        CacheKey cacheKey = null;
        if (objectToClone != null && objectToClone instanceof PersistenceEntity){
            cacheKey = ((PersistenceEntity)objectToClone)._persistence_getCacheKey();
        }
        if (cacheKey == null || cacheKey.isIsolated() || cacheKey.getOwningMap() == null){
            org.eclipse.persistence.internal.sessions.IdentityMapAccessor parentIdentityMapAccessor = session.getParent().getIdentityMapAccessorInstance();
            cacheKey = parentIdentityMapAccessor.getCacheKeyForObject(primaryKey, theClass, descriptor, false);
        }
        Object objectFromCache = null;
        // this check could be simplified to one line but would create a window
        // in which GC could remove the object and we would end up with a null pointer
        // as well we must inspect the cacheKey without locking on it.
        if ((cacheKey != null) && (shouldReturnInvalidatedObjects || !descriptor.getCacheInvalidationPolicy().isInvalidated(cacheKey))) {
            synchronized (cacheKey) {
                //if the object in the cachekey is null but the key is acquired then
                //someone must be rebuilding it or creating a new one.  Sleep until
                // it's finished. A plain wait here would be more efficient but we may not
                // get notified for quite some time (ie deadlock) if the other thread
                //is building the object.  Must wait and not sleep in order for the monitor to be released
                objectFromCache = cacheKey.getObject();
                try {
                    while (cacheKey.isAcquired() && (objectFromCache == null)) {
                        cacheKey.wait(5);
                    }
                } catch (InterruptedException ex) {
                }
                if (objectFromCache == null) {
                    return null;
                }
            }
        } else {
            return null;
        }

        ClassDescriptor concreteDescriptor = descriptor;
        // Ensure correct subclass descriptor.
        if (objectFromCache.getClass() != descriptor.getJavaClass()) {
            concreteDescriptor = session.getDescriptor(objectFromCache);
        }
        ObjectBuilder builder = concreteDescriptor.getObjectBuilder();
        Object workingClone = null;

        // The cache/objects being registered must first be locked to ensure
        // that a merge or refresh does not occur on the object while being cloned to
        // avoid cloning a partially merged/refreshed object.
        // If a cache isolation level is used, then lock the entire cache.
        // otherwise lock the object and it related objects (not using indirection) as a unit.
        // If just a simple object (all indirection) a simple read-lock can be used.
        // PERF: Cache if check to write is required.
        org.eclipse.persistence.internal.sessions.IdentityMapAccessor parentIdentityMapAccessor = session.getParent().getIdentityMapAccessorInstance();
        boolean identityMapLocked = parentIdentityMapAccessor.acquireWriteLock();
        boolean rootOfCloneRecursion = false;
        if (identityMapLocked) {
            session.checkAndRefreshInvalidObject(objectFromCache, cacheKey, descriptor);
        } else {
            // Check if we have locked all required objects already.
            if (this.objectsLockedForClone == null) {
                // PERF: If a simple object just acquire a simple read-lock.
                if (concreteDescriptor.shouldAcquireCascadedLocks()) {
                    this.objectsLockedForClone = parentIdentityMapAccessor.getWriteLockManager().acquireLocksForClone(objectFromCache, concreteDescriptor, cacheKey, session);
                } else {
                    session.checkAndRefreshInvalidObject(objectFromCache, cacheKey, descriptor);
                    cacheKey.acquireReadLock();
                }
                rootOfCloneRecursion = true;
            }
        }
        try {
            // bug:6167576   Must acquire the lock before cloning.
            workingClone = builder.instantiateWorkingCopyClone(objectFromCache, session);
            // PERF: Cache the primary key if implements PersistenceEntity.
            if (workingClone instanceof PersistenceEntity) {
                ((PersistenceEntity)workingClone)._persistence_setId(cacheKey.getKey());
            }
            CacheKey localCacheKey = acquireLock(primaryKey, theClass, descriptor, false);
            try{
                localCacheKey.setObject(workingClone);
                localCacheKey.setReadTime(cacheKey.getReadTime());
                localCacheKey.setWriteLockValue(cacheKey.getWriteLockValue());
                builder.populateAttributesForClone(objectFromCache, cacheKey, workingClone, null, session);
            }finally{
                localCacheKey.release();
            }

            //also clone the fetch group reference if applied
            if (concreteDescriptor.hasFetchGroupManager()) {
                concreteDescriptor.getFetchGroupManager().copyFetchGroupInto(objectFromCache, workingClone, session);
            }
        } finally {
            // If the entire cache was locked, release the cache lock,
            // otherwise either release the cache-key for a simple lock,
            // otherwise release the entire set of locks for related objects if this was the root.
            if (identityMapLocked) {
                parentIdentityMapAccessor.releaseWriteLock();
            } else {
                if (rootOfCloneRecursion) {
                    if (this.objectsLockedForClone == null) {
                        cacheKey.releaseReadLock();
                    } else {
                        for (Iterator iterator = this.objectsLockedForClone.values().iterator(); iterator.hasNext();) {
                            ((CacheKey)iterator.next()).releaseReadLock();
                        }
                        this.objectsLockedForClone = null;
                    }
                    session.executeDeferredEvents();
                }
            }
        }
        concreteDescriptor.getObjectBuilder().instantiateEagerMappings(workingClone, session);
        return workingClone;
    }

    /**
     * INTERNAL:
     * Return the object from the local identity map with the primary and class.
     * This avoids checking the parent cache for the unit of work.
     */
    public Object getFromLocalIdentityMap(Object primaryKey, Class theClass, boolean shouldReturnInvalidatedObjects, ClassDescriptor descriptor) {
        return getFromIdentityMap(primaryKey, null, theClass, shouldReturnInvalidatedObjects, descriptor);
    }

    /**
     * INTERNAL:
     * Query the cache in-memory.
     * If the object is not found null is returned.
     * If the expression is too complex an exception will be thrown.
     */
    @Override
    public Object getFromIdentityMap(Expression selectionCriteria, Class theClass, Record translationRow, int valueHolderPolicy, boolean conforming, boolean shouldReturnInvalidatedObjects, ClassDescriptor descriptor) {
        if (!descriptor.getCachePolicy().isSharedIsolation()) {
            return getIdentityMapManager().getFromIdentityMap(selectionCriteria, theClass, translationRow, valueHolderPolicy, conforming, shouldReturnInvalidatedObjects, descriptor);
        } else {
            return ((IsolatedClientSession)session).getParent().getIdentityMapAccessorInstance().getFromIdentityMap(selectionCriteria, theClass, translationRow, valueHolderPolicy, conforming, shouldReturnInvalidatedObjects, descriptor);
        }
    }

    /**
     * INTERNAL:
     * Return the object from the identity with the primary and class.
     * Only return invalidated objects if requested
     */
    @Override
    public Object getFromIdentityMapWithDeferredLock(Object primaryKey, Class theClass, boolean shouldReturnInvalidatedObjects, ClassDescriptor descriptor) {
        if (!descriptor.getCachePolicy().isSharedIsolation()) {
            if (this.identityMapManager == null) {
                return null;
            }
            return getIdentityMapManager().getFromIdentityMapWithDeferredLock(primaryKey, theClass, shouldReturnInvalidatedObjects, descriptor);
        } else {
            return ((IsolatedClientSession)session).getParent().getIdentityMapAccessorInstance().getFromIdentityMapWithDeferredLock(primaryKey, theClass, shouldReturnInvalidatedObjects, descriptor);
        }
    }

    /**
     * INTERNAL:
     * Get the IdentityMapManager for this IdentityMapAccessor
     * This method should be used for all IdentityMapManager access since it may
     * be overridden in sub classes.
     */
    @Override
    public IdentityMapManager getIdentityMapManager() {
        // PERF: Lazy init manager as normally isolated object are only read in the unit of work.
        if (this.identityMapManager == null) {
            this.identityMapManager = new IdentityMapManager(this.session);
        }
        return this.identityMapManager;
    }

    /**
     * INTERNAL:
     * Get the identity map for the given class from the IdentityMapManager
     */
    @Override
    public IdentityMap getIdentityMap(ClassDescriptor descriptor, boolean returnNullIfMissing) {
        if (!descriptor.getCachePolicy().isSharedIsolation()) {
            return getIdentityMapManager().getIdentityMap(descriptor, returnNullIfMissing);
        } else {
            return ((IsolatedClientSession)session).getParent().getIdentityMapAccessorInstance().getIdentityMap(descriptor, returnNullIfMissing);
        }
    }

    /**
     * INTERNAL:
     * Get the cached results associated with a query.  Results are cached by the
     * values of the parameters to the query so different parameters will have
     * different cached results.
     */
    @Override
    public Object getQueryResult(ReadQuery query, List parameters, boolean checkExpiry) {
        if (((IsolatedClientSession)session).isIsolatedQuery(query)) {
            return getIdentityMapManager().getQueryResult(query, parameters, checkExpiry);
        } else {
            return ((IsolatedClientSession)session).getParent().getIdentityMapAccessorInstance().getQueryResult(query, parameters, checkExpiry);
        }
    }

    /**
     * INTERNAL:
     * get the session associated with this IdentityMapAccessor
     */
    @Override
    public AbstractSession getSession() {
        return session;
    }

    /**
     * INTERNAL:
     * Get the wrapper object from the cache key associated with the given primary key,
     * this is used for EJB.
     */
    @Override
    public Object getWrapper(Object primaryKey, Class theClass) {
        if (!session.getDescriptor(theClass).getCachePolicy().isSharedIsolation()) {
            return getIdentityMapManager().getWrapper(primaryKey, theClass);
        } else {
            return ((IsolatedClientSession)session).getParent().getIdentityMapAccessorInstance().getWrapper(primaryKey, theClass);
        }
    }

    /**
     * INTERNAL:
     * Returns the single write Lock manager for this session
     */
    public WriteLockManager getWriteLockManager() {
        // As there should only be one write lock manager per server session
        // get the one from the parent.
        return ((IsolatedClientSession)session).getParent().getIdentityMapAccessorInstance().getWriteLockManager();
    }

    /**
     * ADVANCED:
     * Extract the write lock value from the identity map.
     */
    @Override
    public Object getWriteLockValue(Object primaryKey, Class theClass, ClassDescriptor descriptor) {
        if (!descriptor.getCachePolicy().isSharedIsolation()) {
            return getIdentityMapManager().getWriteLockValue(primaryKey, theClass, descriptor);
        } else {
            return ((IsolatedClientSession)session).getParent().getIdentityMapAccessorInstance().getWriteLockValue(primaryKey, theClass, descriptor);
        }
    }

    /**
     * PUBLIC:
     * Reset the entire object cache.
     * <p> NOTE: be careful using this method. This method blows away both this session's and its parents caches,
     * this includes the server cache or any other cache. This throws away any objects that have been read in.
     * Extream caution should be used before doing this because object identity will no longer
     * be maintained for any objects currently read in.  This should only be called
     * if the application knows that it no longer has references to object held in the cache.
     */
    @Override
    public void initializeAllIdentityMaps() {
        super.initializeAllIdentityMaps();
        ((IsolatedClientSession)session).getParent().getIdentityMapAccessorInstance().initializeAllIdentityMaps();
    }

    /**
     * PUBLIC:
     * Reset the identity map for only the instances of the class.
     * For inheritance the user must make sure that they only use the root class.
     * Caution must be used in doing this to ensure that the objects within the identity map
     * are not referenced from other objects of other classes or from the application.
     */
    @Override
    public void initializeIdentityMap(Class theClass) {
        getSession().log(SessionLog.FINER, SessionLog.CACHE, "initialize_identitymap", theClass);
        if (!session.getDescriptor(theClass).getCachePolicy().isSharedIsolation()) {
            getIdentityMapManager().initializeIdentityMap(theClass);
        } else {
            ((IsolatedClientSession)session).getParent().getIdentityMapAccessorInstance().initializeIdentityMap(theClass);
        }
    }

    /**
     * Invalidate/remove any results for the class from the query cache.
     * This is used to invalidate the query cache on any change.
     */
    @Override
    public void invalidateQueryCache(Class classThatChanged) {
        if (!session.getDescriptor(classThatChanged).getCachePolicy().isSharedIsolation()) {
            getIdentityMapManager().invalidateQueryCache(classThatChanged);
        } else {
            ((IsolatedClientSession)session).getParent().getIdentityMapAccessorInstance().invalidateQueryCache(classThatChanged);
        }
    }

    /**
     * PUBLIC:
     * Reset the entire local object cache.
     * This throws away any objects that have been read in.
     * Extreme caution should be used before doing this because object identity will no longer
     * be maintained for any objects currently read in.  This should only be called
     * if the application knows that it no longer has references to object held in the cache.
     */
    @Override
    public void initializeIdentityMaps() {
        getSession().log(SessionLog.FINER, SessionLog.CACHE, "initialize_identitymaps");
        getIdentityMapManager().initializeIdentityMaps();
        getSession().getCommitManager().reinitialize();
    }

    /**
     * INTERNAL:
     * Set the results for a query.
     * Query results are cached based on the parameter values provided to the query
     * different parameter values access different caches.
     */
    @Override
    public void putQueryResult(ReadQuery query, List parameters, Object results) {
        if (((IsolatedClientSession)session).isIsolatedQuery(query)) {
            getIdentityMapManager().putQueryResult(query, parameters, results);
        } else {
            ((IsolatedClientSession)session).getParent().getIdentityMapAccessorInstance().putQueryResult(query, parameters, results);
        }
    }

    /**
     * Index the cache key by the index values.
     */
    @Override
    public void putCacheKeyByIndex(CacheIndex index, CacheId indexValues, CacheKey cacheKey, ClassDescriptor descriptor) {
        if (!descriptor.getCachePolicy().isSharedIsolation()) {
            getIdentityMapManager().putCacheKeyByIndex(index, indexValues, cacheKey, descriptor);
        } else {
            ((IsolatedClientSession)session).getParent().getIdentityMapAccessorInstance().putCacheKeyByIndex(index, indexValues, cacheKey, descriptor);
        }
    }

    /**
     * Return the cache key for the cache index or null if not found.
     */
    @Override
    public CacheKey getCacheKeyByIndex(CacheIndex index, CacheId indexValues, boolean shouldCheckExpiry, ClassDescriptor descriptor) {
        if (!descriptor.getCachePolicy().isSharedIsolation()) {
            return getIdentityMapManager().getCacheKeyByIndex(index, indexValues, shouldCheckExpiry, descriptor);
        } else {
            return ((IsolatedClientSession)session).getParent().getIdentityMapAccessorInstance().getCacheKeyByIndex(index, indexValues, shouldCheckExpiry, descriptor);
        }
    }

    /**
     * PUBLIC:
     * Used to print all the objects in the identity map of the passed in class.
     * The output of this method will be logged to this session's SessionLog at SEVERE level.
     */
    @Override
    public void printIdentityMap(Class businessClass) {
        if (getSession().shouldLog(SessionLog.SEVERE, SessionLog.CACHE)) {
            if (!session.getDescriptor(businessClass).getCachePolicy().isSharedIsolation()) {
                getIdentityMapManager().printIdentityMap(businessClass);
            } else {
                ((IsolatedClientSession)session).getParent().getIdentityMapAccessorInstance().printIdentityMap(businessClass);
            }
        }
    }

    /**
     * PUBLIC:
     * Used to print all the objects in every identity map in this session.
     * The output of this method will be logged to this session's SessionLog at SEVERE level.
     */
    @Override
    public void printIdentityMaps() {
        if (getSession().shouldLog(SessionLog.SEVERE, SessionLog.CACHE)) {
            getIdentityMapManager().printIdentityMaps();
            ((IsolatedClientSession)session).getParent().getIdentityMapAccessorInstance().printIdentityMaps();
        }
    }

    /**
     * PUBLIC:
     * Used to print all the locks in every identity map in this session.
     * The output of this method will be logged to this session's SessionLog at FINEST level.
     */
    @Override
    public void printIdentityMapLocks() {
        if (getSession().shouldLog(SessionLog.FINEST, SessionLog.CACHE)) {
            getIdentityMapManager().printLocks();
            ((IsolatedClientSession)session).getParent().getIdentityMapAccessorInstance().printIdentityMapLocks();
        }
    }

    /**
     * ADVANCED:
     * Register the object with the identity map.
     * The object must always be registered with its version number if optimistic locking is used.
     * The readTime may also be included in the cache key as it is constructed.
     */
    public CacheKey internalPutInIdentityMap(Object domainObject, Object key, Object writeLockValue, long readTime, ClassDescriptor descriptor) {
        //no need to unwrap as the put will unwrap later anyway
        if (!descriptor.getCachePolicy().isSharedIsolation()) {
            return getIdentityMapManager().putInIdentityMap(domainObject, key, writeLockValue, readTime, descriptor);
        } else {
            return ((IsolatedClientSession)session).getParent().getIdentityMapAccessorInstance().internalPutInIdentityMap(domainObject, key, writeLockValue, readTime, descriptor);
        }
    }

    /**
     * ADVANCED:
     * Queries the cache in-memory with the passed in criteria and invalidates matching Objects.
     * If the expression is too complex either all or none object of theClass invalidated (depending on shouldInvalidateOnException value).
     * @param selectionCriteria Expression selecting the Objects to be returned
     * @param theClass Class to be considered
     * @param translationRow Record
     * @param shouldInvalidateOnException boolean indicates weather to invalidate the object if conform threw exception.
     */
    @Override
    public void invalidateObjects(Expression selectionCriteria, Class theClass, Record translationRow, boolean shouldInvalidateOnException) {
        if (!session.getDescriptor(theClass).getCachePolicy().isSharedIsolation()) {
            getIdentityMapManager().invalidateObjects(selectionCriteria, theClass, translationRow, shouldInvalidateOnException);
        } else {
            ((IsolatedClientSession)session).getParent().getIdentityMapAccessorInstance().invalidateObjects(selectionCriteria, theClass, translationRow, shouldInvalidateOnException);
        }
    }

    /**
    * INTERNAL:
    * Lock the entire cache if the cache isolation requires.
    * By default concurrent reads and writes are allowed.
    * By write, unit of work merge is meant.
    */
    @Override
    public void releaseWriteLock() {
        //release in the opposite order of the acquire
        ((IsolatedClientSession)session).getParent().getIdentityMapAccessorInstance().releaseWriteLock();
        getIdentityMapManager().releaseWriteLock();
    }

    /**
     * ADVANCED:
     * Remove the object from the object cache.
     */
    @Override
    public Object removeFromIdentityMap(Object key, Class theClass, ClassDescriptor descriptor, Object object) {
        Object removedObject = null;
        if (descriptor.isIsolated() || descriptor.isProtectedIsolation()) {
            removedObject = getIdentityMapManager().removeFromIdentityMap(key, theClass, descriptor, object);
        }
        if (!descriptor.isIsolated()){
            removedObject = ((IsolatedClientSession)session).getParent().getIdentityMapAccessorInstance().removeFromIdentityMap(key, theClass, descriptor, object);
        }
        return removedObject;
    }

    /**
     * INTERNAL:
     * Set the IdentityMapManager for this IdentityMapAccessor
     */
    @Override
    public void setIdentityMapManager(IdentityMapManager identityMapManager) {
        this.identityMapManager = identityMapManager;
    }

    /**
     * INTERNAL:
     * Update the wrapper object the cache key associated with the given primary key,
     * this is used for EJB.
     */
    @Override
    public void setWrapper(Object primaryKey, Class theClass, Object wrapper) {
        if (!getSession().getDescriptor(theClass).getCachePolicy().isSharedIsolation()) {
            getIdentityMapManager().setWrapper(primaryKey, theClass, wrapper);
        } else {
            ((IsolatedClientSession)session).getParent().getIdentityMapAccessorInstance().setWrapper(primaryKey, theClass, wrapper);
        }
    }

    /**
     * ADVANCED:
     * Update the write lock value in the identity map.
     */
    @Override
    public void updateWriteLockValue(Object primaryKey, Class theClass, Object writeLockValue) {
        if (!getSession().getDescriptor(theClass).getCachePolicy().isSharedIsolation()) {
            getIdentityMapManager().setWriteLockValue(primaryKey, theClass, writeLockValue);
        } else {
            ((IsolatedClientSession)session).getParent().getIdentityMapAccessorInstance().updateWriteLockValue(primaryKey, theClass, writeLockValue);
        }
    }
}
