/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*
*/
import java.util.Collection;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
/**
* A map with expiration. This class contains a worker thread that will
* periodically check this class in order to determine if any objects
* should be removed based on the provided time-to-live value.
*
* @author The Apache MINA Project (dev@mina.apache.org)
* @version $Rev: 662890 $, $Date: 2008-06-03 23:14:21 +0200 (Tue, 03 Jun 2008) $
*/
public class ExpiringMap implements Map {
/**
* The default value, 60
*/
public static final int DEFAULT_TIME_TO_LIVE = 60;
/**
* The default value, 1
*/
public static final int DEFAULT_EXPIRATION_INTERVAL = 1;
private static volatile int expirerCount = 1;
private final ConcurrentHashMap delegate;
private final CopyOnWriteArrayList> expirationListeners;
private final Expirer expirer;
/**
* Creates a new instance of ExpiringMap using the default values
* DEFAULT_TIME_TO_LIVE and DEFAULT_EXPIRATION_INTERVAL
*
*/
public ExpiringMap() {
this(DEFAULT_TIME_TO_LIVE, DEFAULT_EXPIRATION_INTERVAL);
}
/**
* Creates a new instance of ExpiringMap using the supplied
* time-to-live value and the default value for DEFAULT_EXPIRATION_INTERVAL
*
* @param timeToLive
* The time-to-live value (seconds)
*/
public ExpiringMap(int timeToLive) {
this(timeToLive, DEFAULT_EXPIRATION_INTERVAL);
}
/**
* Creates a new instance of ExpiringMap using the supplied values and
* a {@link ConcurrentHashMap} for the internal data structure.
*
* @param timeToLive
* The time-to-live value (seconds)
* @param expirationInterval
* The time between checks to see if a value should be removed (seconds)
*/
public ExpiringMap(int timeToLive, int expirationInterval) {
this(new ConcurrentHashMap(),
new CopyOnWriteArrayList>(), timeToLive,
expirationInterval);
}
private ExpiringMap(ConcurrentHashMap delegate,
CopyOnWriteArrayList> expirationListeners,
int timeToLive, int expirationInterval) {
this.delegate = delegate;
this.expirationListeners = expirationListeners;
this.expirer = new Expirer();
expirer.setTimeToLive(timeToLive);
expirer.setExpirationInterval(expirationInterval);
}
public V put(K key, V value) {
ExpiringObject answer = delegate.put(key, new ExpiringObject(key,
value, System.currentTimeMillis()));
if (answer == null) {
return null;
}
return answer.getValue();
}
public V get(Object key) {
ExpiringObject object = delegate.get(key);
if (object != null) {
object.setLastAccessTime(System.currentTimeMillis());
return object.getValue();
}
return null;
}
public V remove(Object key) {
ExpiringObject answer = delegate.remove(key);
if (answer == null) {
return null;
}
return answer.getValue();
}
public boolean containsKey(Object key) {
return delegate.containsKey(key);
}
public boolean containsValue(Object value) {
return delegate.containsValue(value);
}
public int size() {
return delegate.size();
}
public boolean isEmpty() {
return delegate.isEmpty();
}
public void clear() {
delegate.clear();
}
@Override
public int hashCode() {
return delegate.hashCode();
}
public Set keySet() {
return delegate.keySet();
}
@Override
public boolean equals(Object obj) {
return delegate.equals(obj);
}
public void putAll(Map extends K, ? extends V> inMap) {
for (Entry extends K, ? extends V> e : inMap.entrySet()) {
this.put(e.getKey(), e.getValue());
}
}
public Collection values() {
throw new UnsupportedOperationException();
}
public Set> entrySet() {
throw new UnsupportedOperationException();
}
public void addExpirationListener(ExpirationListener listener) {
expirationListeners.add(listener);
}
public void removeExpirationListener(
ExpirationListener listener) {
expirationListeners.remove(listener);
}
public Expirer getExpirer() {
return expirer;
}
public int getExpirationInterval() {
return expirer.getExpirationInterval();
}
public int getTimeToLive() {
return expirer.getTimeToLive();
}
public void setExpirationInterval(int expirationInterval) {
expirer.setExpirationInterval(expirationInterval);
}
public void setTimeToLive(int timeToLive) {
expirer.setTimeToLive(timeToLive);
}
private class ExpiringObject {
private K key;
private V value;
private long lastAccessTime;
private final ReadWriteLock lastAccessTimeLock = new ReentrantReadWriteLock();
ExpiringObject(K key, V value, long lastAccessTime) {
if (value == null) {
throw new IllegalArgumentException(
"An expiring object cannot be null.");
}
this.key = key;
this.value = value;
this.lastAccessTime = lastAccessTime;
}
public long getLastAccessTime() {
lastAccessTimeLock.readLock().lock();
try {
return lastAccessTime;
} finally {
lastAccessTimeLock.readLock().unlock();
}
}
public void setLastAccessTime(long lastAccessTime) {
lastAccessTimeLock.writeLock().lock();
try {
this.lastAccessTime = lastAccessTime;
} finally {
lastAccessTimeLock.writeLock().unlock();
}
}
public K getKey() {
return key;
}
public V getValue() {
return value;
}
@Override
public boolean equals(Object obj) {
return value.equals(obj);
}
@Override
public int hashCode() {
return value.hashCode();
}
}
/**
* A Thread that monitors an {@link ExpiringMap} and will remove
* elements that have passed the threshold.
*
*/
public class Expirer implements Runnable {
private final ReadWriteLock stateLock = new ReentrantReadWriteLock();
private long timeToLiveMillis;
private long expirationIntervalMillis;
private boolean running = false;
private final Thread expirerThread;
/**
* Creates a new instance of Expirer.
*
*/
public Expirer() {
expirerThread = new Thread(this, "ExpiringMapExpirer-"
+ expirerCount++);
expirerThread.setDaemon(true);
}
public void run() {
while (running) {
processExpires();
try {
Thread.sleep(expirationIntervalMillis);
} catch (InterruptedException e) {
}
}
}
private void processExpires() {
long timeNow = System.currentTimeMillis();
for (ExpiringObject o : delegate.values()) {
if (timeToLiveMillis <= 0) {
continue;
}
long timeIdle = timeNow - o.getLastAccessTime();
if (timeIdle >= timeToLiveMillis) {
delegate.remove(o.getKey());
for (ExpirationListener listener : expirationListeners) {
listener.expired(o.getValue());
}
}
}
}
/**
* Kick off this thread which will look for old objects and remove them.
*
*/
public void startExpiring() {
stateLock.writeLock().lock();
try {
if (!running) {
running = true;
expirerThread.start();
}
} finally {
stateLock.writeLock().unlock();
}
}
/**
* If this thread has not started, then start it.
* Otherwise just return;
*/
public void startExpiringIfNotStarted() {
stateLock.readLock().lock();
try {
if (running) {
return;
}
} finally {
stateLock.readLock().unlock();
}
stateLock.writeLock().lock();
try {
if (!running) {
running = true;
expirerThread.start();
}
} finally {
stateLock.writeLock().unlock();
}
}
/**
* Stop the thread from monitoring the map.
*/
public void stopExpiring() {
stateLock.writeLock().lock();
try {
if (running) {
running = false;
expirerThread.interrupt();
}
} finally {
stateLock.writeLock().unlock();
}
}
/**
* Checks to see if the thread is running
*
* @return
* If the thread is running, true. Otherwise false.
*/
public boolean isRunning() {
stateLock.readLock().lock();
try {
return running;
} finally {
stateLock.readLock().unlock();
}
}
/**
* Returns the Time-to-live value.
*
* @return
* The time-to-live (seconds)
*/
public int getTimeToLive() {
stateLock.readLock().lock();
try {
return (int) timeToLiveMillis / 1000;
} finally {
stateLock.readLock().unlock();
}
}
/**
* Update the value for the time-to-live
*
* @param timeToLive
* The time-to-live (seconds)
*/
public void setTimeToLive(long timeToLive) {
stateLock.writeLock().lock();
try {
this.timeToLiveMillis = timeToLive * 1000;
} finally {
stateLock.writeLock().unlock();
}
}
/**
* Get the interval in which an object will live in the map before
* it is removed.
*
* @return
* The time in seconds.
*/
public int getExpirationInterval() {
stateLock.readLock().lock();
try {
return (int) expirationIntervalMillis / 1000;
} finally {
stateLock.readLock().unlock();
}
}
/**
* Set the interval in which an object will live in the map before
* it is removed.
*
* @param expirationInterval
* The time in seconds
*/
public void setExpirationInterval(long expirationInterval) {
stateLock.writeLock().lock();
try {
this.expirationIntervalMillis = expirationInterval * 1000;
} finally {
stateLock.writeLock().unlock();
}
}
}
}
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*
*/
/**
* A listener for expired object events.
*
* @author The Apache MINA Project (dev@mina.apache.org)
* @version $Rev: 589932 $, $Date: 2007-10-30 02:50:39 +0100 (Tue, 30 Oct 2007) $
* TODO Make this a inner interface of ExpiringMap
*/
interface ExpirationListener {
void expired(E expiredObject);
}