blob: b472a7b3dd67c8c654803a6a78e818d7eb680a6c [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.cocoon.components.store.impl;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.IOException;
import java.io.Serializable;
import java.util.Collections;
import java.util.Enumeration;
import java.util.List;
import net.sf.ehcache.Cache;
import net.sf.ehcache.CacheException;
import net.sf.ehcache.CacheManager;
import net.sf.ehcache.Element;
import net.sf.ehcache.Status;
import net.sf.ehcache.store.MemoryStoreEvictionPolicy;
import org.apache.cocoon.Constants;
import org.apache.cocoon.util.IOUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.avalon.framework.activity.Disposable;
import org.apache.avalon.framework.activity.Initializable;
import org.apache.avalon.framework.context.Context;
import org.apache.avalon.framework.context.ContextException;
import org.apache.avalon.framework.context.Contextualizable;
import org.apache.avalon.framework.logger.AbstractLogEnabled;
import org.apache.avalon.framework.parameters.ParameterException;
import org.apache.avalon.framework.parameters.Parameterizable;
import org.apache.avalon.framework.parameters.Parameters;
import org.apache.avalon.framework.service.ServiceException;
import org.apache.avalon.framework.service.ServiceManager;
import org.apache.avalon.framework.service.Serviceable;
import org.apache.avalon.framework.thread.ThreadSafe;
import org.apache.excalibur.store.Store;
import org.apache.excalibur.store.StoreJanitor;
/**
* Store implementation based on EHCache.
* (http://ehcache.sourceforge.net/)
* @version $Id$
*/
public class EHDefaultStore extends AbstractLogEnabled
implements Store, Contextualizable, Serviceable, Parameterizable, Initializable, Disposable, ThreadSafe {
// ---------------------------------------------------- Constants
private static final String CONFIG_FILE = "org/apache/cocoon/components/store/impl/ehcache.xml";
private static int instanceCount = 0;
// ---------------------------------------------------- Instance variables
private Cache cache;
private CacheManager cacheManager;
private final String cacheName;
// configuration options
private int maxObjects;
private boolean overflowToDisk;
private boolean diskPersistent;
private boolean eternal;
private long timeToLiveSeconds;
private long timeToIdleSeconds;
/** The service manager */
private ServiceManager manager;
/** The store janitor */
private StoreJanitor storeJanitor;
private File workDir;
private File cacheDir;
private String diskStorePath; // The directory to be used a disk store path. Uses java.io.tmpdir if the argument is null.
// ---------------------------------------------------- Lifecycle
public EHDefaultStore() {
instanceCount++;
this.cacheName = "cocoon-ehcache-" + instanceCount;
}
/* (non-Javadoc)
* @see org.apache.avalon.framework.context.Contextualizable#contextualize(org.apache.avalon.framework.context.Context)
*/
public void contextualize(Context context) throws ContextException {
this.workDir = (File)context.get(Constants.CONTEXT_WORK_DIR);
this.cacheDir = (File)context.get(Constants.CONTEXT_CACHE_DIR);
}
/* (non-Javadoc)
* @see org.apache.avalon.framework.service.Serviceable#service(org.apache.avalon.framework.service.ServiceManager)
*/
public void service(ServiceManager aManager) throws ServiceException {
this.manager = aManager;
this.storeJanitor = (StoreJanitor) this.manager.lookup(StoreJanitor.ROLE);
}
/**
* Configure the store. The following options can be used:
* <ul>
* <li><code>maxobjects</code> (10000) - The maximum number of objects in memory.</li>
* <li><code>overflow-to-disk</code> (true) - Whether to spool elements to disk after
* maxobjects has been exceeded.</li>
* <li><code>eternal</code> (true) - whether or not entries expire. When set to
* <code>false</code> the <code>timeToLiveSeconds</code> and
* <code>timeToIdleSeconds</code> parameters are used to determine when an
* item expires.</li>
* <li><code>timeToLiveSeconds</code> (0) - how long an entry may live in the cache
* before it is removed. The entry will be removed no matter how frequently it is retrieved.</li>
* <li><code>timeToIdleSeconds</code> (0) - the maximum time between retrievals
* of an entry. If the entry is not retrieved for this period, it is removed from the
* cache.</li>
* <li><code>use-cache-directory</code> (false) - If true the <i>cache-directory</i>
* context entry will be used as the location of the disk store.
* Within the servlet environment this is set in web.xml.</li>
* <li><code>use-work-directory</code> (false) - If true the <i>work-directory</i>
* context entry will be used as the location of the disk store.
* Within the servlet environment this is set in web.xml.</li>
* <li><code>directory</code> - Specify an alternative location of the disk store.
* </ul>
*
* <p>
* Setting <code>eternal</code> to <code>false</code> but not setting
* <code>timeToLiveSeconds</code> and/or <code>timeToIdleSeconds</code>, has the
* same effect as setting <code>eternal</code> to <code>true</code>.
* </p>
*
* <p>
* Here is an example to clarify the purpose of the <code>timeToLiveSeconds</code> and
* <code>timeToIdleSeconds</code> parameters:
* </p>
* <ul>
* <li>timeToLiveSeconds = 86400 (1 day)</li>
* <li>timeToIdleSeconds = 10800 (3 hours)</li>
* </ul>
*
* <p>
* With these settings the entry will be removed from the cache after 24 hours. If within
* that 24-hour period the entry is not retrieved within 3 hours after the last retrieval, it will
* also be removed from the cache.
* </p>
*
* <p>
* By setting <code>timeToLiveSeconds</code> to <code>0</code>, an item can stay in
* the cache as long as it is retrieved within <code>timeToIdleSeconds</code> after the
* last retrieval.
* </p>
*
* <p>
* By setting <code>timeToIdleSeconds</code> to <code>0</code>, an item will stay in
* the cache for exactly <code>timeToLiveSeconds</code>.
* </p>
*
* <p>
* <code>disk-persistent</code> Whether the disk store persists between restarts of
* the Virtual Machine. The default value is true.
* </p>
*/
public void parameterize(Parameters parameters) throws ParameterException {
this.maxObjects = parameters.getParameterAsInteger("maxobjects", 10000);
this.overflowToDisk = parameters.getParameterAsBoolean("overflow-to-disk", true);
this.diskPersistent = parameters.getParameterAsBoolean("disk-persistent", true);
this.eternal = parameters.getParameterAsBoolean("eternal", true);
if (!this.eternal) {
this.timeToLiveSeconds = parameters.getParameterAsLong("timeToLiveSeconds", 0L);
this.timeToIdleSeconds = parameters.getParameterAsLong("timeToIdleSeconds", 0L);
}
try {
if (parameters.getParameterAsBoolean("use-cache-directory", false)) {
if (this.getLogger().isDebugEnabled()) {
getLogger().debug("Using cache directory: " + cacheDir);
}
setDirectory(cacheDir);
} else if (parameters.getParameterAsBoolean("use-work-directory", false)) {
if (this.getLogger().isDebugEnabled()) {
getLogger().debug("Using work directory: " + workDir);
}
setDirectory(workDir);
} else if (parameters.getParameter("directory", null) != null) {
String dir = parameters.getParameter("directory");
dir = IOUtils.getContextFilePath(workDir.getPath(), dir);
if (this.getLogger().isDebugEnabled()) {
getLogger().debug("Using directory: " + dir);
}
setDirectory(new File(dir));
} else {
try {
// Legacy: use working directory by default
setDirectory(workDir);
} catch (IOException e) {
// Empty
}
}
} catch (IOException e) {
throw new ParameterException("Unable to set directory", e);
}
}
/**
* Sets the cache directory
*/
private void setDirectory(final File directory) throws IOException {
// Save directory path prefix
String directoryPath = getFullFilename(directory);
directoryPath += File.separator;
// If directory doesn't exist, create it anew
if (!directory.exists()) {
if (!directory.mkdir()) {
throw new IOException("Error creating store directory '" + directoryPath + "': ");
}
}
// Is given file actually a directory?
if (!directory.isDirectory()) {
throw new IOException("'" + directoryPath + "' is not a directory");
}
// Is directory readable and writable?
if (!(directory.canRead() && directory.canWrite())) {
throw new IOException("Directory '" + directoryPath + "' is not readable/writable");
}
this.diskStorePath = directoryPath;
}
/**
* Get the complete filename corresponding to a (typically relative)
* <code>File</code>.
* This method accounts for the possibility of an error in getting
* the filename's <i>canonical</i> path, returning the io/error-safe
* <i>absolute</i> form instead
*
* @param file The file
* @return The file's absolute filename
*/
private static String getFullFilename(File file) {
try {
return file.getCanonicalPath();
} catch (Exception e) {
return file.getAbsolutePath();
}
}
/**
* Initialize the CacheManager and created the Cache.
*/
public void initialize() throws Exception {
// read configuration - we have to replace the diskstorepath in the configuration
// as the diskStorePath argument of the Cache constructor is ignored and set by the
// CacheManager! (see bug COCOON-1927)
String config = org.apache.commons.io.IOUtils.toString(Thread.currentThread().getContextClassLoader().getResourceAsStream(CONFIG_FILE));
config = StringUtils.replace(config, "${diskstorepath}", this.diskStorePath);
this.cacheManager = CacheManager.create(new ByteArrayInputStream(config.getBytes("utf-8")));
this.cache = new Cache(this.cacheName, this.maxObjects, MemoryStoreEvictionPolicy.LRU,
this.overflowToDisk, this.diskStorePath, this.eternal, this.timeToLiveSeconds,
this.timeToIdleSeconds, this.diskPersistent, Cache.DEFAULT_EXPIRY_THREAD_INTERVAL_SECONDS,
null, null);
this.cacheManager.addCache(this.cache);
this.storeJanitor.register(this);
getLogger().info("EHCache cache \"" + this.cacheName + "\" initialized");
}
/**
* Shutdown the CacheManager.
*/
public void dispose() {
if (this.storeJanitor != null) {
this.storeJanitor.unregister(this);
this.manager.release(this.storeJanitor);
this.storeJanitor = null;
}
this.manager = null;
/*
* EHCache can be a bitch when shutting down. Basically every cache registers
* a hook in the Runtime for every persistent cache, that will be executed when
* the JVM exit. It might happen (though) that we are shutting down Cocoon
* because of the same event (someone sending a TERM signal to the VM).
* So what we need to do here is to check if the cache itself is still alive,
* then we're going to shutdown EHCache entirely (if there are other caches open
* they will be shut down as well), if the cache is not alive, either another
* instance of this called the shutdown method on the CacheManager (thanks) or
* otherwise the hook had time to run before we got here.
*/
synchronized (this.cache) {
if (Status.STATUS_ALIVE == this.cache.getStatus()) {
try {
getLogger().info("Disposing EHCache cache \"" + this.cacheName + "\".");
this.cacheManager.shutdown();
} catch (IllegalStateException e) {
getLogger().error("Error disposing EHCache cache \"" + this.cacheName + "\".", e);
}
} else {
getLogger().info("EHCache cache \"" + this.cacheName + "\" already disposed.");
}
}
this.cacheManager = null;
this.cache = null;
}
// ---------------------------------------------------- Store implementation
/* (non-Javadoc)
* @see org.apache.excalibur.store.Store#free()
*/
public Object get(Object key) {
Object value = null;
try {
final Element element = this.cache.get((Serializable) key);
if (element != null) {
value = element.getValue();
}
} catch (CacheException e) {
getLogger().error("Failure retrieving object from store", e);
}
if (getLogger().isDebugEnabled()) {
if (value != null) {
getLogger().debug("Found key: " + key);
} else {
getLogger().debug("NOT Found key: " + key);
}
}
return value;
}
/* (non-Javadoc)
* @see org.apache.excalibur.store.Store#free()
*/
public void store(Object key, Object value) throws IOException {
if (getLogger().isDebugEnabled()) {
getLogger().debug("Store object " + value + " with key "+ key);
}
// without these checks we get cryptic "ClassCastException" messages
if (!(key instanceof Serializable)) {
throw new IOException("Key of class " + key.getClass().getName() + " is not Serializable");
}
if (!(value instanceof Serializable)) {
throw new IOException("Value of class " + value.getClass().getName() + " is not Serializable");
}
final Element element = new Element((Serializable) key, (Serializable) value);
this.cache.put(element);
}
/* (non-Javadoc)
* @see org.apache.excalibur.store.Store#free()
*/
public void free() {
try {
final List keys = this.cache.getKeysNoDuplicateCheck();
if (!keys.isEmpty()) {
// TODO find a way to get to the LRU one.
final Serializable key = (Serializable) keys.get(0);
if (getLogger().isDebugEnabled()) {
getLogger().debug("Freeing cache");
getLogger().debug("key: " + key);
getLogger().debug("value: " + this.cache.get(key));
}
if (!this.cache.remove(key)) {
if (getLogger().isInfoEnabled()) {
getLogger().info("Concurrency condition in free()");
}
}
}
} catch (CacheException e) {
if (getLogger().isWarnEnabled()) {
getLogger().warn("Error in free()", e);
}
}
}
/* (non-Javadoc)
* @see org.apache.excalibur.store.Store#remove(java.lang.Object)
*/
public void remove(Object key) {
if (getLogger().isDebugEnabled()) {
getLogger().debug("Removing item " + key);
}
this.cache.remove((Serializable) key);
}
/* (non-Javadoc)
* @see org.apache.excalibur.store.Store#clear()
*/
public void clear() {
if (getLogger().isDebugEnabled()) {
getLogger().debug("Clearing the store");
}
try {
this.cache.removeAll();
} catch (IllegalStateException e) {
getLogger().error("Failure to clearing store", e);
}
}
/* (non-Javadoc)
* @see org.apache.excalibur.store.Store#containsKey(java.lang.Object)
*/
public boolean containsKey(Object key) {
return this.cache.isKeyInCache((Serializable) key);
}
/* (non-Javadoc)
* @see org.apache.excalibur.store.Store#keys()
*/
public Enumeration keys() {
List keys = null;
try {
keys = this.cache.getKeys();
} catch (CacheException e) {
if (getLogger().isWarnEnabled()) {
getLogger().warn("Error while getting cache keys", e);
}
keys = Collections.EMPTY_LIST;
}
return Collections.enumeration(keys);
}
/* (non-Javadoc)
* @see org.apache.excalibur.store.Store#size()
*/
public int size() {
try {
// cast to int due ehcache implementation returns a long instead of int.
// See: http://ehcache.sourceforge.net/javadoc/net/sf/ehcache/Cache.html#getMemoryStoreSize()
return (int)this.cache.getMemoryStoreSize();
} catch (IllegalStateException e) {
if (getLogger().isWarnEnabled()) {
getLogger().warn("Error while getting cache size", e);
}
return 0;
}
}
}