/* * 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; } } }