com.twelvemonkeys.util.TimeoutMap Maven / Gradle / Ivy
The newest version!
/*
* Copyright (c) 2008, Harald Kuhr
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* * Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* * Neither the name "TwelveMonkeys" nor the
* names of its contributors may be used to endorse or promote products
* derived from this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package com.twelvemonkeys.util;
import java.io.Serializable;
import java.util.*;
/**
* A {@code Map} implementation that removes (exipres) its elements after
* a given period. The map is by default backed by a {@link java.util.HashMap},
* or can be instantiated with any given {@code Map} as backing.
*
* Notes to consider when using this map:
*
* - Elements may not expire on the exact millisecond as expected.
* - The value returned by the {@code size()} method of the map, or any of
* its collection views, may not represent
* the exact number of entries in the map at any given time.
* - Elements in this map may expire at any time
* (but never between invocations of {@code Iterator.hasNext()}
* and {@code Iterator.next()} or {@code Iterator.remove()},
* when iterating the collection views).
*
*
* @author Harald Kuhr
* @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-core/src/main/java/com/twelvemonkeys/util/TimeoutMap.java#2 $
*
* @todo Consider have this Map extend LinkedMap.. That way the removeExpired
* method only have to run from the first element, until it finds an element
* that should not expire, as elements are in insertion order.
* and next expiry time would be the time of the first element.
* @todo Consider running the removeExpiredEntries method in a separate (deamon) thread
* @todo - or document why it is not such a good idea.
*/
public class TimeoutMap extends AbstractDecoratedMap implements ExpiringMap, Serializable, Cloneable {
/**
* Expiry time
*/
protected long mExpiryTime = 60000L; // 1 minute
//////////////////////
private volatile long mNextExpiryTime;
//////////////////////
/**
* Creates a {@code TimeoutMap} with the default expiry time of 1 minute.
* This {@code TimeoutMap} will be backed by a new {@code HashMap} instance.
*
* This is constructor is here to comply with the reccomendations for
* "standard" constructors in the {@code Map} interface.
*
* @see #TimeoutMap(long)
*/
public TimeoutMap() {
super();
}
/**
* Creates a {@code TimeoutMap} containing the same elements as the given map
* with the default expiry time of 1 minute.
* This {@code TimeoutMap} will be backed by a new {@code HashMap} instance,
* and not the map passed in as a paramter.
*
* This is constructor is here to comply with the reccomendations for
* "standard" constructors in the {@code Map} interface.
*
* @param pContents the map whose mappings are to be placed in this map.
* May be {@code null}.
* @see #TimeoutMap(java.util.Map, Map, long)
* @see java.util.Map
*/
public TimeoutMap(Map extends K, ? extends V> pContents) {
super(pContents);
}
/**
* Creates a {@code TimeoutMap} with the given expiry time (milliseconds).
* This {@code TimeoutMap} will be backed by a new {@code HashMap} instance.
*
* @param pExpiryTime the expiry time (time to live) for elements in this map
*/
public TimeoutMap(long pExpiryTime) {
this();
mExpiryTime = pExpiryTime;
}
/**
* Creates a {@code TimeoutMap} with the given expiry time (milliseconds).
* This {@code TimeoutMap} will be backed by the given {@code Map}.
*
* Note that structurally modifying the backing map directly (not
* through this map or its collection views), is not allowed, and will
* produce undeterministic exceptions.
*
* @param pBacking the map that will be used as backing.
* @param pContents the map whose mappings are to be placed in this map.
* May be {@code null}.
* @param pExpiryTime the expiry time (time to live) for elements in this map
*/
public TimeoutMap(Map> pBacking, Map extends K, ? extends V> pContents, long pExpiryTime) {
super(pBacking, pContents);
mExpiryTime = pExpiryTime;
}
/**
* Gets the maximum time any value will be kept in the map, before it expires.
*
* @return the expiry time
*/
public long getExpiryTime() {
return mExpiryTime;
}
/**
* Sets the maximum time any value will be kept in the map, before it expires.
* Removes any items that are older than the specified time.
*
* @param pExpiryTime the expiry time (time to live) for elements in this map
*/
public void setExpiryTime(long pExpiryTime) {
long oldEexpiryTime = mExpiryTime;
mExpiryTime = pExpiryTime;
if (mExpiryTime < oldEexpiryTime) {
// Expire now
mNextExpiryTime = 0;
removeExpiredEntries();
}
}
/**
* Returns the number of key-value mappings in this map. If the
* map contains more than {@code Integer.MAX_VALUE} elements, returns
* {@code Integer.MAX_VALUE}.
*
* @return the number of key-value mappings in this map.
*/
public int size() {
removeExpiredEntries();
return mEntries.size();
}
/**
* Returns {@code true} if this map contains no key-value mappings.
*
* @return {@code true} if this map contains no key-value mappings.
*/
public boolean isEmpty() {
return (size() <= 0);
}
/**
* Returns {@code true} if this map contains a mapping for the specified
* pKey.
*
* @param pKey pKey whose presence in this map is to be tested.
* @return {@code true} if this map contains a mapping for the specified
* pKey.
*/
public boolean containsKey(Object pKey) {
removeExpiredEntries();
return mEntries.containsKey(pKey);
}
/**
* Returns the value to which this map maps the specified pKey. Returns
* {@code null} if the map contains no mapping for this pKey. A return
* value of {@code null} does not necessarily indicate that the
* map contains no mapping for the pKey; it's also possible that the map
* explicitly maps the pKey to {@code null}. The {@code containsKey}
* operation may be used to distinguish these two cases.
*
* @param pKey pKey whose associated value is to be returned.
* @return the value to which this map maps the specified pKey, or
* {@code null} if the map contains no mapping for this pKey.
* @see #containsKey(java.lang.Object)
*/
public V get(Object pKey) {
TimedEntry entry = (TimedEntry) mEntries.get(pKey);
if (entry == null) {
return null;
}
else if (entry.isExpired()) {
//noinspection SuspiciousMethodCalls
mEntries.remove(pKey);
processRemoved(entry);
return null;
}
return entry.getValue();
}
/**
* Associates the specified pValue with the specified pKey in this map
* (optional operation). If the map previously contained a mapping for
* this pKey, the old pValue is replaced.
*
* @param pKey pKey with which the specified pValue is to be associated.
* @param pValue pValue to be associated with the specified pKey.
* @return previous pValue associated with specified pKey, or {@code null}
* if there was no mapping for pKey. A {@code null} return can
* also indicate that the map previously associated {@code null}
* with the specified pKey, if the implementation supports
* {@code null} values.
*/
public V put(K pKey, V pValue) {
TimedEntry entry = (TimedEntry) mEntries.get(pKey);
V oldValue;
if (entry == null) {
oldValue = null;
entry = createEntry(pKey, pValue);
mEntries.put(pKey, entry);
}
else {
oldValue = entry.mValue;
entry.setValue(pValue);
entry.recordAccess(this);
}
// Need to remove expired objects every now and then
// We do it in the put method, to avoid resource leaks over time.
removeExpiredEntries();
mModCount++;
return oldValue;
}
/**
* Removes the mapping for this pKey from this map if present (optional
* operation).
*
* @param pKey pKey whose mapping is to be removed from the map.
* @return previous value associated with specified pKey, or {@code null}
* if there was no mapping for pKey. A {@code null} return can
* also indicate that the map previously associated {@code null}
* with the specified pKey, if the implementation supports
* {@code null} values.
*/
public V remove(Object pKey) {
TimedEntry entry = (TimedEntry) mEntries.remove(pKey);
return (entry != null) ? entry.getValue() : null;
}
/**
* Removes all mappings from this map.
*/
public void clear() {
mEntries.clear(); // Finally something straightforward.. :-)
init();
}
/*protected*/ TimedEntry createEntry(K pKey, V pValue) {
return new TimedEntry(pKey, pValue);
}
/**
* Removes any expired mappings.
*
*/
protected void removeExpiredEntries() {
// Remove any expired elements
long now = System.currentTimeMillis();
if (now > mNextExpiryTime) {
removeExpiredEntriesSynced(now);
}
}
/**
* Okay, I guess this do resemble DCL...
*
* @todo Write some exhausting multi-threaded unit-tests.
*
* @param pTime now
*/
private synchronized void removeExpiredEntriesSynced(long pTime) {
if (pTime > mNextExpiryTime) {
////
long next = Long.MAX_VALUE;
mNextExpiryTime = next; // Avoid multiple runs...
for (Iterator> iterator = new EntryIterator(); iterator.hasNext();) {
TimedEntry entry = (TimedEntry) iterator.next();
////
long expires = entry.expires();
if (expires < next) {
next = expires;
}
////
}
////
mNextExpiryTime = next;
}
}
public Collection values() {
removeExpiredEntries();
return super.values();
}
public Set> entrySet() {
removeExpiredEntries();
return super.entrySet();
}
public Set keySet() {
removeExpiredEntries();
return super.keySet();
}
// Subclass overrides these to alter behavior of views' iterator() method
protected Iterator newKeyIterator() {
return new KeyIterator();
}
protected Iterator newValueIterator() {
return new ValueIterator();
}
protected Iterator> newEntryIterator() {
return new EntryIterator();
}
public void processRemoved(Entry pRemoved) {
}
/**
* Note: Iterating through this iterator will remove any expired values.
*/
private abstract class TimeoutMapIterator implements Iterator {
Iterator>> mIterator = mEntries.entrySet().iterator();
BasicEntry mNext;
long mNow = System.currentTimeMillis();
public void remove() {
mNext = null; // advance
mIterator.remove();
}
public boolean hasNext() {
if (mNext != null) {
return true; // Never expires between hasNext and next/remove!
}
while (mNext == null && mIterator.hasNext()) {
Entry> entry = mIterator.next();
TimedEntry timed = (TimedEntry) entry.getValue();
if (timed.isExpiredBy(mNow)) {
// Remove from map, and continue
mIterator.remove();
processRemoved(timed);
}
else {
// Go with this entry
mNext = timed;
return true;
}
}
return false;
}
BasicEntry nextEntry() {
if (!hasNext()) {
throw new NoSuchElementException();
}
BasicEntry entry = mNext;
mNext = null; // advance
return entry;
}
}
private class KeyIterator extends TimeoutMapIterator {
public K next() {
return nextEntry().mKey;
}
}
private class ValueIterator extends TimeoutMapIterator {
public V next() {
return nextEntry().mValue;
}
}
private class EntryIterator extends TimeoutMapIterator> {
public Entry next() {
return nextEntry();
}
}
/**
* Keeps track of timed objects
*/
private class TimedEntry extends BasicEntry {
private long mTimestamp;
TimedEntry(K pKey, V pValue) {
super(pKey, pValue);
mTimestamp = System.currentTimeMillis();
}
public V setValue(V pValue) {
mTimestamp = System.currentTimeMillis();
return super.setValue(pValue);
}
final boolean isExpired() {
return isExpiredBy(System.currentTimeMillis());
}
final boolean isExpiredBy(final long pTime) {
return pTime > expires();
}
final long expires() {
return mTimestamp + mExpiryTime;
}
}
}