/*
* Copyright 2005 Thomas Yip, Stein M. Hugubakken, Werner Guttmann, Ralf Joachim
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.castor.cache.simple;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Hashtable;
import java.util.Iterator;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.castor.cache.AbstractBaseCache;
import org.castor.cache.CacheAcquireException;
/**
* TimeLimited is a time limted first-in-first-out <tt>Map</tt>. Every object
* being put in the Map will live until the timeout expired.
* <p>
* The expiration time is passed to the cache at initialization by the individual
* cache property <b>ttl</b> which defines the timeout of every object in the cache in
* seconds. If not specified a timeout of 30 seconds will be used.
*
* @author <a href="mailto:yip AT intalio DOT com">Thomas Yip</a>
* @author <a href="mailto:dulci AT start DOT no">Stein M. Hugubakken</a>
* @author <a href="mailto:werner DOT guttmann AT gmx DOT net">Werner Guttmann</a>
* @author <a href="mailto:ralf DOT joachim AT syscon DOT eu">Ralf Joachim</a>
* @version $Revision$ $Date: 2006-04-25 16:09:10 -0600 (Tue, 25 Apr 2006) $
*/
public class TimeLimited extends AbstractBaseCache {
//--------------------------------------------------------------------------
/** The <a href="http://jakarta.apache.org/commons/logging/">Jakarta Commons
* Logging </a> instance used for all logging. */
private static final Log LOG = LogFactory.getLog(TimeLimited.class);
/** The type of the cache. */
public static final String TYPE = "time-limited";
/** Mapped initialization parameter <code>ttl</code>. */
public static final String PARAM_TTL = "ttl";
/** Default ttl of cache. */
public static final int DEFAULT_TTL = 30;
/** Seconds between ticks, default is 1 second. This value is used to decrease
* QueueItem.time on each tick. */
private static final int TICK_DELAY = 1;
/** The Default precision in millisecond is 1000. Precision is the interval
* between each time which the timer thread will wake up and trigger clean up
* of least-recently-used objects. */
private static final int DEFAULT_PRECISION = 1000 * TICK_DELAY;
/** Timer is used to start a task that runs the tick-method. */
private static final TickThread TIMER = new TickThread(DEFAULT_PRECISION);
/** Container for cached objects. */
private Hashtable<Object, QueueItem> _map = new Hashtable<Object, QueueItem>();
/** Real ttl of this cache. */
private int _ttl = DEFAULT_TTL;
//--------------------------------------------------------------------------
// operations for life-cycle management of cache
/**
* {@inheritDoc}
*/
public final void initialize(final Properties params) throws CacheAcquireException {
super.initialize(params);
String param = params.getProperty(PARAM_TTL);
try {
if (param != null) { _ttl = Integer.parseInt(param); }
if (_ttl <= 0) { _ttl = DEFAULT_TTL; }
} catch (NumberFormatException ex) {
_ttl = DEFAULT_TTL;
}
if (TIMER._list.contains(this)) {
TIMER._list.remove(this);
_map.clear();
}
TIMER.addTickerTask(this);
}
//--------------------------------------------------------------------------
// getters/setters for cache configuration
/**
* {@inheritDoc}
*/
public final String getType() { return TYPE; }
/**
* Get real ttl of this cache.
*
* @return Real ttl of this cache.
*/
public final int getTTL() { return _ttl; }
//--------------------------------------------------------------------------
// query operations of map interface
/**
* {@inheritDoc}
*/
public final synchronized int size() { return _map.size(); }
/**
* {@inheritDoc}
*/
public final synchronized boolean isEmpty() { return _map.isEmpty(); }
/**
* {@inheritDoc}
*/
public final synchronized boolean containsKey(final Object key) {
return _map.containsKey(key);
}
/**
* {@inheritDoc}
*/
public final synchronized boolean containsValue(final Object value) {
Iterator<QueueItem> iter = _map.values().iterator();
while (iter.hasNext()) {
QueueItem item = iter.next();
if (value == null) {
if (item._value == null) { return true; }
} else {
if (value.equals(item._value)) { return true; }
}
}
return false;
}
/**
* {@inheritDoc}
*/
public final synchronized Object get(final Object key) {
QueueItem item = _map.get(key);
return (item == null) ? null : item._value;
}
//--------------------------------------------------------------------------
// modification operations of map interface
/**
* {@inheritDoc}
*/
public final synchronized Object put(final Object key, final Object value) {
QueueItem item = _map.get(key);
if (item != null) {
return item.update(value, _ttl);
}
_map.put(key, new QueueItem(key, value, _ttl));
return null;
}
/**
* {@inheritDoc}
*/
public synchronized Object remove(final Object key) {
QueueItem item = _map.remove(key);
return (item == null) ? null : item._value;
}
//--------------------------------------------------------------------------
// bulk operations of map interface
/**
* {@inheritDoc}
*/
public final void putAll(final Map<? extends Object, ? extends Object> map) {
Iterator<? extends Entry<? extends Object, ? extends Object>> iter;
iter = map.entrySet().iterator();
while (iter.hasNext()) {
Entry<? extends Object, ? extends Object> entry = iter.next();
put(entry.getKey(), entry.getValue());
}
}
/**
* {@inheritDoc}
*/
public final synchronized void clear() { _map.clear(); }
//--------------------------------------------------------------------------
// view operations of map interface
/**
* {@inheritDoc}
*/
public final synchronized Set<Object> keySet() {
return Collections.unmodifiableSet(_map.keySet());
}
/**
* {@inheritDoc}
*/
public final synchronized Collection<Object> values() {
Collection<Object> col = new ArrayList<Object>(_map.size());
Iterator<QueueItem> iter = _map.values().iterator();
while (iter.hasNext()) {
QueueItem item = iter.next();
col.add(item._value);
}
return Collections.unmodifiableCollection(col);
}
/**
* {@inheritDoc}
*/
public final synchronized Set<Entry<Object, Object>> entrySet() {
Map<Object, Object> map = new Hashtable<Object, Object>(_map.size());
Iterator<Entry<Object, QueueItem>> iter = _map.entrySet().iterator();
while (iter.hasNext()) {
Entry<Object, QueueItem> entry = iter.next();
QueueItem item = entry.getValue();
map.put(entry.getKey(), item._value);
}
return Collections.unmodifiableSet(map.entrySet());
}
//--------------------------------------------------------------------------
/**
* QueueItem holding value and time of each entry in the cache.
*/
private final class QueueItem {
/** The key. */
private Object _key;
/** The value. */
private Object _value;
/** The time to life (ttl). */
private int _time;
/**
* Construct a new QueueItem with given value and time.
*
* @param key The key.
* @param value The value.
* @param time The time to life (ttl).
*/
private QueueItem(final Object key, final Object value, final int time) {
_key = key;
_value = value;
_time = time;
}
/**
* Update value and time of this QueueItem. Returns the previouly hold value.
*
* @param value The new value.
* @param time The new time to life (ttl).
* @return The previouly hold value.
*/
private Object update(final Object value, final int time) {
Object oldValue = _value;
_value = value;
_time = time;
return oldValue;
}
}
/**
* TickThread to remove cache entries whose ttl has been elapsed.
*/
private static final class TickThread extends Thread {
/** The list of all registered caches. */
private ArrayList<TimeLimited> _list = new ArrayList<TimeLimited>();
/** The intervall to checked cache for elapsed entries in milliseconds. */
private int _tick;
/**
* Construct a TickThread to remove cache entries whose ttl has been elapsed.
* The intervall to checked cache for elapsed entries is specified by tick.
*
* @param tick The intervall to checked cache for elapsed entries in milliseconds.
*/
public TickThread(final int tick) {
super("Time-limited cache daemon");
setDaemon(true);
setPriority(MIN_PRIORITY);
_tick = tick;
start();
}
/**
* Add given cache to list of caches.
*
* @param cache Cache to be added to cache list.
*/
void addTickerTask(final TimeLimited cache) {
_list.add(cache);
}
/**
* After sleeping for <code>tick</code> milliseconds, as specified at construction
* of the TickThread, it iterates through all registered caches and calls their
* tick() method to decrement ttl and remove those entries whose ttl has expired.
* if finished with that all starts from the beginning.
*
* @see java.lang.Runnable#run()
*/
public void run() {
try {
long last = System.currentTimeMillis();
while (true) {
long diff = System.currentTimeMillis() - last;
if (diff < _tick) { sleep(_tick - diff); }
last = System.currentTimeMillis();
for (int i = 0; i < _list.size(); i++) {
_list.get(i).tick();
}
}
} catch (InterruptedException e) {
LOG.error("Time-limited cache daemon has been interrupted", e);
}
}
}
/**
* Called by TickThread to decrement ttl of all entries of the cache and remove
* those whose ttl has been expired.
*/
private synchronized void tick() {
if (!_map.isEmpty()) {
for (Iterator<QueueItem> iter = _map.values().iterator(); iter.hasNext(); ) {
QueueItem queueItem = iter.next();
Object key = queueItem._key;
if (queueItem._time <= 0) {
iter.remove();
if (LOG.isDebugEnabled()) { LOG.trace("dispose(" + key + ")"); }
} else {
queueItem._time -= TICK_DELAY;
}
}
}
}
//--------------------------------------------------------------------------
}