/*
* FixedSizeForgetfulHashMap.java
*
* Created on December 11, 2000, 2:08 PM
*/
package com.limegroup.gnutella.util;
import java.util.AbstractSet;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import com.limegroup.gnutella.Assert;
/**
* A stronger version of ForgetfulHashMap. Like ForgetfulHashMap, this is a
* mapping that "forgets" keys and values using a FIFO replacement policy, much
* like a cache. Unlike ForgetfulHashMap, it has better-defined replacement
* policy. Specifically, it allows a key to be remapped to a different value and
* then "renews" this key so it is the last key to be replaced. All of this is
* done in constant time.<p>
*
* Restrictions:
* <ul>
* <li>The changes to this map should be made only thru the methods provided and not
* thru any iterator/set of keys/values returned.
* <li>Values in the hash map may not be null.
* <li><b>This class is not thread safe.</b> Synchronize externally if needed.
* </ul>
*
* Note that <b>some methods of this are unimplemented</b>. Also note that this
* implements Map but does not extend HashMap, unlike ForgetfulHashMap.
*
* @author Anurag Singla -- initial version
* @author Christopher Rohrs -- cleaned up and added unit tests
*/
public class FixedsizeForgetfulHashMap implements Map
{
/* Implementation note:
*
* To avoid linear-time operations, this maintains an internal linked
* list(removeList) to manage/figure-out which elements to remove when the
* underlying hashMap reaches user defined size, and a new mapping needs to
* be added. Whenever we insert any thing to the underlying hashMap, we
* also add an entry in the removeList (we add the entry at the last of the
* list) When the underlying hashMap reaches the user defined size, we
* remove an element from the underlying hashMap before inserting a new one.
* The element removed is the one which is first in the removeList (ie the
* element that was inserted first.)
*
* If we insert same 'key' twice to the underlying hashMap, we remove
* the previous entry in the removeList(if present) (its similar to
* changing the remove timestamp for that entry). In other words, adding a
* key again, removes the previous footprints (ie it again becomes the last
* element to be removed, irrespective of the history(previous position))
*
* ABSTRACTION FUNCTION: a typical FixedsizeForgetfulHashMap is a list of
* key value pairs [ (K1, V1), ... (KN, VN) ] ordered from oldest to
* youngest where
* K_I=removeList.get(I)
* V_I=map.get(K_I).getValue()
*
* INVARIANTS: here "a=b" is shorthand for "a.equals(b)"
* +for all keys k in map, where ve==map.get(k),
* ve.getListElement() is an element of list
* ve.getListElement().getKey()=k
* k!=null && ve!=null && ve.getValue()!=null (no null values!)
* +for all elements l in removeList, where k=l.getKey() and ve=map.get(l)
* ve!=null (i.e., k is a key in map)
* ve.getListElement=l
*
* A corrolary of this invariant is that no duplicate keys may be stored in
* removeList.
*/
/** The underlying map from keys to [value, list element] pairs */
private Map /* Objects -> ValueElement */ map;
/**
* A linked list of the keys in the hashMap. It is used to remove the
* elements from the underlying hashMap datastructure in FIFO order
* Newer elements are stored in the tail.
*/
private DoublyLinkedList /* of ListElement */ removeList =
new DoublyLinkedList();
/**
* Maximum number of elements to be stored in the underlying hashMap
*/
private int maxSize;
/**
* current number of elements in the underlying hashMap
*/
private int currentSize;
/**
* class to store the value to be stored in the hashMap
* It keeps both the actual value (that user wanted to insert), and the
* entry in the removeList that corresponds to this mapping.
* This information is required so that when we overwrite the mapping (same key,
* but different value), we should update the removeList entries accordingly.
*/
private static class ValueElement
{
/** The element in the remove list that corresponds to this mapping */
DoublyLinkedList.ListElement listElement;
/** The actual value (that user wanted to store in the hash map) */
Object value;
/**
* Creates a new instance with specified values
* @param value The actual value (that user wanted to store in the hash map)
* @param listElement The element in the remove list that corresponds
* to this mapping
*/
public ValueElement(Object value,
DoublyLinkedList.ListElement listElement) {
//update the member fields
this.value = value;
this.listElement = listElement;
}
/** Returns the element in the remove list that corresponds to this
* mapping thats stored in this instance */
public DoublyLinkedList.ListElement getListElement() {
return listElement;
}
/** Returns the value stored */
public Object getValue() {
return value;
}
/**
* Returns true if the value of these elements are equal.
* Needed for map.equals(other.map) to work.
*/
public boolean equals(Object o) {
if ( o == this ) return true;
if ( !(o instanceof ValueElement) )
return false;
ValueElement other = (ValueElement)o;
return value.equals(other.value);
}
/**
* Returns the hashcode of the value element.
* Needed for map.hashCode() to work.
*/
public int hashCode() {
return value.hashCode();
}
}
/**
* Create a new instance that holds only the last "size" entries.
* @param size the number of entries to hold
* @exception IllegalArgumentException if size is less < 1.
*/
public FixedsizeForgetfulHashMap(int size)
{
//allocate space in underlying hashMap
map=new HashMap((size * 4)/3 + 10, 0.75f);
//if size is < 1
if (size < 1)
throw new IllegalArgumentException();
//no elements stored at present. Therefore, set the current size to zero
currentSize = 0;
//set the max size to the size specified
maxSize = size;
}
/** Returns the value associated with this key.
* @return the value associated with this key, or null if no association
* (possibly because the key was expired)
*/
public Object get(Object key)
{
ValueElement pair=(ValueElement)map.get(key);
return (pair==null) ? null : pair.getValue();
}
/**
* Associates the specified value with the specified key in this map.
* If the map previously contained a mapping for this key, the old
* value is replaced. Also if any of the key/value is null, the entry
* is not inserted.
*
* @param key key with which the specified value is to be associated.
* @param value value to be associated with the specified key, which must
* not be null
* @return previous value associated with specified key, or <tt>null</tt>
* if there was no mapping for key..
*/
public Object put(Object key, Object value)
{
//add the new mapping to the underlying hashmap data structure
//add only if not null. This isn't strictly needed our specification
//disallows null keys (implicitly) and null values (explicitly).
if(key == null || value == null)
return null;
//add the mapping
//the method takes care of adding the information to the remove list
//and other details (like updating current count)
Object oldValue = addMapping(key,value);
//return the old value
return oldValue;
}
/**
* Adds the specified key=>value mapping after wrapping the value to
* maintain additional information. If an entry needs to be removed to
* accomodate this new mapping (as it can increase the max number of elements
* to be retained, as specified by the user), it removes the earliest element
* enetred, as explained in the class description. It updates various counts,
* as well as the removeList to reflect the updates
* @param key key with which the specified value is to be associated.
* @param value value to be associated with the specified key.
* @return previous value associated with specified key, or <tt>null</tt>
* if there was no mapping for key. A <tt>null</tt> return can
* also indicate that the HashMap previously associated
* <tt>null</tt> with the specified key.
* @modifies currentCount, 'this', removeList
*/
private Object addMapping(Object key, Object value)
{
//add the newly inserted element to the removeList
DoublyLinkedList.ListElement listElement = removeList.addLast(key);
//insert the mapping in the hashmap (after wrapping the value properly)
//save the element removed
ValueElement ret = (ValueElement)map.put(
key, new ValueElement(value, listElement));
//if a mapping already existed, remove the entry corresponding to
//the old value from the removeList
if(ret != null)
{
removeList.remove(ret.getListElement());
}
else
{
//else increment the count of entries
currentSize++;
}
//if the count is more than max, means we need to remove an entry
if(currentSize > maxSize)
{
//get an element from the remove list to remove
DoublyLinkedList.ListElement toRemove = removeList.removeFirst();
//remove it from the hashMap
map.remove(toRemove.getKey());
//decrement the count
currentSize--;
}
//return the previous mapping
if(ret == null)
return null;
else
return ret.getValue();
}
/**
* Tests if the map is full
* @return true, if the map is full (ie if adding any other entry will
* lead to removal of some other entry to maintain the fixed-size property
* of the map. Returns false, otherwise
*/
public boolean isFull()
{
//if the count is more than max
if(currentSize >= maxSize)
{
return true;
}
else
{
return false;
}
}
/**
* Removes the least recently used entry from the map
* @return Value corresponding to the key-value removed from the map
* @modifies this
*/
public Object removeLRUEntry()
{
//if there are no elements, return null.
if(isEmpty())
return null;
//get an element from the remove list to remove
DoublyLinkedList.ListElement toRemove = removeList.removeFirst();
//remove it from the hashMap
ValueElement removed = (ValueElement)map.remove(toRemove.getKey());
//decrement the count
currentSize--;
//return the removed element (value)
return removed.getValue();
}
/**
* Copies all of the mappings from the specified map to this one.
*
* These mappings replace any mappings that this map had for any of the
* keys currently in the specified Map.
* As this is fixed size mapping, some older entries may get removed
*
* @param t Mappings to be stored in this map.
*/
public void putAll(Map t)
{
Iterator iter=t.keySet().iterator();
while (iter.hasNext())
{
Object key=iter.next();
put(key,t.get(key));
}
}
/**
* Returns a shallow copy of this Map instance: the keys and
* values themselves are not cloned.
*
* @return a shallow copy of this map.
*/
public Object clone()
{
//create a clone map of required size
Map clone = new HashMap((map.size() * 4)/3 + 10, 0.75f);
//get the entrySet corresponding to this map
Set entrySet = map.entrySet();
//iterate over the elements
Iterator iterator = entrySet.iterator();
while(iterator.hasNext())
{
//get the next element
Map.Entry entry = (Map.Entry)iterator.next();
//add it to the clone map
//add only the value (and not the ValueElement wrapper instance
//that is stored internally
clone.put(entry.getKey(),
((ValueElement)entry.getValue()).getValue());
}
//return the clone
return clone;
}
/**
* Removes the mapping for this key from this map if present.
*
* @param key key whose mapping is to be removed from the map.
* @return previous value associated with specified key, or <tt>null</tt>
* if there was no mapping for key.
*/
public Object remove(Object key)
{
//save the element removed
ValueElement ret = (ValueElement)map.remove(key);
//if the mapping existed
if(ret != null)
{
//decrement the current size
currentSize--;
//remove it from the removeList
removeList.remove(ret.getListElement());
return ret.getValue();
}
else
{
return null;
}
}
/**
* Removes all mappings from this map.
*/
public void clear()
{
//clear everything from the underlying data structure
map.clear();
//set the current size to zero
currentSize = 0;
//remove all the entries from remove list
removeList.clear();
}
/////////////////////////// Implemented Map Methods ////////////////
public boolean containsKey(Object key) {
return map.containsKey(key);
}
public boolean equals(Object o) {
if ( o == this ) return true;
if(!(o instanceof FixedsizeForgetfulHashMap))
return false;
FixedsizeForgetfulHashMap other=(FixedsizeForgetfulHashMap)o;
return map.equals(other.map);
}
public int hashCode() {
return map.hashCode();
}
public boolean isEmpty() {
return map.isEmpty();
}
public int size() {
return map.size();
}
/////////////////////////// Unimplemented Map Methods //////////////
/** <b>Partially implemented.</b>
* Only keySet().iterator() is well defined. */
public Set keySet() {
return new KeySet(map.keySet());
}
class KeySet extends AbstractSet {
Set real;
KeySet(Set real) {
this.real=real;
}
public Iterator iterator() {
return new KeyIterator(real.iterator());
}
public int size() {
return FixedsizeForgetfulHashMap.this.size();
}
}
class KeyIterator implements Iterator {
Iterator real;
Object lastYielded=null;
KeyIterator(Iterator real) {
this.real=real;
}
public Object next() {
Object ret=real.next();
lastYielded=ret;
return ret;
}
public boolean hasNext() {
return real.hasNext();
}
/** Same as Iterator.remove(). That means that calling remove()
* multiple times may have undefined results! */
public void remove() {
if (lastYielded==null)
return;
//Cleanup entry in removeList. Note that we cannot simply call
//FixedsizeForgetfulHashMap.this.remove(lastYielded) since that may
//affect the underlying map--while iterating through it.
ValueElement ve = (ValueElement)map.get(lastYielded);
if (ve != null) //not strictly needed by specification of remove.
{
currentSize--;
removeList.remove(ve.getListElement());
}
//Cleanup entry in underlying map. This MUST be done through
//the iterator only, to prevent inconsistent state.
real.remove();
}
}
/** <b>Not implemented; behavior undefined</b> */
public Collection values() {
throw new UnsupportedOperationException();
}
/** <b>Not implemented; behavior undefined</b> */
public boolean containsValue(Object value) {
throw new UnsupportedOperationException();
}
/** <b>Not implemented; behavior undefined</b> */
public Set entrySet() {
throw new UnsupportedOperationException();
}
//////////////////////////////////////////////////////////////////////
/** Tests the invariants described above. */
public void repOk() {
for (Iterator iter=map.keySet().iterator(); iter.hasNext(); ) {
Object k=iter.next();
Assert.that(k!=null, "Null key (1)");
ValueElement ve=(ValueElement)map.get(k);
Assert.that(ve!=null, "Null value element (1)");
Assert.that(ve.getValue()!=null, "Null real value (1)");
Assert.that(removeList.contains(ve.getListElement()),
"Invariant 1a failed");
Assert.that(ve.getListElement().getKey().equals(k),
"Invariant 1b failed");
}
for (Iterator iter=removeList.iterator(); iter.hasNext(); ) {
DoublyLinkedList.ListElement l=
(DoublyLinkedList.ListElement)iter.next();
Object k=l.getKey();
Assert.that(k!=null, "Null key (2)");
ValueElement ve=(ValueElement)map.get(k);
Assert.that(ve!=null, "Null value element (2)");
Assert.that(ve.getListElement().equals(l), "Invariant 2b failed");
}
}
}