/*******************************************************************************
 * Copyright (c) 2008, 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:
 *     12/04/2008 - 2.0 Darani Yallapragada 
 *       - 248780: Initial contribution for JPA 2.0
 *     06/03/2010 - 2.1 Michael O'Brien 
 *       - 248780: Refactor Cache Implementation surrounding evict()
 *         Fix evict() to handle non-Entity classes
 *         Refactor to get IdentityMapAccessor state through EMF reference
 *         Refactor dependencies to use Interfaces instead of Impl subclasses
 *         Handle no CMPPolicy case for getId()
 *         Handle no associated descriptor for Class parameter
 *         MappedSuperclasses passed to evict() cause implementing subclasses to be evicted
 *         Throw an IAE for Interfaces and Embeddable classes passed to evict()
 *     
 ******************************************************************************/
package org.eclipse.persistence.internal.jpa;

import javax.persistence.Cache;
import javax.persistence.PersistenceException;

import org.eclipse.persistence.descriptors.ClassDescriptor;
import org.eclipse.persistence.internal.identitymaps.CacheKey;
import org.eclipse.persistence.internal.localization.ExceptionLocalization;
import org.eclipse.persistence.internal.sessions.AbstractSession;
import org.eclipse.persistence.jpa.JpaCache;
import org.eclipse.persistence.sessions.IdentityMapAccessor;
import org.eclipse.persistence.sessions.Session;

/**
 * Implements the JPA Cache interface using the EclipseLink cache API through IdentityMapAccessor.
 * @author DaraniY
 */
public class CacheImpl implements JpaCache {

    /** The EntityManagerFactory associated with this Cache */
    private EntityManagerFactoryDelegate emf;

    /**
     * @param emf
     */
    public CacheImpl(EntityManagerFactoryDelegate emf) {
        this.emf = emf;
    }

    /**
     * Returns true if the cache contains an Object with the id and Class type, and is valid.
     * @see Cache#contains(Class, Object)
     */
    public boolean contains(Class cls, Object id) {
        getEntityManagerFactory().verifyOpen();
        Object pk =  createPrimaryKeyFromId(cls, id);
        if(null == pk) {
            return false;
        }
        ClassDescriptor descriptor = getSession().getClassDescriptor(cls); // getDescriptor() is the same call
        /**
         * Check for no descriptor associated with the class parameter.
         * This will occur if the class represents a MappedSuperclass (concrete or abstract class),
         * an interface or Embeddable class.
         */
        if(null == descriptor) {
            // do not throw an IAException: cache_impl_class_has_no_descriptor_is_not_a_persistent_type - just return false
            return false;
        }

        // we can assume that all implementors of IdentityMapAccessor implement getCacheKeyforObject
        CacheKey key = ((org.eclipse.persistence.internal.sessions.IdentityMapAccessor)getAccessor())
            .getCacheKeyForObject(pk, cls, descriptor, false);
        return key != null && key.getObject() != null && 
            !descriptor.getCacheInvalidationPolicy().isInvalidated(key);    
    }

    /**
     * INTERNAL:
     * This private method searches the map of descriptors for possible superclasses to the
     * passed in class parameter and invalidates only entities found in the cache.
     * If the class is not an Entity or MappedSuperclass (such as an Embeddable or plain java class)
     *  - nothing will be evicted  
     * @param possibleSuperclass
     * @param id
     */
    private void evictAssignableEntitySuperclass(Class possibleSuperclass, Object id) {
        // just remove the parent entity
        for(ClassDescriptor candidateAssignableDescriptor : getSession().getDescriptors().values()) {
            // In EclipseLink we need only remove the root descriptor that is assignable from this possibleSubclass because the recurse flag defaults to true in invalidateClass()
            // what if we have 2 roots (don't check for !candidateAssignableDescriptor.isChildDescriptor())
            if(!candidateAssignableDescriptor.isDescriptorTypeAggregate() && // a !Embeddable check and !EmbeddableCollection check
               possibleSuperclass.isAssignableFrom(candidateAssignableDescriptor.getJavaClass())) { 
                // id will be null if this private function was called from evict(class)
                if(null == id) {
                    // set the invalidationState to -1 in the cache of a type that can be assigned to the class parameter
                    // this call will invalidate all assignable subclasses from the level of possibleSubclass] in the subtree
                    // we could either loop through each aDescriptor.getJavaClass()
                    // or
                    // let invalidateClass loop for us by passing in the higher [possibleSubclass] - all subclasses of the first parent entity descriptor will be invalidated in this first call
                    getAccessor().invalidateClass(candidateAssignableDescriptor.getJavaClass());
                } else {
                    // evict the class instance that corresponds to the id
                    // initialize the cache of a type that can be assigned to the class parameter
                    getAccessor().invalidateObject(createPrimaryKeyFromId(possibleSuperclass, id), candidateAssignableDescriptor.getJavaClass());
                }
            }
        }
    }
    
    /**
     * Sets an Object with the id and Class type to be invalid in the cache.
     * Remove the data for entities of the specified class (and its
     * subclasses) from the cache.<p>
     * If the class is a MappedSuperclass then the first entity above in the inheritance hierarchy will be evicted
     *   along with all implementing subclasses
     * If the class is not an Entity or MappedSuperclass but is the root of an entity inheritance tree then
     *   evict the subtree 
     * If the class is not an Entity or MappedSuperclass but inherits from one then
     *   evict up to root descriptor
     * @see Cache#evict(Class, Object)
     * @param classToEvict - class to evict - usually representing an Entity or MappedSuperclass
     * @param id - Primary key of the Entity or MappedSuperclass Class
     *    A null id means invalidate the class - possibly the entire tree or subtree
     */
    public void evict(Class classToEvict, Object id) {
        evict(classToEvict, id, false);
    }
    
    /**
     * Sets an Object with the id and Class type to be invalid in the cache.
     * Remove the data for entities of the specified class (and its
     * subclasses) from the cache.<p>
     * If the class is a MappedSuperclass then the first entity above in the inheritance hierarchy will be evicted
     *   along with all implementing subclasses
     * If the class is not an Entity or MappedSuperclass but is the root of an entity inheritance tree then
     *   evict the subtree 
     * If the class is not an Entity or MappedSuperclass but inherits from one then
     *   evict up to root descriptor
     * @see Cache#evict(Class, Object)
     * @param classToEvict - class to evict - usually representing an Entity or MappedSuperclass
     * @param id - Primary key of the Entity or MappedSuperclass Class
     *    A null id means invalidate the class - possibly the entire tree or subtree
     * @param invalidateInCluster - Invalidate the object id in the cluster, this only applies to a non-null id.
     */
    public void evict(Class classToEvict, Object id, boolean invalidateInCluster) {
        getEntityManagerFactory().verifyOpen();
        /**
         * The following descriptor lookup will return the Entity representing the classToEvict parameter,
         * or it will return the first Entity superclass of a MappedSuperclass in the hierarchy
         * - in this case all subclasses of the parent Entity will be evicted.
         * The descriptor will be null if the classToEvict represents a non-Entity or non-MappedSuperclass like an Embeddable or plain java class
         * - in this case nothing will be evicted.
         */
        ClassDescriptor aPossibleSuperclassDescriptor = getSession().getClassDescriptor(classToEvict);
        // Do not recurse if the class to evict is below its' descriptor in the inheritance tree        
        if(null != aPossibleSuperclassDescriptor) {
            // Evict all Entity or MappedSuperclass classes
            if(null != id) {
                Object cacheKey = createPrimaryKeyFromId(classToEvict, id);
                if(null != cacheKey) {
                    getAccessor().invalidateObject(cacheKey, classToEvict, invalidateInCluster);
                }
            } else {
                // 312503: always evict all implementing entity subclasses of an entity or mappedSuperclass
                boolean invalidateRecursively = aPossibleSuperclassDescriptor.getJavaClass().equals(classToEvict);
                getAccessor().invalidateClass(classToEvict, invalidateRecursively);
            }
        } else {
            // Evict the first possible parent Entity superclass of a non-Entity and non-MappedSuperclass class
            evictAssignableEntitySuperclass(classToEvict, id);
        }
    }

    /**
     * Sets all instances of the class to be invalid.
     * Remove the data for entities of the specified class (and its
     * subclasses) from the cache.<p>
     * If the class is a MappedSuperclass then the first entity above in the inheritance hierarchy will be evicted
     *   along with all implementing subclasses
     * If the class is not an Entity or MappedSuperclass (such as an Embeddable or plain java class)
     *  - nothing will be evicted  
     * @see Cache#evict(Class) 
     * @param entityOrMappedSuperclassToEvict - Entity or MappedSuperclass Class
     */
    public void evict(Class entityOrMappedSuperclassToEvict) {
        // A null id means invalidate the class - possibly the entire tree or subtree
        evict(entityOrMappedSuperclassToEvict, null);
    }    
    
    /**
     * Sets all instances in the cache to be invalid.
     * @see Cache#evict(Object)
     */
    public void evictAll() {
        getEntityManagerFactory().verifyOpen();
        getEntityManagerFactory().getDatabaseSession().getIdentityMapAccessor().invalidateAll();
    }

    /**
     * Return the EclipseLink cache key object from the JPA Id object.
     */
    private Object createPrimaryKeyFromId(Class cls, Object id) {
        Object cacheKey = null;
        ClassDescriptor aDescriptor = getSession().getDescriptor(cls);
        // Check that we have a descriptor associated with the class (Entity or MappedSuperclass)
        if(null == aDescriptor) {
            // No descriptor found, throw exception for Embeddable or non-persistable java class
            throw new IllegalArgumentException(ExceptionLocalization.buildMessage(
                    "cache_impl_class_has_no_descriptor_is_not_a_persistent_type", 
                    new Object[] {cls}));
        }
        // The policy is not set if the mapping is natively defined outside of JPA
        if(aDescriptor.hasCMPPolicy()) {
            // we assume that the PK id parameter is correct and do not throw a cache_descriptor_has_no_cmppolicy_set_cannot_create_primary_key exception 
            // The primaryKey may be the same object as the id parameter
            cacheKey = aDescriptor.getCMPPolicy().createPrimaryKeyFromId(id, getEntityManagerFactory().getDatabaseSession());
        }
        return cacheKey;
    }
    
    /**
     * ADVANCED:
     * Resets the entire Object cache, and the Query cache.
     * <p> NOTE: Be careful using this method. This method blows away both this session's and its parent's caches.
     * This includes the server cache or any other 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 Objects held in the cache.
     */
    public void clear() {
        getEntityManagerFactory().verifyOpen();
        getAccessor().initializeAllIdentityMaps();
    }

    /**
     * ADVANCED:
     * Resets the cache for only the instances of the given Class type.
     * For inheritance the user must make sure that they only use the root class,
     * clearing a subclass cache is not allowed (as they share their parents cache).
     * <p> NOTE: Caution must be used in doing this to ensure that the Objects within the cache
     * are not referenced from other Objects of other classes or from the application.
     */
    public void clear(Class cls) {
        getEntityManagerFactory().verifyOpen();
        getAccessor().initializeIdentityMap(cls);
    }

    /**
     * Clear all the query results caches.
     */
    public void clearQueryCache() {
        getEntityManagerFactory().verifyOpen();
        getAccessor().clearQueryCache();
    }

    /**
     * Clear the named query results cache associated with the query name.
     */
    public void clearQueryCache(String queryName) {
        getEntityManagerFactory().verifyOpen();
        getAccessor().clearQueryCache(queryName);
    }

    /**
     * Clear all named query results cache associated with entity class.
     */
    public void clearQueryCache(Class entityClass) {
        getEntityManagerFactory().verifyOpen();
        getAccessor().invalidateQueryCache(entityClass);
    }

    /**
     * Returns the remaining life of the given Object (in milliseconds).  This method is associated with use of
     * cache invalidation feature and returns the difference between the next expiry
     * time of the Object and its read time.  The method will return 0 for invalidated Objects.
     */
    public long timeToLive(Object object) {
        getEntityManagerFactory().verifyOpen();
        return getAccessor().getRemainingValidTime(object);
    }

    /**
     * Returns true if the Object with the same id and Class type of the
     * the given Object is valid in the cache.
     */
    public boolean isValid(Object object) {
        getEntityManagerFactory().verifyOpen();
        return getAccessor().isValid(object);
    }

    /**
     * Returns true if the Object with the id and Class type is valid in the cache.
     */
    public boolean isValid(Class cls, Object id) {
        getEntityManagerFactory().verifyOpen();
        Object cacheKey = createPrimaryKeyFromId(cls, id);
        if(null != cacheKey) {
            return getAccessor().isValid(cacheKey, cls);
        } else {
            return false;
        }
    }

    /**
     * Used to print all the Objects in the cache.
     * The output of this method will be logged to this persistence unit's SessionLog at SEVERE level.
     */
    public void print() {
        getEntityManagerFactory().verifyOpen();
        getAccessor().printIdentityMaps();
    }

    /**
     * Used to print all the Objects in the cache of the Class type.
     * The output of this method will be logged to this persistence unit's SessionLog at SEVERE level.
     */
    public void print(Class cls) {
        getEntityManagerFactory().verifyOpen();
        getAccessor().printIdentityMap(cls);
    }

    /**
     * Used to print all the currently locked cache keys in the cache.
     * The output of this method will be logged to this persistence unit's SessionLog at SEVERE level.
     */
    public void printLocks() {
        getEntityManagerFactory().verifyOpen();
        getAccessor().printIdentityMapLocks();
    }

    /**
     * This can be used to help debugging an Object identity problem.
     * An Object identity problem is when an Object in the cache references an 
     * Object that is not in the cache. This method will validate that all cached 
     * Objects are in a correct state.
     */
    public void validate() {
        getEntityManagerFactory().verifyOpen();
        getAccessor().validateCache();
    }

    /**
     * Returns the Object from the cache map with the id 
     * and Class type.
     */
    public Object getObject(Class cls, Object id) {
        getEntityManagerFactory().verifyOpen();
        Object cacheKey = createPrimaryKeyFromId(cls, id);        
        return getAccessor().getFromIdentityMap(cacheKey, cls);
    }

    /**
     * ADVANCED:
     * Puts the given Object into the cache.
     * This is a very advanced method, and caution should be used in adding objects to the cache
     * as other objects may have relationships to previous object, or this object may have
     * relationships to other objects.
     */
    public Object putObject(Object object) {
        getEntityManagerFactory().verifyOpen();
        return getAccessor().putInIdentityMap(object);
    }

    /**
     * ADVANCED:
     * Removes the Object from the cache.
     * <p> NOTE: Caution should be used when calling to avoid violating Object identity.
     * The application should only call this if its known that no references to the Object exist.
     */
    public Object removeObject(Object object) {
        getEntityManagerFactory().verifyOpen();
        return getAccessor().removeFromIdentityMap(object);
    }

    /**
     * ADVANCED:
     * Removes the Object with the id and Class type from the cache.
     * <p> NOTE: Caution should be used when calling to avoid violating Object identity.
     * The application should only call this if its known that no references to the Object exist.
     */
    public Object removeObject(Class cls, Object id) {
        getEntityManagerFactory().verifyOpen();
        Object cacheKey = createPrimaryKeyFromId(cls, id);
        return getAccessor().removeFromIdentityMap(cacheKey, cls);
    }

    /**
     * Returns true if the cache contains an Object with the same id and Class type of the given object.
     */
    public boolean contains(Object object) {
        return contains(object.getClass(), getId(object));
    }

    /**
     * Sets the object to be invalid in the cache.
     * @see JpaCache#evict(Object)
     */
    public void evict(Object object) {
        getEntityManagerFactory().verifyOpen();
        getAccessor().invalidateObject(object);
    }

    
    /**
     * Sets an Object to be invalid in the cache.
     * If true is passed, the object is also invalidated across cache coordination.
     * Cache coordination must be enabled for this to have an affect.
     */
    public void evict(Object object, boolean invalidateInCluster) {
        getEntityManagerFactory().verifyOpen();
        getAccessor().invalidateObject(object, invalidateInCluster);        
    }
    
    /**
     * INTERNAL:
     * Return the EntityManagerFactory associated with this CacheImpl.
     * @return
     */
    protected EntityManagerFactoryDelegate getEntityManagerFactory() {
        return this.emf;
    }
    
    /**
     * INTERNAL:
     * Return the Session associated with the EntityManagerFactory.
     * @return
     */
    protected Session getSession() {
        return getEntityManagerFactory().getDatabaseSession();
    }

    /**
     * INTERNAL:
     * Return the IdentityMapAccessor associated with the session on the EntityManagerFactory on this CacheImpl.
     * @return
     */
    protected IdentityMapAccessor getAccessor() {
        return getSession().getIdentityMapAccessor();
    }

    /**
     * This method will return the objects's Id.
     * If the descriptor associated with the domain object is null - an IllegalArgumentException is thrown.
     * If the CMPPolicy associated with the domain object's descriptor is null 
     * the Id will be determined using the ObjectBuilder on the descriptor - which may return
     * the Id stored in the weaved _persistence_primaryKey field.
     * @See {@link JpaCache#getId(Object)}
     */
    public Object getId(Object object) {
        getEntityManagerFactory().verifyOpen();
        ClassDescriptor aDescriptor = getSession().getDescriptor(object.getClass());
        // Handle a null descriptor from a detached entity (closed EntityManager), or the entity exists in another session
        if(null == aDescriptor) {
            throw new IllegalArgumentException(ExceptionLocalization.buildMessage(
                    "cache_impl_object_has_no_descriptor_is_not_a_persistent_type", 
                    new Object[] {object}));
        }
        
        // Handle a null CMPPolicy from a MappedSuperclass
        if(!aDescriptor.hasCMPPolicy()) {
            // the following code gets the key either from the weaved _persistence_primaryKey field or using valueFromObject() if not bytecode enhanced
            return aDescriptor.getObjectBuilder().extractPrimaryKeyFromObject(object, (AbstractSession)getSession());
        } else {
            // Get identifier via EMF
            return getEntityManagerFactory().getIdentifier(object);
        }
    }
    
    public <T> T unwrap(Class<T> cls) {
        if (cls.equals(JpaCache.class)){
            return (T) this;
        }
        if (cls.equals(IdentityMapAccessor.class)){
            return (T) getAccessor();
        }
        throw new PersistenceException(ExceptionLocalization.buildMessage("unable_to_unwrap_jpa", new String[]{Cache.class.getName(),cls.getName()}));
    }
}
