org.craftercms.core.cache.impl.CacheImpl.java Source code

Java tutorial

Introduction

Here is the source code for org.craftercms.core.cache.impl.CacheImpl.java

Source

/*
 * Copyright (C) 2007-2013 Crafter Software Corporation.
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
package org.craftercms.core.cache.impl;

import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;

import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.craftercms.core.cache.Cache;
import org.craftercms.core.cache.CacheItem;
import org.craftercms.core.cache.CacheLoader;
import org.craftercms.core.exception.InternalCacheEngineException;
import org.craftercms.core.exception.InvalidScopeException;
import org.craftercms.core.util.cache.CachingAwareObject;
import org.craftercms.core.util.generators.TimestampGenerator;
import org.craftercms.core.util.generators.impl.IncrementalTimestampGenerator;
import org.springframework.beans.factory.annotation.Required;

/**
 * {@link org.craftercms.core.cache.Cache} that implements common functionality, such as logging,
 * expiration/refresh check every tick and
 * dependency management, and uses and underlying {@link CacheStoreAdapter} to store the items.
 *
 * @author Sumer Jabri
 * @author Alfonso Vsquez
 */
public class CacheImpl implements Cache {

    private static final Log logger = LogFactory.getLog(CacheImpl.class);
    /**
     * Holds the current number of ticks.
     */
    protected AtomicInteger ticks;
    /**
     * Adapter for the cache data structure.
     */
    protected CacheStoreAdapter cacheStoreAdapter;
    /**
     * Used to refresh a list of items.
     */
    protected CacheRefresher cacheRefresher;
    /**
     * The timestamp generator. Timestamps are used to check when dependencies have changed.
     */
    protected TimestampGenerator timestampGenerator;

    /**
     * Default constructor. Sets <code>timestampGenerator</code> to {@link org.craftercms.core.util.generators.impl
     * .IncrementalTimestampGenerator}.
     */
    public CacheImpl() {
        ticks = new AtomicInteger(0);
        timestampGenerator = new IncrementalTimestampGenerator();
    }

    /**
     * Sets the underlying {@link CacheStoreAdapter}
     *
     * @param cacheStoreAdapter the adapter for the cache data structure
     */
    @Required
    public void setCacheStoreAdapter(CacheStoreAdapter cacheStoreAdapter) {
        this.cacheStoreAdapter = cacheStoreAdapter;
    }

    /**
     * Sets the {@link CacheRefresher}, used to refresh the items of the cache.
     */
    public void setCacheRefresher(CacheRefresher cacheRefresher) {
        this.cacheRefresher = cacheRefresher;
    }

    /**
     * Sets the timestamp generator. Timestamps are used to check when dependencies have changed.
     */
    public void setTimestampGenerator(TimestampGenerator timestampGenerator) {
        this.timestampGenerator = timestampGenerator;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public boolean hasScope(String scope) throws InternalCacheEngineException {
        try {
            return cacheStoreAdapter.hasScope(scope);
        } catch (Exception ex) {
            throw new InternalCacheEngineException(
                    "Exception while checking if the scope " + scope + " exists in the" + " cache", ex);
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public Collection<String> getScopes() throws InternalCacheEngineException {
        try {
            return cacheStoreAdapter.getScopes();
        } catch (Exception ex) {
            throw new InternalCacheEngineException("Exception while getting the list of available scopes", ex);
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void addScope(String scope, int maxItemsInMemory) throws InternalCacheEngineException {
        try {
            cacheStoreAdapter.addScope(scope, maxItemsInMemory);
        } catch (Exception ex) {
            throw new InternalCacheEngineException("Exception while adding scope " + scope, ex);
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void removeScope(String scope) throws InvalidScopeException, InternalCacheEngineException {
        try {
            cacheStoreAdapter.removeScope(scope);
        } catch (InvalidScopeException ex) {
            throw ex;
        } catch (Exception ex) {
            throw new InternalCacheEngineException("Exception while removing scope " + scope, ex);
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public int getSize(String scope) throws InvalidScopeException, InternalCacheEngineException {
        try {
            return cacheStoreAdapter.getSize(scope);
        } catch (InvalidScopeException ex) {
            throw ex;
        } catch (Exception ex) {
            throw new InternalCacheEngineException("Exception while getting size of scope " + scope, ex);
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public Collection<Object> getKeys(String scope) throws InvalidScopeException, InternalCacheEngineException {
        try {
            return cacheStoreAdapter.getKeys(scope);
        } catch (InvalidScopeException ex) {
            throw ex;
        } catch (Exception ex) {
            throw new InternalCacheEngineException("Exception while getting keys of items within scope " + scope,
                    ex);
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public boolean hasKey(String scope, Object key) throws InvalidScopeException, InternalCacheEngineException {
        try {
            return cacheStoreAdapter.hasKey(scope, key);
        } catch (InvalidScopeException ex) {
            throw ex;
        } catch (Exception ex) {
            throw new InternalCacheEngineException(
                    "Exception while checking if the scope " + scope + " has key " + key, ex);
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public CacheItem get(String scope, Object key) throws InvalidScopeException, InternalCacheEngineException {
        try {
            CacheItem item = cacheStoreAdapter.get(scope, key);
            if (item != null && logger.isDebugEnabled()) {
                logger.debug("Cache hit: found " + item);
            } else if (logger.isDebugEnabled()) {
                logger.debug("Cache miss: item with key " + key + " not found in scope " + scope);
            }

            return item;
        } catch (InvalidScopeException ex) {
            throw ex;
        } catch (Exception ex) {
            throw new InternalCacheEngineException(
                    "Exception while getting item with key " + key + " from scope " + scope, ex);
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public CacheItem getWithDependencyCheck(String scope, Object key)
            throws InvalidScopeException, InternalCacheEngineException {
        try {
            CacheItem item = cacheStoreAdapter.get(scope, key);
            if (item != null) {
                if (logger.isDebugEnabled()) {
                    logger.debug("Cache hit: found " + item);
                }

                if (haveDependenciesChanged(item)) {
                    if (logger.isDebugEnabled()) {
                        logger.debug("Dependencies have changed for " + item + ". Removing it from the cache.");
                    }

                    cacheStoreAdapter.remove(scope, key);

                    return null;
                } else {
                    return item;
                }
            } else {
                if (logger.isDebugEnabled()) {
                    logger.debug("Cache miss: item with key " + key + " not found in scope " + scope);
                }

                return null;
            }
        } catch (InvalidScopeException ex) {
            throw ex;
        } catch (Exception ex) {
            throw new InternalCacheEngineException(
                    "Exception while getting item with key " + key + " from scope " + scope, ex);
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void put(String scope, Object key, Object value)
            throws InvalidScopeException, InternalCacheEngineException {
        put(scope, key, value, null, CacheItem.NEVER_EXPIRE, CacheItem.NEVER_REFRESH, null);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void put(String scope, Object key, Object value, long expireAfter, long refreshFrequency,
            CacheLoader loader, Object... loaderParams) throws InvalidScopeException, InternalCacheEngineException {
        put(scope, key, value, null, expireAfter, refreshFrequency, loader, loaderParams);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void put(String scope, Object key, Object value, List<Object> dependencyKeys)
            throws InvalidScopeException, InternalCacheEngineException {
        put(scope, key, value, dependencyKeys, CacheItem.NEVER_EXPIRE, CacheItem.NEVER_REFRESH, null);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void put(String scope, Object key, Object value, List<Object> dependencyKeys, long expireAfter,
            long refreshFrequency, CacheLoader loader, Object... loaderParams)
            throws InvalidScopeException, InternalCacheEngineException {
        if (expireAfter < 0) {
            throw new IllegalArgumentException("The expireAfter argument should be 0 or positive");
        }

        if (refreshFrequency < 0) {
            throw new IllegalArgumentException("The refreshFrequency argument should be 0 or positive");
        }

        if (value instanceof CachingAwareObject) {
            CachingAwareObject cachingAwareObj = (CachingAwareObject) value;

            if (CollectionUtils.isEmpty(dependencyKeys)) {
                dependencyKeys = cachingAwareObj.getDependencyKeys();
            }

            cachingAwareObj.setCachingTime(System.currentTimeMillis());
            cachingAwareObj.setScope(scope);
            cachingAwareObj.setKey(key);
        }

        try {
            CacheItem item = new CacheItemImpl(scope, ticks.get(), key, value, expireAfter, refreshFrequency,
                    timestampGenerator.generate(), dependencyKeys, loader, loaderParams);

            cacheStoreAdapter.put(item);

            if (logger.isDebugEnabled()) {
                logger.debug("Put into cache: " + item);
            }
        } catch (InvalidScopeException ex) {
            throw ex;
        } catch (Exception ex) {
            throw new InternalCacheEngineException(
                    "Exception while putting item with key " + key + " into scope " + scope, ex);
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public boolean remove(String scope, Object key) throws InvalidScopeException, InternalCacheEngineException {
        if (logger.isDebugEnabled()) {
            logger.debug("Removing item with key " + key + " from scope " + scope);
        }

        try {
            return cacheStoreAdapter.remove(scope, key);
        } catch (InvalidScopeException ex) {
            throw ex;
        } catch (Exception ex) {
            throw new InternalCacheEngineException(
                    "Exception while removing item with key " + key + " from scope " + scope, ex);
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void clearAll() throws InternalCacheEngineException {
        try {
            cacheStoreAdapter.clearAll();
        } catch (Exception ex) {
            throw new InternalCacheEngineException("Exception while clearing all items from the cache", ex);
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void clearScope(String scope) throws InvalidScopeException, InternalCacheEngineException {
        try {
            cacheStoreAdapter.clearScope(scope);
        } catch (InvalidScopeException ex) {
            throw ex;
        } catch (Exception ex) {
            throw new InternalCacheEngineException("Exception while clearing all items from scope " + scope, ex);
        }
    }

    /**
     * Called when a tick occurs. A tick is a logical unit of time. It basically symbolizes the time span between
     * calls to this method (i.e. 15 mins) by a job scheduler like Quartz.
     * <p/>
     * Checks if some of the {@link CacheItem}s in the cache have expired or need to be refreshed. If the item
     * has expired, it is removed from the cache. If it needs to be refreshed, it is added to a list of items that
     * need to be refreshed that is later passed to the {@link CacheRefresher}.
     */
    public void tick() {
        ticks.incrementAndGet();

        if (logger.isDebugEnabled()) {
            logger.debug("Tick!");
        }

        List<CacheItem> itemsToRefresh = new ArrayList<CacheItem>();

        try {
            Collection<String> scopes = cacheStoreAdapter.getScopes();
            if (CollectionUtils.isNotEmpty(scopes)) {
                for (String scope : scopes) {
                    Collection<Object> keys = cacheStoreAdapter.getKeys(scope);
                    if (CollectionUtils.isNotEmpty(keys)) {
                        for (Object key : keys) {
                            CacheItem item = cacheStoreAdapter.get(scope, key);
                            if (item != null) {
                                doChecks(item, itemsToRefresh);
                            } else {
                                if (logger.isDebugEnabled()) {
                                    logger.debug(item + " was removed before it could be checked for "
                                            + "expiration/refresh");
                                }
                            }
                        }
                    }
                }
            }

            if (cacheRefresher != null && CollectionUtils.isNotEmpty(itemsToRefresh)) {
                cacheRefresher.refreshItems(itemsToRefresh, this);
            }
        } catch (Exception ex) {
            logger.warn("Exception while checking items for expiration/refresh", ex);
        }
    }

    /**
     * Checks if the given {@link CacheItem} has expired or needs to be refreshed.
     *
     * @param item
     * @param itemsToRefresh the list of items where to put the specified item if it needs to be refreshed
     */
    protected void doChecks(CacheItem item, List<CacheItem> itemsToRefresh) {
        boolean expired;
        boolean willBeRefreshed;

        try {
            expired = checkForExpiration(item);
            if (expired) {
                if (logger.isDebugEnabled()) {
                    logger.debug(item + " was removed because it expired");
                }
            } else {
                willBeRefreshed = checkForRefresh(item, itemsToRefresh);
                if (willBeRefreshed) {
                    if (logger.isDebugEnabled()) {
                        logger.debug(item + " will be refreshed");
                    }
                }
            }
        } catch (Exception ex) {
            logger.warn("Exception while checking " + item + " for expiration/refresh", ex);
        }
    }

    /**
     * Checks if the given {@link CacheItem} has expired. If so, it is removed from its scope in the
     * {@link CacheStoreAdapter}.
     *
     * @return true if the item has expired and was removed from the scope, false otherwise
     * @throws Exception
     */
    protected boolean checkForExpiration(CacheItem item) throws Exception {
        if (item.isExpired(ticks.get())) {
            cacheStoreAdapter.remove(item.getScope(), item.getKey());

            return true;
        } else {
            return false;
        }
    }

    /**
     * Checks if a given {@link CacheItem} needs to be refreshed. If so, then it is added to the listen of items to
     * refresh.
     *
     * @param item
     * @param itemsToRefresh the list of items where to put the specified item if it needs to be refreshed
     * @return true if the item will be refreshed, false otherwise
     */
    protected boolean checkForRefresh(CacheItem item, List<CacheItem> itemsToRefresh) {
        if (item.getLoader() != null && item.needsRefresh(ticks.get())) {
            itemsToRefresh.add(item);

            return true;
        } else {
            return false;
        }
    }

    /**
     * Checks if some of the dependencies of the given {@link CacheItem} have changed, that means, they're no longer
     * in the cache, their timestamp is greater than the item's timestamp (which means that they were put in the
     * cache after the item was put) or their own dependencies have changed (recursive). If the dependencies have
     * changed, the item needs to be taken out of the cache.
     *
     * @return true if some of the dependencies have changed, false if none of them have changed.
     * @throws Exception
     */
    protected boolean haveDependenciesChanged(CacheItem item) throws Exception {
        List<Object> dependencyKeys = item.getDependencyKeys();
        if (CollectionUtils.isNotEmpty(dependencyKeys)) {
            for (Object dependencyKey : dependencyKeys) {
                CacheItem dependency = cacheStoreAdapter.get(item.getScope(), dependencyKey);
                if (dependency == null || item.getTimestamp() < dependency.getTimestamp()
                        || haveDependenciesChanged(dependency)) {
                    return true;
                }
            }
        }

        return false;
    }

}