mirror of
https://github.com/haraldk/TwelveMonkeys.git
synced 2026-05-20 00:00:03 -04:00
454 lines
15 KiB
Java
Executable File
454 lines
15 KiB
Java
Executable File
/*
|
|
* 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 (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.
|
|
* <p>
|
|
* Notes to consider when using this map:
|
|
* </p>
|
|
* <ul>
|
|
* <li>Elements may not expire on the exact millisecond as expected.</li>
|
|
* <li>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.</li>
|
|
* <li>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).</li>
|
|
* </ul>
|
|
*
|
|
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
|
|
* @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<K, V> extends AbstractDecoratedMap<K, V> implements ExpiringMap<K, V>, Serializable, Cloneable {
|
|
/**
|
|
* Expiry time
|
|
*/
|
|
protected long expiryTime = 60000L; // 1 minute
|
|
|
|
//////////////////////
|
|
private volatile long nextExpiryTime;
|
|
//////////////////////
|
|
|
|
/**
|
|
* Creates a {@code TimeoutMap} with the default expiry time of 1 minute.
|
|
* This {@code TimeoutMap} will be backed by a new {@code HashMap} instance.
|
|
* <p>
|
|
* <small>This is constructor is here to comply with the recommendations for
|
|
* "standard" constructors in the {@code Map} interface.</small>
|
|
* </p>
|
|
*
|
|
* @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 <em>not</em> the map passed in as a paramter.
|
|
* <p>
|
|
* <small>This is constructor is here to comply with the recommendations for
|
|
* "standard" constructors in the {@code Map} interface.</small>
|
|
* </p>
|
|
*
|
|
* @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}.
|
|
* <p>
|
|
* <em>Note that structurally modifying the backing map directly (not
|
|
* through this map or its collection views), is not allowed, and will
|
|
* produce undeterministic exceptions.</em>
|
|
* </p>
|
|
*
|
|
* @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<K, Map.Entry<K, V>> 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 <i>necessarily</i> 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<K, V> entry = (TimedEntry<K, V>) 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<K, V> entry = (TimedEntry<K, V>) 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<K, V> entry = (TimedEntry<K, V>) 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<K, V> createEntry(K pKey, V pValue) {
|
|
return new TimedEntry<K, V>(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<Entry<K, V>> iterator = new EntryIterator(); iterator.hasNext();) {
|
|
TimedEntry<K, V> entry = (TimedEntry<K, V>) iterator.next();
|
|
////
|
|
long expires = entry.expires();
|
|
if (expires < next) {
|
|
next = expires;
|
|
}
|
|
////
|
|
}
|
|
////
|
|
nextExpiryTime = next;
|
|
}
|
|
}
|
|
|
|
public Collection<V> values() {
|
|
removeExpiredEntries();
|
|
return super.values();
|
|
}
|
|
|
|
public Set<Entry<K, V>> entrySet() {
|
|
removeExpiredEntries();
|
|
return super.entrySet();
|
|
}
|
|
|
|
public Set<K> keySet() {
|
|
removeExpiredEntries();
|
|
return super.keySet();
|
|
}
|
|
|
|
// Subclass overrides these to alter behavior of views' iterator() method
|
|
protected Iterator<K> newKeyIterator() {
|
|
return new KeyIterator();
|
|
}
|
|
|
|
protected Iterator<V> newValueIterator() {
|
|
return new ValueIterator();
|
|
}
|
|
|
|
protected Iterator<Entry<K, V>> newEntryIterator() {
|
|
return new EntryIterator();
|
|
}
|
|
|
|
public void processRemoved(Entry pRemoved) {
|
|
}
|
|
|
|
/**
|
|
* Note: Iterating through this iterator will remove any expired values.
|
|
*/
|
|
private abstract class TimeoutMapIterator<E> implements Iterator<E> {
|
|
Iterator<Entry<K, Entry<K, V>>> mIterator = entries.entrySet().iterator();
|
|
BasicEntry<K, V> 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<K, Entry<K, V>> entry = mIterator.next();
|
|
TimedEntry<K, V> timed = (TimedEntry<K, V>) 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<K, V> nextEntry() {
|
|
if (!hasNext()) {
|
|
throw new NoSuchElementException();
|
|
}
|
|
|
|
BasicEntry<K, V> entry = mNext;
|
|
mNext = null; // advance
|
|
return entry;
|
|
}
|
|
}
|
|
|
|
private class KeyIterator extends TimeoutMapIterator<K> {
|
|
public K next() {
|
|
return nextEntry().mKey;
|
|
}
|
|
}
|
|
|
|
private class ValueIterator extends TimeoutMapIterator<V> {
|
|
public V next() {
|
|
return nextEntry().mValue;
|
|
}
|
|
}
|
|
|
|
private class EntryIterator extends TimeoutMapIterator<Entry<K, V>> {
|
|
public Entry<K, V> next() {
|
|
return nextEntry();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Keeps track of timed objects
|
|
*/
|
|
private class TimedEntry<K, V> extends BasicEntry<K, V> {
|
|
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 + expiryTime;
|
|
}
|
|
}
|
|
}
|