/*
* Copyright 2009 Martin Grotzke
*
* 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 de.javakaffee.web.msm;
import java.util.AbstractList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.RandomAccess;
/**
* An LRUCache that supports a maximum number of cache entries and a time to
* live for them. The TTL is measured from insertion time to access time.
*
* @author <a href="mailto:martin.grotzke@javakaffee.de">Martin Grotzke</a>
* @version $Id$
* @param <K>
* the type of the key
* @param <V>
* the type of the value
*/
public class LRUCache<K, V> {
private final int _size;
private final long _ttl;
private final LinkedHashMap<K, ManagedItem<V>> _map;
/**
* Creates a new instance with the given maximum size.
*
* @param size
* the number of items to keep at max
*/
public LRUCache( final int size ) {
this( size, -1 );
}
/**
* Create a new LRUCache with a maximum number of cache entries and a
* specified time to live for cache entries. The TTL is measured from
* insertion time to access time.
*
* @param size
* the maximum number of cached items
* @param ttlInMillis
* the time to live in milli seconds. Specify -1 for no limit
*/
public LRUCache( final int size, final long ttlInMillis ) {
_size = size;
_ttl = ttlInMillis;
_map = new LinkedHashMap<K, ManagedItem<V>>( size / 2, 0.75f, true );
}
/**
* Removes all cache entries.
*/
public void clear() {
synchronized ( _map ) {
_map.clear();
}
}
/**
* Put the key and value.
*
* @param key
* the key
* @param value
* the value
* @return the previously associated value or <code>null</code>.
*/
public V put( final K key, final V value ) {
synchronized ( _map ) {
final ManagedItem<V> previous = _map.put( key, new ManagedItem<V>( value, System.currentTimeMillis() ) );
while ( _map.size() > _size ) {
_map.remove( _map.keySet().iterator().next() );
}
return previous != null
? previous._value
: null;
}
}
/**
* If the specified key is not already associated with a value or if it's
* associated with a different value, associate it with the given value.
* This is equivalent to
*
* <pre>
* <code> if (map.get(key) == null || !map.get(key).equals(value))
* return map.put(key, value);
* else
* return map.get(key);
* </code>
* </pre>
*
* except that the action is performed atomically.
*
* @param key
* the key to associate the value with.
* @param value
* the value to associate with the provided key.
* @return the previous value associated with the specified key, or null if
* there was no mapping for the key
*/
public V putIfDifferent( final K key, final V value ) {
synchronized ( _map ) {
final ManagedItem<V> item = _map.get( key );
if ( item == null || item._value == null || !item._value.equals( value ) ) {
return put( key, value );
} else {
return item._value;
}
}
}
/**
* Removes the mapping for the specified key from this map if present.
*
* @param key key whose mapping is to be removed from the map
* @return
* @return the previous value associated with <tt>key</tt>, or
* <tt>null</tt> if there was no mapping for <tt>key</tt>.
* (A <tt>null</tt> return can also indicate that the map
* previously associated <tt>null</tt> with <tt>key</tt>.)
*/
public V remove( final K key ) {
synchronized ( _map ) {
final ManagedItem<V> removed = _map.remove( key );
return removed != null ? removed._value : null;
}
}
/**
* Returns the value that was stored to the given key.
*
* @param key
* the key
* @return the stored value or <code>null</code>
*/
public V get( final K key ) {
synchronized ( _map ) {
final ManagedItem<V> item = _map.get( key );
if ( item == null ) {
return null;
}
if ( _ttl > -1 && System.currentTimeMillis() - item._insertionTime > _ttl ) {
_map.remove( key );
return null;
}
return item._value;
}
}
/**
* Determines if the given key is cached without "touching" this key.
*
* @param key
* the key
* @return <code>true</code> if the given key is present in the underlying map, otherwise <code>false</code>.
*/
public boolean containsKey( final K key ) {
synchronized ( _map ) {
return _map.containsKey( key );
}
}
/**
* The list of all keys, whose order is the order in which its entries were last accessed,
* from least-recently accessed to most-recently.
*
* @return a new list.
*/
public List<K> getKeys() {
synchronized ( _map ) {
return new java.util.ArrayList<K>( _map.keySet() );
}
}
/**
* The keys sorted by the given value comparator.
*
* @return the underlying set, see {@link LinkedHashMap#keySet()}.
*/
public List<K> getKeysSortedByValue( final Comparator<V> comparator ) {
synchronized ( _map ) {
@SuppressWarnings( "unchecked" )
final
Entry<K, ManagedItem<V>>[] a = _map.entrySet().toArray( new Map.Entry[_map.size()] );
final Comparator<Entry<K, ManagedItem<V>>> c = new Comparator<Entry<K, ManagedItem<V>>>() {
@Override
public int compare( final Entry<K, ManagedItem<V>> o1, final Entry<K, ManagedItem<V>> o2 ) {
return comparator.compare( o1.getValue()._value, o2.getValue()._value );
}
};
Arrays.sort(a, c);
return new LRUCache.ArrayList<K, V>( a );
}
}
/**
* Stores a value with the timestamp this value was added to the cache.
*
* @param <T>
* the type of the value
*/
private static final class ManagedItem<T> {
private final T _value;
private final long _insertionTime;
private ManagedItem( final T value, final long accessTime ) {
_value = value;
_insertionTime = accessTime;
}
}
/**
* An array list over the backed array of keys and managed items.
* @author Martin Grotzke
*
* @param <E>
* @param <V>
*/
private static class ArrayList<E, V> extends AbstractList<E> implements RandomAccess {
private final Entry<E, ManagedItem<V>>[] a;
ArrayList( final Entry<E, ManagedItem<V>>[] array ) {
if ( array == null ) {
throw new NullPointerException();
}
a = array;
}
@Override
public int size() {
return a.length;
}
@Override
public <T> T[] toArray( final T[] a ) {
throw new UnsupportedOperationException( "Not implemented." );
}
@Override
public E get( final int index ) {
return a[index].getKey();
}
@Override
public E set( final int index, final E element ) {
throw new UnsupportedOperationException( "Not implemented." );
}
@Override
public int indexOf( final Object o ) {
if ( o == null ) {
for ( int i = 0; i < a.length; i++ ) {
if ( a[i] == null || a[i].getKey() == null ) {
return i;
}
}
}
else {
for ( int i = 0; i < a.length; i++ ) {
if ( o.equals( a[i].getKey() ) ) {
return i;
}
}
}
return -1;
}
@Override
public boolean contains( final Object o ) {
return indexOf( o ) != -1;
}
}
}