com.twelvemonkeys.util.TimeoutMap Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of common-lang Show documentation
Show all versions of common-lang Show documentation
TwelveMonkeys Common language support classes.
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 of the copyright holder 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 HOLDER 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 (expires) 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 expiryTime = 60000L; // 1 minute
//////////////////////
private volatile long nextExpiryTime = Long.MAX_VALUE;
//////////////////////
/**
* 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 recommendations 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 recommendations 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();
expiryTime = 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);
expiryTime = pExpiryTime;
}
/**
* Gets the maximum time any value will be kept in the map, before it expires.
*
* @return the expiry time
*/
public long getExpiryTime() {
return expiryTime;
}
/**
* 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 = expiryTime;
expiryTime = pExpiryTime;
if (expiryTime < oldEexpiryTime) {
// Expire now
nextExpiryTime = 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 entries.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 entries.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) entries.get(pKey);
if (entry == null) {
return null;
}
else if (entry.isExpired()) {
//noinspection SuspiciousMethodCalls
entries.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) entries.get(pKey);
V oldValue;
if (entry == null) {
oldValue = null;
entry = createEntry(pKey, pValue);
entries.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();
modCount++;
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) entries.remove(pKey);
return (entry != null) ? entry.getValue() : null;
}
/**
* Removes all mappings from this map.
*/
public void clear() {
entries.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 > nextExpiryTime) {
removeExpiredEntriesSynced(now);
}
}
/**
* Okay, I guess this do resemble DCL...
*
* @param pTime now
*/
// TODO: Write some exhausting multi-threaded unit-tests.
private synchronized void removeExpiredEntriesSynced(long pTime) {
if (pTime > nextExpiryTime) {
////
long next = Long.MAX_VALUE;
nextExpiryTime = 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;
}
////
}
////
nextExpiryTime = 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 = entries.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);
updateTimestamp();
}
public V setValue(V pValue) {
updateTimestamp();
return super.setValue(pValue);
}
private void updateTimestamp() {
mTimestamp = System.currentTimeMillis();
long expires = expires();
if (expires < nextExpiryTime) {
nextExpiryTime = expires;
}
}
final boolean isExpired() {
return isExpiredBy(System.currentTimeMillis());
}
final boolean isExpiredBy(final long pTime) {
return pTime > expires();
}
final long expires() {
return mTimestamp + expiryTime;
}
}
}