001: /*
002: * Licensed to the Apache Software Foundation (ASF) under one or more
003: * contributor license agreements. See the NOTICE file distributed with
004: * this work for additional information regarding copyright ownership.
005: * The ASF licenses this file to You under the Apache License, Version 2.0
006: * (the "License"); you may not use this file except in compliance with
007: * the License. You may obtain a copy of the License at
008: *
009: * http://www.apache.org/licenses/LICENSE-2.0
010: *
011: * Unless required by applicable law or agreed to in writing, software
012: * distributed under the License is distributed on an "AS IS" BASIS,
013: * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014: * See the License for the specific language governing permissions and
015: * limitations under the License.
016: */
017: package org.apache.cocoon.components.store.impl;
018:
019: import java.io.ByteArrayInputStream;
020: import java.io.File;
021: import java.io.IOException;
022: import java.io.Serializable;
023: import java.util.Collections;
024: import java.util.Enumeration;
025: import java.util.List;
026:
027: import net.sf.ehcache.Cache;
028: import net.sf.ehcache.CacheException;
029: import net.sf.ehcache.CacheManager;
030: import net.sf.ehcache.Element;
031: import net.sf.ehcache.Status;
032: import net.sf.ehcache.store.MemoryStoreEvictionPolicy;
033:
034: import org.apache.cocoon.Constants;
035: import org.apache.cocoon.util.IOUtils;
036: import org.apache.commons.lang.StringUtils;
037:
038: import org.apache.avalon.framework.activity.Disposable;
039: import org.apache.avalon.framework.activity.Initializable;
040: import org.apache.avalon.framework.context.Context;
041: import org.apache.avalon.framework.context.ContextException;
042: import org.apache.avalon.framework.context.Contextualizable;
043: import org.apache.avalon.framework.logger.AbstractLogEnabled;
044: import org.apache.avalon.framework.parameters.ParameterException;
045: import org.apache.avalon.framework.parameters.Parameterizable;
046: import org.apache.avalon.framework.parameters.Parameters;
047: import org.apache.avalon.framework.service.ServiceException;
048: import org.apache.avalon.framework.service.ServiceManager;
049: import org.apache.avalon.framework.service.Serviceable;
050: import org.apache.avalon.framework.thread.ThreadSafe;
051: import org.apache.excalibur.store.Store;
052: import org.apache.excalibur.store.StoreJanitor;
053:
054: /**
055: * Store implementation based on EHCache.
056: * (http://ehcache.sourceforge.net/)
057: * @version $Id: EHDefaultStore.java 506982 2007-02-13 12:07:06Z cziegeler $
058: */
059: public class EHDefaultStore extends AbstractLogEnabled implements
060: Store, Contextualizable, Serviceable, Parameterizable,
061: Initializable, Disposable, ThreadSafe {
062:
063: // ---------------------------------------------------- Constants
064:
065: private static final String CONFIG_FILE = "org/apache/cocoon/components/store/impl/ehcache.xml";
066:
067: private static int instanceCount = 0;
068:
069: // ---------------------------------------------------- Instance variables
070:
071: private Cache cache;
072: private CacheManager cacheManager;
073:
074: private final String cacheName;
075:
076: // configuration options
077: private int maxObjects;
078: private boolean overflowToDisk;
079: private boolean diskPersistent;
080: private boolean eternal;
081: private long timeToLiveSeconds;
082: private long timeToIdleSeconds;
083:
084: /** The service manager */
085: private ServiceManager manager;
086:
087: /** The store janitor */
088: private StoreJanitor storeJanitor;
089:
090: private File workDir;
091: private File cacheDir;
092: private String diskStorePath; // The directory to be used a disk store path. Uses java.io.tmpdir if the argument is null.
093:
094: // ---------------------------------------------------- Lifecycle
095:
096: public EHDefaultStore() {
097: instanceCount++;
098: this .cacheName = "cocoon-ehcache-" + instanceCount;
099: }
100:
101: /* (non-Javadoc)
102: * @see org.apache.avalon.framework.context.Contextualizable#contextualize(org.apache.avalon.framework.context.Context)
103: */
104: public void contextualize(Context context) throws ContextException {
105: this .workDir = (File) context.get(Constants.CONTEXT_WORK_DIR);
106: this .cacheDir = (File) context.get(Constants.CONTEXT_CACHE_DIR);
107: }
108:
109: /* (non-Javadoc)
110: * @see org.apache.avalon.framework.service.Serviceable#service(org.apache.avalon.framework.service.ServiceManager)
111: */
112: public void service(ServiceManager aManager)
113: throws ServiceException {
114: this .manager = aManager;
115: this .storeJanitor = (StoreJanitor) this .manager
116: .lookup(StoreJanitor.ROLE);
117: }
118:
119: /**
120: * Configure the store. The following options can be used:
121: * <ul>
122: * <li><code>maxobjects</code> (10000) - The maximum number of objects in memory.</li>
123: * <li><code>overflow-to-disk</code> (true) - Whether to spool elements to disk after
124: * maxobjects has been exceeded.</li>
125: * <li><code>eternal</code> (true) - whether or not entries expire. When set to
126: * <code>false</code> the <code>timeToLiveSeconds</code> and
127: * <code>timeToIdleSeconds</code> parameters are used to determine when an
128: * item expires.</li>
129: * <li><code>timeToLiveSeconds</code> (0) - how long an entry may live in the cache
130: * before it is removed. The entry will be removed no matter how frequently it is retrieved.</li>
131: * <li><code>timeToIdleSeconds</code> (0) - the maximum time between retrievals
132: * of an entry. If the entry is not retrieved for this period, it is removed from the
133: * cache.</li>
134: * <li><code>use-cache-directory</code> (false) - If true the <i>cache-directory</i>
135: * context entry will be used as the location of the disk store.
136: * Within the servlet environment this is set in web.xml.</li>
137: * <li><code>use-work-directory</code> (false) - If true the <i>work-directory</i>
138: * context entry will be used as the location of the disk store.
139: * Within the servlet environment this is set in web.xml.</li>
140: * <li><code>directory</code> - Specify an alternative location of the disk store.
141: * </ul>
142: *
143: * <p>
144: * Setting <code>eternal</code> to <code>false</code> but not setting
145: * <code>timeToLiveSeconds</code> and/or <code>timeToIdleSeconds</code>, has the
146: * same effect as setting <code>eternal</code> to <code>true</code>.
147: * </p>
148: *
149: * <p>
150: * Here is an example to clarify the purpose of the <code>timeToLiveSeconds</code> and
151: * <code>timeToIdleSeconds</code> parameters:
152: * </p>
153: * <ul>
154: * <li>timeToLiveSeconds = 86400 (1 day)</li>
155: * <li>timeToIdleSeconds = 10800 (3 hours)</li>
156: * </ul>
157: *
158: * <p>
159: * With these settings the entry will be removed from the cache after 24 hours. If within
160: * that 24-hour period the entry is not retrieved within 3 hours after the last retrieval, it will
161: * also be removed from the cache.
162: * </p>
163: *
164: * <p>
165: * By setting <code>timeToLiveSeconds</code> to <code>0</code>, an item can stay in
166: * the cache as long as it is retrieved within <code>timeToIdleSeconds</code> after the
167: * last retrieval.
168: * </p>
169: *
170: * <p>
171: * By setting <code>timeToIdleSeconds</code> to <code>0</code>, an item will stay in
172: * the cache for exactly <code>timeToLiveSeconds</code>.
173: * </p>
174: *
175: * <p>
176: * <code>disk-persistent</code> Whether the disk store persists between restarts of
177: * the Virtual Machine. The default value is true.
178: * </p>
179: */
180: public void parameterize(Parameters parameters)
181: throws ParameterException {
182:
183: this .maxObjects = parameters.getParameterAsInteger(
184: "maxobjects", 10000);
185: this .overflowToDisk = parameters.getParameterAsBoolean(
186: "overflow-to-disk", true);
187: this .diskPersistent = parameters.getParameterAsBoolean(
188: "disk-persistent", true);
189:
190: this .eternal = parameters
191: .getParameterAsBoolean("eternal", true);
192: if (!this .eternal) {
193: this .timeToLiveSeconds = parameters.getParameterAsLong(
194: "timeToLiveSeconds", 0L);
195: this .timeToIdleSeconds = parameters.getParameterAsLong(
196: "timeToIdleSeconds", 0L);
197: }
198:
199: try {
200: if (parameters.getParameterAsBoolean("use-cache-directory",
201: false)) {
202: if (this .getLogger().isDebugEnabled()) {
203: getLogger().debug(
204: "Using cache directory: " + cacheDir);
205: }
206: setDirectory(cacheDir);
207: } else if (parameters.getParameterAsBoolean(
208: "use-work-directory", false)) {
209: if (this .getLogger().isDebugEnabled()) {
210: getLogger().debug(
211: "Using work directory: " + workDir);
212: }
213: setDirectory(workDir);
214: } else if (parameters.getParameter("directory", null) != null) {
215: String dir = parameters.getParameter("directory");
216: dir = IOUtils
217: .getContextFilePath(workDir.getPath(), dir);
218: if (this .getLogger().isDebugEnabled()) {
219: getLogger().debug("Using directory: " + dir);
220: }
221: setDirectory(new File(dir));
222: } else {
223: try {
224: // Legacy: use working directory by default
225: setDirectory(workDir);
226: } catch (IOException e) {
227: // Empty
228: }
229: }
230: } catch (IOException e) {
231: throw new ParameterException("Unable to set directory", e);
232: }
233:
234: }
235:
236: /**
237: * Sets the cache directory
238: */
239: private void setDirectory(final File directory) throws IOException {
240:
241: // Save directory path prefix
242: String directoryPath = getFullFilename(directory);
243: directoryPath += File.separator;
244:
245: // If directory doesn't exist, create it anew
246: if (!directory.exists()) {
247: if (!directory.mkdir()) {
248: throw new IOException(
249: "Error creating store directory '"
250: + directoryPath + "': ");
251: }
252: }
253:
254: // Is given file actually a directory?
255: if (!directory.isDirectory()) {
256: throw new IOException("'" + directoryPath
257: + "' is not a directory");
258: }
259:
260: // Is directory readable and writable?
261: if (!(directory.canRead() && directory.canWrite())) {
262: throw new IOException("Directory '" + directoryPath
263: + "' is not readable/writable");
264: }
265: this .diskStorePath = directoryPath;
266: }
267:
268: /**
269: * Get the complete filename corresponding to a (typically relative)
270: * <code>File</code>.
271: * This method accounts for the possibility of an error in getting
272: * the filename's <i>canonical</i> path, returning the io/error-safe
273: * <i>absolute</i> form instead
274: *
275: * @param file The file
276: * @return The file's absolute filename
277: */
278: private static String getFullFilename(File file) {
279: try {
280: return file.getCanonicalPath();
281: } catch (Exception e) {
282: return file.getAbsolutePath();
283: }
284: }
285:
286: /**
287: * Initialize the CacheManager and created the Cache.
288: */
289: public void initialize() throws Exception {
290: // read configuration - we have to replace the diskstorepath in the configuration
291: // as the diskStorePath argument of the Cache constructor is ignored and set by the
292: // CacheManager! (see bug COCOON-1927)
293: String config = org.apache.commons.io.IOUtils.toString(Thread
294: .currentThread().getContextClassLoader()
295: .getResourceAsStream(CONFIG_FILE));
296: config = StringUtils.replace(config, "${diskstorepath}",
297: this .diskStorePath);
298: this .cacheManager = CacheManager
299: .create(new ByteArrayInputStream(config
300: .getBytes("utf-8")));
301: this .cache = new Cache(this .cacheName, this .maxObjects,
302: MemoryStoreEvictionPolicy.LRU, this .overflowToDisk,
303: this .diskStorePath, this .eternal,
304: this .timeToLiveSeconds, this .timeToIdleSeconds,
305: this .diskPersistent,
306: Cache.DEFAULT_EXPIRY_THREAD_INTERVAL_SECONDS, null,
307: null);
308: this .cacheManager.addCache(this .cache);
309: this .storeJanitor.register(this );
310: getLogger().info(
311: "EHCache cache \"" + this .cacheName + "\" initialized");
312: }
313:
314: /**
315: * Shutdown the CacheManager.
316: */
317: public void dispose() {
318: if (this .storeJanitor != null) {
319: this .storeJanitor.unregister(this );
320: this .manager.release(this .storeJanitor);
321: this .storeJanitor = null;
322: }
323: this .manager = null;
324: /*
325: * EHCache can be a bitch when shutting down. Basically every cache registers
326: * a hook in the Runtime for every persistent cache, that will be executed when
327: * the JVM exit. It might happen (though) that we are shutting down Cocoon
328: * because of the same event (someone sending a TERM signal to the VM).
329: * So what we need to do here is to check if the cache itself is still alive,
330: * then we're going to shutdown EHCache entirely (if there are other caches open
331: * they will be shut down as well), if the cache is not alive, either another
332: * instance of this called the shutdown method on the CacheManager (thanks) or
333: * otherwise the hook had time to run before we got here.
334: */
335: synchronized (this .cache) {
336: if (Status.STATUS_ALIVE == this .cache.getStatus()) {
337: try {
338: getLogger().info(
339: "Disposing EHCache cache \""
340: + this .cacheName + "\".");
341: this .cacheManager.shutdown();
342: } catch (IllegalStateException e) {
343: getLogger().error(
344: "Error disposing EHCache cache \""
345: + this .cacheName + "\".", e);
346: }
347: } else {
348: getLogger().info(
349: "EHCache cache \"" + this .cacheName
350: + "\" already disposed.");
351: }
352: }
353: this .cacheManager = null;
354: this .cache = null;
355: }
356:
357: // ---------------------------------------------------- Store implementation
358:
359: /* (non-Javadoc)
360: * @see org.apache.excalibur.store.Store#free()
361: */
362: public Object get(Object key) {
363: Object value = null;
364: try {
365: final Element element = this .cache.get((Serializable) key);
366: if (element != null) {
367: value = element.getValue();
368: }
369: } catch (CacheException e) {
370: getLogger()
371: .error("Failure retrieving object from store", e);
372: }
373: if (getLogger().isDebugEnabled()) {
374: if (value != null) {
375: getLogger().debug("Found key: " + key);
376: } else {
377: getLogger().debug("NOT Found key: " + key);
378: }
379: }
380: return value;
381: }
382:
383: /* (non-Javadoc)
384: * @see org.apache.excalibur.store.Store#free()
385: */
386: public void store(Object key, Object value) throws IOException {
387: if (getLogger().isDebugEnabled()) {
388: getLogger().debug(
389: "Store object " + value + " with key " + key);
390: }
391:
392: // without these checks we get cryptic "ClassCastException" messages
393: if (!(key instanceof Serializable)) {
394: throw new IOException("Key of class "
395: + key.getClass().getName() + " is not Serializable");
396: }
397: if (!(value instanceof Serializable)) {
398: throw new IOException("Value of class "
399: + value.getClass().getName()
400: + " is not Serializable");
401: }
402:
403: final Element element = new Element((Serializable) key,
404: (Serializable) value);
405: this .cache.put(element);
406: }
407:
408: /* (non-Javadoc)
409: * @see org.apache.excalibur.store.Store#free()
410: */
411: public void free() {
412: try {
413: final List keys = this .cache.getKeysNoDuplicateCheck();
414: if (!keys.isEmpty()) {
415: // TODO find a way to get to the LRU one.
416: final Serializable key = (Serializable) keys.get(0);
417: if (getLogger().isDebugEnabled()) {
418: getLogger().debug("Freeing cache");
419: getLogger().debug("key: " + key);
420: getLogger().debug("value: " + this .cache.get(key));
421: }
422: if (!this .cache.remove(key)) {
423: if (getLogger().isInfoEnabled()) {
424: getLogger().info(
425: "Concurrency condition in free()");
426: }
427: }
428: }
429: } catch (CacheException e) {
430: if (getLogger().isWarnEnabled()) {
431: getLogger().warn("Error in free()", e);
432: }
433: }
434: }
435:
436: /* (non-Javadoc)
437: * @see org.apache.excalibur.store.Store#remove(java.lang.Object)
438: */
439: public void remove(Object key) {
440: if (getLogger().isDebugEnabled()) {
441: getLogger().debug("Removing item " + key);
442: }
443: this .cache.remove((Serializable) key);
444: }
445:
446: /* (non-Javadoc)
447: * @see org.apache.excalibur.store.Store#clear()
448: */
449: public void clear() {
450: if (getLogger().isDebugEnabled()) {
451: getLogger().debug("Clearing the store");
452: }
453: try {
454: this .cache.removeAll();
455: } catch (IllegalStateException e) {
456: getLogger().error("Failure to clearing store", e);
457: }
458: }
459:
460: /* (non-Javadoc)
461: * @see org.apache.excalibur.store.Store#containsKey(java.lang.Object)
462: */
463: public boolean containsKey(Object key) {
464: try {
465: return this .cache.get((Serializable) key) != null;
466: } catch (CacheException e) {
467: getLogger()
468: .error("Failure retrieving object from store", e);
469: }
470: return false;
471: }
472:
473: /* (non-Javadoc)
474: * @see org.apache.excalibur.store.Store#keys()
475: */
476: public Enumeration keys() {
477: List keys = null;
478: try {
479: keys = this .cache.getKeys();
480: } catch (CacheException e) {
481: if (getLogger().isWarnEnabled()) {
482: getLogger().warn("Error while getting cache keys", e);
483: }
484: keys = Collections.EMPTY_LIST;
485: }
486: return Collections.enumeration(keys);
487: }
488:
489: /* (non-Javadoc)
490: * @see org.apache.excalibur.store.Store#size()
491: */
492: public int size() {
493: try {
494: // cast to int due ehcache implementation returns a long instead of int.
495: // See: http://ehcache.sourceforge.net/javadoc/net/sf/ehcache/Cache.html#getMemoryStoreSize()
496: return (int) this .cache.getMemoryStoreSize();
497: } catch (IllegalStateException e) {
498: if (getLogger().isWarnEnabled()) {
499: getLogger().warn("Error while getting cache size", e);
500: }
501: return 0;
502: }
503: }
504: }
|