Initial move to Gradle.

This commit is contained in:
Erik C. Thauvin 2017-05-21 00:18:26 -07:00
parent bd8f23d7d6
commit 993384c173
147 changed files with 673 additions and 1 deletions

View file

@ -0,0 +1,411 @@
/*
* Copyright (c) 2002-2003 by OpenSymphony
* All rights reserved.
*/
package com.opensymphony.oscache.base;
import com.opensymphony.oscache.base.algorithm.AbstractConcurrentReadCache;
import com.opensymphony.oscache.base.events.*;
import com.opensymphony.oscache.base.persistence.PersistenceListener;
import com.opensymphony.oscache.util.StringUtil;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import java.util.*;
import javax.swing.event.EventListenerList;
/**
* An AbstractCacheAdministrator defines an abstract cache administrator, implementing all
* the basic operations related to the configuration of a cache, including assigning
* any configured event handlers to cache objects.<p>
*
* Extend this class to implement a custom cache administrator.
*
* @version $Revision$
* @author a href="mailto:mike@atlassian.com">Mike Cannon-Brookes</a>
* @author <a href="mailto:fbeauregard@pyxis-tech.com">Francois Beauregard</a>
* @author <a href="mailto:abergevin@pyxis-tech.com">Alain Bergevin</a>
* @author <a href="mailto:fabian.crabus@gurulogic.de">Fabian Crabus</a>
* @author <a href="&#109;a&#105;&#108;&#116;&#111;:chris&#64;swebtec.&#99;&#111;&#109;">Chris Miller</a>
*/
public abstract class AbstractCacheAdministrator implements java.io.Serializable {
private static transient final Log log = LogFactory.getLog(AbstractCacheAdministrator.class);
/**
* A boolean cache configuration property that indicates whether the cache
* should cache objects in memory. Set this property to <code>false</code>
* to disable in-memory caching.
*/
public final static String CACHE_MEMORY_KEY = "cache.memory";
/**
* An integer cache configuration property that specifies the maximum number
* of objects to hold in the cache. Setting this to a negative value will
* disable the capacity functionality - there will be no limit to the number
* of objects that are held in cache.
*/
public final static String CACHE_CAPACITY_KEY = "cache.capacity";
/**
* A String cache configuration property that specifies the classname of
* an alternate caching algorithm. This class must extend
* {@link com.opensymphony.oscache.base.algorithm.AbstractConcurrentReadCache}
* By default caches will use {@link com.opensymphony.oscache.base.algorithm.LRUCache} as
* the default algorithm if the cache capacity is set to a postive value, or
* {@link com.opensymphony.oscache.base.algorithm.UnlimitedCache} if the
* capacity is negative (ie, disabled).
*/
public final static String CACHE_ALGORITHM_KEY = "cache.algorithm";
/**
* A boolean cache configuration property that indicates whether the persistent
* cache should be unlimited in size, or should be restricted to the same size
* as the in-memory cache. Set this property to <code>true</code> to allow the
* persistent cache to grow without bound.
*/
public final static String CACHE_DISK_UNLIMITED_KEY = "cache.unlimited.disk";
/**
* The configuration key that specifies whether we should block waiting for new
* content to be generated, or just serve the old content instead. The default
* behaviour is to serve the old content since that provides the best performance
* (at the cost of serving slightly stale data).
*/
public final static String CACHE_BLOCKING_KEY = "cache.blocking";
/**
* A String cache configuration property that specifies the classname that will
* be used to provide cache persistence. This class must extend {@link PersistenceListener}.
*/
public static final String PERSISTENCE_CLASS_KEY = "cache.persistence.class";
/**
* A String cache configuration property that specifies if the cache persistence
* will only be used in overflow mode, that is, when the memory cache capacity has been reached.
*/
public static final String CACHE_PERSISTENCE_OVERFLOW_KEY = "cache.persistence.overflow.only";
/**
* A String cache configuration property that holds a comma-delimited list of
* classnames. These classes specify the event handlers that are to be applied
* to the cache.
*/
public static final String CACHE_ENTRY_EVENT_LISTENERS_KEY = "cache.event.listeners";
protected Config config = null;
/**
* Holds a list of all the registered event listeners. Event listeners are specified
* using the {@link #CACHE_ENTRY_EVENT_LISTENERS_KEY} configuration key.
*/
protected EventListenerList listenerList = new EventListenerList();
/**
* The algorithm class being used, as specified by the {@link #CACHE_ALGORITHM_KEY}
* configuration property.
*/
protected String algorithmClass = null;
/**
* The cache capacity (number of entries), as specified by the {@link #CACHE_CAPACITY_KEY}
* configuration property.
*/
protected int cacheCapacity = -1;
/**
* Whether the cache blocks waiting for content to be build, or serves stale
* content instead. This value can be specified using the {@link #CACHE_BLOCKING_KEY}
* configuration property.
*/
private boolean blocking = false;
/**
* Whether or not to store the cache entries in memory. This is configurable using the
* {@link com.opensymphony.oscache.base.AbstractCacheAdministrator#CACHE_MEMORY_KEY} property.
*/
private boolean memoryCaching = true;
/**
* Whether the persistent cache should be used immediately or only when the memory capacity
* has been reached, ie. overflow only.
* This can be set via the {@link #CACHE_PERSISTENCE_OVERFLOW_KEY} configuration property.
*/
private boolean overflowPersistence;
/**
* Whether the disk cache should be unlimited in size, or matched 1-1 to the memory cache.
* This can be set via the {@link #CACHE_DISK_UNLIMITED_KEY} configuration property.
*/
private boolean unlimitedDiskCache;
/**
* Create the AbstractCacheAdministrator.
* This will initialize all values and load the properties from oscache.properties.
*/
protected AbstractCacheAdministrator() {
this(null);
}
/**
* Create the AbstractCacheAdministrator.
*
* @param p the configuration properties for this cache.
*/
protected AbstractCacheAdministrator(Properties p) {
loadProps(p);
initCacheParameters();
if (log.isDebugEnabled()) {
log.debug("Constructed AbstractCacheAdministrator()");
}
}
/**
* Sets the algorithm to use for the cache.
*
* @see com.opensymphony.oscache.base.algorithm.LRUCache
* @see com.opensymphony.oscache.base.algorithm.FIFOCache
* @see com.opensymphony.oscache.base.algorithm.UnlimitedCache
* @param newAlgorithmClass The class to use (eg.
* <code>"com.opensymphony.oscache.base.algorithm.LRUCache"</code>)
*/
public void setAlgorithmClass(String newAlgorithmClass) {
algorithmClass = newAlgorithmClass;
}
/**
* Indicates whether the cache will block waiting for new content to
* be built, or serve stale content instead of waiting. Regardless of this
* setting, the cache will <em>always</em> block if new content is being
* created, ie, there's no stale content in the cache that can be served.
*/
public boolean isBlocking() {
return blocking;
}
/**
* Sets the cache capacity (number of items). Administrator implementations
* should override this method to ensure that their {@link Cache} objects
* are updated correctly (by calling {@link AbstractConcurrentReadCache#setMaxEntries(int)}}}.
*
* @param newCacheCapacity The new capacity
*/
protected void setCacheCapacity(int newCacheCapacity) {
cacheCapacity = newCacheCapacity;
}
/**
* Whether entries are cached in memory or not.
* Default is true.
* Set by the <code>cache.memory</code> property.
*
* @return Status whether or not memory caching is used.
*/
public boolean isMemoryCaching() {
return memoryCaching;
}
/**
* Retrieves the value of one of the configuration properties.
*
* @param key The key assigned to the property
* @return Property value, or <code>null</code> if the property could not be found.
*/
public String getProperty(String key) {
return config.getProperty(key);
}
/**
* Indicates whether the unlimited disk cache is enabled or not.
*/
public boolean isUnlimitedDiskCache() {
return unlimitedDiskCache;
}
/**
* Check if we use overflowPersistence
*
* @return Returns the overflowPersistence.
*/
public boolean isOverflowPersistence() {
return this.overflowPersistence;
}
/**
* Sets the overflowPersistence flag
*
* @param overflowPersistence The overflowPersistence to set.
*/
public void setOverflowPersistence(boolean overflowPersistence) {
this.overflowPersistence = overflowPersistence;
}
/**
* Retrieves an array containing instances all of the {@link CacheEventListener}
* classes that are specified in the OSCache configuration file.
*/
protected CacheEventListener[] getCacheEventListeners() {
List classes = StringUtil.split(config.getProperty(CACHE_ENTRY_EVENT_LISTENERS_KEY), ',');
CacheEventListener[] listeners = new CacheEventListener[classes.size()];
for (int i = 0; i < classes.size(); i++) {
String className = (String) classes.get(i);
try {
Class clazz = Class.forName(className);
if (!CacheEventListener.class.isAssignableFrom(clazz)) {
log.error("Specified listener class '" + className + "' does not implement CacheEventListener. Ignoring this listener.");
} else {
listeners[i] = (CacheEventListener) clazz.newInstance();
}
} catch (ClassNotFoundException e) {
log.error("CacheEventListener class '" + className + "' not found. Ignoring this listener.", e);
} catch (InstantiationException e) {
log.error("CacheEventListener class '" + className + "' could not be instantiated because it is not a concrete class. Ignoring this listener.", e);
} catch (IllegalAccessException e) {
log.error("CacheEventListener class '" + className + "' could not be instantiated because it is not public. Ignoring this listener.", e);
}
}
return listeners;
}
/**
* If there is a <code>PersistenceListener</code> in the configuration
* it will be instantiated and applied to the given cache object. If the
* <code>PersistenceListener</code> cannot be found or instantiated, an
* error will be logged but the cache will not have a persistence listener
* applied to it and no exception will be thrown.<p>
*
* A cache can only have one <code>PersistenceListener</code>.
*
* @param cache the cache to apply the <code>PersistenceListener</code> to.
*
* @return the same cache object that was passed in.
*/
protected Cache setPersistenceListener(Cache cache) {
String persistenceClassname = config.getProperty(PERSISTENCE_CLASS_KEY);
try {
Class clazz = Class.forName(persistenceClassname);
PersistenceListener persistenceListener = (PersistenceListener) clazz.newInstance();
cache.setPersistenceListener(persistenceListener.configure(config));
} catch (ClassNotFoundException e) {
log.error("PersistenceListener class '" + persistenceClassname + "' not found. Check your configuration.", e);
} catch (Exception e) {
log.error("Error instantiating class '" + persistenceClassname + "'", e);
}
return cache;
}
/**
* Applies all of the recognised listener classes to the supplied
* cache object. Recognised classes are {@link CacheEntryEventListener}
* and {@link CacheMapAccessEventListener}.<p>
*
* @param cache The cache to apply the configuration to.
* @return cache The configured cache object.
*/
protected Cache configureStandardListeners(Cache cache) {
if (config.getProperty(PERSISTENCE_CLASS_KEY) != null) {
cache = setPersistenceListener(cache);
}
if (config.getProperty(CACHE_ENTRY_EVENT_LISTENERS_KEY) != null) {
// Grab all the specified listeners and add them to the cache's
// listener list. Note that listeners that implement more than
// one of the event interfaces will be added multiple times.
CacheEventListener[] listeners = getCacheEventListeners();
for (int i = 0; i < listeners.length; i++) {
// Pass through the configuration to those listeners that require it
if (listeners[i] instanceof LifecycleAware) {
try {
((LifecycleAware) listeners[i]).initialize(cache, config);
} catch (InitializationException e) {
log.error("Could not initialize listener '" + listeners[i].getClass().getName() + "'. Listener ignored.", e);
continue;
}
}
if (listeners[i] instanceof CacheEventListener) {
cache.addCacheEventListener(listeners[i]);
}
}
}
return cache;
}
/**
* Finalizes all the listeners that are associated with the given cache object.
* Any <code>FinalizationException</code>s that are thrown by the listeners will
* be caught and logged.
*/
protected void finalizeListeners(Cache cache) {
// It's possible for cache to be null if getCache() was never called (CACHE-63)
if (cache == null) {
return;
}
Object[] listeners = cache.listenerList.getListenerList();
for (int i = listeners.length - 2; i >= 0; i -= 2) {
if (listeners[i + 1] instanceof LifecycleAware) {
try {
((LifecycleAware) listeners[i + 1]).finialize();
} catch (FinalizationException e) {
log.error("Listener could not be finalized", e);
}
}
}
}
/**
* Initialize the core cache parameters from the configuration properties.
* The parameters that are initialized are:
* <ul>
* <li>the algorithm class ({@link #CACHE_ALGORITHM_KEY})</li>
* <li>the cache size ({@link #CACHE_CAPACITY_KEY})</li>
* <li>whether the cache is blocking or non-blocking ({@link #CACHE_BLOCKING_KEY})</li>
* <li>whether caching to memory is enabled ({@link #CACHE_MEMORY_KEY})</li>
* <li>whether the persistent cache is unlimited in size ({@link #CACHE_DISK_UNLIMITED_KEY})</li>
* </ul>
*/
private void initCacheParameters() {
algorithmClass = getProperty(CACHE_ALGORITHM_KEY);
blocking = "true".equalsIgnoreCase(getProperty(CACHE_BLOCKING_KEY));
String cacheMemoryStr = getProperty(CACHE_MEMORY_KEY);
if ((cacheMemoryStr != null) && cacheMemoryStr.equalsIgnoreCase("false")) {
memoryCaching = false;
}
unlimitedDiskCache = Boolean.valueOf(config.getProperty(CACHE_DISK_UNLIMITED_KEY)).booleanValue();
overflowPersistence = Boolean.valueOf(config.getProperty(CACHE_PERSISTENCE_OVERFLOW_KEY)).booleanValue();
String cacheSize = getProperty(CACHE_CAPACITY_KEY);
try {
if ((cacheSize != null) && (cacheSize.length() > 0)) {
cacheCapacity = Integer.parseInt(cacheSize);
}
} catch (NumberFormatException e) {
log.error("The value supplied for the cache capacity, '" + cacheSize + "', is not a valid number. The cache capacity setting is being ignored.");
}
}
/**
* Load the properties file from the classpath.
*/
private void loadProps(Properties p) {
config = new Config(p);
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,311 @@
/*
* Copyright (c) 2002-2003 by OpenSymphony
* All rights reserved.
*/
package com.opensymphony.oscache.base;
import com.opensymphony.oscache.web.filter.ResponseContent;
import java.io.Serializable;
import java.util.Collection;
import java.util.HashSet;
import java.util.Set;
/**
* A CacheEntry instance represents one entry in the cache. It holds the object that
* is being cached, along with a host of information about that entry such as the
* cache key, the time it was cached, whether the entry has been flushed or not and
* the groups it belongs to.
*
* @version $Revision$
* @author <a href="mailto:mike@atlassian.com">Mike Cannon-Brookes</a>
* @author <a href="mailto:tgochenour@peregrine.com">Todd Gochenour</a>
* @author <a href="mailto:fbeauregard@pyxis-tech.com">Francois Beauregard</a>
*/
public class CacheEntry implements Serializable {
/**
* Default initialization value for the creation time and the last
* update time. This is a placeholder that indicates the value has
* not been set yet.
*/
private static final byte NOT_YET = -1;
/**
* Specifying this as the refresh period for the
* {@link #needsRefresh(int)} method will ensure
* an entry does not become stale until it is
* either explicitly flushed or a custom refresh
* policy causes the entry to expire.
*/
public static final int INDEFINITE_EXPIRY = -1;
/**
* The entry refresh policy object to use for this cache entry. This is optional.
*/
private EntryRefreshPolicy policy = null;
/**
* The actual content that is being cached. Wherever possible this object
* should be serializable. This allows <code>PersistenceListener</code>s
* to serialize the cache entries to disk or database.
*/
private Object content = null;
/**
* The set of cache groups that this cache entry belongs to, if any.
*/
private Set groups = null;
/**
* The unique cache key for this entry
*/
private String key;
/**
* <code>true</code> if this entry was flushed
*/
private boolean wasFlushed = false;
/**
* The time this entry was created.
*/
private long created = NOT_YET;
/**
* The time this emtry was last updated.
*/
private long lastUpdate = NOT_YET;
/**
* Construct a new CacheEntry using the key provided.
*
* @param key The key of this CacheEntry
*/
public CacheEntry(String key) {
this(key, null);
}
/**
* Construct a CacheEntry.
*
* @param key The unique key for this <code>CacheEntry</code>.
* @param policy Object that implements refresh policy logic. This parameter
* is optional.
*/
public CacheEntry(String key, EntryRefreshPolicy policy) {
this(key, policy, null);
}
/**
* Construct a CacheEntry.
*
* @param key The unique key for this <code>CacheEntry</code>.
* @param policy The object that implements the refresh policy logic. This
* parameter is optional.
* @param groups The groups that this <code>CacheEntry</code> belongs to. This
* parameter is optional.
*/
public CacheEntry(String key, EntryRefreshPolicy policy, String[] groups) {
this.key = key;
if (groups != null) {
this.groups = new HashSet(groups.length);
for (int i = 0; i < groups.length; i++) {
this.groups.add(groups[i]);
}
}
this.policy = policy;
this.created = System.currentTimeMillis();
}
/**
* Sets the actual content that is being cached. Wherever possible this
* object should be <code>Serializable</code>, however it is not an
* absolute requirement when using a memory-only cache. Being <code>Serializable</code>
* allows <code>PersistenceListener</code>s to serialize the cache entries to disk
* or database.
*
* @param value The content to store in this CacheEntry.
*/
public synchronized void setContent(Object value) {
content = value;
lastUpdate = System.currentTimeMillis();
wasFlushed = false;
}
/**
* Get the cached content from this CacheEntry.
*
* @return The content of this CacheEntry.
*/
public Object getContent() {
return content;
}
/**
* Get the date this CacheEntry was created.
*
* @return The date this CacheEntry was created.
*/
public long getCreated() {
return created;
}
/**
* Sets the cache groups for this entry.
*
* @param groups A string array containing all the group names
*/
public synchronized void setGroups(String[] groups) {
if (groups != null) {
this.groups = new HashSet(groups.length);
for (int i = 0; i < groups.length; i++) {
this.groups.add(groups[i]);
}
} else {
this.groups = null;
}
lastUpdate = System.currentTimeMillis();
}
/**
* Sets the cache groups for this entry
*
* @param groups A collection containing all the group names
*/
public synchronized void setGroups(Collection groups) {
if (groups != null) {
this.groups = new HashSet(groups);
} else {
this.groups = null;
}
lastUpdate = System.currentTimeMillis();
}
/**
* Gets the cache groups that this cache entry belongs to.
* These returned groups should be treated as immuatable.
*
* @return A set containing the names of all the groups that
* this cache entry belongs to.
*/
public Set getGroups() {
return groups;
}
/**
* Get the key of this CacheEntry
*
* @return The key of this CacheEntry
*/
public String getKey() {
return key;
}
/**
* Set the date this CacheEntry was last updated.
*
* @param update The time (in milliseconds) this CacheEntry was last updated.
*/
public void setLastUpdate(long update) {
lastUpdate = update;
}
/**
* Get the date this CacheEntry was last updated.
*
* @return The date this CacheEntry was last updated.
*/
public long getLastUpdate() {
return lastUpdate;
}
/**
* Indicates whether this CacheEntry is a freshly created one and
* has not yet been assigned content or placed in a cache.
*
* @return <code>true</code> if this entry is newly created
*/
public boolean isNew() {
return lastUpdate == NOT_YET;
}
/**
* Get the size of the cache entry in bytes (roughly).<p>
*
* Currently this method only handles <code>String<code>s and
* {@link ResponseContent} objects.
*
* @return The approximate size of the entry in bytes, or -1 if the
* size could not be estimated.
*/
public int getSize() {
// a char is two bytes
int size = (key.length() * 2) + 4;
if (content.getClass() == String.class) {
size += ((content.toString().length() * 2) + 4);
} else if (content instanceof ResponseContent) {
size += ((ResponseContent) content).getSize();
} else {
return -1;
}
//add created, lastUpdate, and wasFlushed field sizes (1, 8, and 8)
return size + 17;
}
/**
* Flush the entry from cache.
* note that flushing the cache doesn't actually remove the cache contents
* it just tells the CacheEntry that it needs a refresh next time it is asked
* this is so that the content is still there for a <usecached />.
*/
public void flush() {
wasFlushed = true;
}
/**
* Check if this CacheEntry needs to be refreshed.
*
* @param refreshPeriod The period of refresh (in seconds). Passing in
* {@link #INDEFINITE_EXPIRY} will result in the content never becoming
* stale unless it is explicitly flushed, or expired by a custom
* {@link EntryRefreshPolicy}. Passing in 0 will always result in a
* refresh being required.
*
* @return Whether or not this CacheEntry needs refreshing.
*/
public boolean needsRefresh(int refreshPeriod) {
boolean needsRefresh;
// needs a refresh if it has never been updated
if (lastUpdate == NOT_YET) {
needsRefresh = true;
}
// Was it flushed from cache?
else if (wasFlushed) {
needsRefresh = true;
} else if (refreshPeriod == 0) {
needsRefresh = true;
}
// check what the policy has to say if there is one
else if (policy != null) {
needsRefresh = policy.needsRefresh(this);
}
// check if the last update + update period is in the past
else if ((refreshPeriod >= 0) && (System.currentTimeMillis() >= (lastUpdate + (refreshPeriod * 1000L)))) {
needsRefresh = true;
} else {
needsRefresh = false;
}
return needsRefresh;
}
}

View file

@ -0,0 +1,189 @@
/*
* Copyright (c) 2002-2007 by OpenSymphony
* All rights reserved.
*/
package com.opensymphony.oscache.base;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.util.Properties;
/**
* Responsible for holding the Cache configuration properties. If the default
* constructor is used, this class will load the properties from the
* <code>cache.configuration</code>.
*
* @author <a href="mailto:fabian.crabus@gurulogic.de">Fabian Crabus</a>
* @version $Revision$
*/
public class Config implements java.io.Serializable {
private static final transient Log log = LogFactory.getLog(Config.class);
/**
* Name of the properties file.
*/
private final static String PROPERTIES_FILENAME = "/oscache.properties";
/**
* Properties map to hold the cache configuration.
*/
private Properties properties = null;
/**
* Create an OSCache Config that loads properties from oscache.properties.
* The file must be present in the root of OSCache's classpath. If the file
* cannot be loaded, an error will be logged and the configuration will
* remain empty.
*/
public Config() {
this(null);
}
/**
* Create an OSCache configuration with the specified properties.
* Note that it is the responsibility of the caller to provide valid
* properties as no error checking is done to ensure that required
* keys are present. If you're unsure of what keys should be present,
* have a look at a sample oscache.properties file.
*
* @param p The properties to use for this configuration. If null,
* then the default properties are loaded from the <code>oscache.properties</code>
* file.
*/
public Config(Properties p) {
if (log.isDebugEnabled()) {
log.debug("OSCache: Config called");
}
if (p == null) {
this.properties = loadProperties(PROPERTIES_FILENAME, "the default configuration");
} else {
this.properties = p;
}
}
/**
* Retrieve the value of the named configuration property. If the property
* cannot be found this method will return <code>null</code>.
*
* @param key The name of the property.
* @return The property value, or <code>null</code> if the value could
* not be found.
*
* @throws IllegalArgumentException if the supplied key is null.
*/
public String getProperty(String key) {
if (key == null) {
throw new IllegalArgumentException("key is null");
}
if (properties == null) {
return null;
}
return properties.getProperty(key);
}
/**
* Retrieves all of the configuration properties. This property set
* should be treated as immutable.
*
* @return The configuration properties.
*/
public Properties getProperties() {
return properties;
}
public Object get(Object key) {
return properties.get(key);
}
/**
* Sets a configuration property.
*
* @param key The unique name for this property.
* @param value The value assigned to this property.
*
* @throws IllegalArgumentException if the supplied key is null.
*/
public void set(Object key, Object value) {
if (key == null) {
throw new IllegalArgumentException("key is null");
}
if (value == null) {
return;
}
if (properties == null) {
properties = new Properties();
}
properties.put(key, value);
}
/**
* Load the properties from the specified URL.
* @param url a non null value of the URL to the properties
* @param info additional logger information if the properties can't be read
* @return the loaded properties specified by the URL
* @since 2.4
*/
public static Properties loadProperties(URL url, String info) {
log.info("OSCache: Getting properties from URL " + url + " for " + info);
Properties properties = new Properties();
InputStream in = null;
try {
in = url.openStream();
properties.load(in);
log.info("OSCache: Properties read " + properties);
} catch (Exception e) {
log.error("OSCache: Error reading from " + url, e);
log.error("OSCache: Ensure the properties information in " + url+ " is readable and in your classpath.");
} finally {
try {
in.close();
} catch (IOException e) {
log.warn("OSCache: IOException while closing InputStream: " + e.getMessage());
}
}
return properties;
}
/**
* Load the specified properties file from the classpath. If the file
* cannot be found or loaded, an error will be logged and no
* properties will be set.
* @param filename the properties file with path
* @param info additional logger information if file can't be read
* @return the loaded properties specified by the filename
* @since 2.4
*/
public static Properties loadProperties(String filename, String info) {
URL url = null;
ClassLoader threadContextClassLoader = Thread.currentThread().getContextClassLoader();
if (threadContextClassLoader != null) {
url = threadContextClassLoader.getResource(filename);
}
if (url == null) {
url = Config.class.getResource(filename);
if (url == null) {
log.warn("OSCache: No properties file found in the classpath by filename " + filename);
return new Properties();
}
}
return loadProperties(url, info);
}
}

View file

@ -0,0 +1,31 @@
/*
* Copyright (c) 2002-2003 by OpenSymphony
* All rights reserved.
*/
package com.opensymphony.oscache.base;
import java.io.Serializable;
/**
* Interface that allows custom code to be called when checking to see if a cache entry
* has expired. This is useful when the rules that determine when content needs refreshing
* are beyond the base funtionality offered by OSCache.
*
* @version $Revision$
* @author <a href="mailto:fbeauregard@pyxis-tech.com">Francois Beauregard</a>
*/
public interface EntryRefreshPolicy extends Serializable {
/**
* Indicates whether the supplied <code>CacheEntry</code> needs to be refreshed.
* This will be called when retrieving an entry from the cache - if this method
* returns <code>true</code> then a <code>NeedsRefreshException</code> will be
* thrown.
*
* @param entry The cache entry that is being tested.
* @return <code>true</code> if the content needs refreshing, <code>false</code> otherwise.
*
* @see NeedsRefreshException
* @see CacheEntry
*/
public boolean needsRefresh(CacheEntry entry);
}

View file

@ -0,0 +1,157 @@
/*
* Copyright (c) 2002-2003 by OpenSymphony
* All rights reserved.
*/
package com.opensymphony.oscache.base;
/**
* Holds the state of a Cache Entry that is in the process of being (re)generated.
* This is not synchronized; the synchronization must be handled by the calling
* classes.
*
* @author <a href="&#109;a&#105;&#108;&#116;&#111;:chris&#64;swebtec.&#99;&#111;&#109;">Chris Miller</a>
* @author Author: $
* @version Revision: $
*/
public class EntryUpdateState {
/**
* The initial state when this object is first created
*/
public static final int NOT_YET_UPDATING = -1;
/**
* Update in progress state
*/
public static final int UPDATE_IN_PROGRESS = 0;
/**
* Update complete state
*/
public static final int UPDATE_COMPLETE = 1;
/**
* Update cancelled state
*/
public static final int UPDATE_CANCELLED = 2;
/**
* Current update state
*/
int state = NOT_YET_UPDATING;
/**
* A counter of the number of threads that are coordinated through this instance. When this counter gets to zero, then the reference to this
* instance may be released from the Cache instance.
* This is counter is protected by the EntryStateUpdate instance monitor.
*/
private int nbConcurrentUses = 1;
/**
* This is the initial state when an instance this object is first created.
* It indicates that a cache entry needs updating, but no thread has claimed
* responsibility for updating it yet.
*/
public boolean isAwaitingUpdate() {
return state == NOT_YET_UPDATING;
}
/**
* The thread that was responsible for updating the cache entry (ie, the thread
* that managed to grab the update lock) has decided to give up responsibility
* for performing the update. OSCache will notify any other threads that are
* waiting on the update so one of them can take over the responsibility.
*/
public boolean isCancelled() {
return state == UPDATE_CANCELLED;
}
/**
* The update of the cache entry has been completed.
*/
public boolean isComplete() {
return state == UPDATE_COMPLETE;
}
/**
* The cache entry is currently being generated by the thread that got hold of
* the update lock.
*/
public boolean isUpdating() {
return state == UPDATE_IN_PROGRESS;
}
/**
* Updates the state to <code>UPDATE_CANCELLED</code>. This should <em>only<em>
* be called by the thread that managed to get the update lock.
* @return the counter value after the operation completed
*/
public int cancelUpdate() {
if (state != UPDATE_IN_PROGRESS) {
throw new IllegalStateException("Cannot cancel cache update - current state (" + state + ") is not UPDATE_IN_PROGRESS");
}
state = UPDATE_CANCELLED;
return decrementUsageCounter();
}
/**
* Updates the state to <code>UPDATE_COMPLETE</code>. This should <em>only</em>
* be called by the thread that managed to get the update lock.
* @return the counter value after the operation completed
*/
public int completeUpdate() {
if (state != UPDATE_IN_PROGRESS) {
throw new IllegalStateException("Cannot complete cache update - current state (" + state + ") is not UPDATE_IN_PROGRESS");
}
state = UPDATE_COMPLETE;
return decrementUsageCounter();
}
/**
* Attempt to change the state to <code>UPDATE_IN_PROGRESS</code>. Calls
* to this method must be synchronized on the EntryUpdateState instance.
* @return the counter value after the operation completed
*/
public int startUpdate() {
if ((state != NOT_YET_UPDATING) && (state != UPDATE_CANCELLED)) {
throw new IllegalStateException("Cannot begin cache update - current state (" + state + ") is not NOT_YET_UPDATING or UPDATE_CANCELLED");
}
state = UPDATE_IN_PROGRESS;
return incrementUsageCounter();
}
/**
* Increments the usage counter by one
* @return the counter value after the increment
*/
public synchronized int incrementUsageCounter() {
nbConcurrentUses++;
return nbConcurrentUses;
}
/**
* Gets the current usage counter value
* @return a positive number.
*/
public synchronized int getUsageCounter() {
return nbConcurrentUses;
}
/**
* Decrements the usage counter by one. This method may only be called when the usage number is greater than zero
* @return the counter value after the decrement
*/
public synchronized int decrementUsageCounter() {
if (nbConcurrentUses <=0) {
throw new IllegalStateException("Cannot decrement usage counter, it is already equals to [" + nbConcurrentUses + "]");
}
nbConcurrentUses--;
return nbConcurrentUses;
}
}

View file

@ -0,0 +1,23 @@
/*
* Copyright (c) 2002-2003 by OpenSymphony
* All rights reserved.
*/
package com.opensymphony.oscache.base;
/**
* Thrown by {@link LifecycleAware} listeners that are not able to finalize
* themselves.
*
* @version $Revision$
* @author <a href="&#109;a&#105;&#108;&#116;&#111;:chris&#64;swebtec.&#99;&#111;&#109;">Chris Miller</a>
*/
public class FinalizationException extends Exception {
public FinalizationException() {
super();
}
public FinalizationException(String message) {
super(message);
}
}

View file

@ -0,0 +1,23 @@
/*
* Copyright (c) 2002-2003 by OpenSymphony
* All rights reserved.
*/
package com.opensymphony.oscache.base;
/**
* Thrown by {@link LifecycleAware} listeners that are not able to initialize
* themselves.
*
* @version $Revision$
* @author <a href="&#109;a&#105;&#108;&#116;&#111;:chris&#64;swebtec.&#99;&#111;&#109;">Chris Miller</a>
*/
public class InitializationException extends Exception {
public InitializationException() {
super();
}
public InitializationException(String message) {
super(message);
}
}

View file

@ -0,0 +1,42 @@
/*
* Copyright (c) 2002-2003 by OpenSymphony
* All rights reserved.
*/
package com.opensymphony.oscache.base;
/**
* Event handlers implement this so they can be notified when a cache
* is created and also when it is destroyed. This allows event handlers
* to load any configuration and/or resources they need on startup and
* then release them again when the cache is shut down.
*
* @author <a href="&#109;a&#105;&#108;&#116;&#111;:chris&#64;swebtec.&#99;&#111;&#109;">Chris Miller</a>
*
* @see com.opensymphony.oscache.base.events.CacheEventListener
*/
public interface LifecycleAware {
/**
* Called by the cache administrator class when a cache is instantiated.
*
* @param cache the cache instance that this listener is attached to.
* @param config The cache's configuration details. This allows the event handler
* to initialize itself based on the cache settings, and also to receive <em>additional</em>
* settings that were part of the cache configuration but that the cache
* itself does not care about. If you are using <code>cache.properties</code>
* for your configuration, simply add any additional properties that your event
* handler requires and they will be passed through in this parameter.
*
* @throws InitializationException thrown when there was a problem initializing the
* listener. The cache administrator will log this error and disable the listener.
*/
public void initialize(Cache cache, Config config) throws InitializationException;
/**
* Called by the cache administrator class when a cache is destroyed.
*
* @throws FinalizationException thrown when there was a problem finalizing the
* listener. The cache administrator will catch and log this error.
*/
public void finialize() throws FinalizationException;
}

View file

@ -0,0 +1,51 @@
/*
* Copyright (c) 2002-2007 by OpenSymphony
* All rights reserved.
*/
package com.opensymphony.oscache.base;
/**
* This exception is thrown when retrieving an item from cache and it is
* expired.
* Note that for fault tolerance purposes, it is possible to retrieve the
* current cached object from the exception.
*
* <p>January, 2004 - The OSCache developers are aware of the fact that throwing
* an exception for a perfect valid situation (cache miss) is design smell. This will
* be removed in the near future, and other means of refreshing the cache will be
* provided.</p>
*
* @author <a href="mailto:fbeauregard@pyxis-tech.com">Francois Beauregard</a>
* @version $Revision$
*/
public final class NeedsRefreshException extends Exception {
/**
* Current object in the cache
*/
private Object cacheContent = null;
/**
* Create a NeedsRefreshException
*/
public NeedsRefreshException(String message, Object cacheContent) {
super(message);
this.cacheContent = cacheContent;
}
/**
* Create a NeedsRefreshException
*/
public NeedsRefreshException(Object cacheContent) {
super();
this.cacheContent = cacheContent;
}
/**
* Retrieve current object in the cache
*/
public Object getCacheContent() {
return cacheContent;
}
}

View file

@ -0,0 +1,92 @@
/*
* Copyright (c) 2002-2003 by OpenSymphony
* All rights reserved.
*/
package com.opensymphony.oscache.base.algorithm;
import java.util.*;
/**
* FIFO (First In First Out) based queue algorithm for the cache.
*
* No synchronization is required in this class since the
* <code>AbstractConcurrentReadCache</code> already takes care of any
* synchronization requirements.
*
* @version $Revision$
* @author <a href="mailto:mike@atlassian.com">Mike Cannon-Brookes</a>
* @author <a href="mailto:abergevin@pyxis-tech.com">Alain Bergevin</a>
* @author <a href="&#109;a&#105;&#108;&#116;&#111;:chris&#64;swebtec.&#99;&#111;&#109;">Chris Miller</a>
*/
public class FIFOCache extends AbstractConcurrentReadCache {
private static final long serialVersionUID = -10333778645392679L;
/**
* A queue containing all cache keys
*/
private Collection list = new LinkedHashSet();
/**
* Constructs a FIFO Cache.
*/
public FIFOCache() {
super();
}
/**
* Constructs a FIFO Cache of the specified capacity.
*
* @param capacity The maximum cache capacity.
*/
public FIFOCache(int capacity) {
this();
maxEntries = capacity;
}
/**
* An object was retrieved from the cache. This implementation
* does noting since this event has no impact on the FIFO algorithm.
*
* @param key The cache key of the item that was retrieved.
*/
protected void itemRetrieved(Object key) {
}
/**
* An object was put in the cache. This implementation just adds
* the key to the end of the list if it doesn't exist in the list
* already.
*
* @param key The cache key of the item that was put.
*/
protected void itemPut(Object key) {
if (!list.contains(key)) {
list.add(key);
}
}
/**
* An item needs to be removed from the cache. The FIFO implementation
* removes the first element in the list (ie, the item that has been in
* the cache for the longest time).
*
* @return The key of whichever item was removed.
*/
protected Object removeItem() {
Iterator it = list.iterator();
Object toRemove = it.next();
it.remove();
return toRemove;
}
/**
* Remove specified key since that object has been removed from the cache.
*
* @param key The cache key of the item that was removed.
*/
protected void itemRemoved(Object key) {
list.remove(key);
}
}

View file

@ -0,0 +1,156 @@
/*
* Copyright (c) 2002-2003 by OpenSymphony
* All rights reserved.
*/
package com.opensymphony.oscache.base.algorithm;
import java.util.*;
/**
* <p>LRU (Least Recently Used) algorithm for the cache.</p>
*
* <p>Since release 2.3 this class requires Java 1.4
* to use the <code>LinkedHashSet</code>. Use prior OSCache release which
* require the Jakarta commons-collections <code>SequencedHashMap</code>
* class or the <code>LinkedList</code> class if neither of the above
* classes are available.</p>
*
* <p>No synchronization is required in this class since the
* <code>AbstractConcurrentReadCache</code> already takes care of any
* synchronization requirements.</p>
*
* @version $Revision$
* @author <a href="mailto:salaman@teknos.com">Victor Salaman</a>
* @author <a href="mailto:fbeauregard@pyxis-tech.com">Francois Beauregard</a>
* @author <a href="mailto:abergevin@pyxis-tech.com">Alain Bergevin</a>
* @author <a href="&#109;a&#105;&#108;&#116;&#111;:chris&#64;swebtec.&#99;&#111;&#109;">Chris Miller</a>
*/
public class LRUCache extends AbstractConcurrentReadCache {
private static final long serialVersionUID = -7379608101794788534L;
/**
* Cache queue containing all cache keys.
*/
private Collection list = new LinkedHashSet();
/**
* A flag indicating whether there is a removal operation in progress.
*/
private volatile boolean removeInProgress = false;
/**
* Constructs an LRU Cache.
*/
public LRUCache() {
super();
}
/**
* Constructors a LRU Cache of the specified capacity.
*
* @param capacity The maximum cache capacity.
*/
public LRUCache(int capacity) {
this();
maxEntries = capacity;
}
/**
* An item was retrieved from the list. The LRU implementation moves
* the retrieved item's key to the front of the list.
*
* @param key The cache key of the item that was retrieved.
*/
protected void itemRetrieved(Object key) {
// Prevent list operations during remove
while (removeInProgress) {
try {
Thread.sleep(5);
} catch (InterruptedException ie) {
}
}
// We need to synchronize here because AbstractConcurrentReadCache
// doesn't prevent multiple threads from calling this method simultaneously.
synchronized (list) {
list.remove(key);
list.add(key);
}
}
/**
* An object was put in the cache. This implementation adds/moves the
* key to the end of the list.
*
* @param key The cache key of the item that was put.
*/
protected void itemPut(Object key) {
// Since this entry was just accessed, move it to the back of the list.
synchronized (list) { // A further fix for CACHE-44
list.remove(key);
list.add(key);
}
}
/**
* An item needs to be removed from the cache. The LRU implementation
* removes the first element in the list (ie, the item that was least-recently
* accessed).
*
* @return The key of whichever item was removed.
*/
protected Object removeItem() {
Object toRemove = null;
removeInProgress = true;
try {
while (toRemove == null) {
try {
toRemove = removeFirst();
} catch (Exception e) {
// List is empty.
// this is theorically possible if we have more than the size concurrent
// thread in getItem. Remove completed but add not done yet.
// We simply wait for add to complete.
do {
try {
Thread.sleep(5);
} catch (InterruptedException ie) {
}
} while (list.isEmpty());
}
}
} finally {
removeInProgress = false;
}
return toRemove;
}
/**
* Remove specified key since that object has been removed from the cache.
*
* @param key The cache key of the item that was removed.
*/
protected void itemRemoved(Object key) {
list.remove(key);
}
/**
* Removes the first object from the list of keys.
*
* @return the object that was removed
*/
private Object removeFirst() {
Object toRemove = null;
synchronized (list) { // A further fix for CACHE-44 and CACHE-246
Iterator it = list.iterator();
toRemove = it.next();
it.remove();
}
return toRemove;
}
}

View file

@ -0,0 +1,71 @@
/*
* Copyright (c) 2002-2006 by OpenSymphony
* All rights reserved.
*/
package com.opensymphony.oscache.base.algorithm;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
/**
* A simple unlimited cache that has no upper bound to the number of
* cache entries it can contain.
*
* @version $Revision$
* @author <a href="mailto:fbeauregard@pyxis-tech.com">Francois Beauregard</a>
* @author <a href="mailto:abergevin@pyxis-tech.com">Alain Bergevin</a>
*/
public final class UnlimitedCache extends AbstractConcurrentReadCache {
private static transient final long serialVersionUID = 7615611393249532285L;
private final Log log = LogFactory.getLog(this.getClass());
/**
* Creates an unlimited cache by calling the super class's constructor
* with an <code>UNLIMITED</code> maximum number of entries.
*/
public UnlimitedCache() {
super();
maxEntries = UNLIMITED;
}
/**
* Overrides the <code>setMaxEntries</code> with an empty implementation.
* This property cannot be modified and is ignored for an
* <code>UnlimitedCache</code>.
*/
public void setMaxEntries(int maxEntries) {
log.warn("Cache max entries can't be set in " + this.getClass().getName() + ", ignoring value " + maxEntries + ".");
}
/**
* Implements <code>itemRetrieved</code> with an empty implementation.
* The unlimited cache doesn't care that an item was retrieved.
*/
protected void itemRetrieved(Object key) {
}
/**
* Implements <code>itemPut</code> with an empty implementation.
* The unlimited cache doesn't care that an item was put in the cache.
*/
protected void itemPut(Object key) {
}
/**
* This method just returns <code>null</code> since items should
* never end up being removed from an unlimited cache!
*/
protected Object removeItem() {
return null;
}
/**
* An empty implementation. The unlimited cache doesn't care that an
* item was removed.
*/
protected void itemRemoved(Object key) {
}
}

View file

@ -0,0 +1,37 @@
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">
<html>
<head>
<!--
@(#)package.html
Copyright (c) 2002-2003 by OpenSymphony
All rights reserved.
-->
</head>
<body bgcolor="white">
Provides the classes that implement the caching algorithms used by OSCache, all of
which are based on a derivative of Doug Lea's <code>ConcurrentReaderHashMap</code>.
<h2>Package Specification</h2>
<h2>Related Documentation</h2>
For overviews, tutorials, examples, guides, and tool documentation, please see:
<ul>
<li><a href="http://www.opensymphony.com/oscache">The OSCache Homepage</a>
</ul>
For further information on Doug Lea's concurrency package, please see:
<ul>
<li><a href="http://gee.cs.oswego.edu/dl/classes/EDU/oswego/cs/dl/util/concurrent/intro.html">http://gee.cs.oswego.edu/dl/classes/EDU/oswego/cs/dl/util/concurrent/intro.html</a>
</ul>
<!-- Put @see and @since tags down here. -->
</body>
</html>

View file

@ -0,0 +1,76 @@
/*
* Copyright (c) 2002-2003 by OpenSymphony
* All rights reserved.
*/
package com.opensymphony.oscache.base.events;
import com.opensymphony.oscache.base.Cache;
import com.opensymphony.oscache.base.CacheEntry;
/**
* CacheEntryEvent is the object created when an event occurs on a
* cache entry (Add, update, remove, flush). It contains the entry itself and
* its map.
*
* @version $Revision$
* @author <a href="mailto:fbeauregard@pyxis-tech.com">Francois Beauregard</a>
*/
public final class CacheEntryEvent extends CacheEvent {
/**
* The cache where the entry resides.
*/
private Cache map = null;
/**
* The entry that the event applies to.
*/
private CacheEntry entry = null;
/**
* Constructs a cache entry event object with no specified origin
*
* @param map The cache map of the cache entry
* @param entry The cache entry that the event applies to
*/
public CacheEntryEvent(Cache map, CacheEntry entry) {
this(map, entry, null);
}
/**
* Constructs a cache entry event object
*
* @param map The cache map of the cache entry
* @param entry The cache entry that the event applies to
* @param origin The origin of this event
*/
public CacheEntryEvent(Cache map, CacheEntry entry, String origin) {
super(origin);
this.map = map;
this.entry = entry;
}
/**
* Retrieve the cache entry that the event applies to.
*/
public CacheEntry getEntry() {
return entry;
}
/**
* Retrieve the cache entry key
*/
public String getKey() {
return entry.getKey();
}
/**
* Retrieve the cache map where the entry resides.
*/
public Cache getMap() {
return map;
}
public String toString() {
return "key=" + entry.getKey();
}
}

View file

@ -0,0 +1,55 @@
/*
* Copyright (c) 2002-2003 by OpenSymphony
* All rights reserved.
*/
package com.opensymphony.oscache.base.events;
/**
* This is the interface to listen to cache entry events. There is a method
* for each event type. These methods are called via a dispatcher. If you
* want to be notified when an event occurs on an entry, group or across a
* pattern, register a listener and implement this interface.
*
* @version $Revision$
* @author <a href="mailto:fbeauregard@pyxis-tech.com">Francois Beauregard</a>
*/
public interface CacheEntryEventListener extends CacheEventListener {
/**
* Event fired when an entry is added to the cache.
*/
void cacheEntryAdded(CacheEntryEvent event);
/**
* Event fired when an entry is flushed from the cache.
*/
void cacheEntryFlushed(CacheEntryEvent event);
/**
* Event fired when an entry is removed from the cache.
*/
void cacheEntryRemoved(CacheEntryEvent event);
/**
* Event fired when an entry is updated in the cache.
*/
void cacheEntryUpdated(CacheEntryEvent event);
/**
* Event fired when a group is flushed from the cache.
*/
void cacheGroupFlushed(CacheGroupEvent event);
/**
* Event fired when a key pattern is flushed from the cache.
* Note that this event will <em>not</em> be fired if the pattern
* is <code>null</code> or an empty string, instead the flush
* request will silently be ignored.
*/
void cachePatternFlushed(CachePatternEvent event);
/**
* An event that is fired when an entire cache gets flushed.
*/
void cacheFlushed(CachewideEvent event);
}

View file

@ -0,0 +1,54 @@
/*
* Copyright (c) 2002-2003 by OpenSymphony
* All rights reserved.
*/
package com.opensymphony.oscache.base.events;
/**
* This is all the possible events that may occur on a cache entry or
* collection of cache entries.<p>
* There is a corresponding interface {@link CacheEntryEventListener} for
* handling these events.
*
* @version $Revision$
* @author <a href="mailto:fbeauregard@pyxis-tech.com">Francois Beauregard</a>
*/
public final class CacheEntryEventType {
/**
* Get an event type for an entry added.
*/
public static final CacheEntryEventType ENTRY_ADDED = new CacheEntryEventType();
/**
* Get an event type for an entry updated.
*/
public static final CacheEntryEventType ENTRY_UPDATED = new CacheEntryEventType();
/**
* Get an event type for an entry flushed.
*/
public static final CacheEntryEventType ENTRY_FLUSHED = new CacheEntryEventType();
/**
* Get an event type for an entry removed.
*/
public static final CacheEntryEventType ENTRY_REMOVED = new CacheEntryEventType();
/**
* Get an event type for a group flush event.
*/
public static final CacheEntryEventType GROUP_FLUSHED = new CacheEntryEventType();
/**
* Get an event type for a pattern flush event.
*/
public static final CacheEntryEventType PATTERN_FLUSHED = new CacheEntryEventType();
/**
* Private constructor to ensure that no object of that type are
* created externally.
*/
private CacheEntryEventType() {
}
}

View file

@ -0,0 +1,48 @@
/*
* Copyright (c) 2002-2003 by OpenSymphony
* All rights reserved.
*/
package com.opensymphony.oscache.base.events;
/**
* The root event class for all cache events. Each subclasses of this class
* classifies a particular type of cache event.
*
* @author <a href="&#109;a&#105;&#108;&#116;&#111;:chris&#64;swebtec.&#99;&#111;&#109;">Chris Miller</a>
* Date: 20-May-2003
* Time: 15:25:02
*/
public abstract class CacheEvent {
/**
* An optional tag that can be attached to the event to specify the event's origin.
*/
protected String origin = null;
/**
* No-argument constructor so subtypes can easily implement <code>Serializable</code>
*/
public CacheEvent() {
}
/**
* Creates a cache event object that came from the specified origin.
*
* @param origin A string that indicates where this event was fired from.
* This value is optional; <code>null</code> can be passed in if an
* origin is not required.
*/
public CacheEvent(String origin) {
this.origin = origin;
}
/**
* Retrieves the origin of this event, if one was specified. This is most
* useful when an event handler causes another event to fire - by checking
* the origin the handler is able to prevent recursive events being
* fired.
*/
public String getOrigin() {
return origin;
}
}

View file

@ -0,0 +1,16 @@
/*
* Copyright (c) 2002-2003 by OpenSymphony
* All rights reserved.
*/
package com.opensymphony.oscache.base.events;
import java.util.EventListener;
/**
* This is the base interface for cache events.
*
* @version $Revision$
* @author <a href="&#109;a&#105;&#108;&#116;&#111;:chris&#64;swebtec.&#99;&#111;&#109;">Chris Miller</a>
*/
public interface CacheEventListener extends EventListener {
}

View file

@ -0,0 +1,71 @@
/*
* Copyright (c) 2002-2003 by OpenSymphony
* All rights reserved.
*/
package com.opensymphony.oscache.base.events;
import com.opensymphony.oscache.base.Cache;
/**
* CacheGroupEvent is an event that occurs at the cache group level
* (Add, update, remove, flush). It contains the group name and the
* originating cache object.
*
* @version $Revision$
* @author <a href="&#109;a&#105;&#108;&#116;&#111;:chris&#64;swebtec.&#99;&#111;&#109;">Chris Miller</a>
*/
public final class CacheGroupEvent extends CacheEvent {
/**
* The cache where the entry resides.
*/
private Cache map = null;
/**
* The group that the event applies to.
*/
private String group = null;
/**
* Constructs a cache group event with no origin
*
* @param map The cache map of the cache entry
* @param group The cache group that the event applies to.
*/
public CacheGroupEvent(Cache map, String group) {
this(map, group, null);
}
/**
* Constructs a cache group event
*
* @param map The cache map of the cache entry
* @param group The cache group that the event applies to.
* @param origin An optional tag that can be attached to the event to
* specify the event's origin. This is useful to prevent events from being
* fired recursively in some situations, such as when an event handler
* causes another event to be fired.
*/
public CacheGroupEvent(Cache map, String group, String origin) {
super(origin);
this.map = map;
this.group = group;
}
/**
* Retrieve the cache group that the event applies to.
*/
public String getGroup() {
return group;
}
/**
* Retrieve the cache map where the group resides.
*/
public Cache getMap() {
return map;
}
public String toString() {
return "groupName=" + group;
}
}

View file

@ -0,0 +1,71 @@
/*
* Copyright (c) 2002-2003 by OpenSymphony
* All rights reserved.
*/
package com.opensymphony.oscache.base.events;
import com.opensymphony.oscache.base.CacheEntry;
/**
* Cache map access event. This is the object created when an event occurs on a
* cache map (cache Hit, cache miss). It contains the entry that was referenced
* by the event and the event type.
*
* @version $Revision$
* @author <a href="mailto:fbeauregard@pyxis-tech.com">Francois Beauregard</a>
*/
public final class CacheMapAccessEvent extends CacheEvent {
/**
* The cache entry that the event applies to.
*/
private CacheEntry entry = null;
/**
* Type of the event.
*/
private CacheMapAccessEventType eventType = null;
/**
* Constructor.
* <p>
* @param eventType Type of the event.
* @param entry The cache entry that the event applies to.
*/
public CacheMapAccessEvent(CacheMapAccessEventType eventType, CacheEntry entry) {
this(eventType, entry, null);
}
/**
* Constructor.
* <p>
* @param eventType Type of the event.
* @param entry The cache entry that the event applies to.
* @param origin The origin of the event
*/
public CacheMapAccessEvent(CacheMapAccessEventType eventType, CacheEntry entry, String origin) {
super(origin);
this.eventType = eventType;
this.entry = entry;
}
/**
* Retrieve the cache entry that the event applies to.
*/
public CacheEntry getCacheEntry() {
return entry;
}
/**
* Retrieve the cache entry key that the event applies to.
*/
public String getCacheEntryKey() {
return entry.getKey();
}
/**
* Retrieve the type of the event.
*/
public CacheMapAccessEventType getEventType() {
return eventType;
}
}

View file

@ -0,0 +1,21 @@
/*
* Copyright (c) 2002-2003 by OpenSymphony
* All rights reserved.
*/
package com.opensymphony.oscache.base.events;
/**
* This is the interface to listen to cache map access events. The events are
* cache hits and misses, and are dispatched through this interface
*
* @version $Revision$
* @author <a href="mailto:fbeauregard@pyxis-tech.com">Francois Beauregard</a>
*/
public interface CacheMapAccessEventListener extends CacheEventListener {
/**
* Event fired when an entry is accessed.
* Use getEventType to differentiate between access events.
*/
public void accessed(CacheMapAccessEvent event);
}

View file

@ -0,0 +1,37 @@
/*
* Copyright (c) 2002-2003 by OpenSymphony
* All rights reserved.
*/
package com.opensymphony.oscache.base.events;
/**
* This is an enumeration of the cache events that represent the
* various outcomes of cache accesses.
*
* @version $Revision$
* @author <a href="mailto:fbeauregard@pyxis-tech.com">Francois Beauregard</a>
*/
public final class CacheMapAccessEventType {
/**
* Get an event type for a cache hit.
*/
public static final CacheMapAccessEventType HIT = new CacheMapAccessEventType();
/**
* Get an event type for a cache miss.
*/
public static final CacheMapAccessEventType MISS = new CacheMapAccessEventType();
/**
* Get an event type for when the data was found in the cache but was stale.
*/
public static final CacheMapAccessEventType STALE_HIT = new CacheMapAccessEventType();
/**
* Private constructor to ensure that no object of this type are
* created externally.
*/
private CacheMapAccessEventType() {
}
}

View file

@ -0,0 +1,69 @@
/*
* Copyright (c) 2002-2003 by OpenSymphony
* All rights reserved.
*/
package com.opensymphony.oscache.base.events;
import com.opensymphony.oscache.base.Cache;
/**
* A CachePatternEvent is fired when a pattern has been applied to a cache.
*
* @version $Revision$
* @author <a href="&#109;a&#105;&#108;&#116;&#111;:chris&#64;swebtec.&#99;&#111;&#109;">Chris Miller</a>
*/
public final class CachePatternEvent extends CacheEvent {
/**
* The cache the pattern is being applied to.
*/
private Cache map = null;
/**
* The pattern that is being applied.
*/
private String pattern = null;
/**
* Constructs a cache pattern event with no origin
*
* @param map The cache map that the pattern was applied to
* @param pattern The pattern that was applied
*/
public CachePatternEvent(Cache map, String pattern) {
this(map, pattern, null);
}
/**
* Constructs a cache pattern event
*
* @param map The cache map that the pattern was applied to
* @param pattern The cache pattern that the event applies to.
* @param origin An optional tag that can be attached to the event to
* specify the event's origin. This is useful to prevent events from being
* fired recursively in some situations, such as when an event handler
* causes another event to be fired, or for logging purposes.
*/
public CachePatternEvent(Cache map, String pattern, String origin) {
super(origin);
this.map = map;
this.pattern = pattern;
}
/**
* Retrieve the cache map that had the pattern applied.
*/
public Cache getMap() {
return map;
}
/**
* Retrieve the pattern that was applied to the cache.
*/
public String getPattern() {
return pattern;
}
public String toString() {
return "pattern=" + pattern;
}
}

View file

@ -0,0 +1,59 @@
/*
* Copyright (c) 2002-2003 by OpenSymphony
* All rights reserved.
*/
package com.opensymphony.oscache.base.events;
import com.opensymphony.oscache.base.Cache;
import java.util.Date;
/**
* A <code>CachewideEvent<code> represents and event that occurs on
* the the entire cache, eg a cache flush or clear.
*
* @version $Revision$
* @author <a href="&#109;a&#105;&#108;&#116;&#111;:chris&#64;swebtec.&#99;&#111;&#109;">Chris Miller</a>
*/
public final class CachewideEvent extends CacheEvent {
/**
* The cache where the event occurred.
*/
private Cache cache = null;
/**
* The date/time for when the flush is scheduled
*/
private Date date = null;
/**
* Constructs a cachewide event with the specified origin.
*
* @param cache The cache map that the event occurred on.
* @param date The date/time that this cachewide event is scheduled for
* (eg, the date that the cache is to be flushed).
* @param origin An optional tag that can be attached to the event to
* specify the event's origin. This is useful to prevent events from being
* fired recursively in some situations, such as when an event handler
* causes another event to be fired.
*/
public CachewideEvent(Cache cache, Date date, String origin) {
super(origin);
this.date = date;
this.cache = cache;
}
/**
* Retrieve the cache map that the event occurred on.
*/
public Cache getCache() {
return cache;
}
/**
* Retrieve the date/time that the cache flush is scheduled for.
*/
public Date getDate() {
return date;
}
}

View file

@ -0,0 +1,26 @@
/*
* Copyright (c) 2002-2003 by OpenSymphony
* All rights reserved.
*/
package com.opensymphony.oscache.base.events;
/**
* This is an enumeration holding all the events that can
* occur at the cache-wide level.
*
* @author <a href="&#109;a&#105;&#108;&#116;&#111;:chris&#64;swebtec.&#99;&#111;&#109;">Chris Miller</a>
*/
public class CachewideEventType {
/**
* Get an event type for a cache flush event.
*/
public static final CachewideEventType CACHE_FLUSHED = new CachewideEventType();
/**
* Private constructor to ensure that no object of this type are
* created externally.
*/
private CachewideEventType() {
}
}

View file

@ -0,0 +1,78 @@
/*
* Copyright (c) 2002-2003 by OpenSymphony
* All rights reserved.
*/
package com.opensymphony.oscache.base.events;
import java.util.Date;
/**
* A <code>ScopeEvent</code> is created when an event occurs across one or all scopes.
* This type of event is only applicable to the <code>ServletCacheAdministrator</code>.
*
* @version $Revision$
* @author <a href="mailto:fbeauregard@pyxis-tech.com">Francois Beauregard</a>
*/
public final class ScopeEvent extends CacheEvent {
/**
* Date that the event applies to.
*/
private Date date = null;
/**
* Type of the event.
*/
private ScopeEventType eventType = null;
/**
* Scope that applies to this event.
*/
private int scope = 0;
/**
* Constructs a scope event object with no specified origin.
*
* @param eventType Type of the event.
* @param scope Scope that applies to the event.
* @param date Date that the event applies to.
*/
public ScopeEvent(ScopeEventType eventType, int scope, Date date) {
this(eventType, scope, date, null);
}
/**
* Constructs a scope event object.
*
* @param eventType Type of the event.
* @param scope Scope that applies to the event.
* @param date Date that the event applies to.
* @param origin The origin of this event.
*/
public ScopeEvent(ScopeEventType eventType, int scope, Date date, String origin) {
super(origin);
this.eventType = eventType;
this.scope = scope;
this.date = date;
}
/**
* Retrieve the event date
*/
public Date getDate() {
return date;
}
/**
* Retrieve the type of the event.
*/
public ScopeEventType getEventType() {
return eventType;
}
/**
* Retrieve the scope that applies to the event.
*/
public int getScope() {
return scope;
}
}

View file

@ -0,0 +1,21 @@
/*
* Copyright (c) 2002-2003 by OpenSymphony
* All rights reserved.
*/
package com.opensymphony.oscache.base.events;
/**
* This is the interface to listen to scope events. The events are
* scope flushed and all scope flushed, and are dispatched thru this interface
*
* @version $Revision$
* @author <a href="mailto:fbeauregard@pyxis-tech.com">Francois Beauregard</a>
*/
public interface ScopeEventListener extends CacheEventListener {
/**
* Event fired when a specific or all scopes are flushed.
* Use getEventType to differentiate between the two.
*/
public void scopeFlushed(ScopeEvent event);
}

View file

@ -0,0 +1,33 @@
/*
* Copyright (c) 2002-2003 by OpenSymphony
* All rights reserved.
*/
package com.opensymphony.oscache.base.events;
/**
* This is an enumeration of all the possible events that may occur
* at the scope level. Scope-level events are only relevant to the
* <code>ServletCacheAdministrator</code>.
*
* @version $Revision$
* @author <a href="mailto:fbeauregard@pyxis-tech.com">Francois Beauregard</a>
*/
public final class ScopeEventType {
/**
* Specifies an event type for the all scope flushed event.
*/
public static final ScopeEventType ALL_SCOPES_FLUSHED = new ScopeEventType();
/**
* Specifies an event type for the flushing of a specific scope.
*/
public static final ScopeEventType SCOPE_FLUSHED = new ScopeEventType();
/**
* Private constructor to ensure that no object of that type are
* created externally.
*/
private ScopeEventType() {
}
}

View file

@ -0,0 +1,32 @@
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">
<html>
<head>
<!--
@(#)package.html
Copyright (c) 2002-2003 by OpenSymphony
All rights reserved.
-->
</head>
<body bgcolor="white">
Provides the base classes and interfaces that allow pluggable event handlers to be
incorporated into OSCache.
<h2>Package Specification</h2>
<h2>Related Documentation</h2>
For overviews, tutorials, examples, guides, and tool documentation, please see:
<ul>
<li><a href="http://www.opensymphony.com/oscache">The OSCache Homepage</a>
</ul>
<!-- Put @see and @since tags down here. -->
</body>
</html>

View file

@ -0,0 +1,31 @@
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">
<html>
<head>
<!--
@(#)package.html
Copyright (c) 2002-2003 by OpenSymphony
All rights reserved.
-->
</head>
<body bgcolor="white">
Provides the base classes and interfaces that make up the core of OSCache.
<h2>Package Specification</h2>
<h2>Related Documentation</h2>
For overviews, tutorials, examples, guides, and tool documentation, please see:
<ul>
<li><a href="http://www.opensymphony.com/oscache">The OSCache Homepage</a>
</ul>
<!-- Put @see and @since tags down here. -->
</body>
</html>

View file

@ -0,0 +1,33 @@
/*
* Copyright (c) 2002-2003 by OpenSymphony
* All rights reserved.
*/
package com.opensymphony.oscache.base.persistence;
/**
* Exception thrown when an error occurs in a PersistenceListener implementation.
*
* @version $Revision$
* @author <a href="mailto:fbeauregard@pyxis-tech.com">Francois Beauregard</a>
*/
public final class CachePersistenceException extends Exception {
/**
* Creates new CachePersistenceException without detail message.
*/
public CachePersistenceException() {
}
/**
* Constructs an CachePersistenceException with the specified detail message.
*
* @param msg the detail message.
*/
public CachePersistenceException(String msg) {
super(msg);
}
public CachePersistenceException(String message, Throwable cause) {
super(message, cause);
}
}

View file

@ -0,0 +1,96 @@
/*
* Copyright (c) 2002-2003 by OpenSymphony
* All rights reserved.
*/
package com.opensymphony.oscache.base.persistence;
import com.opensymphony.oscache.base.Config;
import java.util.Set;
/**
* Defines the methods that are required to persist cache data.
* To provide a custom persistence mechanism you should implement this
* interface and supply the fully-qualified classname to the cache via
* the <code>cache.persistence.class</code> configuration property.
*
* @version $Revision$
* @author <a href="mailto:fbeauregard@pyxis-tech.com">Francois Beauregard</a>
*/
public interface PersistenceListener {
/**
* Verify if an object is currently stored in the persistent cache.
*
* @param key The cache key of the object to check.
*/
public boolean isStored(String key) throws CachePersistenceException;
/**
* Verify if a group is currently stored in the persistent cache.
*
* @param groupName The name of the group to check.
*/
public boolean isGroupStored(String groupName) throws CachePersistenceException;
/**
* Clear the entire persistent cache (including the root)
*/
public void clear() throws CachePersistenceException;
/**
* Allow the persistence code to initialize itself based on the supplied
* cache configuration.
*/
public PersistenceListener configure(Config config);
/**
* Removes an object from the persistent cache
*/
public void remove(String key) throws CachePersistenceException;
/**
* Removes a group from the persistent cache.
*
* @param groupName The name of the cache group to remove.
*/
public void removeGroup(String groupName) throws CachePersistenceException;
/**
* Retrieves an object from the persistent cache.
*
* @param key The unique cache key that maps to the object
* being retrieved.
* @return The object, or <code>null</code> if no object was found
* matching the supplied key.
*/
public Object retrieve(String key) throws CachePersistenceException;
/**
* Stores an object in the persistent cache.
*
* @param key The key to uniquely identify this object.
* @param obj The object to persist. Most implementations
* of this interface will require this object implements
* <code>Serializable</code>.
*/
public void store(String key, Object obj) throws CachePersistenceException;
/**
* Stores a group in the persistent cache.
*
* @param groupName The name of the group to persist.
* @param group A set containing the keys of all the <code>CacheEntry</code>
* objects that belong to this group.
*/
public void storeGroup(String groupName, Set group) throws CachePersistenceException;
/**
* Retrieves a group from the persistent cache.
*
* @param groupName The name of the group to retrieve.
* @return The returned set should contain the keys
* of all the <code>CacheEntry</code> objects that belong
* to this group.
*/
Set retrieveGroup(String groupName) throws CachePersistenceException;
}

View file

@ -0,0 +1,31 @@
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">
<html>
<head>
<!--
@(#)package.html
Copyright (c) 2002-2003 by OpenSymphony
All rights reserved.
-->
</head>
<body bgcolor="white">
Provides the interfaces that provide persistence storage of cached objects.
<h2>Package Specification</h2>
<h2>Related Documentation</h2>
For overviews, tutorials, examples, guides, and tool documentation, please see:
<ul>
<li><a href="http://www.opensymphony.com/oscache">The OSCache Homepage</a>
</ul>
<!-- Put @see and @since tags down here. -->
</body>
</html>

View file

@ -0,0 +1,195 @@
/*
* Copyright (c) 2002-2003 by OpenSymphony
* All rights reserved.
*/
package com.opensymphony.oscache.extra;
import com.opensymphony.oscache.base.events.*;
/**
* Implementation of a CacheEntryEventListener. It use the events to count
* the operations performed on the cache.
* <p>
* We are not using any synchronized so that this does not become a bottleneck.
* The consequence is that on retrieving values, the operations that are
* currently being done won't be counted.
*
* @version $Revision$
* @author <a href="mailto:abergevin@pyxis-tech.com">Alain Bergevin</a>
* @author <a href="&#109;a&#105;&#108;&#116;&#111;:chris&#64;swebtec.&#99;&#111;&#109;">Chris Miller</a>
*/
public class CacheEntryEventListenerImpl implements CacheEntryEventListener {
/**
* Counter for the cache flushes
*/
private int cacheFlushedCount = 0;
/**
* Counter for the added entries
*/
private int entryAddedCount = 0;
/**
* Counter for the flushed entries
*/
private int entryFlushedCount = 0;
/**
* Counter for the removed entries
*/
private int entryRemovedCount = 0;
/**
* Counter for the updated entries
*/
private int entryUpdatedCount = 0;
/**
* Counter for the flushed groups
*/
private int groupFlushedCount = 0;
/**
* Counter for the pattern flushes
*/
private int patternFlushedCount = 0;
/**
* Constructor, empty for us
*/
public CacheEntryEventListenerImpl() {
}
/**
* Gets the add counter
*
* @return The added counter
*/
public int getEntryAddedCount() {
return entryAddedCount;
}
/**
* Gets the flushed counter
*
* @return The flushed counter
*/
public int getEntryFlushedCount() {
return entryFlushedCount;
}
/**
* Gets the removed counter
*
* @return The removed counter
*/
public int getEntryRemovedCount() {
return entryRemovedCount;
}
/**
* Gets the updated counter
*
* @return The updated counter
*/
public int getEntryUpdatedCount() {
return entryUpdatedCount;
}
/**
* Gets the group flush counter
*
* @return The number of group flush calls that have occurred
*/
public int getGroupFlushedCount() {
return groupFlushedCount;
}
/**
* Gets the pattern flush counter
*
* @return The number of pattern flush calls that have occurred
*/
public int getPatternFlushedCount() {
return patternFlushedCount;
}
/**
* Gets the cache flush counter
*
* @return The number of times the entire cache has been flushed
*/
public int getCacheFlushedCount() {
return cacheFlushedCount;
}
/**
* Handles the event fired when an entry is added in the cache.
*
* @param event The event triggered when a cache entry has been added
*/
public void cacheEntryAdded(CacheEntryEvent event) {
entryAddedCount++;
}
/**
* Handles the event fired when an entry is flushed from the cache.
*
* @param event The event triggered when a cache entry has been flushed
*/
public void cacheEntryFlushed(CacheEntryEvent event) {
entryFlushedCount++;
}
/**
* Handles the event fired when an entry is removed from the cache.
*
* @param event The event triggered when a cache entry has been removed
*/
public void cacheEntryRemoved(CacheEntryEvent event) {
entryRemovedCount++;
}
/**
* Handles the event fired when an entry is updated in the cache.
*
* @param event The event triggered when a cache entry has been updated
*/
public void cacheEntryUpdated(CacheEntryEvent event) {
entryUpdatedCount++;
}
/**
* Handles the event fired when a group is flushed from the cache.
*
* @param event The event triggered when a cache group has been flushed
*/
public void cacheGroupFlushed(CacheGroupEvent event) {
groupFlushedCount++;
}
/**
* Handles the event fired when a pattern is flushed from the cache.
*
* @param event The event triggered when a cache pattern has been flushed
*/
public void cachePatternFlushed(CachePatternEvent event) {
patternFlushedCount++;
}
/**
* Handles the event fired when a cache flush occurs.
*
* @param event The event triggered when an entire cache is flushed
*/
public void cacheFlushed(CachewideEvent event) {
cacheFlushedCount++;
}
/**
* Returns the internal values in a string form
*/
public String toString() {
return ("Added " + entryAddedCount + ", Updated " + entryUpdatedCount + ", Flushed " + entryFlushedCount + ", Removed " + entryRemovedCount + ", Groups Flushed " + groupFlushedCount + ", Patterns Flushed " + patternFlushedCount + ", Cache Flushed " + cacheFlushedCount);
}
}

View file

@ -0,0 +1,111 @@
/*
* Copyright (c) 2002-2003 by OpenSymphony
* All rights reserved.
*/
package com.opensymphony.oscache.extra;
import com.opensymphony.oscache.base.events.CacheMapAccessEvent;
import com.opensymphony.oscache.base.events.CacheMapAccessEventListener;
import com.opensymphony.oscache.base.events.CacheMapAccessEventType;
/**
* Implementation of a CacheMapAccessEventListener. It uses the events to count
* the cache hit and misses.
* <p>
* We are not using any synchronized so that this does not become a bottleneck.
* The consequence is that on retrieving values, the operations that are
* currently being done won't be counted.
*
* @version $Revision$
* @author <a href="mailto:abergevin@pyxis-tech.com">Alain Bergevin</a>
* @author <a href="&#109;a&#105;&#108;&#116;&#111;:chris&#64;swebtec.&#99;&#111;&#109;">Chris Miller</a>
*/
public class CacheMapAccessEventListenerImpl implements CacheMapAccessEventListener {
/**
* Hit counter
*/
private int hitCount = 0;
/**
* Miss counter
*/
private int missCount = 0;
/**
* Stale hit counter
*/
private int staleHitCount = 0;
/**
* Constructor, empty for us
*/
public CacheMapAccessEventListenerImpl() {
}
/**
* Returns the cache's current hit count
*
* @return The hit count
*/
public int getHitCount() {
return hitCount;
}
/**
* Returns the cache's current miss count
*
* @return The miss count
*/
public int getMissCount() {
return missCount;
}
/**
* Returns the cache's current stale hit count
*/
public int getStaleHitCount() {
return staleHitCount;
}
/**
* This method handles an event each time the cache is accessed
*
* @param event The event triggered when the cache was accessed
*/
public void accessed(CacheMapAccessEvent event) {
// Retrieve the event type and update the counters
CacheMapAccessEventType type = event.getEventType();
// Handles a hit event
if (type == CacheMapAccessEventType.HIT) {
hitCount++;
}
// Handles a stale hit event
else if (type == CacheMapAccessEventType.STALE_HIT) {
staleHitCount++;
}
// Handles a miss event
else if (type == CacheMapAccessEventType.MISS) {
missCount++;
} else {
// Unknown event!
throw new IllegalArgumentException("Unknown Cache Map Access event received");
}
}
/**
* Resets all of the totals to zero
*/
public void reset() {
hitCount = 0;
staleHitCount = 0;
missCount = 0;
}
/**
* Return the counters in a string form
*/
public String toString() {
return ("Hit count = " + hitCount + ", stale hit count = " + staleHitCount + " and miss count = " + missCount);
}
}

View file

@ -0,0 +1,147 @@
/*
* Copyright (c) 2002-2003 by OpenSymphony
* All rights reserved.
*/
package com.opensymphony.oscache.extra;
import com.opensymphony.oscache.base.events.ScopeEvent;
import com.opensymphony.oscache.base.events.ScopeEventListener;
import com.opensymphony.oscache.base.events.ScopeEventType;
/**
* Implementation of a ScopeEventListener that keeps track of the scope flush events.
* We are not using any synchronized so that this does not become a bottleneck.
* The consequence is that on retrieving values, the operations that are
* currently being done won't be counted.
*
* @version $Revision$
* @author <a href="mailto:abergevin@pyxis-tech.com">Alain Bergevin</a>
*/
public class ScopeEventListenerImpl implements ScopeEventListener {
/**
* Scope names
*/
public static final String[] SCOPE_NAMES = {
null, "page", "request", "session", "application"
};
/**
* Number of known scopes
*/
public static final int NB_SCOPES = SCOPE_NAMES.length - 1;
/**
* Page scope number
*/
public static final int PAGE_SCOPE = 1;
/**
* Request scope number
*/
public static final int REQUEST_SCOPE = 2;
/**
* Session scope number
*/
public static final int SESSION_SCOPE = 3;
/**
* Application scope number
*/
public static final int APPLICATION_SCOPE = 4;
/**
* Flush counter for all scopes.
* Add one to the number of scope because the array is being used
* from position 1 instead of 0 for convenience
*/
private int[] scopeFlushCount = new int[NB_SCOPES + 1];
public ScopeEventListenerImpl() {
}
/**
* Gets the flush count for scope {@link ScopeEventListenerImpl#APPLICATION_SCOPE}.
* <p>
* @return The total number of application flush
*/
public int getApplicationScopeFlushCount() {
return scopeFlushCount[APPLICATION_SCOPE];
}
/**
* Gets the flush count for scope {@link ScopeEventListenerImpl#PAGE_SCOPE}.
* @return The total number of page flush
*/
public int getPageScopeFlushCount() {
return scopeFlushCount[PAGE_SCOPE];
}
/**
* Gets the flush count for scope {@link ScopeEventListenerImpl#REQUEST_SCOPE}.
* @return The total number of request flush
*/
public int getRequestScopeFlushCount() {
return scopeFlushCount[REQUEST_SCOPE];
}
/**
* Gets the flush count for scope {@link ScopeEventListenerImpl#SESSION_SCOPE}.
* @return The total number of session flush
*/
public int getSessionScopeFlushCount() {
return scopeFlushCount[SESSION_SCOPE];
}
/**
* Returns the total flush count.
* @return The total number of scope flush
*/
public int getTotalScopeFlushCount() {
int total = 0;
for (int count = 1; count <= NB_SCOPES; count++) {
total += scopeFlushCount[count];
}
return total;
}
/**
* Handles all the scope flush events.
* @param event The scope event
*/
public void scopeFlushed(ScopeEvent event) {
// Get the event type and process it
ScopeEventType eventType = event.getEventType();
if (eventType == ScopeEventType.ALL_SCOPES_FLUSHED) {
// All 4 scopes were flushed, increment the counters
for (int count = 1; count <= NB_SCOPES; count++) {
scopeFlushCount[count]++;
}
} else if (eventType == ScopeEventType.SCOPE_FLUSHED) {
// Get back the scope from the event and increment the flush count
scopeFlushCount[event.getScope()]++;
} else {
// Unknown event!
throw new IllegalArgumentException("Unknown Scope Event type received");
}
}
/**
* Returns all the flush counter in a string form.
*/
public String toString() {
StringBuffer returnString = new StringBuffer("Flush count for ");
for (int count = 1; count <= NB_SCOPES; count++) {
returnString.append("scope " + SCOPE_NAMES[count] + " = " + scopeFlushCount[count] + ", ");
}
// Remove the last 2 chars, which are ", "
returnString.setLength(returnString.length() - 2);
return returnString.toString();
}
}

View file

@ -0,0 +1,295 @@
/*
* Copyright (c) 2002-2007 by OpenSymphony
* All rights reserved.
*/
package com.opensymphony.oscache.extra;
import com.opensymphony.oscache.base.Cache;
import com.opensymphony.oscache.base.events.CacheEntryEvent;
import com.opensymphony.oscache.base.events.CacheEntryEventListener;
import com.opensymphony.oscache.base.events.CacheGroupEvent;
import com.opensymphony.oscache.base.events.CacheMapAccessEvent;
import com.opensymphony.oscache.base.events.CacheMapAccessEventListener;
import com.opensymphony.oscache.base.events.CacheMapAccessEventType;
import com.opensymphony.oscache.base.events.CachePatternEvent;
import com.opensymphony.oscache.base.events.CachewideEvent;
import com.opensymphony.oscache.base.events.ScopeEvent;
import com.opensymphony.oscache.base.events.ScopeEventListener;
import com.opensymphony.oscache.extra.ScopeEventListenerImpl;
/**
* A simple implementation of a statistic reporter which uses the
* event listeners. It uses the events to count the cache hit and
* misses and of course the flushes.
* <p>
* We are not using any synchronized so that this does not become a bottleneck.
* The consequence is that on retrieving values, the operations that are
* currently being done won't be counted.
*/
public class StatisticListenerImpl implements CacheMapAccessEventListener,
CacheEntryEventListener, ScopeEventListener {
/**
* Hit counter.
*/
private static int hitCount = 0;
/**
* Miss counter.
*/
private static int missCount = 0;
/**
* Stale hit counter.
*/
private static int staleHitCount = 0;
/**
* Hit counter sum.
*/
private static int hitCountSum = 0;
/**
* Miss counter sum.
*/
private static int missCountSum = 0;
/**
* Stale hit counter.
*/
private static int staleHitCountSum = 0;
/**
* Flush hit counter.
*/
private static int flushCount = 0;
/**
* Miss counter sum.
*/
private static int entriesAdded = 0;
/**
* Stale hit counter.
*/
private static int entriesRemoved = 0;
/**
* Flush hit counter.
*/
private static int entriesUpdated = 0;
/**
* Constructor, empty for us.
*/
public StatisticListenerImpl() {
}
/**
* This method handles an event each time the cache is accessed.
*
* @param event
* The event triggered when the cache was accessed
* @see com.opensymphony.oscache.base.events.CacheMapAccessEventListener#accessed(CacheMapAccessEvent)
*/
public void accessed(CacheMapAccessEvent event) {
// Retrieve the event type and update the counters
CacheMapAccessEventType type = event.getEventType();
// Handles a hit event
if (type == CacheMapAccessEventType.HIT) {
hitCount++;
} else if (type == CacheMapAccessEventType.STALE_HIT) { // Handles a
// stale hit
// event
staleHitCount++;
} else if (type == CacheMapAccessEventType.MISS) { // Handles a miss
// event
missCount++;
}
}
/**
* Logs the flush of the cache.
*
* @param info the string to be logged.
*/
private void flushed(String info) {
flushCount++;
hitCountSum += hitCount;
staleHitCountSum += staleHitCount;
missCountSum += missCount;
hitCount = 0;
staleHitCount = 0;
missCount = 0;
}
/**
* Event fired when a specific or all scopes are flushed.
*
* @param event ScopeEvent
* @see com.opensymphony.oscache.base.events.ScopeEventListener#scopeFlushed(ScopeEvent)
*/
public void scopeFlushed(ScopeEvent event) {
flushed("scope " + ScopeEventListenerImpl.SCOPE_NAMES[event.getScope()]);
}
/**
* Event fired when an entry is added to the cache.
*
* @param event CacheEntryEvent
* @see com.opensymphony.oscache.base.events.CacheEntryEventListener#cacheEntryAdded(CacheEntryEvent)
*/
public void cacheEntryAdded(CacheEntryEvent event) {
entriesAdded++;
}
/**
* Event fired when an entry is flushed from the cache.
*
* @param event CacheEntryEvent
* @see com.opensymphony.oscache.base.events.CacheEntryEventListener#cacheEntryFlushed(CacheEntryEvent)
*/
public void cacheEntryFlushed(CacheEntryEvent event) {
// do nothing, because a group or other flush is coming
if (!Cache.NESTED_EVENT.equals(event.getOrigin())) {
flushed("entry " + event.getKey() + " / " + event.getOrigin());
}
}
/**
* Event fired when an entry is removed from the cache.
*
* @param event CacheEntryEvent
* @see com.opensymphony.oscache.base.events.CacheEntryEventListener#cacheEntryRemoved(CacheEntryEvent)
*/
public void cacheEntryRemoved(CacheEntryEvent event) {
entriesRemoved++;
}
/**
* Event fired when an entry is updated in the cache.
*
* @param event CacheEntryEvent
* @see com.opensymphony.oscache.base.events.CacheEntryEventListener#cacheEntryUpdated(CacheEntryEvent)
*/
public void cacheEntryUpdated(CacheEntryEvent event) {
entriesUpdated++;
}
/**
* Event fired when a group is flushed from the cache.
*
* @param event CacheGroupEvent
* @see com.opensymphony.oscache.base.events.CacheEntryEventListener#cacheGroupFlushed(CacheGroupEvent)
*/
public void cacheGroupFlushed(CacheGroupEvent event) {
flushed("group " + event.getGroup());
}
/**
* Event fired when a key pattern is flushed from the cache.
*
* @param event CachePatternEvent
* @see com.opensymphony.oscache.base.events.CacheEntryEventListener#cachePatternFlushed(CachePatternEvent)
*/
public void cachePatternFlushed(CachePatternEvent event) {
flushed("pattern " + event.getPattern());
}
/**
* An event that is fired when an entire cache gets flushed.
*
* @param event CachewideEvent
* @see com.opensymphony.oscache.base.events.CacheEntryEventListener#cacheFlushed(CachewideEvent)
*/
public void cacheFlushed(CachewideEvent event) {
flushed("wide " + event.getDate());
}
/**
* Return the counters in a string form.
*
* @return String
*/
public String toString() {
return "StatisticListenerImpl: Hit = " + hitCount + " / " + hitCountSum
+ ", stale hit = " + staleHitCount + " / " + staleHitCountSum
+ ", miss = " + missCount + " / " + missCountSum + ", flush = "
+ flushCount + ", entries (added, removed, updates) = "
+ entriesAdded + ", " + entriesRemoved + ", " + entriesUpdated;
}
/**
* @return Returns the entriesAdded.
*/
public int getEntriesAdded() {
return entriesAdded;
}
/**
* @return Returns the entriesRemoved.
*/
public int getEntriesRemoved() {
return entriesRemoved;
}
/**
* @return Returns the entriesUpdated.
*/
public int getEntriesUpdated() {
return entriesUpdated;
}
/**
* @return Returns the flushCount.
*/
public int getFlushCount() {
return flushCount;
}
/**
* @return Returns the hitCount.
*/
public int getHitCount() {
return hitCount;
}
/**
* @return Returns the hitCountSum.
*/
public int getHitCountSum() {
return hitCountSum;
}
/**
* @return Returns the missCount.
*/
public int getMissCount() {
return missCount;
}
/**
* @return Returns the missCountSum.
*/
public int getMissCountSum() {
return missCountSum;
}
/**
* @return Returns the staleHitCount.
*/
public int getStaleHitCount() {
return staleHitCount;
}
/**
* @return Returns the staleHitCountSum.
*/
public int getStaleHitCountSum() {
return staleHitCountSum;
}
}

View file

@ -0,0 +1,32 @@
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">
<html>
<head>
<!--
@(#)package.html
Copyright (c) 2002-2003 by OpenSymphony
All rights reserved.
-->
</head>
<body bgcolor="white">
Provides some basic event handler implementations that aren't essential to the core
OSCache code, but form a useful starting point for basic logging or further development.
<h2>Package Specification</h2>
<h2>Related Documentation</h2>
For overviews, tutorials, examples, guides, and tool documentation, please see:
<ul>
<li><a href="http://www.opensymphony.com/oscache">The OSCache Homepage</a>
</ul>
<!-- Put @see and @since tags down here. -->
</body>
</html>

View file

@ -0,0 +1,307 @@
/*
* Copyright (c) 2002-2003 by OpenSymphony
* All rights reserved.
*/
package com.opensymphony.oscache.general;
import com.opensymphony.oscache.base.*;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import java.util.Date;
import java.util.Properties;
/**
* A GeneralCacheAdministrator creates, flushes and administers the cache.
*
* EXAMPLES :
* <pre><code>
* // ---------------------------------------------------------------
* // Typical use with fail over
* // ---------------------------------------------------------------
* String myKey = "myKey";
* String myValue;
* int myRefreshPeriod = 1000;
* try {
* // Get from the cache
* myValue = (String) admin.getFromCache(myKey, myRefreshPeriod);
* } catch (NeedsRefreshException nre) {
* try {
* // Get the value (probably by calling an EJB)
* myValue = "This is the content retrieved.";
* // Store in the cache
* admin.putInCache(myKey, myValue);
* } catch (Exception ex) {
* // We have the current content if we want fail-over.
* myValue = (String) nre.getCacheContent();
* // It is essential that cancelUpdate is called if the
* // cached content is not rebuilt
* admin.cancelUpdate(myKey);
* }
* }
*
*
*
* // ---------------------------------------------------------------
* // Typical use without fail over
* // ---------------------------------------------------------------
* String myKey = "myKey";
* String myValue;
* int myRefreshPeriod = 1000;
* try {
* // Get from the cache
* myValue = (String) admin.getFromCache(myKey, myRefreshPeriod);
* } catch (NeedsRefreshException nre) {
* try {
* // Get the value (probably by calling an EJB)
* myValue = "This is the content retrieved.";
* // Store in the cache
* admin.putInCache(myKey, myValue);
* updated = true;
* } finally {
* if (!updated) {
* // It is essential that cancelUpdate is called if the
* // cached content could not be rebuilt
* admin.cancelUpdate(myKey);
* }
* }
* }
* // ---------------------------------------------------------------
* // ---------------------------------------------------------------
* </code></pre>
*
* @version $Revision$
* @author <a href="mailto:fbeauregard@pyxis-tech.com">Francois Beauregard</a>
* @author <a href="mailto:abergevin@pyxis-tech.com">Alain Bergevin</a>
*/
public class GeneralCacheAdministrator extends AbstractCacheAdministrator {
private static transient final Log log = LogFactory.getLog(GeneralCacheAdministrator.class);
/**
* Application cache
*/
private Cache applicationCache = null;
/**
* Create the cache administrator.
*/
public GeneralCacheAdministrator() {
this(null);
}
/**
* Create the cache administrator with the specified properties
*/
public GeneralCacheAdministrator(Properties p) {
super(p);
log.info("Constructed GeneralCacheAdministrator()");
createCache();
}
/**
* Grabs a cache
*
* @return The cache
*/
public Cache getCache() {
return applicationCache;
}
/**
* Remove an object from the cache
*
* @param key The key entered by the user.
*/
public void removeEntry(String key) {
getCache().removeEntry(key);
}
/**
* Get an object from the cache
*
* @param key The key entered by the user.
* @return The object from cache
* @throws NeedsRefreshException when no cache entry could be found with the
* supplied key, or when an entry was found but is considered out of date. If
* the cache entry is a new entry that is currently being constructed this method
* will block until the new entry becomes available. Similarly, it will block if
* a stale entry is currently being rebuilt by another thread and cache blocking is
* enabled (<code>cache.blocking=true</code>).
*/
public Object getFromCache(String key) throws NeedsRefreshException {
return getCache().getFromCache(key);
}
/**
* Get an object from the cache
*
* @param key The key entered by the user.
* @param refreshPeriod How long the object can stay in cache in seconds. To
* allow the entry to stay in the cache indefinitely, supply a value of
* {@link CacheEntry#INDEFINITE_EXPIRY}
* @return The object from cache
* @throws NeedsRefreshException when no cache entry could be found with the
* supplied key, or when an entry was found but is considered out of date. If
* the cache entry is a new entry that is currently being constructed this method
* will block until the new entry becomes available. Similarly, it will block if
* a stale entry is currently being rebuilt by another thread and cache blocking is
* enabled (<code>cache.blocking=true</code>).
*/
public Object getFromCache(String key, int refreshPeriod) throws NeedsRefreshException {
return getCache().getFromCache(key, refreshPeriod);
}
/**
* Get an object from the cache
*
* @param key The key entered by the user.
* @param refreshPeriod How long the object can stay in cache in seconds. To
* allow the entry to stay in the cache indefinitely, supply a value of
* {@link CacheEntry#INDEFINITE_EXPIRY}
* @param cronExpression A cron expression that the age of the cache entry
* will be compared to. If the entry is older than the most recent match for the
* cron expression, the entry will be considered stale.
* @return The object from cache
* @throws NeedsRefreshException when no cache entry could be found with the
* supplied key, or when an entry was found but is considered out of date. If
* the cache entry is a new entry that is currently being constructed this method
* will block until the new entry becomes available. Similarly, it will block if
* a stale entry is currently being rebuilt by another thread and cache blocking is
* enabled (<code>cache.blocking=true</code>).
*/
public Object getFromCache(String key, int refreshPeriod, String cronExpression) throws NeedsRefreshException {
return getCache().getFromCache(key, refreshPeriod, cronExpression);
}
/**
* Cancels a pending cache update. This should only be called by a thread
* that received a {@link NeedsRefreshException} and was unable to generate
* some new cache content.
*
* @param key The cache entry key to cancel the update of.
*/
public void cancelUpdate(String key) {
getCache().cancelUpdate(key);
}
/**
* Shuts down the cache administrator.
*/
public void destroy() {
finalizeListeners(applicationCache);
}
// METHODS THAT DELEGATES TO THE CACHE ---------------------
/**
* Flush the entire cache immediately.
*/
public void flushAll() {
getCache().flushAll(new Date());
}
/**
* Flush the entire cache at the given date.
*
* @param date The time to flush
*/
public void flushAll(Date date) {
getCache().flushAll(date);
}
/**
* Flushes a single cache entry.
*/
public void flushEntry(String key) {
getCache().flushEntry(key);
}
/**
* Flushes all items that belong to the specified group.
*
* @param group The name of the group to flush
*/
public void flushGroup(String group) {
getCache().flushGroup(group);
}
/**
* Allows to flush all items that have a specified pattern in the key.
*
* @param pattern Pattern.
* @deprecated For performance and flexibility reasons it is preferable to
* store cache entries in groups and use the {@link #flushGroup(String)} method
* instead of relying on pattern flushing.
*/
public void flushPattern(String pattern) {
getCache().flushPattern(pattern);
}
/**
* Put an object in a cache
*
* @param key The key entered by the user
* @param content The object to store
* @param policy Object that implements refresh policy logic
*/
public void putInCache(String key, Object content, EntryRefreshPolicy policy) {
Cache cache = getCache();
cache.putInCache(key, content, policy);
}
/**
* Put an object in a cache
*
* @param key The key entered by the user
* @param content The object to store
*/
public void putInCache(String key, Object content) {
putInCache(key, content, (EntryRefreshPolicy) null);
}
/**
* Puts an object in a cache
*
* @param key The unique key for this cached object
* @param content The object to store
* @param groups The groups that this object belongs to
*/
public void putInCache(String key, Object content, String[] groups) {
getCache().putInCache(key, content, groups);
}
/**
* Puts an object in a cache
*
* @param key The unique key for this cached object
* @param content The object to store
* @param groups The groups that this object belongs to
* @param policy The refresh policy to use
*/
public void putInCache(String key, Object content, String[] groups, EntryRefreshPolicy policy) {
getCache().putInCache(key, content, groups, policy, null);
}
/**
* Sets the cache capacity (number of items). If the cache contains
* more than <code>capacity</code> items then items will be removed
* to bring the cache back down to the new size.
*
* @param capacity The new capacity of the cache
*/
public void setCacheCapacity(int capacity) {
super.setCacheCapacity(capacity);
getCache().setCapacity(capacity);
}
/**
* Creates a cache in this admin
*/
private void createCache() {
log.info("Creating new cache");
applicationCache = new Cache(isMemoryCaching(), isUnlimitedDiskCache(), isOverflowPersistence(), isBlocking(), algorithmClass, cacheCapacity);
configureStandardListeners(applicationCache);
}
}

View file

@ -0,0 +1,31 @@
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">
<html>
<head>
<!--
@(#)package.html
Copyright (c) 2002-2003 by OpenSymphony
All rights reserved.
-->
</head>
<body bgcolor="white">
Provides a generic administrator class for the cache.
<h2>Package Specification</h2>
<h2>Related Documentation</h2>
For overviews, tutorials, examples, guides, and tool documentation, please see:
<ul>
<li><a href="http://www.opensymphony.com/oscache">The OSCache Homepage</a>
</ul>
<!-- Put @see and @since tags down here. -->
</body>
</html>

View file

@ -0,0 +1,161 @@
package com.opensymphony.oscache.hibernate;
import java.util.Map;
import org.hibernate.cache.Cache;
import org.hibernate.cache.CacheException;
import org.hibernate.cache.Timestamper;
import com.opensymphony.oscache.base.NeedsRefreshException;
import com.opensymphony.oscache.general.GeneralCacheAdministrator;
/**
* Cache plugin for Hibernate 3.2 and OpenSymphony OSCache 2.4.
* <p/>
* The OSCache implementation assumes that identifiers have well-behaved <tt>toString()</tt> methods.
* This implementation <b>must</b> be threadsafe.
*
* @version $Revision:$
*/
public class OSCache implements Cache {
/** The OSCache 2.4 cache administrator. */
private GeneralCacheAdministrator cache;
private final int refreshPeriod;
private final String cron;
private final String regionName;
private final String[] regionGroups;
public OSCache(GeneralCacheAdministrator cache, int refreshPeriod, String cron, String region) {
this.cache = cache;
this.refreshPeriod = refreshPeriod;
this.cron = cron;
this.regionName = region;
this.regionGroups = new String[] {region};
}
/**
* @see org.hibernate.cache.Cache#get(java.lang.Object)
*/
public Object get(Object key) throws CacheException {
try {
return cache.getFromCache( toString(key), refreshPeriod, cron );
}
catch (NeedsRefreshException e) {
cache.cancelUpdate( toString(key) );
return null;
}
}
/**
* @see org.hibernate.cache.Cache#put(java.lang.Object, java.lang.Object)
*/
public void put(Object key, Object value) throws CacheException {
cache.putInCache( toString(key), value, regionGroups );
}
/**
* @see org.hibernate.cache.Cache#remove(java.lang.Object)
*/
public void remove(Object key) throws CacheException {
cache.flushEntry( toString(key) );
}
/**
* @see org.hibernate.cache.Cache#clear()
*/
public void clear() throws CacheException {
cache.flushGroup(regionName);
}
/**
* @see org.hibernate.cache.Cache#destroy()
*/
public void destroy() throws CacheException {
synchronized (cache) {
cache.destroy();
}
}
/**
* @see org.hibernate.cache.Cache#lock(java.lang.Object)
*/
public void lock(Object key) throws CacheException {
// local cache, so we use synchronization
}
/**
* @see org.hibernate.cache.Cache#unlock(java.lang.Object)
*/
public void unlock(Object key) throws CacheException {
// local cache, so we use synchronization
}
/**
* @see org.hibernate.cache.Cache#nextTimestamp()
*/
public long nextTimestamp() {
return Timestamper.next();
}
/**
* @see org.hibernate.cache.Cache#getTimeout()
*/
public int getTimeout() {
return Timestamper.ONE_MS * 60000; //ie. 60 seconds
}
/**
* @see org.hibernate.cache.Cache#toMap()
*/
public Map toMap() {
throw new UnsupportedOperationException();
}
/**
* @see org.hibernate.cache.Cache#getElementCountOnDisk()
*/
public long getElementCountOnDisk() {
return -1;
}
/**
* @see org.hibernate.cache.Cache#getElementCountInMemory()
*/
public long getElementCountInMemory() {
return -1;
}
/**
* @see org.hibernate.cache.Cache#getSizeInMemory()
*/
public long getSizeInMemory() {
return -1;
}
/**
* @see org.hibernate.cache.Cache#getRegionName()
*/
public String getRegionName() {
return regionName;
}
/**
* @see org.hibernate.cache.Cache#update(java.lang.Object, java.lang.Object)
*/
public void update(Object key, Object value) throws CacheException {
put(key, value);
}
/**
* @see org.hibernate.cache.Cache#read(java.lang.Object)
*/
public Object read(Object key) throws CacheException {
return get(key);
}
private String toString(Object key) {
return String.valueOf(key) + "." + regionName;
}
}

View file

@ -0,0 +1,123 @@
package com.opensymphony.oscache.hibernate;
import java.util.Properties;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.hibernate.cache.Cache;
import org.hibernate.cache.CacheException;
import org.hibernate.cache.CacheProvider;
import org.hibernate.cache.Timestamper;
import org.hibernate.util.StringHelper;
import com.opensymphony.oscache.base.CacheEntry;
import com.opensymphony.oscache.base.Config;
import com.opensymphony.oscache.general.GeneralCacheAdministrator;
import com.opensymphony.oscache.util.StringUtil;
/**
* Cache provider plugin for Hibernate 3.2 and OpenSymphony OSCache 2.4.
* <p/>
* This implementation assumes that identifiers have well-behaved <tt>toString()</tt> methods.
* <p/>
* To enable OSCache for Hibernate's second level cache add the following line to Hibernate's configuration e.g. <code>hibernate.cfg.xml</code>):
* <code>hibernate.cache.provider_class=com.opensymphony.oscache.hibernate.OSCacheProvider</code>
* To configure a different configuration file use the following parameter in the Hibernate's configuration:
* <code>com.opensymphony.oscache.configurationResourceName=[path to oscache-hibernate.properties]</code>
*
* @version $Revision:$
*/
public class OSCacheProvider implements CacheProvider {
private static final Log LOG = LogFactory.getLog(OSCacheProvider.class);
/** In the Hibernate system property you can specify the location of the oscache configuration file name. */
public static final String OSCACHE_CONFIGURATION_RESOURCE_NAME = "com.opensymphony.oscache.configurationResourceName";
/** The <tt>OSCache</tt> refresh period property suffix. */
public static final String OSCACHE_REFRESH_PERIOD = "refresh.period";
/** The <tt>OSCache</tt> CRON expression property suffix. */
public static final String OSCACHE_CRON = "cron";
private static GeneralCacheAdministrator cache;
/**
* Builds a new {@link Cache} instance, and gets it's properties from the
* GeneralCacheAdministrator {@link GeneralCacheAdministrator}
* which reads the properties file (<code>oscache.properties</code>) in the start method:
* @see com.opensymphony.oscache.hibernate.OSCacheProvider#start(java.util.Properties)
*
* @param region the region of the cache
* @param properties not used
* @return the hibernate 2nd level cache
* @throws CacheException
*
* @see org.hibernate.cache.CacheProvider#buildCache(java.lang.String, java.util.Properties)
*/
public Cache buildCache(String region, Properties properties) throws CacheException {
if (cache != null) {
LOG.debug("building cache in OSCacheProvider...");
String refreshPeriodString = cache.getProperty( StringHelper.qualify(region, OSCACHE_REFRESH_PERIOD) );
int refreshPeriod = refreshPeriodString==null ? CacheEntry.INDEFINITE_EXPIRY : Integer.parseInt( refreshPeriodString.trim() );
String cron = cache.getProperty( StringHelper.qualify(region, OSCACHE_CRON) );
return new OSCache(cache, refreshPeriod, cron, region);
}
throw new CacheException("OSCache was stopped or wasn't configured via method start.");
}
/**
* @see org.hibernate.cache.CacheProvider#nextTimestamp()
*/
public long nextTimestamp() {
return Timestamper.next();
}
/**
* This method isn't documented in Hibernate:
* @see org.hibernate.cache.CacheProvider#isMinimalPutsEnabledByDefault()
*/
public boolean isMinimalPutsEnabledByDefault() {
return false;
}
/**
* @see org.hibernate.cache.CacheProvider#stop()
*/
public void stop() {
if (cache != null) {
LOG.debug("Stopping OSCacheProvider...");
cache.destroy();
cache = null;
LOG.debug("OSCacheProvider stopped.");
}
}
/**
* @see org.hibernate.cache.CacheProvider#start(java.util.Properties)
*/
public void start(Properties hibernateSystemProperties) throws CacheException {
if (cache == null) {
// construct the cache
LOG.debug("Starting OSCacheProvider...");
String configResourceName = null;
if (hibernateSystemProperties != null) {
configResourceName = (String) hibernateSystemProperties.get(OSCACHE_CONFIGURATION_RESOURCE_NAME);
}
if (StringUtil.isEmpty(configResourceName)) {
cache = new GeneralCacheAdministrator();
} else {
Properties propertiesOSCache = Config.loadProperties(configResourceName, this.getClass().getName());
cache = new GeneralCacheAdministrator(propertiesOSCache);
}
LOG.debug("OSCacheProvider started.");
} else {
LOG.warn("Tried to restart OSCacheProvider, which is already running.");
}
}
}

View file

@ -0,0 +1,31 @@
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">
<html>
<head>
<!--
@(#)package.html
Copyright (c) 2002-2007 by OpenSymphony
All rights reserved.
-->
</head>
<body bgcolor="white">
Provides Hibernate 3.2 classes for OSCache.
<h2>Package Specification</h2>
<h2>Related Documentation</h2>
For overviews, tutorials, examples, guides, and tool documentation, please see:
<ul>
<li><a href="http://www.opensymphony.com/oscache">The OSCache Homepage</a>
</ul>
<!-- Put @see and @since tags down here. -->
</body>
</html>

View file

@ -0,0 +1,177 @@
/*
* Copyright (c) 2002-2003 by OpenSymphony
* All rights reserved.
*/
package com.opensymphony.oscache.plugins.clustersupport;
import com.opensymphony.oscache.base.*;
import com.opensymphony.oscache.base.events.*;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import java.util.Date;
/**
* Implementation of a CacheEntryEventListener. It broadcasts the flush events
* across a cluster to other listening caches. Note that this listener cannot
* be used in conjection with session caches.
*
* @version $Revision$
* @author <a href="&#109;a&#105;&#108;&#116;&#111;:chris&#64;swebtec.&#99;&#111;&#109;">Chris Miller</a>
*/
public abstract class AbstractBroadcastingListener implements CacheEntryEventListener, LifecycleAware {
private final static Log log = LogFactory.getLog(AbstractBroadcastingListener.class);
/**
* The name to use for the origin of cluster events. Using this ensures
* events are not fired recursively back over the cluster.
*/
protected static final String CLUSTER_ORIGIN = "CLUSTER";
protected Cache cache = null;
public AbstractBroadcastingListener() {
if (log.isInfoEnabled()) {
log.info("AbstractBroadcastingListener registered");
}
}
/**
* Event fired when an entry is flushed from the cache. This broadcasts
* the flush message to any listening nodes on the network.
*/
public void cacheEntryFlushed(CacheEntryEvent event) {
if (!Cache.NESTED_EVENT.equals(event.getOrigin()) && !CLUSTER_ORIGIN.equals(event.getOrigin())) {
if (log.isDebugEnabled()) {
log.debug("cacheEntryFlushed called (" + event + ")");
}
sendNotification(new ClusterNotification(ClusterNotification.FLUSH_KEY, event.getKey()));
}
}
/**
* Event fired when an entry is removed from the cache. This broadcasts
* the remove method to any listening nodes on the network, as long as
* this event wasn't from a broadcast in the first place.
*/
public void cacheGroupFlushed(CacheGroupEvent event) {
if (!Cache.NESTED_EVENT.equals(event.getOrigin()) && !CLUSTER_ORIGIN.equals(event.getOrigin())) {
if (log.isDebugEnabled()) {
log.debug("cacheGroupFushed called (" + event + ")");
}
sendNotification(new ClusterNotification(ClusterNotification.FLUSH_GROUP, event.getGroup()));
}
}
public void cachePatternFlushed(CachePatternEvent event) {
if (!Cache.NESTED_EVENT.equals(event.getOrigin()) && !CLUSTER_ORIGIN.equals(event.getOrigin())) {
if (log.isDebugEnabled()) {
log.debug("cachePatternFushed called (" + event + ")");
}
sendNotification(new ClusterNotification(ClusterNotification.FLUSH_PATTERN, event.getPattern()));
}
}
public void cacheFlushed(CachewideEvent event) {
if (!Cache.NESTED_EVENT.equals(event.getOrigin()) && !CLUSTER_ORIGIN.equals(event.getOrigin())) {
if (log.isDebugEnabled()) {
log.debug("cacheFushed called (" + event + ")");
}
sendNotification(new ClusterNotification(ClusterNotification.FLUSH_CACHE, event.getDate()));
}
}
// --------------------------------------------------------
// The remaining events are of no interest to this listener
// --------------------------------------------------------
public void cacheEntryAdded(CacheEntryEvent event) {
}
public void cacheEntryRemoved(CacheEntryEvent event) {
}
public void cacheEntryUpdated(CacheEntryEvent event) {
}
public void cacheGroupAdded(CacheGroupEvent event) {
}
public void cacheGroupEntryAdded(CacheGroupEvent event) {
}
public void cacheGroupEntryRemoved(CacheGroupEvent event) {
}
public void cacheGroupRemoved(CacheGroupEvent event) {
}
public void cacheGroupUpdated(CacheGroupEvent event) {
}
/**
* Called by the cache administrator class when a cache is instantiated.
*
* @param cache the cache instance that this listener is attached to.
* @param config The cache's configuration details. This allows the event handler
* to initialize itself based on the cache settings, and also to receive <em>additional</em>
* settings that were part of the cache configuration but that the cache
* itself does not care about. If you are using <code>cache.properties</code>
* for your configuration, simply add any additional properties that your event
* handler requires and they will be passed through in this parameter.
*
* @throws InitializationException thrown when there was a problem initializing the
* listener. The cache administrator will log this error and disable the listener.
*/
public void initialize(Cache cache, Config config) throws InitializationException {
this.cache = cache;
}
/**
* Handles incoming notification messages. This method should be called by the
* underlying broadcasting implementation when a message is received from another
* node in the cluster.
*
* @param message The incoming cluster notification message object.
*/
public void handleClusterNotification(ClusterNotification message) {
if (cache == null) {
log.warn("A cluster notification (" + message + ") was received, but no cache is registered on this machine. Notification ignored.");
return;
}
if (log.isInfoEnabled()) {
log.info("Cluster notification (" + message + ") was received.");
}
switch (message.getType()) {
case ClusterNotification.FLUSH_KEY:
cache.flushEntry((String) message.getData(), CLUSTER_ORIGIN);
break;
case ClusterNotification.FLUSH_GROUP:
cache.flushGroup((String) message.getData(), CLUSTER_ORIGIN);
break;
case ClusterNotification.FLUSH_PATTERN:
cache.flushPattern((String) message.getData(), CLUSTER_ORIGIN);
break;
case ClusterNotification.FLUSH_CACHE:
cache.flushAll((Date) message.getData(), CLUSTER_ORIGIN);
break;
default:
log.error("The cluster notification (" + message + ") is of an unknown type. Notification ignored.");
}
}
/**
* Called when a cluster notification message is to be broadcast. Implementing
* classes should use their underlying transport to broadcast the message across
* the cluster.
*
* @param message The notification message to broadcast.
*/
abstract protected void sendNotification(ClusterNotification message);
}

View file

@ -0,0 +1,86 @@
/*
* Copyright (c) 2002-2003 by OpenSymphony
* All rights reserved.
*/
package com.opensymphony.oscache.plugins.clustersupport;
import java.io.Serializable;
/**
* A notification message that holds information about a cache event. This
* class is <code>Serializable</code> to allow it to be sent across the
* network to other machines running in a cluster.
*
* @author <a href="&#109;a&#105;&#108;&#116;&#111;:chris&#64;swebtec.&#99;&#111;&#109;">Chris Miller</a>
* @author $Author$
* @version $Revision$
*/
public class ClusterNotification implements Serializable {
/**
* Specifies a notification message that indicates a particular cache key
* should be flushed.
*/
public static final int FLUSH_KEY = 1;
/**
* Specifies a notification message that indicates an entire cache group
* should be flushed.
*/
public static final int FLUSH_GROUP = 2;
/**
* Specifies a notification message that indicates all entries in the cache
* that match the specified pattern should be flushed.
*/
public static final int FLUSH_PATTERN = 3;
/**
* Specifies a notification message indicating that an entire cache should
* be flushed.
*/
public static final int FLUSH_CACHE = 4;
/**
* Any additional data that may be required
*/
protected Serializable data;
/**
* The type of notification message.
*/
protected int type;
/**
* Creates a new notification message object to broadcast to other
* listening nodes in the cluster.
*
* @param type The type of notification message. Valid types are
* {@link #FLUSH_KEY} and {@link #FLUSH_GROUP}.
* @param data Specifies the object key or group name to flush.
*/
public ClusterNotification(int type, Serializable data) {
this.type = type;
this.data = data;
}
/**
* Holds any additional data that was required
*/
public Serializable getData() {
return data;
}
/**
* The type of notification message.
*/
public int getType() {
return type;
}
public String toString() {
StringBuffer buf = new StringBuffer();
buf.append("type=").append(type).append(", data=").append(data);
return buf.toString();
}
}

View file

@ -0,0 +1,180 @@
/*
* Copyright (c) 2002-2003 by OpenSymphony
* All rights reserved.
*/
package com.opensymphony.oscache.plugins.clustersupport;
import com.opensymphony.oscache.base.Cache;
import com.opensymphony.oscache.base.Config;
import com.opensymphony.oscache.base.FinalizationException;
import com.opensymphony.oscache.base.InitializationException;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import javax.jms.*;
import javax.naming.InitialContext;
/**
* A JMS 1.0.x based clustering implementation. This implementation is independent of
* the JMS provider and uses non-persistent messages on a publish subscribe protocol.
*
* @author <a href="&#109;a&#105;&#108;&#116;&#111;:chris&#64;swebtec.&#99;&#111;&#109;">Chris Miller</a>
*/
public class JMS10BroadcastingListener extends AbstractBroadcastingListener {
private final static Log log = LogFactory.getLog(JMS10BroadcastingListener.class);
/**
* The name of this cluster. Used to identify the sender of a message.
*/
private String clusterNode;
/**
*The JMS connection used
*/
private TopicConnection connection;
/**
* Th object used to publish new messages
*/
private TopicPublisher publisher;
/**
* The current JMS session
*/
private TopicSession publisherSession;
/**
* <p>Called by the cache administrator class when a cache is instantiated.</p>
* <p>The JMS broadcasting implementation requires the following configuration
* properties to be specified in <code>oscache.properties</code>:
* <ul>
* <li><b>cache.cluster.jms.topic.factory</b> - The JMS connection factory to use</li>
* <li><b>cache.cluster.jms.topic.name</b> - The JMS topic name</li>
* <li><b>cache.cluster.jms.node.name</b> - The name of this node in the cluster. This should
* be unique for each node.</li>
* Please refer to the clustering documentation for further details on configuring
* the JMS clustered caching.</p>
*
* @param cache the cache instance that this listener is attached to.
*
* @throws InitializationException thrown when there was a
* problem initializing the listener. The cache administrator will log this error and
* disable the listener.
*/
public void initialize(Cache cache, Config config) throws InitializationException {
super.initialize(cache, config);
// Get the name of this node
clusterNode = config.getProperty("cache.cluster.jms.node.name");
String topic = config.getProperty("cache.cluster.jms.topic.name");
String topicFactory = config.getProperty("cache.cluster.jms.topic.factory");
if (log.isInfoEnabled()) {
log.info("Starting JMS clustering (node name=" + clusterNode + ", topic=" + topic + ", topic factory=" + topicFactory + ")");
}
try {
// Make sure you have specified the necessary JNDI properties (usually in
// a jndi.properties resource file, or as system properties)
InitialContext jndi = new InitialContext();
// Look up a JMS connection factory
TopicConnectionFactory connectionFactory = (TopicConnectionFactory) jndi.lookup(topicFactory);
// Create a JMS connection
connection = connectionFactory.createTopicConnection();
// Create session objects
publisherSession = connection.createTopicSession(false, Session.AUTO_ACKNOWLEDGE);
TopicSession subSession = connection.createTopicSession(false, Session.AUTO_ACKNOWLEDGE);
// Look up the JMS topic
Topic chatTopic = (Topic) jndi.lookup(topic);
// Create the publisher and subscriber
publisher = publisherSession.createPublisher(chatTopic);
TopicSubscriber subscriber = subSession.createSubscriber(chatTopic);
// Set the message listener
subscriber.setMessageListener(new MessageListener() {
public void onMessage(Message message) {
try {
//check the message type
ObjectMessage objectMessage = null;
if (!(message instanceof ObjectMessage)) {
log.error("Cannot handle message of type (class=" + message.getClass().getName() + "). Notification ignored.");
return;
}
objectMessage = (ObjectMessage) message;
//check the message content
if (!(objectMessage.getObject() instanceof ClusterNotification)) {
log.error("An unknown cluster notification message received (class=" + objectMessage.getObject().getClass().getName() + "). Notification ignored.");
return;
}
if (log.isDebugEnabled()) {
log.debug(objectMessage.getObject());
}
// This prevents the notification sent by this node from being handled by itself
if (!objectMessage.getStringProperty("nodeName").equals(clusterNode)) {
//now handle the message
ClusterNotification notification = (ClusterNotification) objectMessage.getObject();
handleClusterNotification(notification);
}
} catch (JMSException jmsEx) {
log.error("Cannot handle cluster Notification", jmsEx);
}
}
});
// Start the JMS connection; allows messages to be delivered
connection.start();
} catch (Exception e) {
throw new InitializationException("Initialization of the JMS10BroadcastingListener failed: " + e);
}
}
/**
* Called by the cache administrator class when a cache is destroyed.
*
* @throws FinalizationException thrown when there was a problem finalizing the
* listener. The cache administrator will catch and log this error.
*/
public void finialize() throws FinalizationException {
try {
if (log.isInfoEnabled()) {
log.info("Shutting down JMS clustering...");
}
connection.close();
if (log.isInfoEnabled()) {
log.info("JMS clustering shutdown complete.");
}
} catch (JMSException e) {
log.warn("A problem was encountered when closing the JMS connection", e);
}
}
protected void sendNotification(ClusterNotification message) {
try {
ObjectMessage objectMessage = publisherSession.createObjectMessage();
objectMessage.setObject(message);
//sign the message, with the name of this node
objectMessage.setStringProperty("nodeName", clusterNode);
publisher.publish(objectMessage);
} catch (JMSException e) {
log.error("Cannot send notification " + message, e);
}
}
}

View file

@ -0,0 +1,191 @@
/*
* Copyright (c) 2002-2003 by OpenSymphony
* All rights reserved.
*/
package com.opensymphony.oscache.plugins.clustersupport;
import com.opensymphony.oscache.base.Cache;
import com.opensymphony.oscache.base.Config;
import com.opensymphony.oscache.base.FinalizationException;
import com.opensymphony.oscache.base.InitializationException;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import javax.jms.*;
import javax.naming.InitialContext;
import javax.naming.NamingException;
/**
* A JMS based clustering implementation. This implementation is independent of the
* JMS provider and uses non-persistent messages on a publish subscribe protocol.
*
* @author <a href="mailto:motoras@linuxmail.org">Romulus Pasca</a>
*/
public class JMSBroadcastingListener extends AbstractBroadcastingListener {
private final static Log log = LogFactory.getLog(JMSBroadcastingListener.class);
/**
*The JMS connection used
*/
private Connection connection;
/**
* Th object used to publish new messages
*/
private MessageProducer messagePublisher;
/**
* The current JMS session
*/
private Session publisherSession;
/**
* The name of this cluster. Used to identify the sender of a message.
*/
private String clusterNode;
/**
* <p>Called by the cache administrator class when a cache is instantiated.</p>
* <p>The JMS broadcasting implementation requires the following configuration
* properties to be specified in <code>oscache.properties</code>:
* <ul>
* <li><b>cache.cluster.jms.topic.factory</b> - The JMS connection factory to use</li>
* <li><b>cache.cluster.jms.topic.name</b> - The JMS topic name</li>
* <li><b>cache.cluster.jms.node.name</b> - The name of this node in the cluster. This
* should be unique for each node.</li>
* Please refer to the clustering documentation for further details on configuring
* the JMS clustered caching.</p>
*
* @param cache the cache instance that this listener is attached to.
*
* @throws com.opensymphony.oscache.base.InitializationException thrown when there was a
* problem initializing the listener. The cache administrator will log this error and
* disable the listener.
*/
public void initialize(Cache cache, Config config) throws InitializationException {
super.initialize(cache, config);
// Get the name of this node
clusterNode = config.getProperty("cache.cluster.jms.node.name");
String topic = config.getProperty("cache.cluster.jms.topic.name");
String topicFactory = config.getProperty("cache.cluster.jms.topic.factory");
if (log.isInfoEnabled()) {
log.info("Starting JMS clustering (node name=" + clusterNode + ", topic=" + topic + ", topic factory=" + topicFactory + ")");
}
try {
// Make sure you have specified the necessary JNDI properties (usually in
// a jndi.properties resource file, or as system properties)
InitialContext jndi = getInitialContext();
// Look up a JMS connection factory
ConnectionFactory connectionFactory = (ConnectionFactory) jndi.lookup(topicFactory);
// Create a JMS connection
connection = connectionFactory.createConnection();
// Create session objects
publisherSession = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
Session subSession = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
// Look up the JMS topic
Topic chatTopic = (Topic) jndi.lookup(topic);
// Create the publisher and subscriber
messagePublisher = publisherSession.createProducer(chatTopic);
MessageConsumer messageConsumer = subSession.createConsumer(chatTopic);
// Set the message listener
messageConsumer.setMessageListener(new MessageListener() {
public void onMessage(Message message) {
try {
//check the message type
ObjectMessage objectMessage = null;
if (!(message instanceof ObjectMessage)) {
log.error("Cannot handle message of type (class=" + message.getClass().getName() + "). Notification ignored.");
return;
}
objectMessage = (ObjectMessage) message;
//check the message content
if (!(objectMessage.getObject() instanceof ClusterNotification)) {
log.error("An unknown cluster notification message received (class=" + objectMessage.getObject().getClass().getName() + "). Notification ignored.");
return;
}
if (log.isDebugEnabled()) {
log.debug(objectMessage.getObject());
}
// This prevents the notification sent by this node from being handled by itself
if (!objectMessage.getStringProperty("nodeName").equals(clusterNode)) {
//now handle the message
ClusterNotification notification = (ClusterNotification) objectMessage.getObject();
handleClusterNotification(notification);
}
} catch (JMSException jmsEx) {
log.error("Cannot handle cluster Notification", jmsEx);
}
}
});
// Start the JMS connection; allows messages to be delivered
connection.start();
} catch (Exception e) {
throw new InitializationException("Initialization of the JMSBroadcastingListener failed: " + e);
}
}
/**
* Called by the cache administrator class when a cache is destroyed.
*
* @throws com.opensymphony.oscache.base.FinalizationException thrown when there was a problem finalizing the
* listener. The cache administrator will catch and log this error.
*/
public void finialize() throws FinalizationException {
try {
if (log.isInfoEnabled()) {
log.info("Shutting down JMS clustering...");
}
connection.close();
if (log.isInfoEnabled()) {
log.info("JMS clustering shutdown complete.");
}
} catch (JMSException e) {
log.warn("A problem was encountered when closing the JMS connection", e);
}
}
protected void sendNotification(ClusterNotification message) {
try {
ObjectMessage objectMessage = publisherSession.createObjectMessage();
objectMessage.setObject(message);
//sign the message, with the name of this node
objectMessage.setStringProperty("nodeName", clusterNode);
messagePublisher.send(objectMessage);
} catch (JMSException e) {
log.error("Cannot send notification " + message, e);
}
}
/**
* @return creates a context for performing naming operations.
* @throws NamingException if a naming exception is encountered
*/
protected InitialContext getInitialContext() throws NamingException {
return new InitialContext();
}
}

View file

@ -0,0 +1,204 @@
/*
* Copyright (c) 2002-2003 by OpenSymphony
* All rights reserved.
*/
package com.opensymphony.oscache.plugins.clustersupport;
import com.opensymphony.oscache.base.Cache;
import com.opensymphony.oscache.base.Config;
import com.opensymphony.oscache.base.FinalizationException;
import com.opensymphony.oscache.base.InitializationException;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.jgroups.Address;
import org.jgroups.Channel;
import org.jgroups.blocks.NotificationBus;
import java.io.Serializable;
/**
* <p>A concrete implementation of the {@link AbstractBroadcastingListener} based on
* the JavaGroups library. This Class uses JavaGroups to broadcast cache flush
* messages across a cluster.</p>
*
* <p>One of the following properties should be configured in <code>oscache.properties</code> for
* this listener:
* <ul>
* <li><b>cache.cluster.multicast.ip</b> - The multicast IP that JavaGroups should use for broadcasting</li>
* <li><b>cache.cluster.properties</b> - The JavaGroups channel properties to use. Allows for precise
* control over the behaviour of JavaGroups</li>
* </ul>
* Please refer to the clustering documentation for further details on the configuration of this listener.</p>
*
* @author <a href="&#109;a&#105;&#108;&#116;&#111;:chris&#64;swebtec.&#99;&#111;&#109;">Chris Miller</a>
*/
public class JavaGroupsBroadcastingListener extends AbstractBroadcastingListener implements NotificationBus.Consumer {
private final static Log log = LogFactory.getLog(JavaGroupsBroadcastingListener.class);
private static final String BUS_NAME = "OSCacheBus";
private static final String CHANNEL_PROPERTIES = "cache.cluster.properties";
private static final String MULTICAST_IP_PROPERTY = "cache.cluster.multicast.ip";
/**
* The first half of the default channel properties. They default channel properties are:
* <pre>
* UDP(mcast_addr=*.*.*.*;mcast_port=45566;ip_ttl=32;\
* mcast_send_buf_size=150000;mcast_recv_buf_size=80000):\
* PING(timeout=2000;num_initial_members=3):\
* MERGE2(min_interval=5000;max_interval=10000):\
* FD_SOCK:VERIFY_SUSPECT(timeout=1500):\
* pbcast.NAKACK(gc_lag=50;retransmit_timeout=300,600,1200,2400,4800;max_xmit_size=8192):\
* UNICAST(timeout=300,600,1200,2400):\
* pbcast.STABLE(desired_avg_gossip=20000):\
* FRAG(frag_size=8096;down_thread=false;up_thread=false):\
* pbcast.GMS(join_timeout=5000;join_retry_timeout=2000;shun=false;print_local_addr=true)
* </pre>
*
* Where <code>*.*.*.*</code> is the specified multicast IP, which defaults to <code>231.12.21.132</code>.
*/
private static final String DEFAULT_CHANNEL_PROPERTIES_PRE = "UDP(mcast_addr=";
/**
* The second half of the default channel properties. They default channel properties are:
* <pre>
* UDP(mcast_addr=*.*.*.*;mcast_port=45566;ip_ttl=32;\
* mcast_send_buf_size=150000;mcast_recv_buf_size=80000):\
* PING(timeout=2000;num_initial_members=3):\
* MERGE2(min_interval=5000;max_interval=10000):\
* FD_SOCK:VERIFY_SUSPECT(timeout=1500):\
* pbcast.NAKACK(gc_lag=50;retransmit_timeout=300,600,1200,2400,4800;max_xmit_size=8192):\
* UNICAST(timeout=300,600,1200,2400):\
* pbcast.STABLE(desired_avg_gossip=20000):\
* FRAG(frag_size=8096;down_thread=false;up_thread=false):\
* pbcast.GMS(join_timeout=5000;join_retry_timeout=2000;shun=false;print_local_addr=true)
* </pre>
*
* Where <code>*.*.*.*</code> is the specified multicast IP, which defaults to <code>231.12.21.132</code>.
*/
private static final String DEFAULT_CHANNEL_PROPERTIES_POST = ";mcast_port=45566;ip_ttl=32;mcast_send_buf_size=150000;mcast_recv_buf_size=80000):" + "PING(timeout=2000;num_initial_members=3):MERGE2(min_interval=5000;max_interval=10000):FD_SOCK:VERIFY_SUSPECT(timeout=1500):" + "pbcast.NAKACK(gc_lag=50;retransmit_timeout=300,600,1200,2400,4800;max_xmit_size=8192):UNICAST(timeout=300,600,1200,2400):pbcast.STABLE(desired_avg_gossip=20000):" + "FRAG(frag_size=8096;down_thread=false;up_thread=false):pbcast.GMS(join_timeout=5000;join_retry_timeout=2000;shun=false;print_local_addr=true)";
private static final String DEFAULT_MULTICAST_IP = "231.12.21.132";
private NotificationBus bus;
/**
* Initializes the broadcasting listener by starting up a JavaGroups notification
* bus instance to handle incoming and outgoing messages.
*
* @param config An OSCache configuration object.
* @throws com.opensymphony.oscache.base.InitializationException If this listener has
* already been initialized.
*/
public synchronized void initialize(Cache cache, Config config) throws InitializationException {
super.initialize(cache, config);
String properties = config.getProperty(CHANNEL_PROPERTIES);
String multicastIP = config.getProperty(MULTICAST_IP_PROPERTY);
if ((properties == null) && (multicastIP == null)) {
multicastIP = DEFAULT_MULTICAST_IP;
}
if (properties == null) {
properties = DEFAULT_CHANNEL_PROPERTIES_PRE + multicastIP.trim() + DEFAULT_CHANNEL_PROPERTIES_POST;
} else {
properties = properties.trim();
}
if (log.isInfoEnabled()) {
log.info("Starting a new JavaGroups broadcasting listener with properties=" + properties);
}
try {
bus = new NotificationBus(BUS_NAME, properties);
bus.start();
bus.getChannel().setOpt(Channel.LOCAL, new Boolean(false));
bus.setConsumer(this);
log.info("JavaGroups clustering support started successfully");
} catch (Exception e) {
throw new InitializationException("Initialization failed: " + e);
}
}
/**
* Shuts down the JavaGroups being managed by this listener. This
* occurs once the cache is shut down and this listener is no longer
* in use.
*
* @throws com.opensymphony.oscache.base.FinalizationException
*/
public synchronized void finialize() throws FinalizationException {
if (log.isInfoEnabled()) {
log.info("JavaGroups shutting down...");
}
// It's possible that the notification bus is null (CACHE-154)
if (bus != null) {
bus.stop();
bus = null;
} else {
log.warn("Notification bus wasn't initialized or finialize was invoked before!");
}
if (log.isInfoEnabled()) {
log.info("JavaGroups shutdown complete.");
}
}
/**
* Uses JavaGroups to broadcast the supplied notification message across the cluster.
*
* @param message The cluster nofication message to broadcast.
*/
protected void sendNotification(ClusterNotification message) {
bus.sendNotification(message);
}
/**
* Handles incoming notification messages from JavaGroups. This method should
* never be called directly.
*
* @param serializable The incoming message object. This must be a {@link ClusterNotification}.
*/
public void handleNotification(Serializable serializable) {
if (!(serializable instanceof ClusterNotification)) {
log.error("An unknown cluster notification message received (class=" + serializable.getClass().getName() + "). Notification ignored.");
return;
}
handleClusterNotification((ClusterNotification) serializable);
}
/**
* We are not using the caching, so we just return something that identifies
* us. This method should never be called directly.
*/
public Serializable getCache() {
return "JavaGroupsBroadcastingListener: " + bus.getLocalAddress();
}
/**
* A callback that is fired when a new member joins the cluster. This
* method should never be called directly.
*
* @param address The address of the member who just joined.
*/
public void memberJoined(Address address) {
if (log.isInfoEnabled()) {
log.info("A new member at address '" + address + "' has joined the cluster");
}
}
/**
* A callback that is fired when an existing member leaves the cluster.
* This method should never be called directly.
*
* @param address The address of the member who left.
*/
public void memberLeft(Address address) {
if (log.isInfoEnabled()) {
log.info("Member at address '" + address + "' left the cluster");
}
}
}

View file

@ -0,0 +1,34 @@
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">
<html>
<head>
<!--
@(#)package.html
Copyright (c) 2002-2003 by OpenSymphony
All rights reserved.
-->
</head>
<body bgcolor="white">
Provides support for broadcasting flush events so that OSCache can function across a
cluster.
<h2>Package Specification</h2>
<h2>Related Documentation</h2>
For overviews, tutorials, examples, guides, and tool documentation, please see:
<ul>
<li><a href="http://www.opensymphony.com/oscache">The OSCache Homepage</a>
<li><a href="http://www.opensymphony.com/oscache/clustering.jsp">Clustering OSCache</a>
</ul>
<!-- Put @see and @since tags down here. -->
@since 2.0
</body>
</html>

View file

@ -0,0 +1,529 @@
/*
* Copyright (c) 2002-2003 by OpenSymphony
* All rights reserved.
*/
package com.opensymphony.oscache.plugins.diskpersistence;
import com.opensymphony.oscache.base.Config;
import com.opensymphony.oscache.base.persistence.CachePersistenceException;
import com.opensymphony.oscache.base.persistence.PersistenceListener;
import com.opensymphony.oscache.web.ServletCacheAdministrator;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import java.io.*;
import java.util.Set;
import javax.servlet.jsp.PageContext;
/**
* Persist the cache data to disk.
*
* The code in this class is totally not thread safe it is the resonsibility
* of the cache using this persistence listener to handle the concurrency.
*
* @author <a href="mailto:fbeauregard@pyxis-tech.com">Francois Beauregard</a>
* @author <a href="mailto:abergevin@pyxis-tech.com">Alain Bergevin</a>
* @author <a href="&#109;a&#105;&#108;&#116;&#111;:chris&#64;swebtec.&#99;&#111;&#109;">Chris Miller</a>
* @author <a href="mailto:amarch@soe.sony.com">Andres March</a>
*/
public abstract class AbstractDiskPersistenceListener implements PersistenceListener, Serializable {
private static final long serialVersionUID = 6679402628276452293L;
private static final Log LOG = LogFactory.getLog(AbstractDiskPersistenceListener.class);
/** lock for workaround the Sun Bug ID 4742723 */
private static final Object MKDIRS_LOCK = new Object();
public final static String CACHE_PATH_KEY = "cache.path";
/**
* File extension for disk cache file
*/
protected final static String CACHE_EXTENSION = "cache";
/**
* The directory that cache groups are stored under
*/
protected final static String GROUP_DIRECTORY = "__groups__";
/**
* Sub path name for application cache
*/
protected final static String APPLICATION_CACHE_SUBPATH = "application";
/**
* Sub path name for session cache
*/
protected final static String SESSION_CACHE_SUBPATH = "session";
/**
* Property to get the temporary working directory of the servlet container.
*/
protected static final String CONTEXT_TMPDIR = "javax.servlet.context.tempdir";
private static transient final Log log = LogFactory.getLog(AbstractDiskPersistenceListener.class);
/**
* Base path where the disk cache reside.
*/
private File cachePath = null;
private File contextTmpDir;
/**
* Root path for disk cache
*/
private String root = null;
/**
* Get the physical cache path on disk.
*
* @return A file representing the physical cache location.
*/
public File getCachePath() {
return cachePath;
}
/**
* Get the root directory for persisting the cache on disk.
* This path includes scope and sessionId, if any.
*
* @return A String representing the root directory.
*/
public String getRoot() {
return root;
}
/**
* Get the servlet context tmp directory.
*
* @return A file representing the servlet context tmp directory.
*/
public File getContextTmpDir() {
return contextTmpDir;
}
/**
* Verify if a group exists in the cache
*
* @param group The group name to check
* @return True if it exists
* @throws CachePersistenceException
*/
public boolean isGroupStored(String group) throws CachePersistenceException {
try {
File file = getCacheGroupFile(group);
return file.exists();
} catch (Exception e) {
throw new CachePersistenceException("Unable verify group '" + group + "' exists in the cache: " + e);
}
}
/**
* Verify if an object is currently stored in the cache
*
* @param key The object key
* @return True if it exists
* @throws CachePersistenceException
*/
public boolean isStored(String key) throws CachePersistenceException {
try {
File file = getCacheFile(key);
return file.exists();
} catch (Exception e) {
throw new CachePersistenceException("Unable verify id '" + key + "' is stored in the cache: " + e);
}
}
/**
* Clears the whole cache directory, starting from the root
*
* @throws CachePersistenceException
*/
public void clear() throws CachePersistenceException {
clear(root);
}
/**
* Initialises this <tt>DiskPersistenceListener</tt> using the supplied
* configuration.
*
* @param config The OSCache configuration
*/
public PersistenceListener configure(Config config) {
String sessionId = null;
int scope = 0;
initFileCaching(config.getProperty(CACHE_PATH_KEY));
if (config.getProperty(ServletCacheAdministrator.HASH_KEY_SESSION_ID) != null) {
sessionId = config.getProperty(ServletCacheAdministrator.HASH_KEY_SESSION_ID);
}
if (config.getProperty(ServletCacheAdministrator.HASH_KEY_SCOPE) != null) {
scope = Integer.parseInt(config.getProperty(ServletCacheAdministrator.HASH_KEY_SCOPE));
}
StringBuffer root = new StringBuffer(getCachePath().getPath());
root.append("/");
root.append(getPathPart(scope));
if ((sessionId != null) && (sessionId.length() > 0)) {
root.append("/");
root.append(sessionId);
}
this.root = root.toString();
this.contextTmpDir = (File) config.get(ServletCacheAdministrator.HASH_KEY_CONTEXT_TMPDIR);
return this;
}
/**
* Delete a single cache entry.
*
* @param key The object key to delete
* @throws CachePersistenceException
*/
public void remove(String key) throws CachePersistenceException {
File file = getCacheFile(key);
remove(file);
}
/**
* Deletes an entire group from the cache.
*
* @param groupName The name of the group to delete
* @throws CachePersistenceException
*/
public void removeGroup(String groupName) throws CachePersistenceException {
File file = getCacheGroupFile(groupName);
remove(file);
}
/**
* Retrieve an object from the disk
*
* @param key The object key
* @return The retrieved object
* @throws CachePersistenceException
*/
public Object retrieve(String key) throws CachePersistenceException {
return retrieve(getCacheFile(key));
}
/**
* Retrieves a group from the cache, or <code>null</code> if the group
* file could not be found.
*
* @param groupName The name of the group to retrieve.
* @return A <code>Set</code> containing keys of all of the cache
* entries that belong to this group.
* @throws CachePersistenceException
*/
public Set retrieveGroup(String groupName) throws CachePersistenceException {
File groupFile = getCacheGroupFile(groupName);
try {
return (Set) retrieve(groupFile);
} catch (ClassCastException e) {
throw new CachePersistenceException("Group file " + groupFile + " was not persisted as a Set: " + e);
}
}
/**
* Stores an object in cache
*
* @param key The object's key
* @param obj The object to store
* @throws CachePersistenceException
*/
public void store(String key, Object obj) throws CachePersistenceException {
File file = getCacheFile(key);
store(file, obj);
}
/**
* Stores a group in the persistent cache. This will overwrite any existing
* group with the same name
*/
public void storeGroup(String groupName, Set group) throws CachePersistenceException {
File groupFile = getCacheGroupFile(groupName);
store(groupFile, group);
}
/**
* Allows to translate to the temp dir of the servlet container if cachePathStr
* is javax.servlet.context.tempdir.
*
* @param cachePathStr Cache path read from the properties file.
* @return Adjusted cache path
*/
protected String adjustFileCachePath(String cachePathStr) {
if (cachePathStr.compareToIgnoreCase(CONTEXT_TMPDIR) == 0) {
cachePathStr = contextTmpDir.getAbsolutePath();
}
return cachePathStr;
}
/**
* Set caching to file on or off.
* If the <code>cache.path</code> property exists, we assume file caching is turned on.
* By the same token, to turn off file caching just remove this property.
*/
protected void initFileCaching(String cachePathStr) {
if (cachePathStr != null) {
cachePath = new File(cachePathStr);
try {
if (!cachePath.exists()) {
if (log.isInfoEnabled()) {
log.info("cache.path '" + cachePathStr + "' does not exist, creating");
}
// http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=4742723
synchronized (MKDIRS_LOCK) {
cachePath.mkdirs();
}
}
if (!cachePath.isDirectory()) {
log.error("cache.path '" + cachePathStr + "' is not a directory");
cachePath = null;
} else if (!cachePath.canWrite()) {
log.error("cache.path '" + cachePathStr + "' is not a writable location");
cachePath = null;
}
} catch (Exception e) {
log.error("cache.path '" + cachePathStr + "' could not be used", e);
cachePath = null;
}
} else {
// Use default value
}
}
// try 30s to delete the file
private static final long DELETE_THREAD_SLEEP = 500;
private static final int DELETE_COUNT = 60;
protected void remove(File file) throws CachePersistenceException {
int count = DELETE_COUNT;
try {
// Loop until we are able to delete (No current read).
// The cache must ensure that there are never two concurrent threads
// doing write (store and delete) operations on the same item.
// Delete only should be enough but file.exists prevents infinite loop
while (file.exists() && !file.delete() && count != 0) {
count--;
try {
Thread.sleep(DELETE_THREAD_SLEEP);
} catch (InterruptedException ignore) {
}
}
} catch (Exception e) {
throw new CachePersistenceException("Unable to remove file '" + file + "' from the disk cache.", e);
}
if (file.exists() && count == 0) {
throw new CachePersistenceException("Unable to delete '" + file + "' from the disk cache. "+DELETE_COUNT+" attempts at "+DELETE_THREAD_SLEEP+" milliseconds intervals.");
}
}
/**
* Stores an object using the supplied file object
*
* @param file The file to use for storing the object
* @param obj the object to store
* @throws CachePersistenceException
*/
protected void store(File file, Object obj) throws CachePersistenceException {
// check if file exists before testing if parent exists
if (!file.exists()) {
// check if the directory structure required exists and create it if it doesn't
File filepath = new File(file.getParent());
try {
if (!filepath.exists()) {
// http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=4742723
synchronized (MKDIRS_LOCK) {
filepath.mkdirs();
}
}
} catch (Exception e) {
throw new CachePersistenceException("Unable to create the directory " + filepath, e);
}
}
// Write the object to disk
try {
FileOutputStream fout = new FileOutputStream(file);
try {
ObjectOutputStream oout = new ObjectOutputStream(new BufferedOutputStream(fout));
try {
oout.writeObject(obj);
oout.flush();
} finally {
try {
oout.close();
} catch (Exception e) {
LOG.warn("Problem closing file of disk cache.", e);
}
}
} finally {
try {
fout.close();
} catch (Exception e) {
LOG.warn("Problem closing file of disk cache.", e);
}
}
} catch (Exception e) {
int count = DELETE_COUNT;
while (file.exists() && !file.delete() && count != 0) {
count--;
try {
Thread.sleep(DELETE_THREAD_SLEEP);
} catch (InterruptedException ignore) {
}
}
throw new CachePersistenceException("Unable to write file '" + file + "' in the disk cache.", e);
}
}
/**
* Build fully qualified cache file for the specified cache entry key.
*
* @param key Cache Entry Key.
* @return File reference.
*/
protected File getCacheFile(String key) {
char[] fileChars = getCacheFileName(key);
File file = new File(root, new String(fileChars) + "." + CACHE_EXTENSION);
return file;
}
/**
* Build cache file name for the specified cache entry key.
*
* @param key Cache Entry Key.
* @return char[] file name.
*/
protected abstract char[] getCacheFileName(String key);
/**
* Builds a fully qualified file name that specifies a cache group entry.
*
* @param group The name of the group
* @return A File reference
*/
private File getCacheGroupFile(String group) {
int AVERAGE_PATH_LENGTH = 30;
if ((group == null) || (group.length() == 0)) {
throw new IllegalArgumentException("Invalid group '" + group + "' specified to getCacheGroupFile.");
}
StringBuffer path = new StringBuffer(AVERAGE_PATH_LENGTH);
// Build a fully qualified file name for this group
path.append(GROUP_DIRECTORY).append('/');
path.append(getCacheFileName(group)).append('.').append(CACHE_EXTENSION);
return new File(root, path.toString());
}
/**
* This allows to persist different scopes in different path in the case of
* file caching.
*
* @param scope Cache scope.
* @return The scope subpath
*/
private String getPathPart(int scope) {
if (scope == PageContext.SESSION_SCOPE) {
return SESSION_CACHE_SUBPATH;
} else {
return APPLICATION_CACHE_SUBPATH;
}
}
/**
* Clears a whole directory, starting from the specified
* directory
*
* @param baseDirName The root directory to delete
* @throws CachePersistenceException
*/
private void clear(String baseDirName) throws CachePersistenceException {
File baseDir = new File(baseDirName);
File[] fileList = baseDir.listFiles();
try {
if (fileList != null) {
// Loop through all the files and directory to delete them
for (int count = 0; count < fileList.length; count++) {
if (fileList[count].isFile()) {
fileList[count].delete();
} else {
// Make a recursive call to delete the directory
clear(fileList[count].toString());
fileList[count].delete();
}
}
}
// Delete the root directory
baseDir.delete();
} catch (Exception e) {
throw new CachePersistenceException("Unable to clear the cache directory");
}
}
/**
* Retrives a serialized object from the supplied file, or returns
* <code>null</code> if the file does not exist.
*
* @param file The file to deserialize
* @return The deserialized object
* @throws CachePersistenceException
*/
private Object retrieve(File file) throws CachePersistenceException {
Object readContent = null;
boolean fileExist;
try {
fileExist = file.exists();
} catch (Exception e) {
throw new CachePersistenceException("Unable to verify if file '" + file + "' exists.", e);
}
// Read the file if it exists
if (fileExist) {
ObjectInputStream oin = null;
try {
BufferedInputStream in = new BufferedInputStream(new FileInputStream(file));
oin = new ObjectInputStream(in);
readContent = oin.readObject();
} catch (Exception e) {
// We expect this exception to occur.
// This is when the item will be invalidated (written or deleted)
// during read.
// The cache has the logic to retry reading.
throw new CachePersistenceException("Unable to read file '" + file.getAbsolutePath() + "' from the disk cache.", e);
} finally {
// HHDE: no need to close in. Will be closed by oin
try {
oin.close();
} catch (Exception ex) {
}
}
}
return readContent;
}
}

View file

@ -0,0 +1,53 @@
/*
* Copyright (c) 2002-2003 by OpenSymphony
* All rights reserved.
*/
package com.opensymphony.oscache.plugins.diskpersistence;
/**
* Persist the cache data to disk.
*
* The code in this class is totally not thread safe it is the resonsibility
* of the cache using this persistence listener to handle the concurrency.
*
* @version $Revision$
* @author <a href="mailto:fbeauregard@pyxis-tech.com">Francois Beauregard</a>
* @author <a href="mailto:abergevin@pyxis-tech.com">Alain Bergevin</a>
* @author <a href="&#109;a&#105;&#108;&#116;&#111;:chris&#64;swebtec.&#99;&#111;&#109;">Chris Miller</a>
*/
public class DiskPersistenceListener extends AbstractDiskPersistenceListener {
private static final String CHARS_TO_CONVERT = "./\\ :;\"\'_?";
/**
* Build cache file name for the specified cache entry key.
*
* @param key Cache Entry Key.
* @return char[] file name.
*/
protected char[] getCacheFileName(String key) {
if ((key == null) || (key.length() == 0)) {
throw new IllegalArgumentException("Invalid key '" + key + "' specified to getCacheFile.");
}
char[] chars = key.toCharArray();
StringBuffer sb = new StringBuffer(chars.length + 8);
for (int i = 0; i < chars.length; i++) {
char c = chars[i];
int pos = CHARS_TO_CONVERT.indexOf(c);
if (pos >= 0) {
sb.append('_');
sb.append(i);
} else {
sb.append(c);
}
}
char[] fileChars = new char[sb.length()];
sb.getChars(0, fileChars.length, fileChars, 0);
return fileChars;
}
}

View file

@ -0,0 +1,118 @@
/*
* Copyright (c) 2002-2007 by OpenSymphony
* All rights reserved.
*/
package com.opensymphony.oscache.plugins.diskpersistence;
import com.opensymphony.oscache.base.Config;
import com.opensymphony.oscache.base.persistence.PersistenceListener;
import java.io.File;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
/**
* Persists cache data to disk. Provides a hash of the standard key name as the file name.
*
* A configurable hash algorithm is used to create a digest of the cache key for the
* disk filename. This is to allow for more sane filenames for objects which dont generate
* friendly cache keys.
*
* @author <a href="mailto:jparrott@soe.sony.com">Jason Parrott</a>
*/
public class HashDiskPersistenceListener extends AbstractDiskPersistenceListener {
private static final Log LOG = LogFactory.getLog(HashDiskPersistenceListener.class);
private static final int DIR_LEVELS = 3;
public final static String HASH_ALGORITHM_KEY = "cache.persistence.disk.hash.algorithm";
public final static String DEFAULT_HASH_ALGORITHM = "MD5";
protected MessageDigest md = null;
/**
* Initializes the <tt>HashDiskPersistenceListener</tt>. Namely this involves only setting up the
* message digester to hash the key values.
* @see com.opensymphony.oscache.base.persistence.PersistenceListener#configure(com.opensymphony.oscache.base.Config)
*/
public PersistenceListener configure(Config config) {
try {
if (config.getProperty(HashDiskPersistenceListener.HASH_ALGORITHM_KEY) != null) {
try {
md = MessageDigest.getInstance(config.getProperty(HashDiskPersistenceListener.HASH_ALGORITHM_KEY));
} catch (NoSuchAlgorithmException e) {
md = MessageDigest.getInstance(HashDiskPersistenceListener.DEFAULT_HASH_ALGORITHM);
}
} else {
md = MessageDigest.getInstance(HashDiskPersistenceListener.DEFAULT_HASH_ALGORITHM);
}
} catch (NoSuchAlgorithmException e) {
LOG.warn("No hash algorithm available for disk persistence", e);
throw new RuntimeException("No hash algorithm available for disk persistence", e);
}
return super.configure(config);
}
/**
* Generates a file name for the given cache key. In this case the file name is attempted to be
* generated from the hash of the standard key name. Cache algorithm is configured via the
* <em>cache.persistence.disk.hash.algorithm</em> configuration variable.
* @param key cache entry key
* @return char[] file name
*/
protected synchronized char[] getCacheFileName(String key) {
if ((key == null) || (key.length() == 0)) {
throw new IllegalArgumentException("Invalid key '" + key + "' specified to getCacheFile.");
}
String hexDigest = byteArrayToHexString(md.digest(key.getBytes()));
// CACHE-249: Performance improvement for large disk persistence usage
StringBuffer filename = new StringBuffer(hexDigest.length() + 2 * DIR_LEVELS);
for (int i=0; i < DIR_LEVELS; i++) {
filename.append(hexDigest.charAt(i)).append(File.separator);
}
filename.append(hexDigest);
return filename.toString().toCharArray();
}
/**
* Nibble conversion. Thanks to our friends at:
* http://www.devx.com/tips/Tip/13540
* @param in the byte array to convert
* @return a java.lang.String based version of they byte array
*/
static String byteArrayToHexString(byte[] in) {
if ((in == null) || (in.length <= 0)) {
return null;
}
StringBuffer out = new StringBuffer(in.length * 2);
for (int i = 0; i < in.length; i++) {
byte ch = (byte) (in[i] & 0xF0); // Strip off high nibble
ch = (byte) (ch >>> 4);
// shift the bits down
ch = (byte) (ch & 0x0F);
// must do this is high order bit is on!
out.append(PSEUDO[(int) ch]); // convert the nibble to a String Character
ch = (byte) (in[i] & 0x0F); // Strip off low nibble
out.append(PSEUDO[(int) ch]); // convert the nibble to a String Character
}
return out.toString();
}
static final String[] PSEUDO = {
"0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "A", "B", "C", "D",
"E", "F"
};
}

View file

@ -0,0 +1,31 @@
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">
<html>
<head>
<!--
@(#)package.html
Copyright (c) 2002-2003 by OpenSymphony
All rights reserved.
-->
</head>
<body bgcolor="white">
Provides support for persisting cached objects to disk.
<h2>Package Specification</h2>
<h2>Related Documentation</h2>
For overviews, tutorials, examples, guides, and tool documentation, please see:
<ul>
<li><a href="http://www.opensymphony.com/oscache">The OSCache Homepage</a>
</ul>
<!-- Put @see and @since tags down here. -->
</body>
</html>

View file

@ -0,0 +1,50 @@
/*
* Copyright (c) 2002-2003 by OpenSymphony
* All rights reserved.
*/
package com.opensymphony.oscache.util;
/**
* <p>This code is borrowed directly from OSCore, but is duplicated
* here to avoid having to add a dependency on the entire OSCore jar.</p>
*
* <p>If much more code from OSCore is needed then it might be wiser to
* bite the bullet and add a dependency.</p>
*/
public class ClassLoaderUtil {
private ClassLoaderUtil() {
}
/**
* Load a class with a given name.
*
* It will try to load the class in the following order:
* <ul>
* <li>From Thread.currentThread().getContextClassLoader()
* <li>Using the basic Class.forName()
* <li>From ClassLoaderUtil.class.getClassLoader()
* <li>From the callingClass.getClassLoader()
* </ul>
*
* @param className The name of the class to load
* @param callingClass The Class object of the calling object
* @throws ClassNotFoundException If the class cannot be found anywhere.
*/
public static Class loadClass(String className, Class callingClass) throws ClassNotFoundException {
try {
return Thread.currentThread().getContextClassLoader().loadClass(className);
} catch (ClassNotFoundException e) {
try {
return Class.forName(className);
} catch (ClassNotFoundException ex) {
try {
return ClassLoaderUtil.class.getClassLoader().loadClass(className);
} catch (ClassNotFoundException exc) {
return callingClass.getClassLoader().loadClass(className);
}
}
}
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,67 @@
/*
* Copyright (c) 2002-2003 by OpenSymphony
* All rights reserved.
*/
package com.opensymphony.oscache.util;
import java.util.ArrayList;
import java.util.List;
/**
* Provides common utility methods for handling strings.
*
* @author <a href="&#109;a&#105;&#108;&#116;&#111;:chris&#64;swebtec.&#99;&#111;&#109;">Chris Miller</a>
*/
public class StringUtil {
private StringUtil() {
}
/**
* Splits a string into substrings based on the supplied delimiter
* character. Each extracted substring will be trimmed of leading
* and trailing whitespace.
*
* @param str The string to split
* @param delimiter The character that delimits the string
* @return A string array containing the resultant substrings
*/
public static final List split(String str, char delimiter) {
// return no groups if we have an empty string
if ((str == null) || "".equals(str)) {
return new ArrayList();
}
ArrayList parts = new ArrayList();
int currentIndex;
int previousIndex = 0;
while ((currentIndex = str.indexOf(delimiter, previousIndex)) > 0) {
String part = str.substring(previousIndex, currentIndex).trim();
parts.add(part);
previousIndex = currentIndex + 1;
}
parts.add(str.substring(previousIndex, str.length()).trim());
return parts;
}
/**
* @param s the string to be checked
* @return true if the string parameter contains at least one element
*/
public static final boolean hasLength(String s) {
return (s != null) && (s.length() > 0);
}
/**
* @param s the string to be checked
* @return true if the string parameter is null or doesn't contain any element
* @since 2.4
*/
public static final boolean isEmpty(String s) {
return (s == null) || (s.length() == 0);
}
}

View file

@ -0,0 +1,33 @@
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">
<html>
<head>
<!--
@(#)package.html
Copyright (c) 2002-2003 by OpenSymphony
All rights reserved.
-->
</head>
<body bgcolor="white">
Provides utility classes that perform fairly general-purpose functions and are required
by OSCache.
<h2>Package Specification</h2>
<h2>Related Documentation</h2>
For overviews, tutorials, examples, guides, and tool documentation, please see:
<ul>
<li><a href="http://www.opensymphony.com/oscache">The OSCache Homepage</a>
<li><a href="http://www.opensymphony.com/oscache/cron.jsp">Using Cron Expressions with OSCache</a>
</ul>
<!-- Put @see and @since tags down here. -->
</body>
</html>

View file

@ -0,0 +1,40 @@
/*
* Copyright (c) 2002-2003 by OpenSymphony
* All rights reserved.
*/
package com.opensymphony.oscache.web;
import javax.servlet.ServletContext;
import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;
/**
* Class for a clean startup and shutdown of the ServletCacheAdministrator and its application scoped cache.
* @author <a href="&#109;a&#105;&#108;&#116;&#111;:chris&#64;swebtec.&#99;&#111;&#109;">Chris Miller</a>
*/
public class CacheContextListener implements ServletContextListener {
/**
* This notification occurs when the webapp is ready to process requests.<p>
* We use this hook to cleanly start up the {@link ServletCacheAdministrator}
* and create the application scope cache (which will consequentially
* initialize any listeners configured for it that implement <code>LifecycleAware</code>.)<p>
*
* As of Servlet 2.4, this is guaranteed to be called before any Servlet.init()
* methods.
*/
public void contextInitialized(ServletContextEvent servletContextEvent) {
ServletContext context = servletContextEvent.getServletContext();
ServletCacheAdministrator.getInstance(context);
}
/**
* This notification occurs when the servlet context is about to be shut down.
* We use this hook to cleanly shut down the cache.
*/
public void contextDestroyed(ServletContextEvent servletContextEvent) {
ServletContext context = servletContextEvent.getServletContext();
ServletCacheAdministrator.destroyInstance(context);
}
}

View file

@ -0,0 +1,118 @@
/*
* Copyright (c) 2002-2003 by OpenSymphony
* All rights reserved.
*/
package com.opensymphony.oscache.web;
import com.opensymphony.oscache.base.Cache;
import com.opensymphony.oscache.base.CacheEntry;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import java.io.Serializable;
import javax.servlet.http.HttpSessionBindingEvent;
import javax.servlet.http.HttpSessionBindingListener;
/**
* A simple extension of Cache that implements a session binding listener,
* and deletes it's entries when unbound
*
* @author <a href="mailto:mike@atlassian.com">Mike Cannon-Brookes</a>
* @author <a href="mailto:tgochenour@peregrine.com">Todd Gochenour</a>
* @author <a href="mailto:fbeauregard@pyxis-tech.com">Francois Beauregard</a>
* @version $Revision$
*/
public final class ServletCache extends Cache implements HttpSessionBindingListener, Serializable {
private static transient final Log log = LogFactory.getLog(ServletCache.class);
/**
* The admin for this cache
*/
private ServletCacheAdministrator admin;
/**
* The scope of that cache.
*/
private int scope;
/**
* Create a new ServletCache
*
* @param admin The ServletCacheAdministrator to administer this ServletCache.
* @param scope The scope of all entries in this hashmap
*/
public ServletCache(ServletCacheAdministrator admin, int scope) {
super(admin.isMemoryCaching(), admin.isUnlimitedDiskCache(), admin.isOverflowPersistence());
setScope(scope);
this.admin = admin;
}
/**
* Create a new Cache
*
* @param admin The CacheAdministrator to administer this Cache.
* @param algorithmClass The class that implement an algorithm
* @param limit The maximum cache size in number of entries
* @param scope The cache scope
*/
public ServletCache(ServletCacheAdministrator admin, String algorithmClass, int limit, int scope) {
super(admin.isMemoryCaching(), admin.isUnlimitedDiskCache(), admin.isOverflowPersistence(), admin.isBlocking(), algorithmClass, limit);
setScope(scope);
this.admin = admin;
}
/**
* Get the cache scope
*
* @return The cache scope
*/
public int getScope() {
return scope;
}
private void setScope(int scope) {
this.scope = scope;
}
/**
* When this Cache is bound to the session, do nothing.
*
* @param event The SessionBindingEvent.
*/
public void valueBound(HttpSessionBindingEvent event) {
}
/**
* When the users's session ends, all listeners are finalized and the
* session cache directory is deleted from disk.
*
* @param event The event that triggered this unbinding.
*/
public void valueUnbound(HttpSessionBindingEvent event) {
if (log.isInfoEnabled()) {
// CACHE-229: don't access the session's id, because this can throw an IllegalStateException
log.info("[Cache] Unbound from session " + event.getSession() + " using name " + event.getName());
}
admin.finalizeListeners(this);
clear();
}
/**
* Indicates whether or not the cache entry is stale. This overrides the
* {@link Cache#isStale(CacheEntry, int, String)} method to take into account any
* flushing that may have been applied to the scope that this cache belongs to.
*
* @param cacheEntry The cache entry to test the freshness of.
* @param refreshPeriod The maximum allowable age of the entry, in seconds.
* @param cronExpiry A cron expression that defines fixed expiry dates and/or
* times for this cache entry.
*
* @return <code>true</code> if the entry is stale, <code>false</code> otherwise.
*/
protected boolean isStale(CacheEntry cacheEntry, int refreshPeriod, String cronExpiry) {
return super.isStale(cacheEntry, refreshPeriod, cronExpiry) || admin.isScopeFlushed(cacheEntry, scope);
}
}

View file

@ -0,0 +1,794 @@
/*
* Copyright (c) 2002-2003 by OpenSymphony
* All rights reserved.
*/
package com.opensymphony.oscache.web;
import com.opensymphony.oscache.base.*;
import com.opensymphony.oscache.base.events.ScopeEvent;
import com.opensymphony.oscache.base.events.ScopeEventListener;
import com.opensymphony.oscache.base.events.ScopeEventType;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import java.io.Serializable;
import java.util.*;
import javax.servlet.ServletContext;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;
import javax.servlet.jsp.PageContext;
/**
* A ServletCacheAdministrator creates, flushes and administers the cache.
* <p>
* This is a "servlet Singleton". This means it's not a Singleton in the traditional sense,
* that is stored in a static instance. It's a Singleton _per web app context_.
* <p>
* Once created it manages the cache path on disk through the oscache.properties
* file, and also keeps track of the flush times.
*
* @author <a href="mailto:mike@atlassian.com">Mike Cannon-Brookes</a>
* @author <a href="mailto:tgochenour@peregrine.com">Todd Gochenour</a>
* @author <a href="mailto:fbeauregard@pyxis-tech.com">Francois Beauregard</a>
* @author <a href="mailto:abergevin@pyxis-tech.com">Alain Bergevin</a>
* @author <a href="&#109;a&#105;&#108;&#116;&#111;:chris&#64;swebtec.&#99;&#111;&#109;">Chris Miller</a>
* @version $Revision$
*/
public class ServletCacheAdministrator extends AbstractCacheAdministrator implements Serializable {
private static final transient Log log = LogFactory.getLog(ServletCacheAdministrator.class);
/**
* Constants for properties read/written from/to file
*/
private final static String CACHE_USE_HOST_DOMAIN_KEY = "cache.use.host.domain.in.key";
private final static String CACHE_KEY_KEY = "cache.key";
/**
* The default cache key that is used to store the cache in context.
*/
private final static String DEFAULT_CACHE_KEY = "__oscache_cache";
/**
* Constants for scope's name
*/
public final static String SESSION_SCOPE_NAME = "session";
public final static String APPLICATION_SCOPE_NAME = "application";
/**
* The suffix added to the cache key used to store a
* ServletCacheAdministrator will be stored in the ServletContext
*/
private final static String CACHE_ADMINISTRATOR_KEY_SUFFIX = "_admin";
/**
* The key under which an array of all ServletCacheAdministrator objects
* will be stored in the ServletContext
*/
private final static String CACHE_ADMINISTRATORS_KEY = "__oscache_admins";
/**
* Key used to store the current scope in the configuration. This is a hack
* to let the scope information get passed through to the DiskPersistenceListener,
* and will be removed in a future release.
*/
public final static String HASH_KEY_SCOPE = "scope";
/**
* Key used to store the current session ID in the configuration. This is a hack
* to let the scope information get passed through to the DiskPersistenceListener,
* and will be removed in a future release.
*/
public final static String HASH_KEY_SESSION_ID = "sessionId";
/**
* Key used to store the servlet container temporary directory in the configuration.
* This is a hack to let the scope information get passed through to the
* DiskPersistenceListener, and will be removed in a future release.
*/
public final static String HASH_KEY_CONTEXT_TMPDIR = "context.tempdir";
/**
* The string to use as a file separator.
*/
private final static String FILE_SEPARATOR = "/";
/**
* The character to use as a file separator.
*/
private final static char FILE_SEPARATOR_CHAR = FILE_SEPARATOR.charAt(0);
/**
* Constant for Key generation.
*/
private final static short AVERAGE_KEY_LENGTH = 30;
/**
* Usable caracters for key generation
*/
private static final String m_strBase64Chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
/**
* Map containing the flush times of different scopes
*/
private Map flushTimes;
/**
* Required so we can look up the app scope cache without forcing a session creation.
*/
private transient ServletContext context;
/**
* Key to use for storing and retrieving Object in contexts (Servlet, session).
*/
private String cacheKey;
/**
* Set property cache.use.host.domain.in.key=true to add domain information to key
* generation for hosting multiple sites.
*/
private boolean useHostDomainInKey = false;
/**
* Create the cache administrator.
*
* This will reset all the flush times and load the properties file.
*/
private ServletCacheAdministrator(ServletContext context, Properties p) {
super(p);
config.set(HASH_KEY_CONTEXT_TMPDIR, context.getAttribute("javax.servlet.context.tempdir"));
flushTimes = new HashMap();
initHostDomainInKey();
this.context = context;
}
/**
* Obtain an instance of the CacheAdministrator
*
* @param context The ServletContext that this CacheAdministrator is a Singleton under
* @return Returns the CacheAdministrator instance for this context
*/
public static ServletCacheAdministrator getInstance(ServletContext context) {
return getInstance(context, null);
}
/**
* Obtain an instance of the CacheAdministrator for the specified key
*
* @param context The ServletContext that this CacheAdministrator is a Singleton under
* @param key the cachekey or admincachekey for the CacheAdministrator wanted
* @return Returns the CacheAdministrator instance for this context, or null if no
* CacheAdministrator exists with the key supplied
*/
public static ServletCacheAdministrator getInstanceFromKey(ServletContext context, String key) {
// Note we do not bother to check if the key is null because it mustn't.
if (!key.endsWith(CACHE_ADMINISTRATOR_KEY_SUFFIX)) {
key = key + CACHE_ADMINISTRATOR_KEY_SUFFIX;
}
return (ServletCacheAdministrator) context.getAttribute(key);
}
/**
* Obtain an instance of the CacheAdministrator
*
* @param context The ServletContext that this CacheAdministrator is a Singleton under
* @param p the properties to use for the cache if the cache administrator has not been
* created yet. Once the administrator has been created, the properties parameter is
* ignored for all future invocations. If a null value is passed in, then the properties
* are loaded from the oscache.properties file in the classpath.
* @return Returns the CacheAdministrator instance for this context
*/
public synchronized static ServletCacheAdministrator getInstance(ServletContext context, Properties p)
{
String adminKey = null;
if (p!= null) {
adminKey = p.getProperty(CACHE_KEY_KEY);
}
if (adminKey == null) {
adminKey = DEFAULT_CACHE_KEY;
}
adminKey += CACHE_ADMINISTRATOR_KEY_SUFFIX;
ServletCacheAdministrator admin = (ServletCacheAdministrator) context.getAttribute(adminKey);
// First time we need to create the administrator and store it in the
// servlet context
if (admin == null) {
admin = new ServletCacheAdministrator(context, p);
Map admins = (Map) context.getAttribute(CACHE_ADMINISTRATORS_KEY);
if (admins == null) {
admins = new HashMap();
}
admins.put(adminKey, admin);
context.setAttribute(CACHE_ADMINISTRATORS_KEY, admins);
context.setAttribute(adminKey, admin);
if (log.isInfoEnabled()) {
log.info("Created new instance of ServletCacheAdministrator with key "+adminKey);
}
admin.getAppScopeCache(context);
}
if (admin.context == null) {
admin.context = context;
}
return admin;
}
/**
* Shuts down all servlet cache administrators. This should usually only
* be called when the controlling application shuts down.
*/
public static void destroyInstance(ServletContext context)
{
ServletCacheAdministrator admin;
Map admins = (Map) context.getAttribute(CACHE_ADMINISTRATORS_KEY);
if (admins != null)
{
Set keys = admins.keySet();
Iterator it = keys.iterator();
while (it.hasNext())
{
String adminKey = (String) it.next();
admin = (ServletCacheAdministrator) admins.get( adminKey );
if (admin != null)
{
// Finalize the application scope cache
Cache cache = (Cache) context.getAttribute(admin.getCacheKey());
if (cache != null) {
admin.finalizeListeners(cache);
context.removeAttribute(admin.getCacheKey());
context.removeAttribute(adminKey);
cache = null;
if (log.isInfoEnabled()) {
log.info("Shut down the ServletCacheAdministrator "+adminKey);
}
}
admin = null;
}
}
context.removeAttribute(CACHE_ADMINISTRATORS_KEY);
}
}
/**
* Grabs the cache for the specified scope
*
* @param request The current request
* @param scope The scope of this cache (<code>PageContext.APPLICATION_SCOPE</code>
* or <code>PageContext.SESSION_SCOPE</code>)
* @return The cache
*/
public Cache getCache(HttpServletRequest request, int scope) {
if (scope == PageContext.APPLICATION_SCOPE) {
return getAppScopeCache(context);
}
if (scope == PageContext.SESSION_SCOPE) {
return getSessionScopeCache(request.getSession(true));
}
throw new RuntimeException("The supplied scope value of " + scope + " is invalid. Acceptable values are PageContext.APPLICATION_SCOPE and PageContext.SESSION_SCOPE");
}
/**
* A convenience method to retrieve the application scope cache
* @param context the current <code>ServletContext</code>
* @return the application scope cache. If none is present, one will
* be created.
*/
public Cache getAppScopeCache(ServletContext context) {
Cache cache;
Object obj = context.getAttribute(getCacheKey());
if ((obj == null) || !(obj instanceof Cache)) {
if (log.isInfoEnabled()) {
log.info("Created new application-scoped cache at key: " + getCacheKey());
}
cache = createCache(PageContext.APPLICATION_SCOPE, null);
context.setAttribute(getCacheKey(), cache);
} else {
cache = (Cache) obj;
}
return cache;
}
/**
* A convenience method to retrieve the session scope cache
*
* @param session the current <code>HttpSession</code>
* @return the session scope cache for this session. If none is present,
* one will be created.
*/
public Cache getSessionScopeCache(HttpSession session) {
Cache cache;
Object obj = session.getAttribute(getCacheKey());
if ((obj == null) || !(obj instanceof Cache)) {
if (log.isInfoEnabled()) {
log.info("Created new session-scoped cache in session " + session.getId() + " at key: " + getCacheKey());
}
cache = createCache(PageContext.SESSION_SCOPE, session.getId());
session.setAttribute(getCacheKey(), cache);
} else {
cache = (Cache) obj;
}
return cache;
}
/**
* Get the cache key from the properties. Set it to a default value if it
* is not present in the properties
*
* @return The cache.key property or the DEFAULT_CACHE_KEY
*/
public String getCacheKey() {
if (cacheKey == null) {
cacheKey = getProperty(CACHE_KEY_KEY);
if (cacheKey == null) {
cacheKey = DEFAULT_CACHE_KEY;
}
}
return cacheKey;
}
/**
* Set the flush time for a specific scope to a specific time
*
* @param date The time to flush the scope
* @param scope The scope to be flushed
*/
public void setFlushTime(Date date, int scope) {
if (log.isInfoEnabled()) {
log.info("Flushing scope " + scope + " at " + date);
}
synchronized (flushTimes) {
if (date != null) {
// Trigger a SCOPE_FLUSHED event
dispatchScopeEvent(ScopeEventType.SCOPE_FLUSHED, scope, date, null);
flushTimes.put(new Integer(scope), date);
} else {
logError("setFlushTime called with a null date.");
throw new IllegalArgumentException("setFlushTime called with a null date.");
}
}
}
/**
* Set the flush time for a specific scope to the current time.
*
* @param scope The scope to be flushed
*/
public void setFlushTime(int scope) {
setFlushTime(new Date(), scope);
}
/**
* Get the flush time for a particular scope.
*
* @param scope The scope to get the flush time for.
* @return A date representing the time this scope was last flushed.
* Returns null if it has never been flushed.
*/
public Date getFlushTime(int scope) {
synchronized (flushTimes) {
return (Date) flushTimes.get(new Integer(scope));
}
}
/**
* Retrieve an item from the cache
*
* @param scope The cache scope
* @param request The servlet request
* @param key The key of the object to retrieve
* @param refreshPeriod The time interval specifying if an entry needs refresh
* @return The requested object
* @throws NeedsRefreshException
*/
public Object getFromCache(int scope, HttpServletRequest request, String key, int refreshPeriod) throws NeedsRefreshException {
Cache cache = getCache(request, scope);
key = this.generateEntryKey(key, request, scope);
return cache.getFromCache(key, refreshPeriod);
}
/**
* Checks if the given scope was flushed more recently than the CacheEntry provided.
* Used to determine whether to refresh the particular CacheEntry.
*
* @param cacheEntry The cache entry which we're seeing whether to refresh
* @param scope The scope we're checking
*
* @return Whether or not the scope has been flushed more recently than this cache entry was updated.
*/
public boolean isScopeFlushed(CacheEntry cacheEntry, int scope) {
Date flushDateTime = getFlushTime(scope);
if (flushDateTime != null) {
long lastUpdate = cacheEntry.getLastUpdate();
return (flushDateTime.getTime() >= lastUpdate);
} else {
return false;
}
}
/**
* Register a listener for Cache Map events.
*
* @param listener The object that listens to events.
*/
public void addScopeEventListener(ScopeEventListener listener) {
listenerList.add(ScopeEventListener.class, listener);
}
/**
* Cancels a pending cache update. This should only be called by a thread
* that received a {@link NeedsRefreshException} and was unable to generate
* some new cache content.
*
* @param scope The cache scope
* @param request The servlet request
* @param key The cache entry key to cancel the update of.
*/
public void cancelUpdate(int scope, HttpServletRequest request, String key) {
Cache cache = getCache(request, scope);
key = this.generateEntryKey(key, request, scope);
cache.cancelUpdate(key);
}
/**
* Flush all scopes at a particular time
*
* @param date The time to flush the scope
*/
public void flushAll(Date date) {
synchronized (flushTimes) {
setFlushTime(date, PageContext.APPLICATION_SCOPE);
setFlushTime(date, PageContext.SESSION_SCOPE);
setFlushTime(date, PageContext.REQUEST_SCOPE);
setFlushTime(date, PageContext.PAGE_SCOPE);
}
// Trigger a flushAll event
dispatchScopeEvent(ScopeEventType.ALL_SCOPES_FLUSHED, -1, date, null);
}
/**
* Flush all scopes instantly.
*/
public void flushAll() {
flushAll(new Date());
}
/**
* Generates a cache entry key.
*
* If the string key is not specified, the HTTP request URI and QueryString is used.
* Operating systems that have a filename limitation less than 255 or have
* filenames that are case insensitive may have issues with key generation where
* two distinct pages map to the same key.
* <p>
* POST Requests (which have no distinguishing
* query string) may also generate identical keys for what is actually different pages.
* In these cases, specify an explicit key attribute for the CacheTag.
*
* @param key The key entered by the user
* @param request The current request
* @param scope The scope this cache entry is under
* @return The generated cache key
*/
public String generateEntryKey(String key, HttpServletRequest request, int scope) {
return generateEntryKey(key, request, scope, null, null);
}
/**
* Generates a cache entry key.
*
* If the string key is not specified, the HTTP request URI and QueryString is used.
* Operating systems that have a filename limitation less than 255 or have
* filenames that are case insensitive may have issues with key generation where
* two distinct pages map to the same key.
* <p>
* POST Requests (which have no distinguishing
* query string) may also generate identical keys for what is actually different pages.
* In these cases, specify an explicit key attribute for the CacheTag.
*
* @param key The key entered by the user
* @param request The current request
* @param scope The scope this cache entry is under
* @param language The ISO-639 language code to distinguish different pages in application scope
* @return The generated cache key
*/
public String generateEntryKey(String key, HttpServletRequest request, int scope, String language) {
return generateEntryKey(key, request, scope, language, null);
}
/**
* Generates a cache entry key.
* <p>
* If the string key is not specified, the HTTP request URI and QueryString is used.
* Operating systems that have a filename limitation less than 255 or have
* filenames that are case insensitive may have issues with key generation where
* two distinct pages map to the same key.
* <p>
* POST Requests (which have no distinguishing
* query string) may also generate identical keys for what is actually different pages.
* In these cases, specify an explicit key attribute for the CacheTag.
*
* @param key The key entered by the user
* @param request The current request
* @param scope The scope this cache entry is under
* @param language The ISO-639 language code to distinguish different pages in application scope
* @param suffix The ability to put a suffix at the end of the key
* @return The generated cache key
*/
public String generateEntryKey(String key, HttpServletRequest request, int scope, String language, String suffix) {
/**
* Used for generating cache entry keys.
*/
StringBuffer cBuffer = new StringBuffer(AVERAGE_KEY_LENGTH);
// Append the language if available
if (language != null) {
cBuffer.append(FILE_SEPARATOR).append(language);
}
// Servers for multiple host domains need this distinction in the key
if (useHostDomainInKey) {
cBuffer.append(FILE_SEPARATOR).append(request.getServerName());
}
if (key != null) {
cBuffer.append(FILE_SEPARATOR).append(key);
} else {
String generatedKey = request.getRequestURI();
if (generatedKey.charAt(0) != FILE_SEPARATOR_CHAR) {
cBuffer.append(FILE_SEPARATOR_CHAR);
}
cBuffer.append(generatedKey);
cBuffer.append("_").append(request.getMethod()).append("_");
generatedKey = getSortedQueryString(request);
if (generatedKey != null) {
try {
java.security.MessageDigest digest = java.security.MessageDigest.getInstance("MD5");
byte[] b = digest.digest(generatedKey.getBytes());
cBuffer.append('_');
// Base64 encoding allows for unwanted slash characters.
cBuffer.append(toBase64(b).replace('/', '_'));
} catch (Exception e) {
// Ignore query string
}
}
}
// Do we want a suffix
if ((suffix != null) && (suffix.length() > 0)) {
cBuffer.append(suffix);
}
return cBuffer.toString();
}
/**
* Creates a string that contains all of the request parameters and their
* values in a single string. This is very similar to
* <code>HttpServletRequest.getQueryString()</code> except the parameters are
* sorted by name, and if there is a <code>jsessionid</code> parameter it is
* filtered out.<p>
* If the request has no parameters, this method returns <code>null</code>.
*/
protected String getSortedQueryString(HttpServletRequest request) {
Map paramMap = request.getParameterMap();
if (paramMap.isEmpty()) {
return null;
}
Set paramSet = new TreeMap(paramMap).entrySet();
StringBuffer buf = new StringBuffer();
boolean first = true;
for (Iterator it = paramSet.iterator(); it.hasNext();) {
Map.Entry entry = (Map.Entry) it.next();
String[] values = (String[]) entry.getValue();
for (int i = 0; i < values.length; i++) {
String key = (String) entry.getKey();
if ((key.length() != 10) || !"jsessionid".equals(key)) {
if (first) {
first = false;
} else {
buf.append('&');
}
buf.append(key).append('=').append(values[i]);
}
}
}
// We get a 0 length buffer if the only parameter was a jsessionid
if (buf.length() == 0) {
return null;
} else {
return buf.toString();
}
}
/**
* Log error messages to commons logging.
*
* @param message Message to log.
*/
public void logError(String message) {
log.error("[oscache]: " + message);
}
/**
* Put an object in the cache. This should only be called by a thread
* that received a {@link NeedsRefreshException}. Using session scope
* the thread has to insure that the session wasn't invalidated in
* the meantime. CacheTag and CacheFilter guarantee that the same
* cache is used in cancelUpdate and getFromCache.
*
* @param scope The cache scope
* @param request The servlet request
* @param key The object key
* @param content The object to add
*/
public void putInCache(int scope, HttpServletRequest request, String key, Object content) {
putInCache(scope, request, key, content, null);
}
/**
* Put an object in the cache. This should only be called by a thread
* that received a {@link NeedsRefreshException}. Using session scope
* the thread has to insure that the session wasn't invalidated in
* the meantime. CacheTag and CacheFilter guarantee that the same
* cache is used in cancelUpdate and getFromCache.
*
* @param scope The cache scope
* @param request The servlet request
* @param key The object key
* @param content The object to add
* @param policy The refresh policy
*/
public void putInCache(int scope, HttpServletRequest request, String key, Object content, EntryRefreshPolicy policy) {
Cache cache = getCache(request, scope);
key = this.generateEntryKey(key, request, scope);
cache.putInCache(key, content, policy);
}
/**
* Sets the cache capacity (number of items). If the cache contains
* more than <code>capacity</code> items then items will be removed
* to bring the cache back down to the new size.
*
* @param scope The cache scope
* @param request The servlet request
* @param capacity The new capacity
*/
public void setCacheCapacity(int scope, HttpServletRequest request, int capacity) {
setCacheCapacity(capacity);
getCache(request, scope).setCapacity(capacity);
}
/**
* Unregister a listener for Cache Map events.
*
* @param listener The object that currently listens to events.
*/
public void removeScopeEventListener(ScopeEventListener listener) {
listenerList.remove(ScopeEventListener.class, listener);
}
/**
* Finalizes all the listeners that are associated with the given cache object
*/
protected void finalizeListeners(Cache cache) {
super.finalizeListeners(cache);
}
/**
* Convert a byte array into a Base64 string (as used in mime formats)
*/
private static String toBase64(byte[] aValue) {
int byte1;
int byte2;
int byte3;
int iByteLen = aValue.length;
StringBuffer tt = new StringBuffer();
for (int i = 0; i < iByteLen; i += 3) {
boolean bByte2 = (i + 1) < iByteLen;
boolean bByte3 = (i + 2) < iByteLen;
byte1 = aValue[i] & 0xFF;
byte2 = (bByte2) ? (aValue[i + 1] & 0xFF) : 0;
byte3 = (bByte3) ? (aValue[i + 2] & 0xFF) : 0;
tt.append(m_strBase64Chars.charAt(byte1 / 4));
tt.append(m_strBase64Chars.charAt((byte2 / 16) + ((byte1 & 0x3) * 16)));
tt.append(((bByte2) ? m_strBase64Chars.charAt((byte3 / 64) + ((byte2 & 0xF) * 4)) : '='));
tt.append(((bByte3) ? m_strBase64Chars.charAt(byte3 & 0x3F) : '='));
}
return tt.toString();
}
/**
* Create a cache
*
* @param scope The cache scope
* @param sessionId The sessionId for with the cache will be created
* @return A new cache
*/
private ServletCache createCache(int scope, String sessionId) {
ServletCache newCache = new ServletCache(this, algorithmClass, cacheCapacity, scope);
// TODO - Fix me please!
// Hack! This is nasty - if two sessions are created within a short
// space of time it is possible they will end up with duplicate
// session IDs being passed to the DiskPersistenceListener!...
config.set(HASH_KEY_SCOPE, "" + scope);
config.set(HASH_KEY_SESSION_ID, sessionId);
newCache = (ServletCache) configureStandardListeners(newCache);
return newCache;
}
/**
* Dispatch a scope event to all registered listeners.
*
* @param eventType The type of event
* @param scope Scope that was flushed (Does not apply for FLUSH_ALL event)
* @param date Date of flushing
* @param origin The origin of the event
*/
private void dispatchScopeEvent(ScopeEventType eventType, int scope, Date date, String origin) {
// Create the event
ScopeEvent event = new ScopeEvent(eventType, scope, date, origin);
// Guaranteed to return a non-null array
Object[] listeners = listenerList.getListenerList();
// Process the listeners last to first, notifying
// those that are interested in this event
for (int i = listeners.length - 2; i >= 0; i -= 2) {
if (listeners[i+1] instanceof ScopeEventListener) {
((ScopeEventListener) listeners[i + 1]).scopeFlushed(event);
}
}
}
/**
* Set property cache.use.host.domain.in.key=true to add domain information to key
* generation for hosting multiple sites
*/
private void initHostDomainInKey() {
String propStr = getProperty(CACHE_USE_HOST_DOMAIN_KEY);
useHostDomainInKey = "true".equalsIgnoreCase(propStr);
}
}

View file

@ -0,0 +1,35 @@
/*
* Copyright (c) 2002-2003 by OpenSymphony
* All rights reserved.
*/
package com.opensymphony.oscache.web;
import com.opensymphony.oscache.base.EntryRefreshPolicy;
/**
* Interface to implement an entry refresh policy.
* Specify the name of the implementing class using the refreshpolicyclass
* attribute of the cache tag. If any additional parameters are required,
* they should be supplied using the refreshpolicyparam attribute.<p>
*
* For example:
* <pre>
* &lt;cache:cache key="mykey"
* refreshpolicyclass="com.mycompany.cache.policy.MyRefreshPolicy"
* refreshpolicyparam="...additional data..."&gt;
My cached content
* &lt;/cache:cache&gt;
* </pre>
*
* @version $Revision$
* @author <a href="mailto:fbeauregard@pyxis-tech.com">Francois Beauregard</a>
*/
public interface WebEntryRefreshPolicy extends EntryRefreshPolicy {
/**
* Initializes the refresh policy.
*
* @param key The cache key that is being checked.
* @param param Any optional parameters that were supplied
*/
public void init(String key, String param);
}

View file

@ -0,0 +1,823 @@
/*
* Copyright (c) 2002-2009 by OpenSymphony
* All rights reserved.
*/
package com.opensymphony.oscache.web.filter;
import com.opensymphony.oscache.base.Cache;
import com.opensymphony.oscache.base.Config;
import com.opensymphony.oscache.base.EntryRefreshPolicy;
import com.opensymphony.oscache.base.NeedsRefreshException;
import com.opensymphony.oscache.util.ClassLoaderUtil;
import com.opensymphony.oscache.util.StringUtil;
import com.opensymphony.oscache.web.ServletCacheAdministrator;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import java.io.IOException;
import java.util.List;
import java.util.Properties;
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.jsp.PageContext;
/**
* CacheFilter is a filter that allows for server-side caching of post-processed servlet content.<p>
*
* It also gives great programatic control over refreshing, flushing and updating the cache.<p>
*
* @author <a href="mailto:sergek [ AT ] lokitech.com">Serge Knystautas</a>
* @author <a href="mailto:mike [ AT ] atlassian.com">Mike Cannon-Brookes</a>
* @author <a href="mailto:ltorunski [ AT ] t-online.de">Lars Torunski</a>
* @version $Revision$
*/
public class CacheFilter implements Filter, ICacheKeyProvider, ICacheGroupsProvider {
// Header
public static final String HEADER_LAST_MODIFIED = "Last-Modified";
public static final String HEADER_CONTENT_TYPE = "Content-Type";
public static final String HEADER_CONTENT_ENCODING = "Content-Encoding";
public static final String HEADER_EXPIRES = "Expires";
public static final String HEADER_IF_MODIFIED_SINCE = "If-Modified-Since";
public static final String HEADER_CACHE_CONTROL = "Cache-Control";
public static final String HEADER_ACCEPT_ENCODING = "Accept-Encoding";
public static final String HEADER_ETAG = "ETag";
public static final String HEADER_CONTENT_DISPOSITION = "Content-Disposition";
// Fragment parameter
public static final int FRAGMENT_AUTODETECT = -1;
public static final int FRAGMENT_NO = 0;
public static final int FRAGMENT_YES = 1;
// No cache parameter
public static final int NOCACHE_OFF = 0;
public static final int NOCACHE_SESSION_ID_IN_URL = 1;
// Last Modified parameter
public static final long LAST_MODIFIED_OFF = 0;
public static final long LAST_MODIFIED_ON = 1;
public static final long LAST_MODIFIED_INITIAL = -1;
// Expires parameter
public static final long EXPIRES_OFF = 0;
public static final long EXPIRES_ON = 1;
public static final long EXPIRES_TIME = -1;
// ETag parameter
public static final int ETAG_OFF = 0;
public static final int ETAG_WEAK = 1;
//public static final int ETAG_STRONG = 2;
// Cache Control
public static final long MAX_AGE_NO_INIT = Long.MIN_VALUE;
public static final long MAX_AGE_TIME = Long.MAX_VALUE;
// request attribute to avoid reentrance
private final static String REQUEST_FILTERED = "__oscache_filtered__";
private String requestFiltered;
// the policy for the expires header
private EntryRefreshPolicy expiresRefreshPolicy;
// the logger
private final Log log = LogFactory.getLog(this.getClass());
// filter variables
private FilterConfig config;
private ServletCacheAdministrator admin = null;
private int cacheScope = PageContext.APPLICATION_SCOPE; // filter scope - default is APPLICATION
private int fragment = FRAGMENT_AUTODETECT; // defines if this filter handles fragments of a page - default is auto detect
private int time = 60 * 60; // time before cache should be refreshed - default one hour (in seconds)
private String cron = null; // A cron expression that determines when this cached content will expire - default is null
private int nocache = NOCACHE_OFF; // defines special no cache option for the requests - default is off
private long lastModified = LAST_MODIFIED_INITIAL; // defines if the last-modified-header will be sent - default is intial setting
private long expires = EXPIRES_ON; // defines if the expires-header will be sent - default is on
private int etag = ETAG_WEAK; // defines the type of the etag header - default is weak
private long cacheControlMaxAge = -60; // defines which max-age in Cache-Control to be set - default is 60 seconds for max-age
private ICacheKeyProvider cacheKeyProvider = this; // the provider of the cache key - default is the CacheFilter itselfs
private ICacheGroupsProvider cacheGroupsProvider = this; // the provider of the cache groups - default is the CacheFilter itselfs
private List disableCacheOnMethods = null; // caching can be disabled by defining the http methods - default is off
/**
* Filter clean-up
*/
public void destroy() {
//Not much to do...
}
/**
* The doFilter call caches the response by wrapping the <code>HttpServletResponse</code>
* object so that the output stream can be caught. This works by splitting off the output
* stream into two with the {@link SplitServletOutputStream} class. One stream gets written
* out to the response as normal, the other is fed into a byte array inside a {@link ResponseContent}
* object.
*
* @param request The servlet request
* @param response The servlet response
* @param chain The filter chain
* @throws ServletException IOException
*/
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws ServletException, IOException {
if (log.isInfoEnabled()) {
log.info("OSCache: filter in scope " + cacheScope);
}
// avoid reentrance (CACHE-128) and check if request is cacheable
if (isFilteredBefore(request) || !isCacheableInternal(request)) {
chain.doFilter(request, response);
return;
}
request.setAttribute(requestFiltered, Boolean.TRUE);
HttpServletRequest httpRequest = (HttpServletRequest) request;
// checks if the response will be a fragment of a page
boolean fragmentRequest = isFragment(httpRequest);
// avoid useless session creation for application scope pages (CACHE-129)
Cache cache;
if (cacheScope == PageContext.SESSION_SCOPE) {
cache = admin.getSessionScopeCache(httpRequest.getSession(true));
} else {
cache = admin.getAppScopeCache(config.getServletContext());
}
// generate the cache entry key
String key = cacheKeyProvider.createCacheKey(httpRequest, admin, cache);
try {
ResponseContent respContent = (ResponseContent) cache.getFromCache(key, time, cron);
if (log.isInfoEnabled()) {
log.info("OSCache: Using cached entry for " + key);
}
boolean acceptsGZip = false;
if ((!fragmentRequest) && (lastModified != LAST_MODIFIED_OFF)) {
long clientLastModified = httpRequest.getDateHeader(HEADER_IF_MODIFIED_SINCE); // will return -1 if no header...
// only reply with SC_NOT_MODIFIED
// if the client has already the newest page and the response isn't a fragment in a page
if ((clientLastModified != -1) && (clientLastModified >= respContent.getLastModified())) {
((HttpServletResponse) response).setStatus(HttpServletResponse.SC_NOT_MODIFIED);
return;
}
acceptsGZip = respContent.isContentGZiped() && acceptsGZipEncoding(httpRequest);
}
respContent.writeTo(response, fragmentRequest, acceptsGZip);
// acceptsGZip is used for performance reasons above; use the following line for CACHE-49
// respContent.writeTo(response, fragmentRequest, acceptsGZipEncoding(httpRequest));
} catch (NeedsRefreshException nre) {
boolean updateSucceeded = false;
try {
if (log.isInfoEnabled()) {
log.info("OSCache: New cache entry, cache stale or cache scope flushed for " + key);
}
CacheHttpServletResponseWrapper cacheResponse = new CacheHttpServletResponseWrapper((HttpServletResponse) response, fragmentRequest, time * 1000L, lastModified, expires, cacheControlMaxAge, etag);
chain.doFilter(request, cacheResponse);
cacheResponse.flushBuffer();
// Only cache if the response is cacheable
if (isCacheableInternal(cacheResponse)) {
// get the cache groups of the content
String[] groups = cacheGroupsProvider.createCacheGroups(httpRequest, admin, cache);
// Store as the cache content the result of the response
cache.putInCache(key, cacheResponse.getContent(), groups, expiresRefreshPolicy, null);
updateSucceeded = true;
if (log.isInfoEnabled()) {
log.info("OSCache: New entry added to the cache with key " + key);
}
}
} finally {
if (!updateSucceeded) {
cache.cancelUpdate(key);
}
}
}
}
/**
* Initialize the filter. This retrieves a {@link ServletCacheAdministrator}
* instance and configures the filter based on any initialization parameters.<p>
* The supported initialization parameters are:
* <ul>
*
* <li><b>oscache-properties-file</b> - the properties file that contains the OSCache configuration
* options to be used by the Cache that this Filter should use.</li>
*
* @param filterConfig The filter configuration
*/
public void init(FilterConfig filterConfig) {
// Get whatever settings we want...
config = filterConfig;
log.info("OSCache: Initializing CacheFilter with filter name " + config.getFilterName());
// setting the request filter to avoid reentrance with the same filter
requestFiltered = REQUEST_FILTERED + config.getFilterName();
log.info("Request filter attribute is " + requestFiltered);
// filter Properties file
Properties props = null;
try {
String propertiesfile = config.getInitParameter("oscache-properties-file");
if (propertiesfile != null && propertiesfile.length() > 0) {
props = Config.loadProperties(propertiesfile, "CacheFilter with filter name '" + config.getFilterName()+ "'");
}
} catch (Exception e) {
log.info("OSCache: Init parameter 'oscache-properties-file' not set, using default.");
}
admin = ServletCacheAdministrator.getInstance(config.getServletContext(), props);
// filter parameter time
String timeParam = config.getInitParameter("time");
if (timeParam != null) {
try {
setTime(Integer.parseInt(timeParam));
} catch (NumberFormatException nfe) {
log.error("OSCache: Unexpected value for the init parameter 'time', defaulting to one hour. Message=" + nfe.getMessage());
}
}
// filter parameter scope
String scopeParam = config.getInitParameter("scope");
if (scopeParam != null) {
if ("session".equalsIgnoreCase(scopeParam)) {
setCacheScope(PageContext.SESSION_SCOPE);
} else if ("application".equalsIgnoreCase(scopeParam)) {
setCacheScope(PageContext.APPLICATION_SCOPE);
} else {
log.error("OSCache: Wrong value '" + scopeParam + "' for init parameter 'scope', defaulting to 'application'.");
}
}
// filter parameter cron
setCron(config.getInitParameter("cron"));
// filter parameter fragment
String fragmentParam = config.getInitParameter("fragment");
if (fragmentParam != null) {
if ("no".equalsIgnoreCase(fragmentParam)) {
setFragment(FRAGMENT_NO);
} else if ("yes".equalsIgnoreCase(fragmentParam)) {
setFragment(FRAGMENT_YES);
} else if ("auto".equalsIgnoreCase(fragmentParam)) {
setFragment(FRAGMENT_AUTODETECT);
} else {
log.error("OSCache: Wrong value '" + fragmentParam + "' for init parameter 'fragment', defaulting to 'auto detect'.");
}
}
// filter parameter nocache
String nocacheParam = config.getInitParameter("nocache");
if (nocacheParam != null) {
if ("off".equalsIgnoreCase(nocacheParam)) {
nocache = NOCACHE_OFF;
} else if ("sessionIdInURL".equalsIgnoreCase(nocacheParam)) {
nocache = NOCACHE_SESSION_ID_IN_URL;
} else {
log.error("OSCache: Wrong value '" + nocacheParam + "' for init parameter 'nocache', defaulting to 'off'.");
}
}
// filter parameter last modified
String lastModifiedParam = config.getInitParameter("lastModified");
if (lastModifiedParam != null) {
if ("off".equalsIgnoreCase(lastModifiedParam)) {
lastModified = LAST_MODIFIED_OFF;
} else if ("on".equalsIgnoreCase(lastModifiedParam)) {
lastModified = LAST_MODIFIED_ON;
} else if ("initial".equalsIgnoreCase(lastModifiedParam)) {
lastModified = LAST_MODIFIED_INITIAL;
} else {
log.error("OSCache: Wrong value '" + lastModifiedParam + "' for init parameter 'lastModified', defaulting to 'initial'.");
}
}
// filter parameter expires
String expiresParam = config.getInitParameter("expires");
if (expiresParam != null) {
if ("off".equalsIgnoreCase(expiresParam)) {
setExpires(EXPIRES_OFF);
} else if ("on".equalsIgnoreCase(expiresParam)) {
setExpires(EXPIRES_ON);
} else if ("time".equalsIgnoreCase(expiresParam)) {
setExpires(EXPIRES_TIME);
} else {
log.error("OSCache: Wrong value '" + expiresParam + "' for init parameter 'expires', defaulting to 'on'.");
}
}
// filter parameter expires
String etagParam = config.getInitParameter("etag");
if (etagParam != null) {
if ("off".equalsIgnoreCase(etagParam)) {
setETag(ETAG_OFF);
} else if ("weak".equalsIgnoreCase(etagParam)) {
setETag(ETAG_WEAK);
} else {
log.error("OSCache: Wrong value '" + etagParam + "' for init parameter 'etag', defaulting to 'weak'.");
}
}
// filter parameter Cache-Control
String cacheControlMaxAgeParam = config.getInitParameter("max-age");
if (cacheControlMaxAgeParam != null) {
if (cacheControlMaxAgeParam.equalsIgnoreCase("no init")) {
setCacheControlMaxAge(MAX_AGE_NO_INIT);
} else if (cacheControlMaxAgeParam.equalsIgnoreCase("time")) {
setCacheControlMaxAge(MAX_AGE_TIME);
} else {
try {
setCacheControlMaxAge(Long.parseLong(cacheControlMaxAgeParam));
} catch (NumberFormatException nfe) {
log.error("OSCache: Unexpected value for the init parameter 'max-age', defaulting to '60'. Message=" + nfe.getMessage());
}
}
}
// filter parameter ICacheKeyProvider
ICacheKeyProvider cacheKeyProviderParam = (ICacheKeyProvider)instantiateFromInitParam("ICacheKeyProvider", ICacheKeyProvider.class, this.getClass().getName());
if (cacheKeyProviderParam != null) {
setCacheKeyProvider(cacheKeyProviderParam);
}
// filter parameter ICacheGroupsProvider
ICacheGroupsProvider cacheGroupsProviderParam = (ICacheGroupsProvider)instantiateFromInitParam("ICacheGroupsProvider", ICacheGroupsProvider.class, this.getClass().getName());
if (cacheGroupsProviderParam != null) {
setCacheGroupsProvider(cacheGroupsProviderParam);
}
// filter parameter EntryRefreshPolicy
EntryRefreshPolicy expiresRefreshPolicyParam = (EntryRefreshPolicy)instantiateFromInitParam("EntryRefreshPolicy", EntryRefreshPolicy.class, ExpiresRefreshPolicy.class.getName());
if (expiresRefreshPolicyParam != null) {
setExpiresRefreshPolicy(expiresRefreshPolicyParam);
} else {
// setting the refresh period for this cache filter
setExpiresRefreshPolicy(new ExpiresRefreshPolicy(time));
}
// filter parameter scope
String disableCacheOnMethodsParam = config.getInitParameter("disableCacheOnMethods");
if (StringUtil.hasLength(disableCacheOnMethodsParam)) {
disableCacheOnMethods = StringUtil.split(disableCacheOnMethodsParam, ',');
// log.error("OSCache: Wrong value '" + disableCacheOnMethodsParam + "' for init parameter 'disableCacheOnMethods', defaulting to 'null'.");
}
}
private Object instantiateFromInitParam(String classInitParam, Class interfaceClass, String defaultObjectName) {
String className = config.getInitParameter(classInitParam);
if (className != null) {
try {
Class clazz = ClassLoaderUtil.loadClass(className, this.getClass());
if (!interfaceClass.isAssignableFrom(clazz)) {
log.error("OSCache: Specified class '" + className + "' does not implement" + interfaceClass.getName() + ". Using default " + defaultObjectName + ".");
return null;
} else {
return clazz.newInstance();
}
} catch (ClassNotFoundException e) {
log.error("OSCache: Class '" + className + "' not found. Defaulting to " + defaultObjectName + ".", e);
} catch (InstantiationException e) {
log.error("OSCache: Class '" + className + "' could not be instantiated because it is not a concrete class. Using default object " + defaultObjectName + ".", e);
} catch (IllegalAccessException e) {
log.error("OSCache: Class '"+ className+ "' could not be instantiated because it is not public. Using default object " + defaultObjectName + ".", e);
}
}
return null;
}
/**
* {@link ICacheKeyProvider}
* @see com.opensymphony.oscache.web.filter.ICacheKeyProvider#createCacheKey(javax.servlet.http.HttpServletRequest, ServletCacheAdministrator, Cache)
*/
public String createCacheKey(HttpServletRequest httpRequest, ServletCacheAdministrator scAdmin, Cache cache) {
return scAdmin.generateEntryKey(null, httpRequest, cacheScope);
}
/**
* {@link ICacheGroupsProvider}
* @see com.opensymphony.oscache.web.filter.ICacheGroupsProvider#createCacheGroups(javax.servlet.http.HttpServletRequest, ServletCacheAdministrator, Cache)
*/
public String[] createCacheGroups(HttpServletRequest httpRequest, ServletCacheAdministrator scAdmin, Cache cache) {
return null;
}
/**
* Checks if the request is a fragment in a page.
*
* According to Java Servlet API 2.2 (8.2.1 Dispatching Requests, Included
* Request Parameters), when a servlet is being used from within an include,
* the attribute <code>javax.servlet.include.request_uri</code> is set.
* According to Java Servlet API 2.3 this is excepted for servlets obtained
* by using the getNamedDispatcher method.
*
* @param request the to be handled request
* @return true if the request is a fragment in a page
*/
public boolean isFragment(HttpServletRequest request) {
if (fragment == FRAGMENT_AUTODETECT) {
return request.getAttribute("javax.servlet.include.request_uri") != null;
} else {
return (fragment == FRAGMENT_NO) ? false : true;
}
}
/**
* Checks if the request was filtered before, so
* guarantees to be executed once per request. You
* can override this methods to define a more specific
* behavior.
*
* @param request checks if the request was filtered before.
* @return true if it is the first execution
*/
public boolean isFilteredBefore(ServletRequest request) {
return request.getAttribute(requestFiltered) != null;
}
/*
* isCacheableInternal guarantees that the log information is correct.
*
* @param request The servlet request
* @return Returns a boolean indicating if the request can be cached or not.
*/
private final boolean isCacheableInternal(ServletRequest request) {
final boolean cacheable = isCacheable(request);
if (log.isDebugEnabled()) {
log.debug("OSCache: the request " + ((cacheable) ? "is" : "is not") + " cachable.");
}
return cacheable;
}
/**
* isCacheable is a method allowing a subclass to decide if a request is
* cacheable or not.
*
* @param request The servlet request
* @return Returns a boolean indicating if the request can be cached or not.
*/
public boolean isCacheable(ServletRequest request) {
boolean cacheable = request instanceof HttpServletRequest;
if (cacheable) {
HttpServletRequest requestHttp = (HttpServletRequest) request;
// CACHE-272 don't cache special http request methods
if ((disableCacheOnMethods != null) && (disableCacheOnMethods.contains(requestHttp.getMethod()))) {
return false;
}
if (nocache == NOCACHE_SESSION_ID_IN_URL) { // don't cache requests if session id is in the URL
cacheable = !requestHttp.isRequestedSessionIdFromURL();
}
}
return cacheable;
}
/*
* isCacheableInternal guarantees that the log information is correct.
*
* @param cacheResponse the HTTP servlet response
* @return Returns a boolean indicating if the response can be cached or not.
*/
private final boolean isCacheableInternal(CacheHttpServletResponseWrapper cacheResponse) {
final boolean cacheable = isCacheable(cacheResponse);
if (log.isDebugEnabled()) {
log.debug("OSCache: the response " + ((cacheable) ? "is" : "is not") + " cacheable.");
}
return cacheable;
}
/**
* isCacheable is a method allowing subclass to decide if a response is
* cacheable or not.
*
* @param cacheResponse the HTTP servlet response
* @return Returns a boolean indicating if the response can be cached or not.
*/
public boolean isCacheable(CacheHttpServletResponseWrapper cacheResponse) {
// TODO implement CACHE-137 here
// Only cache if the response was 200
return cacheResponse.getStatus() == HttpServletResponse.SC_OK;
}
/**
* Check if the client browser support gzip compression.
*
* @param request the http request
* @return true if client browser supports GZIP
*/
public boolean acceptsGZipEncoding(HttpServletRequest request) {
String acceptEncoding = request.getHeader(HEADER_ACCEPT_ENCODING);
return (acceptEncoding != null) && (acceptEncoding.indexOf("gzip") != -1);
}
// ---------------------------------
// --- getter and setter methods ---
// ---------------------------------
/**
* @return the max-age of the cache control
* @since 2.4
*/
public long getCacheControlMaxAge() {
if ((cacheControlMaxAge == MAX_AGE_NO_INIT) || (cacheControlMaxAge == MAX_AGE_TIME)) {
return cacheControlMaxAge;
}
return - cacheControlMaxAge;
}
/**
* <b>max-age</b> - defines the cache control response header max-age. Acceptable values are
* <code>MAX_AGE_NO_INIT</code> for don't initializing the max-age cache control,
* <code>MAX_AGE_TIME</code> the max-age information will be based on the time parameter and creation time of the content (expiration timestamp minus current timestamp), and
* <code>[positive integer]</code> value constant in seconds to be set in every response, the default value is 60.
*
* @param cacheControlMaxAge the cacheControlMaxAge to set
* @since 2.4
*/
public void setCacheControlMaxAge(long cacheControlMaxAge) {
if ((cacheControlMaxAge == MAX_AGE_NO_INIT) || (cacheControlMaxAge == MAX_AGE_TIME)) {
this.cacheControlMaxAge = cacheControlMaxAge;
} else if (cacheControlMaxAge >= 0) {
// declare the cache control as a constant
// TODO check if this value can be stored as a positive long without changing it
this.cacheControlMaxAge = - cacheControlMaxAge;
} else {
log.warn("OSCache: 'max-age' must be at least a positive integer, defaulting to '60'. ");
this.cacheControlMaxAge = -60;
}
}
/**
* @return the cacheGroupsProvider
* @since 2.4
*/
public ICacheGroupsProvider getCacheGroupsProvider() {
return cacheGroupsProvider;
}
/**
* <b>ICacheGroupsProvider</b> - Class implementing the interface <code>ICacheGroupsProvider</code>.
* A developer can implement a method which provides cache groups based on the request,
* the servlet cache administrator and cache. The parameter has to be not <code>null</code>.
*
* @param cacheGroupsProvider the cacheGroupsProvider to set
* @since 2.4
*/
public void setCacheGroupsProvider(ICacheGroupsProvider cacheGroupsProvider) {
if (cacheGroupsProvider == null) throw new IllegalArgumentException("The ICacheGroupsProvider is null.");
this.cacheGroupsProvider = cacheGroupsProvider;
}
/**
* @return the cacheKeyProvider
* @since 2.4
*/
public ICacheKeyProvider getCacheKeyProvider() {
return cacheKeyProvider;
}
/**
* <b>ICacheKeyProvider</b> - Class implementing the interface <code>ICacheKeyProvider</code>.
* A developer can implement a method which provides cache keys based on the request,
* the servlect cache administrator and cache. The parameter has to be not <code>null</code>.
*
* @param cacheKeyProvider the cacheKeyProvider to set
* @since 2.4
*/
public void setCacheKeyProvider(ICacheKeyProvider cacheKeyProvider) {
if (cacheKeyProvider == null) throw new IllegalArgumentException("The ICacheKeyProvider is null.");
this.cacheKeyProvider = cacheKeyProvider;
}
/**
* Returns PageContext.APPLICATION_SCOPE or PageContext.SESSION_SCOPE.
* @return the cache scope
* @since 2.4
*/
public int getCacheScope() {
return cacheScope;
}
/**
* <b>scope</b> - the default scope to cache content. Acceptable values
* are <code>PageContext.APPLICATION_SCOPE</code> (default) and <code>PageContext.SESSION_SCOPE</code>.
*
* @param cacheScope the cacheScope to set
* @since 2.4
*/
public void setCacheScope(int cacheScope) {
if ((cacheScope != PageContext.APPLICATION_SCOPE) && (cacheScope != PageContext.SESSION_SCOPE))
throw new IllegalArgumentException("Acceptable values for cache scope are PageContext.APPLICATION_SCOPE or PageContext.SESSION_SCOPE");
this.cacheScope = cacheScope;
}
/**
* @return the cron
* @since 2.4
*/
public String getCron() {
return cron;
}
/**
* <b>cron</b> - defines an expression that determines when the page content will expire.
* This allows content to be expired at particular dates and/or times, rather than once
* a cache entry reaches a certain age.
*
* @param cron the cron to set
* @since 2.4
*/
public void setCron(String cron) {
this.cron = cron;
}
/**
* @return the expires header
* @since 2.4
*/
public long getExpires() {
return expires;
}
/**
* <b>expires</b> - defines if the expires header will be sent in the response. Acceptable values are
* <code>EXPIRES_OFF</code> for don't sending the header, even it is set in the filter chain,
* <code>EXPIRES_ON</code> (default) for sending it if it is set in the filter chain and
* <code>EXPIRES_TIME</code> the expires information will be intialized based on the time parameter and creation time of the content.
*
* @param expires the expires to set
* @since 2.4
*/
public void setExpires(long expires) {
if ((expires < EXPIRES_TIME) || (expires > EXPIRES_ON)) throw new IllegalArgumentException("Expires value out of range.");
this.expires = expires;
}
/**
* @return the etag
* @since 2.4.2
*/
public int getETag() {
return etag;
}
/**
* <b>etag</b> - defines if the Entity tag (ETag) HTTP header is sent in the response. Acceptable values are
* <code>ETAG_OFF</code> for don't sending the header, even it is set in the filter chain,
* <code>ETAG_WEAK</code> (default) for generating a Weak ETag by concatenating the content length and the last modified time in milliseconds.
*
* @param etag the etag to set
* @since 2.4.2
*/
public void setETag(int etag) {
if ((etag < ETAG_OFF) || (etag > ETAG_WEAK)) throw new IllegalArgumentException("ETag value out of range.");
this.etag = etag;
}
/**
* @return the expiresRefreshPolicy
* @since 2.4
*/
public EntryRefreshPolicy getExpiresRefreshPolicy() {
return expiresRefreshPolicy;
}
/**
* <b>EntryRefreshPolicy</b> - Class implementing the interface <code>EntryRefreshPolicy</code>.
* A developer can implement a class which provides a different custom cache invalidation policy for a specific cache entry.
* If not specified, the default policy is timed entry expiry as specified with the <b>time</b> parameter described above.
*
* @param expiresRefreshPolicy the expiresRefreshPolicy to set
* @since 2.4
*/
public void setExpiresRefreshPolicy(EntryRefreshPolicy expiresRefreshPolicy) {
if (expiresRefreshPolicy == null) throw new IllegalArgumentException("The EntryRefreshPolicy is null.");
this.expiresRefreshPolicy = expiresRefreshPolicy;
}
/**
* @return the fragment
* @since 2.4
*/
public int getFragment() {
return fragment;
}
/**
* <b>fragment</b> - defines if this filter handles fragments of a page. Acceptable values
* are <code>FRAGMENT_AUTODETECT</code> (default) for auto detect, <code>FRAGMENT_NO</code> and <code>FRAGMENT_YES</code>.
*
* @param fragment the fragment to set
* @since 2.4
*/
public void setFragment(int fragment) {
if ((fragment < FRAGMENT_AUTODETECT) || (fragment > FRAGMENT_YES)) throw new IllegalArgumentException("Fragment value out of range.");
this.fragment = fragment;
}
/**
* @return the lastModified
* @since 2.4
*/
public long getLastModified() {
return lastModified;
}
/**
* <b>lastModified</b> - defines if the last modified header will be sent in the response. Acceptable values are
* <code>LAST_MODIFIED_OFF</code> for don't sending the header, even it is set in the filter chain,
* <code>LAST_MODIFIED_ON</code> for sending it if it is set in the filter chain and
* <code>LAST_MODIFIED_INITIAL</code> (default) the last modified information will be set based on the current time and changes are allowed.
*
* @param lastModified the lastModified to set
* @since 2.4
*/
public void setLastModified(long lastModified) {
if ((lastModified < LAST_MODIFIED_INITIAL) || (lastModified > LAST_MODIFIED_ON)) throw new IllegalArgumentException("LastModified value out of range.");
this.lastModified = lastModified;
}
/**
* @return the nocache
* @since 2.4
*/
public int getNocache() {
return nocache;
}
/**
* <b>nocache</b> - defines which objects shouldn't be cached. Acceptable values
* are <code>NOCACHE_OFF</code> (default) and <code>NOCACHE_SESSION_ID_IN_URL</code> if the session id is
* contained in the URL.
*
* @param nocache the nocache to set
* @since 2.4
*/
public void setNocache(int nocache) {
if ((nocache < NOCACHE_OFF) || (nocache > NOCACHE_SESSION_ID_IN_URL)) throw new IllegalArgumentException("Nocache value out of range.");
this.nocache = nocache;
}
/**
* @return the time
* @since 2.4
*/
public int getTime() {
return time;
}
/**
* <b>time</b> - the default time (in seconds) to cache content for. The default
* value is 3600 seconds (one hour). Specifying -1 (indefinite expiry) as the cache
* time will ensure a content does not become stale until it is either explicitly
* flushed or the expires refresh policy causes the entry to expire.
*
* @param time the time to set
* @since 2.4
*/
public void setTime(int time) {
this.time = time;
// check if ExpiresRefreshPolicy has to be reset
if (expiresRefreshPolicy instanceof ExpiresRefreshPolicy) {
((ExpiresRefreshPolicy) expiresRefreshPolicy).setRefreshPeriod(time);
}
}
/**
* @link http://java.sun.com/j2ee/sdk_1.3/techdocs/api/javax/servlet/http/HttpServletRequest.html#getMethod()
* @return the list of http method names for which cacheing should be disabled
* @since 2.4
*/
public List getDisableCacheOnMethods() {
return disableCacheOnMethods;
}
/**
* <b>disableCacheOnMethods</b> - Defines the http method name for which cacheing should be disabled.
* The default value is <code>null</code> for cacheing all requests without regarding the method name.
* @link http://java.sun.com/j2ee/sdk_1.3/techdocs/api/javax/servlet/http/HttpServletRequest.html#getMethod()
* @param disableCacheOnMethods the list of http method names for which cacheing should be disabled
* @since 2.4
*/
public void setDisableCacheOnMethods(List disableCacheOnMethods) {
this.disableCacheOnMethods = disableCacheOnMethods;
}
// TODO: check if getter/setter for oscache-properties-file is possible
}

View file

@ -0,0 +1,439 @@
/*
* Copyright (c) 2002-2008 by OpenSymphony
* All rights reserved.
*/
package com.opensymphony.oscache.web.filter;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.util.Locale;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpServletResponseWrapper;
/**
* CacheServletResponse is a serialized representation of a response
*
* @author <a href="mailto:sergek@lokitech.com">Serge Knystautas</a>
* @version $Revision$
*/
public class CacheHttpServletResponseWrapper extends HttpServletResponseWrapper {
private final Log log = LogFactory.getLog(this.getClass());
/**
* We cache the printWriter so we can maintain a single instance
* of it no matter how many times it is requested.
*/
private PrintWriter cachedWriter = null;
private ResponseContent result = null;
private SplitServletOutputStream cacheOut = null;
private boolean fragment = false;
private int status = SC_OK;
private long expires = CacheFilter.EXPIRES_ON;
private long lastModified = CacheFilter.LAST_MODIFIED_INITIAL;
private long cacheControl = -60;
private int etagOption = CacheFilter.ETAG_WEAK;
private String etag = null;
/**
* Constructor
*
* @param response The servlet response
*/
public CacheHttpServletResponseWrapper(HttpServletResponse response) {
this(response, false, Long.MAX_VALUE, CacheFilter.EXPIRES_ON, CacheFilter.LAST_MODIFIED_INITIAL, -60, CacheFilter.ETAG_WEAK);
}
/**
* Constructor
*
* @param response The servlet response
* @param fragment true if the repsonse indicates that it is a fragement of a page
* @param time the refresh time in millis
* @param lastModified defines if last modified header will be send, @see CacheFilter
* @param expires defines if expires header will be send, @see CacheFilter
* @param cacheControl defines if cache control header will be send, @see CacheFilter
* @param etagOption defines if the ETag header will be send, @see CacheFilter
*/
public CacheHttpServletResponseWrapper(HttpServletResponse response, boolean fragment, long time, long lastModified, long expires, long cacheControl, int etagOption) {
super(response);
result = new ResponseContent();
this.fragment = fragment;
this.expires = expires;
this.lastModified = lastModified;
this.cacheControl = cacheControl;
this.etagOption = etagOption;
// only set initial values for last modified and expires, when a complete page is cached
if (!fragment) {
// setting a default last modified value based on object creation and remove the millis
if (lastModified == CacheFilter.LAST_MODIFIED_INITIAL) {
long current = System.currentTimeMillis();
current = current - (current % 1000);
result.setLastModified(current);
super.setDateHeader(CacheFilter.HEADER_LAST_MODIFIED, result.getLastModified());
}
// setting the expires value
if (expires == CacheFilter.EXPIRES_TIME) {
result.setExpires(result.getLastModified() + time);
super.setDateHeader(CacheFilter.HEADER_EXPIRES, result.getExpires());
}
// setting the cache control with max-age
if (this.cacheControl == CacheFilter.MAX_AGE_TIME) {
// set the count down
long maxAge = System.currentTimeMillis();
maxAge = maxAge - (maxAge % 1000) + time;
result.setMaxAge(maxAge);
super.addHeader(CacheFilter.HEADER_CACHE_CONTROL, "max-age=" + time / 1000);
} else if (this.cacheControl != CacheFilter.MAX_AGE_NO_INIT) {
result.setMaxAge(this.cacheControl);
super.addHeader(CacheFilter.HEADER_CACHE_CONTROL, "max-age=" + (-this.cacheControl));
} else if (this.cacheControl == CacheFilter.MAX_AGE_NO_INIT ) {
result.setMaxAge(this.cacheControl);
}
}
}
/**
* Get a response content
*
* @return The content
*/
public ResponseContent getContent() {
// Flush the buffer
try {
flush();
} catch (IOException ignore) {
}
// Create the byte array
result.commit();
// Return the result from this response
return result;
}
/**
* Set the content type
*
* @param value The content type
*/
public void setContentType(String value) {
if (log.isDebugEnabled()) {
log.debug("ContentType: " + value);
}
super.setContentType(value);
result.setContentType(value);
}
/**
* Set the date of a header
*
* @param name The header name
* @param value The date
*/
public void setDateHeader(String name, long value) {
if (log.isDebugEnabled()) {
log.debug("dateheader: " + name + ": " + value);
}
// only set the last modified value, if a complete page is cached
if ((lastModified != CacheFilter.LAST_MODIFIED_OFF) && (CacheFilter.HEADER_LAST_MODIFIED.equalsIgnoreCase(name))) {
if (!fragment) {
result.setLastModified(value);
} // TODO should we return now by fragments to avoid putting the header to the response?
}
// implement RFC 2616 14.21 Expires (without max-age)
if ((expires != CacheFilter.EXPIRES_OFF) && (CacheFilter.HEADER_EXPIRES.equalsIgnoreCase(name))) {
if (!fragment) {
result.setExpires(value);
} // TODO should we return now by fragments to avoid putting the header to the response?
}
super.setDateHeader(name, value);
}
/**
* Add the date of a header
*
* @param name The header name
* @param value The date
*/
public void addDateHeader(String name, long value) {
if (log.isDebugEnabled()) {
log.debug("dateheader: " + name + ": " + value);
}
// only set the last modified value, if a complete page is cached
if ((lastModified != CacheFilter.LAST_MODIFIED_OFF) && (CacheFilter.HEADER_LAST_MODIFIED.equalsIgnoreCase(name))) {
if (!fragment) {
result.setLastModified(value);
} // TODO should we return now by fragments to avoid putting the header to the response?
}
// implement RFC 2616 14.21 Expires (without max-age)
if ((expires != CacheFilter.EXPIRES_OFF) && (CacheFilter.HEADER_EXPIRES.equalsIgnoreCase(name))) {
if (!fragment) {
result.setExpires(value);
} // TODO should we return now by fragments to avoid putting the header to the response?
}
super.addDateHeader(name, value);
}
/**
* Set a header field
*
* @param name The header name
* @param value The header value
*/
public void setHeader(String name, String value) {
if (log.isDebugEnabled()) {
log.debug("header: " + name + ": " + value);
}
if (CacheFilter.HEADER_CONTENT_TYPE.equalsIgnoreCase(name)) {
result.setContentType(value);
}
if (CacheFilter.HEADER_CONTENT_ENCODING.equalsIgnoreCase(name)) {
result.setContentEncoding(value);
}
if (CacheFilter.HEADER_ETAG.equalsIgnoreCase(name)) {
result.setETag(value);
}
if (CacheFilter.HEADER_CONTENT_DISPOSITION.equalsIgnoreCase(name)) {
result.setContentDisposition(value);
}
super.setHeader(name, value);
}
/**
* Add a header field
*
* @param name The header name
* @param value The header value
*/
public void addHeader(String name, String value) {
if (log.isDebugEnabled()) {
log.debug("header: " + name + ": " + value);
}
if (CacheFilter.HEADER_CONTENT_TYPE.equalsIgnoreCase(name)) {
result.setContentType(value);
}
if (CacheFilter.HEADER_CONTENT_ENCODING.equalsIgnoreCase(name)) {
result.setContentEncoding(value);
}
if (CacheFilter.HEADER_ETAG.equalsIgnoreCase(name)) {
result.setETag(value);
}
if (CacheFilter.HEADER_CONTENT_DISPOSITION.equalsIgnoreCase(name)) {
result.setContentDisposition(value);
}
super.addHeader(name, value);
}
/**
* Set the int value of the header
*
* @param name The header name
* @param value The int value
*/
public void setIntHeader(String name, int value) {
if (log.isDebugEnabled()) {
log.debug("intheader: " + name + ": " + value);
}
super.setIntHeader(name, value);
}
/**
* We override this so we can catch the response status. Only
* responses with a status of 200 (<code>SC_OK</code>) will
* be cached.
*/
public void setStatus(int status) {
super.setStatus(status);
this.status = status;
}
/**
* We override this so we can catch the response status. Only
* responses with a status of 200 (<code>SC_OK</code>) will
* be cached.
*/
public void sendError(int status, String string) throws IOException {
super.sendError(status, string);
this.status = status;
}
/**
* We override this so we can catch the response status. Only
* responses with a status of 200 (<code>SC_OK</code>) will
* be cached.
*/
public void sendError(int status) throws IOException {
super.sendError(status);
this.status = status;
}
/**
* We override this so we can catch the response status. Only
* responses with a status of 200 (<code>SC_OK</code>) will
* be cached.
*/
public void setStatus(int status, String string) {
super.setStatus(status, string);
this.status = status;
}
/**
* We override this so we can catch the response status. Only
* responses with a status of 200 (<code>SC_OK</code>) will
* be cached.
*/
public void sendRedirect(String location) throws IOException {
this.status = SC_MOVED_TEMPORARILY;
super.sendRedirect(location);
}
/**
* Retrieves the captured HttpResponse status.
*/
public int getStatus() {
return status;
}
/**
* Set the locale
*
* @param value The locale
*/
public void setLocale(Locale value) {
super.setLocale(value);
result.setLocale(value);
}
/**
* Get an output stream
*
* @throws IOException
*/
public ServletOutputStream getOutputStream() throws IOException {
// Pass this faked servlet output stream that captures what is sent
if (cacheOut == null) {
cacheOut = new SplitServletOutputStream(result.getOutputStream(), super.getOutputStream());
}
return cacheOut;
}
/**
* Get a print writer
*
* @throws IOException
*/
public PrintWriter getWriter() throws IOException {
if (cachedWriter == null) {
String encoding = getCharacterEncoding();
if (encoding != null) {
cachedWriter = new PrintWriter(new OutputStreamWriter(getOutputStream(), encoding));
} else { // using the default character encoding
cachedWriter = new PrintWriter(new OutputStreamWriter(getOutputStream()));
}
}
return cachedWriter;
}
/**
* Flushes all streams.
* @throws IOException
*/
private void flush() throws IOException {
if (cacheOut != null) {
cacheOut.flush();
}
if (cachedWriter != null) {
cachedWriter.flush();
}
}
public void flushBuffer() throws IOException {
// The weak ETag is content size + lastModified
if (etag == null) {
if (etagOption == CacheFilter.ETAG_WEAK) {
etag = "W/\"" + result.getSize() + "-" + result.getLastModified() + "\"";
result.setETag(etag);
}
}
super.flushBuffer();
flush();
}
/**
* Returns a boolean indicating if the response has been committed.
* A committed response has already had its status code and headers written.
*
* @see javax.servlet.ServletResponseWrapper#isCommitted()
*/
public boolean isCommitted() {
return super.isCommitted(); // || (result.getOutputStream() == null);
}
/**
* Clears any data that exists in the buffer as well as the status code and headers.
* If the response has been committed, this method throws an IllegalStateException.
* @see javax.servlet.ServletResponseWrapper#reset()
*/
public void reset() {
log.info("CacheHttpServletResponseWrapper:reset()");
super.reset();
/*
cachedWriter = null;
result = new ResponseContent();
cacheOut = null;
fragment = false;
status = SC_OK;
expires = CacheFilter.EXPIRES_ON;
lastModified = CacheFilter.LAST_MODIFIED_INITIAL;
cacheControl = -60;
etag = null;
// time ?
*/
}
/**
* Clears the content of the underlying buffer in the response without clearing headers or status code.
* If the response has been committed, this method throws an IllegalStateException.
* @see javax.servlet.ServletResponseWrapper#resetBuffer()
*/
public void resetBuffer() {
log.info("CacheHttpServletResponseWrapper:resetBuffer()");
super.resetBuffer();
/*
//cachedWriter = null;
result = new ResponseContent();
//cacheOut = null;
//fragment = false;
*/
}
}

View file

@ -0,0 +1,75 @@
/*
* Copyright (c) 2002-2003 by OpenSymphony
* All rights reserved.
*/
package com.opensymphony.oscache.web.filter;
import com.opensymphony.oscache.base.CacheEntry;
import com.opensymphony.oscache.base.EntryRefreshPolicy;
import com.opensymphony.oscache.base.NeedsRefreshException;
/**
* Checks if a cache filter entry has expired.
* This is useful when expires header are used in the response.
*
* @version $Revision$
* @author <a href="mailto:ltorunski [ AT ] t-online.de">Lars Torunski</a>
*/
public class ExpiresRefreshPolicy implements EntryRefreshPolicy {
/** the refresh period (in milliseconds) of a certain cache filter*/
private long refreshPeriod;
/**
* Constructor ExpiresRefreshPolicy.
*
* @param refreshPeriod the refresh period in seconds
*/
public ExpiresRefreshPolicy(int refreshPeriod) {
this.refreshPeriod = refreshPeriod * 1000L;
}
/**
* Indicates whether the supplied <code>CacheEntry</code> needs to be refreshed.
* This will be called when retrieving an entry from the cache - if this method
* returns <code>true</code> then a <code>NeedsRefreshException</code> will be
* thrown.
*
* @param entry The cache entry which is ignored.
* @return <code>true</code> if the content needs refreshing, <code>false</code> otherwise.
*
* @see NeedsRefreshException
* @see CacheEntry
*/
public boolean needsRefresh(CacheEntry entry) {
long currentTimeMillis = System.currentTimeMillis();
if ((refreshPeriod >= 0) && (currentTimeMillis >= (entry.getLastUpdate() + refreshPeriod))) {
return true;
} else if (entry.getContent() instanceof ResponseContent) {
ResponseContent responseContent = (ResponseContent) entry.getContent();
return currentTimeMillis >= responseContent.getExpires();
} else {
return false;
}
}
/**
* @return the refreshPeriod in seconds
* @since 2.4
*/
public long getRefreshPeriod() {
return refreshPeriod / 1000;
}
/**
* @param refreshPeriod the refresh period in seconds
* @since 2.4
*/
public void setRefreshPeriod(long refreshPeriod) {
this.refreshPeriod = refreshPeriod * 1000L;
}
}

View file

@ -0,0 +1,33 @@
/*
* Copyright (c) 2002-2003 by OpenSymphony
* All rights reserved.
*/
package com.opensymphony.oscache.web.filter;
import javax.servlet.http.HttpServletRequest;
import com.opensymphony.oscache.base.Cache;
import com.opensymphony.oscache.web.ServletCacheAdministrator;
/**
* Provider interface for cache groups creation in CacheFilter. A developer can implement a method which provides
* cache groups based on the request, the servlet cache administrator and cache.
*
* JIRA issue: http://jira.opensymphony.com/browse/CACHE-195
*
* @author <a href="mailto:ltorunski@t-online.de">Lars Torunski</a>
* @version $Revision$
*/
public interface ICacheGroupsProvider {
/**
* Creates the cache groups for the CacheFilter.
*
* @param httpRequest the http request.
* @param scAdmin the ServletCacheAdministrator of the cache
* @param cache the cache of the ServletCacheAdministrator
* @return the cache key
*/
public String[] createCacheGroups(HttpServletRequest httpRequest, ServletCacheAdministrator scAdmin, Cache cache);
}

View file

@ -0,0 +1,33 @@
/*
* Copyright (c) 2002-2003 by OpenSymphony
* All rights reserved.
*/
package com.opensymphony.oscache.web.filter;
import javax.servlet.http.HttpServletRequest;
import com.opensymphony.oscache.base.Cache;
import com.opensymphony.oscache.web.ServletCacheAdministrator;
/**
* Provider interface for cache key creation. A developer can implement a method which provides
* cache keys based on the request, the servlet cache administrator and cache.
*
* JIRA issue: http://jira.opensymphony.com/browse/CACHE-179
*
* @author <a href="mailto:ltorunski@t-online.de">Lars Torunski</a>
* @version $Revision$
*/
public interface ICacheKeyProvider {
/**
* Creates the cache key for the CacheFilter.
*
* @param httpRequest the http request.
* @param scAdmin the ServletCacheAdministrator of the cache
* @param cache the cache of the ServletCacheAdministrator
* @return the cache key
*/
public String createCacheKey(HttpServletRequest httpRequest, ServletCacheAdministrator scAdmin, Cache cache);
}

View file

@ -0,0 +1,265 @@
/*
* Copyright (c) 2002-2009 by OpenSymphony
* All rights reserved.
*/
package com.opensymphony.oscache.web.filter;
import java.io.*;
import java.util.Locale;
import java.util.zip.GZIPInputStream;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletResponse;
/**
* Holds the servlet response in a byte array so that it can be held
* in the cache (and, since this class is serializable, optionally
* persisted to disk).
*
* @version $Revision$
* @author <a href="mailto:sergek@lokitech.com">Serge Knystautas</a>
*/
public class ResponseContent implements Serializable {
private transient ByteArrayOutputStream bout = new ByteArrayOutputStream(1000);
private Locale locale = null;
private String contentEncoding = null;
private String contentType = null;
private byte[] content = null;
private long expires = Long.MAX_VALUE;
private long lastModified = -1;
private long maxAge = -60;
private String etag = null;
private String contentDisposition = null;
public String getContentType() {
return contentType;
}
/**
* Set the content type. We capture this so that when we serve this
* data from cache, we can set the correct content type on the response.
*/
public void setContentType(String value) {
contentType = value;
}
public long getLastModified() {
return lastModified;
}
public void setLastModified(long value) {
lastModified = value;
}
public String getContentEncoding() {
return contentEncoding;
}
public void setContentEncoding(String contentEncoding) {
this.contentEncoding = contentEncoding;
}
public String getETag() {
return etag;
}
public void setETag(String etag) {
this.etag = etag;
}
public String getContentDisposition() {
return contentDisposition;
}
public void setContentDisposition(String contentDisposition) {
this.contentDisposition = contentDisposition;
}
/**
* Set the Locale. We capture this so that when we serve this data from
* cache, we can set the correct locale on the response.
*/
public void setLocale(Locale value) {
locale = value;
}
/**
* @return the expires date and time in milliseconds when the content will be stale
*/
public long getExpires() {
return expires;
}
/**
* Sets the expires date and time in milliseconds.
* @param value time in milliseconds when the content will expire
*/
public void setExpires(long value) {
expires = value;
}
/**
* Returns the max age of the content in milliseconds. If expires header and cache control are
* enabled both, both will be equal.
* @return the max age of the content in milliseconds, if -1 max-age is disabled
*/
public long getMaxAge() {
return maxAge;
}
/**
* Sets the max age date and time in milliseconds. If the parameter is -1, the max-age parameter
* won't be set by default in the Cache-Control header.
* @param value sets the intial
*/
public void setMaxAge(long value) {
maxAge = value;
}
/**
* Get an output stream. This is used by the {@link SplitServletOutputStream}
* to capture the original (uncached) response into a byte array.
* @return the original (uncached) response, returns null if response is already committed.
*/
public OutputStream getOutputStream() {
return bout;
}
/**
* Gets the size of this cached content.
*
* @return The size of the content, in bytes. If no content
* exists, this method returns <code>-1</code>.
*/
public int getSize() {
return (content != null) ? content.length : (-1);
}
/**
* Called once the response has been written in its entirety. This
* method commits the response output stream by converting the output
* stream into a byte array.
*/
public void commit() {
if (bout != null) {
content = bout.toByteArray();
bout = null;
}
}
/**
* Writes this cached data out to the supplied <code>ServletResponse</code>.
*
* @param response The servlet response to output the cached content to.
* @throws IOException
*/
public void writeTo(ServletResponse response) throws IOException {
writeTo(response, false, false);
}
/**
* Writes this cached data out to the supplied <code>ServletResponse</code>.
*
* @param response The servlet response to output the cached content to.
* @param fragment is true if this content a fragment or part of a page
* @param acceptsGZip is true if client browser supports gzip compression
* @throws IOException
*/
public void writeTo(ServletResponse response, boolean fragment, boolean acceptsGZip) throws IOException {
//Send the content type and data to this response
if (contentType != null) {
response.setContentType(contentType);
}
if (fragment) {
// Don't support gzip compression if the content is a fragment of a page
acceptsGZip = false;
} else {
// add special headers for a complete page
if (response instanceof HttpServletResponse) {
HttpServletResponse httpResponse = (HttpServletResponse) response;
// add the last modified header
if (lastModified != -1) {
httpResponse.setDateHeader(CacheFilter.HEADER_LAST_MODIFIED, lastModified);
}
// add the etag header
if (etag != null) {
httpResponse.addHeader(CacheFilter.HEADER_ETAG, etag);
}
// add the content disposition header
if(contentDisposition != null) {
httpResponse.addHeader(CacheFilter.HEADER_CONTENT_DISPOSITION, contentDisposition);
}
// add the expires header
if (expires != Long.MAX_VALUE) {
httpResponse.setDateHeader(CacheFilter.HEADER_EXPIRES, expires);
}
// add the cache-control header for max-age
if (maxAge == CacheFilter.MAX_AGE_NO_INIT || maxAge == CacheFilter.MAX_AGE_TIME) {
// do nothing
} else if (maxAge > 0) { // set max-age based on life time
long currentMaxAge = maxAge / 1000 - System.currentTimeMillis() / 1000;
if (currentMaxAge < 0) {
currentMaxAge = 0;
}
httpResponse.addHeader(CacheFilter.HEADER_CACHE_CONTROL, "max-age=" + currentMaxAge);
} else {
httpResponse.addHeader(CacheFilter.HEADER_CACHE_CONTROL, "max-age=" + (-maxAge));
}
}
}
if (locale != null) {
response.setLocale(locale);
}
OutputStream out = new BufferedOutputStream(response.getOutputStream());
if (isContentGZiped()) {
if (acceptsGZip) {
((HttpServletResponse) response).addHeader(CacheFilter.HEADER_CONTENT_ENCODING, "gzip");
response.setContentLength(content.length);
out.write(content);
} else {
// client doesn't support, so we have to uncompress it
ByteArrayInputStream bais = new ByteArrayInputStream(content);
GZIPInputStream zis = new GZIPInputStream(bais);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
int numBytesRead = 0;
byte[] tempBytes = new byte[4196];
while ((numBytesRead = zis.read(tempBytes, 0, tempBytes.length)) != -1) {
baos.write(tempBytes, 0, numBytesRead);
}
byte[] result = baos.toByteArray();
response.setContentLength(result.length);
out.write(result);
}
} else {
// the content isn't compressed
// regardless if the client browser supports gzip we will just return the content
response.setContentLength(content.length);
out.write(content);
}
out.flush();
}
/**
* @return true if the content is GZIP compressed
*/
public boolean isContentGZiped() {
return "gzip".equals(contentEncoding);
}
}

View file

@ -0,0 +1,94 @@
/*
* Copyright (c) 2002-2003 by OpenSymphony
* All rights reserved.
*/
package com.opensymphony.oscache.web.filter;
import java.io.IOException;
import java.io.OutputStream;
import javax.servlet.ServletOutputStream;
/**
* Extends the base <code>ServletOutputStream</code> class so that
* the stream can be captured as it gets written. This is achieved
* by overriding the <code>write()</code> methods and outputting
* the data to two streams - the original stream and a secondary stream
* that is designed to capture the written data.
*
* @version $Revision$
* @author <a href="mailto:sergek@lokitech.com">Serge Knystautas</a>
*/
public class SplitServletOutputStream extends ServletOutputStream {
OutputStream captureStream = null;
OutputStream passThroughStream = null;
/**
* Constructs a split output stream that both captures and passes through
* the servlet response.
*
* @param captureStream The stream that will be used to capture the data.
* @param passThroughStream The pass-through <code>ServletOutputStream</code>
* that will write the response to the client as originally intended.
*/
public SplitServletOutputStream(OutputStream captureStream, OutputStream passThroughStream) {
this.captureStream = captureStream;
this.passThroughStream = passThroughStream;
}
/**
* Writes the incoming data to both the output streams.
*
* @param value The int data to write.
* @throws IOException
*/
public void write(int value) throws IOException {
captureStream.write(value);
passThroughStream.write(value);
}
/**
* Writes the incoming data to both the output streams.
*
* @param value The bytes to write to the streams.
* @throws IOException
*/
public void write(byte[] value) throws IOException {
captureStream.write(value);
passThroughStream.write(value);
}
/**
* Writes the incoming data to both the output streams.
*
* @param b The bytes to write out to the streams.
* @param off The offset into the byte data where writing should begin.
* @param len The number of bytes to write.
* @throws IOException
*/
public void write(byte[] b, int off, int len) throws IOException {
captureStream.write(b, off, len);
passThroughStream.write(b, off, len);
}
/**
* Flushes both the output streams.
* @throws IOException
*/
public void flush() throws IOException {
super.flush();
captureStream.flush(); //why not?
passThroughStream.flush();
}
/**
* Closes both the output streams.
* @throws IOException
*/
public void close() throws IOException {
super.close();
captureStream.close();
passThroughStream.close();
}
}

View file

@ -0,0 +1,33 @@
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">
<html>
<head>
<!--
@(#)package.html
Copyright (c) 2002-2003 by OpenSymphony
All rights reserved.
-->
</head>
<body bgcolor="white">
Provides the caching filter (and its support classes) that allows HTTP responses
to be cached by OSCache.
<h2>Package Specification</h2>
<h2>Related Documentation</h2>
For overviews, tutorials, examples, guides, and tool documentation, please see:
<ul>
<li><a href="http://www.opensymphony.com/oscache">The OSCache Homepage</a>
<li><a href="http://www.opensymphony.com/oscache/filter.jsp">Caching Filter Documentation</a>
</ul>
<!-- Put @see and @since tags down here. -->
</body>
</html>

View file

@ -0,0 +1,31 @@
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">
<html>
<head>
<!--
@(#)package.html
Copyright (c) 2002-2003 by OpenSymphony
All rights reserved.
-->
</head>
<body bgcolor="white">
Provides classes and interfaces that make up the base of OSCache's web application support.
<h2>Package Specification</h2>
<h2>Related Documentation</h2>
For overviews, tutorials, examples, guides, and tool documentation, please see:
<ul>
<li><a href="http://www.opensymphony.com/oscache">The OSCache Homepage</a>
</ul>
<!-- Put @see and @since tags down here. -->
</body>
</html>

View file

@ -0,0 +1,824 @@
/*
* Copyright (c) 2002-2003 by OpenSymphony
* All rights reserved.
*/
package com.opensymphony.oscache.web.tag;
import com.opensymphony.oscache.base.Cache;
import com.opensymphony.oscache.base.NeedsRefreshException;
import com.opensymphony.oscache.util.StringUtil;
import com.opensymphony.oscache.web.ServletCacheAdministrator;
import com.opensymphony.oscache.web.WebEntryRefreshPolicy;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.jsp.JspTagException;
import javax.servlet.jsp.PageContext;
import javax.servlet.jsp.tagext.BodyTagSupport;
import javax.servlet.jsp.tagext.TryCatchFinally;
/**
* CacheTag is a tag that allows for server-side caching of post-processed JSP content.<p>
*
* It also gives great programatic control over refreshing, flushing and updating the cache.<p>
*
* Usage Example:
* <pre><code>
* &lt;%@ taglib uri="oscache" prefix="cache" %&gt;
* &lt;cache:cache key="mycache"
* scope="application"
* refresh="false"
* time="30">
* jsp content here... refreshed every 30 seconds
* &lt;/cache:cache&gt;
* </code></pre>
*
* @author <a href="mailto:mike@atlassian.com">Mike Cannon-Brookes</a>
* @author <a href="mailto:tgochenour@peregrine.com">Todd Gochenour</a>
* @author <a href="mailto:fbeauregard@pyxis-tech.com">Francois Beauregard</a>
* @author <a href="mailto:abergevin@pyxis-tech.com">Alain Bergevin</a>
* @version $Revision$
*/
public class CacheTag extends BodyTagSupport implements TryCatchFinally {
/**
* Constants for time computation
*/
private final static int SECOND = 1;
private final static int MINUTE = 60 * SECOND;
private final static int HOUR = 60 * MINUTE;
private final static int DAY = 24 * HOUR;
private final static int WEEK = 7 * DAY;
private final static int MONTH = 30 * DAY;
private final static int YEAR = 365 * DAY;
/**
* The key under which the tag counter will be stored in the request
*/
private final static String CACHE_TAG_COUNTER_KEY = "__oscache_tag_counter";
/**
* Constants for refresh time
*/
final static private int ONE_MINUTE = 60;
final static private int ONE_HOUR = 60 * ONE_MINUTE;
final static private int DEFAULT_TIMEOUT = ONE_HOUR;
private static transient Log log = LogFactory.getLog(CacheTag.class);
/**
* Cache modes
*/
final static private int SILENT_MODE = 1;
/**
* A flag to indicate whether a NeedsRefreshException was thrown and
* the update needs to be cancelled
*/
boolean cancelUpdateRequired = false;
private Cache cache = null;
/**
* If no groups are specified, the cached content does not get put into any groups
*/
private List groups = null;
private ServletCacheAdministrator admin = null;
/**
* The actual key to use. This is generated based on the supplied key, scope etc.
*/
private String actualKey = null;
/**
* The content that was retrieved from cache
*/
private String content = null;
/**
* The cron expression that is used to expire cache entries at specific dates and/or times.
*/
private String cron = null;
/**
* if cache key is null, the request URI is used
*/
private String key = null;
/**
* The ISO-639 language code to distinguish different pages in application scope
*/
private String language = null;
/**
* Class used to handle the refresh policy logic
*/
private String refreshPolicyClass = null;
/**
* Parameters that will be passed to the init method of the
* refresh policy instance.
*/
private String refreshPolicyParam = null;
/**
* Whether the cache should be refreshed instantly
*/
private boolean refresh = false;
/**
* used for subtags to tell this tag that we should use the cached version
*/
private boolean useBody = true;
/**
* The cache mode. Valid values are SILENT_MODE
*/
private int mode = 0;
/**
* The cache scope to use
*/
private int scope = PageContext.APPLICATION_SCOPE;
/**
* time (in seconds) before cache should be refreshed
*/
private int time = DEFAULT_TIMEOUT;
/**
* Set the time this cache entry will be cached for. A date and/or time in
* either ISO-8601 format or a simple format can be specified. The acceptable
* syntax for the simple format can be any one of the following:
*
* <ul>
* <li>0 (seconds)
* <li>0s (seconds)
* <li>0m (minutes)
* <li>0h (hours)
* <li>0d (days)
* <li>0w (weeks)
* </ul>
*
* @param duration The duration to cache this content (using either the simple
* or the ISO-8601 format). Passing in a duration of zero will turn off the
* caching, while a negative value will result in the cached content never
* expiring (ie, the cached content will always be served as long as it is
* present).
*/
public void setDuration(String duration) {
try {
// Try Simple Date Format Duration first because it's faster
this.time = parseDuration(duration);
} catch (Exception ex) {
if (log.isDebugEnabled()) {
log.debug("Failed parsing simple duration format '" + duration + "' (" + ex.getMessage() + "). Trying ISO-8601 format...");
}
try {
// Try ISO-8601 Duration
this.time = parseISO_8601_Duration(duration);
} catch (Exception ex1) {
// An invalid duration entered, not much impact.
// The default timeout will be used
log.warn("The requested cache duration '" + duration + "' is invalid (" + ex1.getMessage() + "). Reverting to the default timeout");
this.time = DEFAULT_TIMEOUT;
}
}
}
/**
* Sets the cron expression that should be used to expire content at specific
* dates and/or times.
*/
public void setCron(String cron) {
this.cron = cron;
}
/**
* Sets the groups for this cache entry. Any existing groups will
* be replaced.
*
* @param groups A comma-delimited list of groups that the cache entry belongs to.
*/
public void setGroups(String groups) {
// FIXME: ArrayList doesn't avoid duplicates
this.groups = StringUtil.split(groups, ',');
}
/**
* Adds to the groups for this cache entry.
*
* @param group A group to which the cache entry should belong.
*/
void addGroup(String group) {
if (groups == null) {
// FIXME: ArrayList doesn't avoid duplicates
groups = new ArrayList();
}
groups.add(group);
}
/**
* Adds comma-delimited list of groups that the cache entry belongs to.
*
* @param groups A comma-delimited list of groups that the cache entry belongs to also.
*/
void addGroups(String groupsString) {
if (groups == null) {
// FIXME: ArrayList doesn't avoid duplicates
groups = new ArrayList();
}
groups.addAll(StringUtil.split(groupsString, ','));
}
/**
* Set the key for this cache entry.
*
* @param key The key for this cache entry.
*/
public void setKey(String key) {
this.key = key;
}
/**
* Set the ISO-639 language code to distinguish different pages in application scope
*
* @param language The language code for this cache entry.
*/
public void setLanguage(String language) {
this.language = language;
}
/**
* This method allows the user to programatically decide whether the cached
* content should be refreshed immediately.
*
* @param refresh Whether or not to refresh this cache entry immediately.
*/
public void setRefresh(boolean refresh) {
this.refresh = refresh;
}
/**
* Setting this to <code>true</code> prevents the cache from writing any output
* to the response, however the JSP content is still cached as normal.
* @param mode The cache mode to use.
*/
public void setMode(String mode) {
if ("silent".equalsIgnoreCase(mode)) {
this.mode = SILENT_MODE;
} else {
this.mode = 0;
}
}
/**
* Class used to handle the refresh policy logic
*/
public void setRefreshpolicyclass(String refreshPolicyClass) {
this.refreshPolicyClass = refreshPolicyClass;
}
/**
* Parameters that will be passed to the init method of the
* refresh policy instance.
*/
public void setRefreshpolicyparam(String refreshPolicyParam) {
this.refreshPolicyParam = refreshPolicyParam;
}
// ----------- setMethods ------------------------------------------------------
/**
* Set the scope of this cache.
* <p>
* @param scope The scope of this cache. Either "application" (default) or "session".
*/
public void setScope(String scope) {
if (scope.equalsIgnoreCase(ServletCacheAdministrator.SESSION_SCOPE_NAME)) {
this.scope = PageContext.SESSION_SCOPE;
} else {
this.scope = PageContext.APPLICATION_SCOPE;
}
}
/**
* Set the time this cache entry will be cached for (in seconds)
*
* @param time The time to cache this content (in seconds). Passing in
* a time of zero will turn off the caching. A negative value for the
* time will result in the cached content never expiring (ie, the cached
* content will always be served if it is present)
*/
public void setTime(int time) {
this.time = time;
}
/**
* This controls whether or not the body of the tag is evaluated or used.<p>
*
* It is most often called by the &lt;UseCached /&gt; tag to tell this tag to
* use the cached content.
*
* @see UseCachedTag
* @param useBody Whether or not to use the cached content.
*/
public void setUseBody(boolean useBody) {
if (log.isDebugEnabled()) {
log.debug("<cache>: Set useBody to " + useBody);
}
this.useBody = useBody;
}
/**
* After the cache body, either update the cache, serve new cached content or
* indicate an error.
*
* @throws JspTagException The standard exception thrown.
* @return The standard BodyTag return.
*/
public int doAfterBody() throws JspTagException {
String body = null;
try {
// if we have a body, and we have not been told to use the cached version
if ((bodyContent != null) && (useBody || (time == 0)) && ((body = bodyContent.getString()) != null)) {
if ((time != 0) || (refreshPolicyClass != null)) {
// Instantiate custom refresh policy if needed
WebEntryRefreshPolicy policy = null;
if (refreshPolicyClass != null) {
try {
policy = (WebEntryRefreshPolicy) Class.forName(refreshPolicyClass).newInstance();
policy.init(actualKey, refreshPolicyParam);
} catch (Exception e) {
if (log.isInfoEnabled()) {
log.info("<cache>: Problem instantiating or initializing refresh policy : " + refreshPolicyClass);
}
}
}
if (log.isDebugEnabled()) {
log.debug("<cache>: Updating cache entry with new content : " + actualKey);
}
cancelUpdateRequired = false;
if ((groups == null) || groups.isEmpty()) {
cache.putInCache(actualKey, body, policy);
} else {
String[] groupArray = new String[groups.size()];
groups.toArray(groupArray);
cache.putInCache(actualKey, body, groupArray, policy, null);
}
}
}
// otherwise if we have been told to use the cached content and we have cached content
else {
if (!useBody && (content != null)) {
if (log.isInfoEnabled()) {
log.info("<cache>: Using cached version as instructed, useBody = false : " + actualKey);
}
body = content;
}
// either the cached entry is blank and a subtag has said don't useBody, or body is null
else {
if (log.isInfoEnabled()) {
log.info("<cache>: Missing cached content : " + actualKey);
}
body = "Missing cached content";
}
}
// Only display anything if we're not running in silent mode
if (mode != SILENT_MODE) {
bodyContent.clearBody();
bodyContent.write(body);
bodyContent.writeOut(bodyContent.getEnclosingWriter());
}
} catch (java.io.IOException e) {
throw new JspTagException("IO Error: " + e.getMessage());
}
return SKIP_BODY;
}
public void doCatch(Throwable throwable) throws Throwable {
throw throwable;
}
/**
* The end tag - clean up variables used.
*
* @throws JspTagException The standard exception thrown.
* @return The standard BodyTag return.
*/
public int doEndTag() throws JspTagException {
return EVAL_PAGE;
}
public void doFinally() {
if (cancelUpdateRequired && (actualKey != null)) {
cache.cancelUpdate(actualKey);
}
// reset all states, CACHE-144
groups = null;
scope = PageContext.APPLICATION_SCOPE;
cron = null;
key = null;
language = null;
refreshPolicyClass = null;
refreshPolicyParam = null;
time = DEFAULT_TIMEOUT;
refresh = false;
mode = 0;
}
/**
* The start of the tag.
* <p>
* Grabs the administrator, the cache, the specific cache entry, then decides
* whether to refresh.
* <p>
* If no refresh is needed, this serves the cached content directly.
*
* @throws JspTagException The standard exception thrown.
* @return The standard doStartTag() return.
*/
public int doStartTag() throws JspTagException {
cancelUpdateRequired = false;
useBody = true;
content = null;
// We can only skip the body if the cache has the data
int returnCode = EVAL_BODY_BUFFERED;
if (admin == null) {
admin = ServletCacheAdministrator.getInstance(pageContext.getServletContext());
}
// Retrieve the cache
if (scope == PageContext.SESSION_SCOPE) {
cache = admin.getSessionScopeCache(((HttpServletRequest) pageContext.getRequest()).getSession(true));
} else {
cache = admin.getAppScopeCache(pageContext.getServletContext());
}
// This allows to have multiple cache tags on a single page without
// having to specify keys. However, nested cache tags are not supported.
// In that case you would have to supply a key.
String suffix = null;
if (key == null) {
synchronized (pageContext.getRequest()) {
Object o = pageContext.getRequest().getAttribute(CACHE_TAG_COUNTER_KEY);
if (o == null) {
suffix = "1";
} else {
suffix = Integer.toString(Integer.parseInt((String) o) + 1);
}
}
pageContext.getRequest().setAttribute(CACHE_TAG_COUNTER_KEY, suffix);
}
// Generate the actual cache key
actualKey = admin.generateEntryKey(key, (HttpServletRequest) pageContext.getRequest(), scope, language, suffix);
/*
if
- refresh is not set,
- the cacheEntry itself does not need to be refreshed before 'time' and
- the administrator has not had the cache entry's scope flushed
send out the cached version!
*/
try {
if (refresh) {
// Force a refresh
content = (String) cache.getFromCache(actualKey, 0, cron);
} else {
// Use the specified refresh period
content = (String) cache.getFromCache(actualKey, time, cron);
}
try {
if (log.isDebugEnabled()) {
log.debug("<cache>: Using cached entry : " + actualKey);
}
// Ensure that the cache returns the data correctly. Else re-evaluate the body
if ((content != null)) {
if (mode != SILENT_MODE) {
pageContext.getOut().write(content);
}
returnCode = SKIP_BODY;
}
} catch (IOException e) {
throw new JspTagException("IO Exception: " + e.getMessage());
}
} catch (NeedsRefreshException nre) {
cancelUpdateRequired = true;
content = (String) nre.getCacheContent();
}
if (returnCode == EVAL_BODY_BUFFERED) {
if (log.isDebugEnabled()) {
log.debug("<cache>: Cached content not used: New cache entry, cache stale or scope flushed : " + actualKey);
}
}
return returnCode;
}
/**
* Convert a SimpleDateFormat string to seconds
* Acceptable format are :
* <ul>
* <li>0s (seconds)
* <li>0m (minute)
* <li>0h (hour)
* <li>0d (day)
* <li>0w (week)
* </ul>
* @param duration The simple date time to parse
* @return The value in seconds
*/
private int parseDuration(String duration) {
int time = 0;
//Detect if the factor is specified
try {
time = Integer.parseInt(duration);
} catch (Exception ex) {
//Extract number and ajust this number with the time factor
for (int i = 0; i < duration.length(); i++) {
if (!Character.isDigit(duration.charAt(i))) {
time = Integer.parseInt(duration.substring(0, i));
switch ((int) duration.charAt(i)) {
case (int) 's':
time *= SECOND;
break;
case (int) 'm':
time *= MINUTE;
break;
case (int) 'h':
time *= HOUR;
break;
case (int) 'd':
time *= DAY;
break;
case (int) 'w':
time *= WEEK;
break;
default:
//no defined use as is
}
break;
}
// if
}
// for
}
// catch
return time;
}
/**
* Parse an ISO-8601 format date and return it's value in seconds
*
* @param duration The ISO-8601 date
* @return The equivalent number of seconds
* @throws Exception
*/
private int parseISO_8601_Duration(String duration) throws Exception {
int years = 0;
int months = 0;
int days = 0;
int hours = 0;
int mins = 0;
int secs = 0;
// If there is a negative sign, it must be first
// If it is present, we will ignore it
int index = duration.indexOf("-");
if (index > 0) {
throw new Exception("Invalid duration (- must be at the beginning)");
}
// First caracter must be P
String workValue = duration.substring(index + 1);
if (workValue.charAt(0) != 'P') {
throw new Exception("Invalid duration (P must be at the beginning)");
}
// Must contain a value
workValue = workValue.substring(1);
if (workValue.length() == 0) {
throw new Exception("Invalid duration (nothing specified)");
}
// Check if there is a T
index = workValue.indexOf('T');
String timeString = "";
if (index > 0) {
timeString = workValue.substring(index + 1);
// Time cannot be empty
if (timeString.equals("")) {
throw new Exception("Invalid duration (T with no time)");
}
workValue = workValue.substring(0, index);
} else if (index == 0) {
timeString = workValue.substring(1);
workValue = "";
}
if (!workValue.equals("")) {
validateDateFormat(workValue);
int yearIndex = workValue.indexOf('Y');
int monthIndex = workValue.indexOf('M');
int dayIndex = workValue.indexOf('D');
if ((yearIndex != -1) && (monthIndex != -1) && (yearIndex > monthIndex)) {
throw new Exception("Invalid duration (Date part not properly specified)");
}
if ((yearIndex != -1) && (dayIndex != -1) && (yearIndex > dayIndex)) {
throw new Exception("Invalid duration (Date part not properly specified)");
}
if ((dayIndex != -1) && (monthIndex != -1) && (monthIndex > dayIndex)) {
throw new Exception("Invalid duration (Date part not properly specified)");
}
if (yearIndex >= 0) {
years = (new Integer(workValue.substring(0, yearIndex))).intValue();
}
if (monthIndex >= 0) {
months = (new Integer(workValue.substring(yearIndex + 1, monthIndex))).intValue();
}
if (dayIndex >= 0) {
if (monthIndex >= 0) {
days = (new Integer(workValue.substring(monthIndex + 1, dayIndex))).intValue();
} else {
if (yearIndex >= 0) {
days = (new Integer(workValue.substring(yearIndex + 1, dayIndex))).intValue();
} else {
days = (new Integer(workValue.substring(0, dayIndex))).intValue();
}
}
}
}
if (!timeString.equals("")) {
validateHourFormat(timeString);
int hourIndex = timeString.indexOf('H');
int minuteIndex = timeString.indexOf('M');
int secondIndex = timeString.indexOf('S');
if ((hourIndex != -1) && (minuteIndex != -1) && (hourIndex > minuteIndex)) {
throw new Exception("Invalid duration (Time part not properly specified)");
}
if ((hourIndex != -1) && (secondIndex != -1) && (hourIndex > secondIndex)) {
throw new Exception("Invalid duration (Time part not properly specified)");
}
if ((secondIndex != -1) && (minuteIndex != -1) && (minuteIndex > secondIndex)) {
throw new Exception("Invalid duration (Time part not properly specified)");
}
if (hourIndex >= 0) {
hours = (new Integer(timeString.substring(0, hourIndex))).intValue();
}
if (minuteIndex >= 0) {
mins = (new Integer(timeString.substring(hourIndex + 1, minuteIndex))).intValue();
}
if (secondIndex >= 0) {
if (timeString.length() != (secondIndex + 1)) {
throw new Exception("Invalid duration (Time part not properly specified)");
}
if (minuteIndex >= 0) {
timeString = timeString.substring(minuteIndex + 1, timeString.length() - 1);
} else {
if (hourIndex >= 0) {
timeString = timeString.substring(hourIndex + 1, timeString.length() - 1);
} else {
timeString = timeString.substring(0, timeString.length() - 1);
}
}
if (timeString.indexOf('.') == (timeString.length() - 1)) {
throw new Exception("Invalid duration (Time part not properly specified)");
}
secs = (new Double(timeString)).intValue();
}
}
// Compute Value
return secs + (mins * MINUTE) + (hours * HOUR) + (days * DAY) + (months * MONTH) + (years * YEAR);
}
/**
* Validate the basic date format
*
* @param basicDate The string to validate
* @throws Exception
*/
private void validateDateFormat(String basicDate) throws Exception {
int yearCounter = 0;
int monthCounter = 0;
int dayCounter = 0;
for (int counter = 0; counter < basicDate.length(); counter++) {
// Check if there's any other caracters than Y, M, D and numbers
if (!Character.isDigit(basicDate.charAt(counter)) && (basicDate.charAt(counter) != 'Y') && (basicDate.charAt(counter) != 'M') && (basicDate.charAt(counter) != 'D')) {
throw new Exception("Invalid duration (Date part not properly specified)");
}
// Check if the allowed caracters are present more than 1 time
if (basicDate.charAt(counter) == 'Y') {
yearCounter++;
}
if (basicDate.charAt(counter) == 'M') {
monthCounter++;
}
if (basicDate.charAt(counter) == 'D') {
dayCounter++;
}
}
if ((yearCounter > 1) || (monthCounter > 1) || (dayCounter > 1)) {
throw new Exception("Invalid duration (Date part not properly specified)");
}
}
/**
* Validate the basic hour format
*
* @param basicHour The string to validate
* @throws Exception
*/
private void validateHourFormat(String basicHour) throws Exception {
int minuteCounter = 0;
int secondCounter = 0;
int hourCounter = 0;
for (int counter = 0; counter < basicHour.length(); counter++) {
if (!Character.isDigit(basicHour.charAt(counter)) && (basicHour.charAt(counter) != 'H') && (basicHour.charAt(counter) != 'M') && (basicHour.charAt(counter) != 'S') && (basicHour.charAt(counter) != '.')) {
throw new Exception("Invalid duration (Time part not properly specified)");
}
if (basicHour.charAt(counter) == 'H') {
hourCounter++;
}
if (basicHour.charAt(counter) == 'M') {
minuteCounter++;
}
if (basicHour.charAt(counter) == 'S') {
secondCounter++;
}
}
if ((hourCounter > 1) || (minuteCounter > 1) || (secondCounter > 1)) {
throw new Exception("Invalid duration (Time part not properly specified)");
}
}
}

View file

@ -0,0 +1,171 @@
/*
* Copyright (c) 2002-2003 by OpenSymphony
* All rights reserved.
*/
package com.opensymphony.oscache.web.tag;
import com.opensymphony.oscache.base.Cache;
import com.opensymphony.oscache.web.ServletCacheAdministrator;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.jsp.JspTagException;
import javax.servlet.jsp.PageContext;
import javax.servlet.jsp.tagext.TagSupport;
/**
* FlushTag flushes caches created with &lt;cache&gt;.
*
* This tag provides programmatic control over when caches are flushed,
* and can flush all caches at once.<p>
*
* Usage Examples:
* <pre><code>
* &lt;%@ taglib uri="oscache" prefix="cache" %&gt;
* &lt;cache:flush scope="application" /&gt;
* &lt;cache:flush scope="session" key="foobar" /&gt;
* </code></pre>
*
* Note: If no scope is provided (or scope is null), it will flush
* all caches globally - use with care!<p>
* <p>
* Flushing is done by setting an appropriate application level time,
* which &lt;cache&gt; always looks at before retrieving the cache.
* If this 'flush time' is &gt; that cache's last update, it will refresh
* the cache.
* <p>
* As such caches are not all 'flushed', they are all marked
* to be refreshed at their next access. That is the only way that
* the content can still be available if the refresh fails.
*
* @author <a href="mailto:mike@atlassian.com">Mike Cannon-Brookes</a>
* @author <a href="&#109;a&#105;&#108;&#116;&#111;:chris&#64;swebtec.&#99;&#111;&#109;">Chris Miller</a>
* @version $Revision$
*/
public class FlushTag extends TagSupport {
ServletCacheAdministrator admin = null;
/**
* A cache group.
* If specified, all content in that group will be flushed
*/
String group = null;
/**
* Tag key.
*/
String key = null;
/**
* if pattern value is specified, all keys that contain the pattern are flushed.
*/
String pattern = null;
String scope = null;
int cacheScope = -1;
/**
* The ISO-639 language code to distinguish different pages in application scope.
*/
private String language = null;
/**
* The group to be flushed.
* If specified, all cached content in the group will be flushed.
*
* @param group The name of the group to flush.
*/
public void setGroup(String group) {
this.group = group;
}
/**
* The key to be flushed.
* If specified, only one cache entry will be flushed.
*
* @param value The key of the specific entry to flush.
*/
public void setKey(String value) {
this.key = value;
}
/**
* Set the ISO-639 language code to distinguish different pages in application scope.
*
* @param value The language code for this cache entry.
*/
public void setLanguage(String value) {
this.language = value;
}
/**
* The key pattern to be flushed.
* If specified, all entries that contain the pattern will be flushed.
* @param value The key of the specific entry to flush.
*/
public void setPattern(String value) {
this.pattern = value;
}
/**
* Set the scope of this flush.
*
* @param value The scope - either "application" (default) or "session".
*/
public void setScope(String value) {
if (value != null) {
if (value.equalsIgnoreCase(ServletCacheAdministrator.SESSION_SCOPE_NAME)) {
cacheScope = PageContext.SESSION_SCOPE;
} else if (value.equalsIgnoreCase(ServletCacheAdministrator.APPLICATION_SCOPE_NAME)) {
cacheScope = PageContext.APPLICATION_SCOPE;
}
}
}
/**
* Process the start of the tag.
*
* @throws JspTagException The standard tag exception thrown.
* @return The standard Tag return.
*/
public int doStartTag() throws JspTagException {
if (admin == null) {
admin = ServletCacheAdministrator.getInstance(pageContext.getServletContext());
}
if (group != null) // We're flushing a group
{
if (cacheScope >= 0) {
Cache cache = admin.getCache((HttpServletRequest) pageContext.getRequest(), cacheScope);
cache.flushGroup(group);
} else {
throw new JspTagException("A cache group was specified for flushing, but the scope wasn't supplied or was invalid");
}
} else if (pattern != null) // We're flushing keys which contain the pattern
{
if (cacheScope >= 0) {
Cache cache = admin.getCache((HttpServletRequest) pageContext.getRequest(), cacheScope);
cache.flushPattern(pattern);
} else {
throw new JspTagException("A pattern was specified for flushing, but the scope wasn't supplied or was invalid");
}
} else if (key == null) // we're flushing a whole scope
{
if (cacheScope >= 0) {
admin.setFlushTime(cacheScope);
} else {
admin.flushAll();
}
} else // we're flushing just one key
{
if (cacheScope >= 0) {
String actualKey = admin.generateEntryKey(key, (HttpServletRequest) pageContext.getRequest(), cacheScope, language);
Cache cache = admin.getCache((HttpServletRequest) pageContext.getRequest(), cacheScope);
cache.flushEntry(actualKey);
} else {
throw new JspTagException("A cache key was specified for flushing, but the scope wasn't supplied or was invalid");
}
}
return SKIP_BODY;
}
}

View file

@ -0,0 +1,32 @@
/*
* Copyright (c) 2002-2003 by OpenSymphony
* All rights reserved.
*/
package com.opensymphony.oscache.web.tag;
import javax.servlet.jsp.JspTagException;
import javax.servlet.jsp.tagext.TagSupport;
/**
* GroupTag is a tag that adds a group to an ancestor CacheTag's groups.<p>
*
* @author <a href="mailto:robvdv@yahoo.com">Robert van der Vliet</a>
*/
public class GroupTag extends TagSupport {
private Object group = null;
public int doStartTag() throws JspTagException {
CacheTag ancestorCacheTag = (CacheTag) TagSupport.findAncestorWithClass(this, CacheTag.class);
if (ancestorCacheTag == null) {
throw new JspTagException("GroupTag cannot be used from outside a CacheTag");
}
ancestorCacheTag.addGroup(String.valueOf(group));
return EVAL_BODY_INCLUDE;
}
public void setGroup(Object group) {
this.group = group;
}
}

View file

@ -0,0 +1,33 @@
/*
* Copyright (c) 2002-2003 by OpenSymphony
* All rights reserved.
*/
package com.opensymphony.oscache.web.tag;
import javax.servlet.jsp.JspTagException;
import javax.servlet.jsp.tagext.TagSupport;
/**
* GroupsTag is a tag that add a comma-delimited list of groups to an ancestor CacheTag's groups.<p>
*
* @author <a href="mailto:ltorunski@t-online.de">Lars Torunski</a>
*/
public class GroupsTag extends TagSupport {
private Object groups = null;
public int doStartTag() throws JspTagException {
CacheTag ancestorCacheTag = (CacheTag) TagSupport.findAncestorWithClass(this, CacheTag.class);
if (ancestorCacheTag == null) {
throw new JspTagException("GroupsTag cannot be used from outside a CacheTag");
}
ancestorCacheTag.addGroups(String.valueOf(groups));
return EVAL_BODY_INCLUDE;
}
public void setGroups(Object groups) {
this.groups = groups;
}
}

View file

@ -0,0 +1,60 @@
/*
* Copyright (c) 2002-2003 by OpenSymphony
* All rights reserved.
*/
package com.opensymphony.oscache.web.tag;
import javax.servlet.jsp.JspTagException;
import javax.servlet.jsp.tagext.TagSupport;
/**
* UseCachedTag is a tag that tells a &lt;cache&gt; tag to reuse the cached body.<p>
*
* Usage Example:
* <pre><code>
* &lt;%@ taglib uri="oscache" prefix="cache" %&gt;
* &lt;cache:cache key="mycache" scope="application"&gt;
* if (reuse cached)
* &lt;cache:usecached /&gt;
* else
* some other logic
* &lt;/cache:cache&gt;
* </code></pre>
*
* Note this is very useful with try / catch blocks
* so that you can still produce old cached data if an
* exception occurs, eg your database goes down.<p>
*
* @author <a href="mailto:mike@atlassian.com">Mike Cannon-Brookes</a>
* @version $Revision$
*/
public class UseCachedTag extends TagSupport {
boolean use = true;
/**
* Set the decision to use the body content of the ancestor &lt;cache&gt; or not.
*
* @param value Whether or not to use the body content. Default is true.
*/
public void setUse(boolean value) {
this.use = value;
}
/**
* The start tag.
*
* @throws JspTagException The standard tag exception thrown.
* @return The standard Tag return.
*/
public int doStartTag() throws JspTagException {
CacheTag cacheTag = (CacheTag) TagSupport.findAncestorWithClass(this, CacheTag.class);
if (cacheTag == null) {
throw new JspTagException("A UseCached tag must be nested within a Cache tag");
}
cacheTag.setUseBody(!use);
return SKIP_BODY;
}
}

View file

@ -0,0 +1,33 @@
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">
<html>
<head>
<!--
@(#)package.html
Copyright (c) 2002-2003 by OpenSymphony
All rights reserved.
-->
</head>
<body bgcolor="white">
Provides the tag libraries that allow OSCache to be accessed via JSP custom tags for
caching portions of JSP pages.
<h2>Package Specification</h2>
<h2>Related Documentation</h2>
For overviews, tutorials, examples, guides, and tool documentation, please see:
<ul>
<li><a href="http://www.opensymphony.com/oscache">The OSCache Homepage</a>
<li><a href="http://www.opensymphony.com/oscache/tags.jsp">Tag Reference</a>
</ul>
<!-- Put @see and @since tags down here. -->
</body>
</html>