/**********************************************************************************
* $URL: https://source.sakaiproject.org/svn/kernel/trunk/kernel-impl/src/main/java/org/sakaiproject/memory/impl/MemCache.java $
* $Id: MemCache.java 105077 2012-02-24 22:54:29Z ottenhoff@longsight.com $
***********************************************************************************
*
* Copyright (c) 2003, 2004, 2005, 2006, 2007, 2008 Sakai Foundation
*
* Licensed under the Educational Community License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.opensource.org/licenses/ECL-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
**********************************************************************************/
package org.sakaiproject.memory.impl;
import java.lang.ref.SoftReference;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Observable;
import java.util.Observer;
import java.util.Set;
import java.util.Vector;
import net.sf.ehcache.Ehcache;
import net.sf.ehcache.Element;
import net.sf.ehcache.event.CacheEventListener;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.sakaiproject.component.cover.ComponentManager;
import org.sakaiproject.event.api.Event;
import org.sakaiproject.event.api.EventTrackingService;
import org.sakaiproject.memory.api.Cache;
import org.sakaiproject.memory.api.CacheRefresher;
import org.sakaiproject.memory.api.DerivedCache;
/**
* <p>
* A Cache of objects with keys with a limited lifespan.
* </p>
* <p>
* When the object expires, the cache calls upon a CacheRefresher to update the key's value. The update is done in a separate thread.
* </p>
*/
public class MemCache implements Cache, Observer
{
/** Our logger. */
private static Log M_log = LogFactory.getLog(MemCache.class);
/** Underlying cache implementation */
protected net.sf.ehcache.Ehcache cache;
/** The object that will deal with expired entries. */
protected CacheRefresher m_refresher = null;
/** The string that all resources in this cache will start with. */
protected String m_resourcePattern = null;
/** If true, we are disabled. */
protected boolean m_disabled = false;
/** If true, we have all the entries that there are in the cache. */
protected boolean m_complete = false;
/** Alternate isComplete, based on patterns. */
protected Set<String> m_partiallyComplete = new HashSet<String>();
/** If true, we are going to hold any events we see in the m_heldEvents list for later processing. */
protected boolean m_holdEventProcessing = false;
/** The events we are holding for later processing. */
protected List<Event> m_heldEvents = new Vector<Event>();
/** Constructor injected memory service. */
protected BasicMemoryService m_memoryService = null;
/** Constructor injected event tracking service. */
protected EventTrackingService m_eventTrackingService = null;
/** My (optional) DerivedCache. */
protected DerivedCache m_derivedCache = null;
/**
* The cache entry. Holds a time stamped payload.
*/
protected class CacheEntry extends SoftReference
{
/** Set if our payload is supposed to be null. */
protected boolean m_nullPayload = false;
/**
* Construct to cache the payload for the duration.
*
* @param payload
* The thing to cache.
* @param duration
* The time (seconds) to keep this cached.
* @deprecated
*/
public CacheEntry(Object payload, int duration)
{
// put the payload into the soft reference
super(payload);
// is it supposed to be null?
m_nullPayload = (payload == null);
} // CacheEntry
/**
* Get the cached object.
*
* @param key
* The key for this entry (if null, we won't try to refresh if missing)
* @return The cached object.
*/
public Object getPayload(Object key)
{
// if we hold null, this is easy
if (m_nullPayload)
{
return null;
}
// get the payload
Object payload = this.get();
// if it has been garbage collected, and we can, refresh it
if (payload == null)
{
if ((m_refresher != null) && (key != null))
{
// ask the refresher for the value
payload = m_refresher.refresh(key, null, null);
if (m_memoryService.getCacheLogging())
{
M_log.info("cache miss: refreshing: key: " + key + " new payload: " + payload);
}
// store this new value
put(key, payload);
}
else
{
if (m_memoryService.getCacheLogging())
{
M_log.info("cache miss: no refresh: key: " + key);
}
}
}
return payload;
}
} // CacheEntry
/**
* Construct the Cache. No automatic refresh handling.
*/
public MemCache(BasicMemoryService memoryService,
EventTrackingService eventTrackingService, Ehcache cache)
{
// inject our dependencies
m_memoryService = memoryService;
m_eventTrackingService = eventTrackingService;
this.cache = cache;
}
/**
* Construct the Cache. Attempts to keep complete on Event notification by calling the refresher.
*
* @param refresher
* The object that will handle refreshing of event notified modified or added entries.
* @param pattern
* The "startsWith()" string for all resources that may be in this cache - if null, don't watch events for updates.
*/
public MemCache(BasicMemoryService memoryService,
EventTrackingService eventTrackingService,
CacheRefresher refresher, String pattern, Ehcache cache)
{
this(memoryService, eventTrackingService, cache);
m_resourcePattern = pattern;
if (refresher != null)
{
m_refresher = refresher;
}
// register to get events - first, before others
if (pattern != null)
{
m_eventTrackingService.addPriorityObserver(this);
}
}
/**
* Construct the Cache. Automatic refresh handling if refresher is not null.
*
* @param refresher
* The object that will handle refreshing of expired entries.
* @param sleep
* The number of seconds to sleep between expiration checks.
* @deprecated long sleep no longer used with ehcache
*/
public MemCache(BasicMemoryService memoryService,
EventTrackingService eventTrackingService,
CacheRefresher refresher, long sleep, Ehcache cache)
{
this(memoryService, eventTrackingService, cache);
if (refresher != null)
{
m_refresher = refresher;
}
}
/**
* Construct the Cache. Automatic refresh handling if refresher is not null.
*
* @param refresher
* The object that will handle refreshing of expired entries.
*/
public MemCache(BasicMemoryService memoryService,
EventTrackingService eventTrackingService,
CacheRefresher refresher, Ehcache cache)
{
this(memoryService, eventTrackingService, cache);
if (refresher != null)
{
m_refresher = refresher;
}
}
/**
* Construct the Cache. Event scanning if pattern not null - will expire entries.
*
* @param sleep
* The number of seconds to sleep between expiration checks.
* @param pattern
* The "startsWith()" string for all resources that may be in this cache - if null, don't watch events for expiration.
* @deprecated long sleep no longer used with ehcache
*/
public MemCache(BasicMemoryService memoryService,
EventTrackingService eventTrackingService, long sleep,
String pattern, Ehcache cache)
{
this(memoryService, eventTrackingService, pattern, cache);
}
/**
* Construct the Cache. Event scanning if pattern not null - will expire entries.
*
* @param sleep
* The number of seconds to sleep between expiration checks.
* @param pattern
* The "startsWith()" string for all resources that may be in this cache - if null, don't watch events for expiration.
*/
public MemCache(BasicMemoryService memoryService,
EventTrackingService eventTrackingService, String pattern,
Ehcache cache)
{
this(memoryService, eventTrackingService, cache);
m_resourcePattern = pattern;
// register to get events - first, before others
if (pattern != null)
{
m_eventTrackingService.addPriorityObserver(this);
}
}
/**
* Clean up.
*/
public void destroy()
{
cache.removeAll(); //TODO Do we boolean doNotNotifyCacheReplicators? Ian?
cache.getStatistics().clearStatistics();
// if we are not in a global shutdown
if (!ComponentManager.hasBeenClosed())
{
// remove my event notification registration
m_eventTrackingService.deleteObserver(this);
}
}
/**
* {@inheritDoc}
*/
public void attachDerivedCache(DerivedCache cache)
{
// Note: only one is supported
if (cache == null)
{
m_derivedCache = null;
}
else
{
if (m_derivedCache != null)
{
M_log.warn("attachDerivedCache - already got one!");
}
else
{
m_derivedCache = cache;
}
// If has the (ehcache) EventCacheListener marker interface then
// also attach the cache as a listener that implements the
// ehcache event listener interface.
if (cache instanceof CacheEventListener) {
// add ehcahe event listener
Ehcache ehc = this.cache;
ehc.getCacheEventNotificationService().registerListener((CacheEventListener)cache);
}
}
}
/**
* Cache an object
*
* @param key
* The key with which to find the object.
* @param payload
* The object to cache.
* @param duration
* The time to cache the object (seconds).
* @deprecated
*/
public void put(Object key, Object payload, int duration)
{
if (M_log.isDebugEnabled()) {
M_log.debug("put(Object " + key + ", Object " + payload + ", int "
+ duration + ")");
}
put(key, payload);
}
/**
* Cache an object - don't automatically exipire it.
*
* @param key
* The key with which to find the object.
* @param payload
* The object to cache.
* @param duration
* The time to cache the object (seconds).
*/
public void put(Object key, Object payload)
{
if (M_log.isDebugEnabled()) {
M_log.debug("put(Object " + key + ", Object " + payload + ")");
}
if (disabled()) return;
cache.put(new Element(key, payload));
if (m_derivedCache != null) m_derivedCache.notifyCachePut(key, payload);
}
/**
* Test for an entry in the cache - expired or not.
*
* @param key
* The cache key.
* @return true if the key maps to a cache entry, false if not.
* @deprecated
*/
public boolean containsKeyExpiredOrNot(Object key)
{
if ( disabled() ) {
return false;
}
return cache.isKeyInCache(key);
} // containsKeyExpiredOrNot
/**
* Test for a non expired entry in the cache.
*
* @param key
* The cache key.
* @return true if the key maps to a non-expired cache entry, false if not.
*/
public boolean containsKey(Object key) {
if (M_log.isDebugEnabled())
{
M_log.debug("containsKey(Object " + key + ")");
}
if (disabled())
return false;
if ( cache.isKeyInCache(key) ) {
return ( cache.get(key) != null );
}
return false;
} // containsKey
/**
* Expire this object.
*
* @param key
* The cache key.
*/
public void expire(Object key)
{
if (M_log.isDebugEnabled())
{
M_log.debug("expire(Object " + key + ")");
}
if (disabled()) return;
// remove it
remove(key);
} // expire
/**
* Get the entry, or null if not there (expired entries are returned, too).
*
* @param key
* The cache key.
* @return The payload, or null if the payload is null, the key is not found. (Note: use containsKey() to remove this ambiguity).
* @deprecated
*/
public Object getExpiredOrNot(Object key)
{
if (M_log.isDebugEnabled())
{
M_log.debug("getExpiredOrNot(Object " + key + ")");
}
return get(key);
} // getExpiredOrNot
/**
* Get the non expired entry, or null if not there (or expired)
*
* @param key
* The cache key.
* @return The payload, or null if the payload is null, the key is not found, or the entry has expired (Note: use containsKey() to remove this ambiguity).
*/
public Object get(Object key)
{
if (M_log.isDebugEnabled())
{
M_log.debug("get(Object " + key + ")");
}
if (disabled()) return null;
final Element e = cache.get(key);
return(e != null ? e.getObjectValue() : null);
} // get
/**
* Get all the non-expired non-null entries.
*
* @return all the non-expired non-null entries, or an empty list if none.
* @deprecated
*/
public List getAll()
{ //TODO Why would you ever getAll objects from cache?
M_log.debug("getAll()");
if (disabled()) return Collections.emptyList();
final List<Object> keys = cache.getKeysWithExpiryCheck();
final List<Object> rv = new ArrayList<Object>(keys.size()); // return value
for (Object key : keys) {
final Object value = cache.get(key).getObjectValue();
if (value != null)
rv.add(value);
}
return rv;
} // getAll
/**
* Get all the non-expired non-null entries that are in the specified reference path. Note: only works with String keys.
*
* @param path
* The reference path.
* @return all the non-expired non-null entries, or an empty list if none.
*/
public List getAll(String path)
{
if (M_log.isDebugEnabled()) {
M_log.debug("getAll(String " + path + ")");
}
if (disabled()) return Collections.emptyList();
final List<Object> keys = cache.getKeysWithExpiryCheck();
final List<Object> rv = new ArrayList<Object>(keys.size()); // return value
for (Object key : keys) {
// take only if keys start with path, and have no SEPARATOR following other than at the end %%%
if (key instanceof String && referencePath((String) key).equals(path)) {
rv.add(cache.get(key).getObjectValue());
}
}
return rv;
} // getAll
/**
* Get all the keys
*
* @return The List of key values (Object).
*/
public List getKeys()
{
M_log.debug("getKeys()");
return cache.getKeys();
} // getKeys
/**
* Get all the keys, each modified to remove the resourcePattern prefix. Note: only works with String keys.
*
* @return The List of keys converted from references to ids (String).
*/
public List getIds() {
M_log.debug("getIds()");
if (disabled())
return Collections.emptyList();
final List<Object> keys = cache.getKeysWithExpiryCheck();
final List<Object> rv = new ArrayList<Object>(keys.size()); // return
// value
for (Object key : keys) {
if (key instanceof String) {
int i = ((String) key).indexOf(m_resourcePattern);
if (i != -1)
key = ((String) key).substring(i
+ m_resourcePattern.length());
rv.add(key);
}
}
return rv;
} // getIds
/**
* Clear all entries.
*/
public void clear()
{
M_log.debug("clear()");
cache.removeAll();
cache.getStatistics().clearStatistics();
if (m_derivedCache != null) m_derivedCache.notifyCacheClear();
} // clear
/**
* Remove this entry from the cache.
*
* @param key
* The cache key.
*/
public void remove(Object key)
{
if (M_log.isDebugEnabled()) {
M_log.debug("remove(Object " + key + ")");
}
if (disabled()) return;
// We could get things wrong here.
final Object value = get(key);
boolean found = cache.remove(key);
if (m_derivedCache != null)
{
Object old = null;
if (found)
{
old = value;
}
m_derivedCache.notifyCacheRemove(key, old);
}
} // remove
/**
* Disable the cache.
*/
public void disable()
{
M_log.debug("disable()");
m_disabled = true;
m_eventTrackingService.deleteObserver(this);
clear();
} // disable
/**
* Enable the cache.
*/
public void enable()
{
M_log.debug("enable()");
m_disabled = false;
if (m_resourcePattern != null)
{
m_eventTrackingService.addPriorityObserver(this);
}
} // enable
/**
* Is the cache disabled?
*
* @return true if the cache is disabled, false if it is enabled.
*/
public boolean disabled()
{
M_log.debug("disabled()");
return m_disabled;
} // disabled
/**
* Are we complete?
*
* @return true if we have all the possible entries cached, false if not.
*/
public boolean isComplete()
{
M_log.debug("isComplete()");
if (disabled()) return false;
return m_complete;
} // isComplete
/**
* Set the cache to be complete, containing all possible entries.
*/
public void setComplete()
{
M_log.debug("setComplete()");
if (disabled()) return;
m_complete = true;
} // isComplete
/**
* Are we complete for one level of the reference hierarchy?
*
* @param path
* The reference to the completion level.
* @return true if we have all the possible entries cached, false if not.
*/
public boolean isComplete(String path)
{
if (M_log.isDebugEnabled()) {
M_log.debug("isComplete(String " + path + ")");
}
return m_partiallyComplete.contains(path);
} // isComplete
/**
* Set the cache to be complete for one level of the reference hierarchy.
*
* @param path
* The reference to the completion level.
*/
public void setComplete(String path)
{
if (M_log.isDebugEnabled()) {
M_log.debug("setComplete(String " + path + ")");
}
m_partiallyComplete.add(path);
} // setComplete
/**
* Set the cache to hold events for later processing to assure an atomic "complete" load.
*/
public void holdEvents()
{
M_log.debug("holdEvents()");
m_holdEventProcessing = true;
} // holdEvents
/**
* Restore normal event processing in the cache, and process any held events now.
*/
public void processEvents()
{
M_log.debug("processEvents()");
m_holdEventProcessing = false;
for (int i = 0; i < m_heldEvents.size(); i++)
{
Event event = (Event) m_heldEvents.get(i);
continueUpdate(event);
}
m_heldEvents.clear();
} // holdEvents
/**********************************************************************************************************************************************************************************************************************************************************
* Cacher implementation
*********************************************************************************************************************************************************************************************************************************************************/
/**
* Clear out as much as possible anything cached; re-sync any cache that is needed to be kept.
*/
public void resetCache()
{
M_log.debug("resetCache()");
clear();
} // resetCache
/**
* Return the size of the cacher - indicating how much memory in use.
*
* @return The size of the cacher.
*/
public long getSize()
{
M_log.debug("getSize()");
return cache.getStatistics().getObjectCount();
}
/**
* Return a description of the cacher.
*
* @return The cacher's description.
*/
public String getDescription()
{
final StringBuilder buf = new StringBuilder();
buf.append("MemCache");
if (m_disabled)
{
buf.append(" disabled");
}
if (m_complete)
{
buf.append(" complete");
}
if (m_resourcePattern != null)
{
buf.append(" " + m_resourcePattern);
}
if (m_partiallyComplete.size() > 0)
{
buf.append(" partially_complete[");
for (Object element : m_partiallyComplete) {
buf.append(" " + element);
}
buf.append("]");
}
final long hits = cache.getStatistics().getCacheHits();
final long misses = cache.getStatistics().getCacheMisses();
final long total = hits + misses;
buf.append(" hits:" + hits + " misses:" + misses + " hit%:"
+ ((total > 0) ? "" + ((100l * hits) / total) : "n/a"));
return buf.toString();
}
/**********************************************************************************************************************************************************************************************************************************************************
* Observer implementation
*********************************************************************************************************************************************************************************************************************************************************/
/**
* This method is called whenever the observed object is changed. An application calls an <tt>Observable</tt> object's <code>notifyObservers</code> method to have all the object's observers notified of the change. default implementation is to
* cause the courier service to deliver to the interface controlled by my controller. Extensions can override.
*
* @param o
* the observable object.
* @param arg
* an argument passed to the <code>notifyObservers</code> method.
*/
public void update(Observable o, Object arg)
{
if (disabled()) return;
// arg is Event
if (!(arg instanceof Event)) return;
Event event = (Event) arg;
// if this is just a read, not a modify event, we can ignore it
if (!event.getModify()) return;
String key = event.getResource();
// if this resource is not in my pattern of resources, we can ignore it
if (!key.startsWith(m_resourcePattern)) return;
// if we are holding event processing
if (m_holdEventProcessing)
{
m_heldEvents.add(event);
return;
}
continueUpdate(event);
} // update
/**
* Complete the update, given an event that we know we need to act upon.
*
* @param event
* The event to process.
*/
protected void continueUpdate(Event event)
{
String key = event.getResource();
if (M_log.isDebugEnabled())
M_log.debug(this + ".update() [" + m_resourcePattern
+ "] resource: " + key + " event: " + event.getEvent());
// do we have this in our cache?
Object oldValue = get(key);
if (containsKey(key))
{
// invalidate our copy
remove(key);
}
// if we are being complete, we need to get this cached.
if (m_complete)
{
// we can only get it cached if we have a refresher
if (m_refresher != null)
{
// ask the refresher for the value
Object value = m_refresher.refresh(key, oldValue, event);
if (value != null)
{
put(key, value);
}
}
else
{
// we can no longer claim to be complete
m_complete = false;
}
}
// if we are partially complete
else if (!m_partiallyComplete.isEmpty())
{
// what is the reference path that this key lives within?
String path = referencePath(key);
// if we are partially complete for this path
if (m_partiallyComplete.contains(path))
{
// we can only get it cached if we have a refresher
if (m_refresher != null)
{
// ask the refresher for the value
Object value = m_refresher.refresh(key, oldValue, event);
if (value != null)
{
put(key, value);
}
}
else
{
// we can no longer claim to be complete for this path
m_partiallyComplete.remove(path);
}
}
}
} // continueUpdate
/**
* Compute the reference path (i.e. the container) for a given reference.
*
* @param ref
* The reference string.
* @return The reference root for the given reference.
*/
protected String referencePath(String ref)
{
String path = null;
// Note: there may be a trailing separator
int pos = ref.lastIndexOf("/", ref.length() - 2);
// if no separators are found, place it even before the root!
if (pos == -1)
{
path = "";
}
// use the string up to and including that last separator
else
{
path = ref.substring(0, pos + 1);
}
return path;
} // referencePath
}