/*******************************************************************************
 * Copyright (c) 1998, 2008 Oracle. 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:
 *     Gordon Yorke - Initial Feature development
 ******************************************************************************/
package org.eclipse.persistence.sessions.interceptors;

import java.util.Enumeration;
import java.util.HashMap;
import java.util.Vector;

import org.eclipse.persistence.descriptors.ClassDescriptor;
import org.eclipse.persistence.internal.helper.Helper;
import org.eclipse.persistence.internal.identitymaps.CacheKey;
import org.eclipse.persistence.internal.identitymaps.IdentityMap;
import org.eclipse.persistence.internal.sessions.AbstractSession;

/**
 * <p>
 * <b>Purpose</b>: Define a class through which Cache access can be
 * intercepted.
 * <p>
 * <b>Description</b>: EclipseLink makes extensive use of caching
 * functionality. This class provides the mechanism for intercepting the cache
 * access. Once intercepted applications can allow the process to continue by
 * calling the method on the class attribute targetIdentityMap as described in
 * the API javadocs or redirect the process entirely. * As with IdentityMaps an
 * entire class inheritance heirachy will share the same interceptor.
 * <p>
 * To implement a CacheInterceptor users should subclass this class implementing
 * those methods they wish to intercept.
 * <p>
 * <b>Configuration</b>: Interceptors may be added to a session by using a
 * Session or Descriptor customizer to set the class name of the interceptor on
 * the target ClassDescriptor through the method
 * setCacheInterceptorClass(Class).
 */
public abstract class CacheInterceptor implements IdentityMap{
    /**
     * This attribute stores the actual IdentityMap as configured by the user.
     */
    protected IdentityMap targetIdentityMap;

    protected AbstractSession interceptedSession;
    
    public CacheInterceptor(IdentityMap targetIdentityMap, AbstractSession interceptedSession){
        this.targetIdentityMap = targetIdentityMap;
        this.interceptedSession = interceptedSession;
    }
    /**
     * Acquire a deferred lock on the object.
     * This is used while reading if the object has relationships without indirection.
     * This first thread will get an active lock.
     * Other threads will get deferred locks, all threads will wait until all other threads are complete before releasing their locks.
     */
    public CacheKey acquireDeferredLock(Vector primaryKey){
        return createCacheKeyInterceptor(this.targetIdentityMap.acquireDeferredLock(primaryKey));
    }

    /**
     * Acquire an active lock on the object.
     * This is used by reading (when using indirection or no relationships) and by merge.
     */
    public CacheKey acquireLock(Vector primaryKey, boolean forMerge){
        return createCacheKeyInterceptor(this.targetIdentityMap.acquireLock(primaryKey, forMerge));
    }

    /**
     * Acquire an active lock on the object, if not already locked.
     * This is used by merge for missing existing objects.
     */
    public CacheKey acquireLockNoWait(Vector primaryKey, boolean forMerge){
        CacheKey cacheKeyToBeWrapped = this.targetIdentityMap.acquireLockNoWait(primaryKey, forMerge);
        if (cacheKeyToBeWrapped != null){
            return createCacheKeyInterceptor(cacheKeyToBeWrapped);
        }
        return null;
    }
    
    /**
     * Acquire an active lock on the object, if not already locked.
     * This is used by merge for missing existing objects.
     */
    public CacheKey acquireLockWithWait(Vector primaryKey, boolean forMerge, int wait) {
        CacheKey cacheKeyToBeWrapped = this.targetIdentityMap.acquireLockWithWait(primaryKey, forMerge, wait);
        if (cacheKeyToBeWrapped != null) {
            return createCacheKeyInterceptor(cacheKeyToBeWrapped);
        }
        return null;
    }

    /**
     * Acquire a read lock on the object.
     * This is used by UnitOfWork cloning.
     * This will allow multiple users to read the same object but prevent writes to the object while the read lock is held.
     */
    public CacheKey acquireReadLockOnCacheKey(Vector primaryKey){
        return createCacheKeyInterceptor(this.targetIdentityMap.acquireReadLockOnCacheKey(primaryKey));
    }
    
    /**
     * Acquire a read lock on the object, if not already locked.
     * This is used by UnitOfWork cloning.
     * This will allow multiple users to read the same object but prevent writes to the object while the read lock is held.
     */
    public CacheKey acquireReadLockOnCacheKeyNoWait(Vector primaryKey){
        CacheKey cacheKeyToBeWrapped = this.targetIdentityMap.acquireReadLockOnCacheKeyNoWait(primaryKey);
        if (cacheKeyToBeWrapped != null){
            return createCacheKeyInterceptor(cacheKeyToBeWrapped);
        }
        return null;
    }
    
    /**
     * Add all locked CacheKeys to the map grouped by thread.
     * Used to print all the locks in the identity map.
     */
    public void collectLocks(HashMap threadList){
        this.targetIdentityMap.collectLocks(threadList);
    }

    /**
     * Clone the map and all of the CacheKeys.
     * This is used by UnitOfWork commitAndResumeOnFailure to avoid corrupting the cache during a failed commit.
     */
    public abstract Object clone();

    /**
     * Return true if an CacheKey with the primary key is in the map.
     * User API.
     * @param primaryKey is the primary key for the object to search for.
     */
    public boolean containsKey(Vector primaryKey){
        return this.targetIdentityMap.containsKey(primaryKey);
    }
    

    protected abstract CacheKeyInterceptor createCacheKeyInterceptor(CacheKey wrappedCacheKey);
    
    /**
     * Allow for the cache to be iterated on.  This method is only used during debugging when
     * validateCache() has been called to print out the contents of the cache.
     */
    public Enumeration elements(){
        return this.targetIdentityMap.elements();
    }

    /**
     * Return the object cached in the identity map or null if it could not be found.
     * User API.
     */
    public Object get(Vector primaryKey){
        return this.targetIdentityMap.get(primaryKey);
    }
    
    /**
     * Get the cache key (with object) for the primary key.
     */
    public CacheKey getCacheKey(Vector primaryKey){
        return this.targetIdentityMap.getCacheKey(primaryKey);
    }

    /**
     * Get the cache key (with object) for the primary key.
     */
    public CacheKey getCacheKeyForLock(Vector primaryKey){
        return this.targetIdentityMap.getCacheKeyForLock(primaryKey);
    }

    /**
     * Return the descriptor that this is the map for.
     */
    public ClassDescriptor getDescriptor(){
        return this.targetIdentityMap.getDescriptor();
    }

    /**
     * Return the class that this is the map for.
     */
    public Class getDescriptorClass(){
        return this.targetIdentityMap.getDescriptorClass();
    }
    
    /**
     * @return The maxSize for the IdentityMap (NOTE: some subclasses may use this differently).
     */
    public int getMaxSize(){
        return this.targetIdentityMap.getMaxSize();
    }

    /**
     * Return the number of CacheKeys in the IdentityMap.
     * This may contain weak referenced objects that have been garbage collected.
     */
    public int getSize(){
        return this.targetIdentityMap.getSize();
    }

    /**
     * Return the number of actual objects of type myClass in the IdentityMap.
     * Recurse = true will include subclasses of myClass in the count.
     */
    public int getSize(Class myClass, boolean recurse){
        return this.targetIdentityMap.getSize(myClass, recurse);
    }

    /**
     * Return the instance of the IdentityMap that this intercpetor is wrapping.  Any call that
     * is to be passed on to the underlying EclipseLink cache should be passed to this IdentityMap.
     */
    public IdentityMap getTargetIdenttyMap(){
        return this.targetIdentityMap;
    }
    
    /**
     * Get the wrapper object from the cache key associated with the given primary key,
     * this is used for EJB2.
     */
    public Object getWrapper(Vector primaryKey){
        return this.targetIdentityMap.getWrapper(primaryKey);
    }
    
    /**
     * Get the write lock value from the cache key associated to the primarykey.
     * User API.
     */
    public Object getWriteLockValue(Vector primaryKey){
        return this.targetIdentityMap.getWriteLockValue(primaryKey);
    }
    
    /**
     * Allow for the CacheKeys to be iterated on.
     */
    public Enumeration keys(){
        return this.targetIdentityMap.keys();
    }

    /**
     * Store the object in the cache at its primary key.
     * This is used by InsertObjectQuery, typically into the UnitOfWork identity map.
     * Merge and reads do not use put, but acquireLock.
     * Also an advanced (very) user API.
     * @param primaryKey is the primary key for the object.
     * @param object is the domain object to cache.
     * @param writeLockValue is the current write lock value of object, if null the version is ignored.
     */
    public CacheKey put(Vector primaryKey, Object object, Object writeLockValue, long readTime){
        return this.targetIdentityMap.put(primaryKey, object, writeLockValue, readTime);
    }

    /**
     * Remove the CacheKey with the primaryKey from the map.
     * This is used by DeleteObjectQuery and merge.
     * This is also an advanced (very) user API.
     */
    public Object remove(Vector primaryKey, Object object){
        return this.targetIdentityMap.remove(primaryKey, object);
    }

    /**
     * Remove the CacheKey from the map.
     */
    public Object remove(CacheKey cacheKey){
        if (cacheKey.isWrapper()){
            return this.targetIdentityMap.remove(cacheKey.getWrappedCacheKey());
        }
        return this.targetIdentityMap.remove(cacheKey);
    }

    /**
     * This method will be used to update the max cache size, any objects exceeding the max cache size will
     * be remove from the cache. Please note that this does not remove the object from the identityMap, except in
     * the case of the CacheIdentityMap.
     */
    public void updateMaxSize(int maxSize){
        this.targetIdentityMap.updateMaxSize(maxSize);
    }
    
    /**
     * Set the descriptor that this is the map for.
     */
    public void setDescriptor(ClassDescriptor descriptor) {
        this.targetIdentityMap.setDescriptor(descriptor);
    }

    /**
     * Update the wrapper object in the CacheKey associated with the given primaryKey,
     * this is used for EJB2.
     */
    public void setWrapper(Vector primaryKey, Object wrapper){
        this.targetIdentityMap.setWrapper(primaryKey, wrapper);
    }

    /**
     * Update the write lock value of the CacheKey associated with the given primaryKey.
     * This is used by UpdateObjectQuery, and is also an advanced (very) user API.
     */
    public void setWriteLockValue(Vector primaryKey, Object writeLockValue) {
        this.targetIdentityMap.setWriteLockValue(primaryKey, writeLockValue);
    }

    public String toString() {
        return Helper.getShortClassName("Intercepted " + this.targetIdentityMap.getClass()) + "[" + getSize() + "]";
    }
}
