/*
Copyright (C) 2001, 2006 United States Government
as represented by the Administrator of the
National Aeronautics and Space Administration.
All Rights Reserved.
*/
package gov.nasa.worldwind.cache;
import gov.nasa.worldwind.util.Logging;
/**
* @author Eric Dalgliesh
* @version $Id: BasicMemoryCache.java 2471 2007-07-31 21:50:57Z tgaskins $
*/
public final class BasicMemoryCache implements MemoryCache
{
private static class CacheEntry implements Comparable<CacheEntry>
{
Object key;
Object clientObject;
private long lastUsed;
private long clientObjectSize;
CacheEntry(Object key, Object clientObject, long clientObjectSize)
{
this.key = key;
this.clientObject = clientObject;
this.lastUsed = System.nanoTime();
this.clientObjectSize = clientObjectSize;
}
public int compareTo(CacheEntry that)
{
if (that == null)
{
String msg = Logging.getMessage("nullValue.CacheEntryIsNull");
Logging.logger().severe(msg);
throw new IllegalArgumentException(msg);
}
return this.lastUsed < that.lastUsed ? -1 : this.lastUsed == that.lastUsed ? 0 : 1;
}
public String toString()
{
return key.toString() + " " + clientObject.toString() + " " + lastUsed + " " + clientObjectSize;
}
}
private java.util.concurrent.ConcurrentHashMap<Object, CacheEntry> entries;
private java.util.concurrent.CopyOnWriteArrayList<MemoryCache.CacheListener> listeners;
private Long capacityInBytes;
private Long currentUsedCapacity;
private Long lowWater;
private String name = "";
/**
* Constructs a new cache using <code>capacity</code> for maximum size, and <code>loWater</code> for the low water.
*
* @param loWater the low water level
* @param capacity the maximum capacity
*/
public BasicMemoryCache(long loWater, long capacity)
{
this.entries = new java.util.concurrent.ConcurrentHashMap<Object, CacheEntry>();
this.listeners = new java.util.concurrent.CopyOnWriteArrayList<MemoryCache.CacheListener>();
this.capacityInBytes = capacity;
this.lowWater = loWater;
this.currentUsedCapacity = (long) 0;
}
/**
* @return the number of objects currently stored in this cache
*/
public int getNumObjects()
{
return this.entries.size();
}
/**
* @return the capacity of the cache in bytes
*/
public long getCapacity()
{
return this.capacityInBytes;
}
/**
* @return the number of bytes that the cache currently holds
*/
public synchronized long getUsedCapacity()
{
return this.currentUsedCapacity;
}
/**
* @return the amount of free space left in the cache (in bytes)
*/
public synchronized long getFreeCapacity()
{
return this.capacityInBytes - this.currentUsedCapacity;
}
public void setName(String name)
{
this.name = name != null ? name : "";
}
public String getName()
{
return name;
}
/**
* Adds a cache listener, MemoryCache listeners are used to notify classes when an item is removed from the cache.
*
* @param listener The new <code>CacheListener</code>
* @throws IllegalArgumentException is <code>listener</code> is null
*/
public synchronized void addCacheListener(MemoryCache.CacheListener listener)
{
if (listener == null)
{
String message = Logging.getMessage("BasicMemoryCache.nullListenerAdded");
Logging.logger().warning(message);
throw new IllegalArgumentException(message);
}
this.listeners.add(listener);
}
/**
* Removes a cache listener, objects using this listener will no longer receive notification of cache events.
*
* @param listener The <code>CacheListener</code> to remove
* @throws IllegalArgumentException if <code>listener</code> is null
*/
public synchronized void removeCacheListener(MemoryCache.CacheListener listener)
{
if (listener == null)
{
String message = Logging.getMessage("BasicMemoryCache.nullListenerRemoved");
Logging.logger().warning(message);
throw new IllegalArgumentException(message);
}
this.listeners.remove(listener);
}
/**
* Sets the new capacity (in bytes) for the cache. When decreasing cache size, it is recommended to check that the
* lowWater variable is suitable. If the capacity infringes on items stored in the cache, these items are removed.
* Setting a new low water is up to the user, that is, it remains unchanged and may be higher than the maximum
* capacity. When the low water level is higher than or equal to the maximum capacity, it is ignored, which can lead
* to poor performance when adding entries.
*
* @param newCapacity the new capacity of the cache.
*/
public synchronized void setCapacity(long newCapacity)
{
this.makeSpace(this.capacityInBytes - newCapacity);
this.capacityInBytes = newCapacity;
}
/**
* Sets the new low water level in bytes, which controls how aggresively the cache discards items.
* <p/>
* When the cache fills, it removes items until it reaches the low water level.
* <p/>
* Setting a high loWater level will increase cache misses, but decrease average add time, but setting a low loWater
* will do the opposite.
*
* @param loWater the new low water level in bytes.
*/
public synchronized void setLowWater(long loWater)
{
if (loWater < this.capacityInBytes && loWater >= 0)
{
this.lowWater = loWater;
}
}
/**
* Returns the low water level in bytes. When the cache fills, it removes items until it reaches the low water
* level.
*
* @return the low water level in bytes.
*/
public long getLowWater()
{
return this.lowWater;
}
/**
* Returns true if the cache contains the item referenced by key. No guarantee is made as to whether or not the item
* will remain in the cache for any period of time.
* <p/>
* This function does not cause the object referenced by the key to be marked as accessed. <code>getObject()</code>
* should be used for that purpose
*
* @param key The key of a specific object
* @return true if the cache holds the item referenced by key
* @throws IllegalArgumentException if <code>key</code> is null
*/
public boolean contains(Object key)
{
if (key == null)
{
String msg = Logging.getMessage("nullValue.KeyIsNull");
Logging.logger().severe(msg);
throw new IllegalArgumentException(msg);
}
return this.entries.containsKey(key);
}
/**
* Adds an object to the cache. The add fails if the object or key is null, or if the size is zero, negative or
* greater than the maximmum capacity
*
* @param key The unique reference key that identifies this object.
* @param clientObject The actual object to be cached.
* @param clientObjectSize The size of the object in bytes.
* @return returns true if clientObject was added, false otherwise.
*/
public synchronized boolean add(Object key, Object clientObject, long clientObjectSize)
{
if (key == null || clientObject == null || clientObjectSize <= 0 || clientObjectSize > this.capacityInBytes)
{
Logging.logger().warning("BasicMemoryCache.CacheItemNotAdded");
return false;
// the logic behind not throwing an exception is that whether we throw an exception or not,
// the object won't be added. This doesn't matter because that object could be removed before
// it is accessed again anyway.
}
CacheEntry existing = this.entries.get(key);
if (existing != null) // replacing
{
this.removeEntry(existing);
}
if (this.currentUsedCapacity + clientObjectSize > this.capacityInBytes)
{
this.makeSpace(clientObjectSize);
}
this.currentUsedCapacity += clientObjectSize;
BasicMemoryCache.CacheEntry entry = new BasicMemoryCache.CacheEntry(key, clientObject, clientObjectSize);
this.entries.putIfAbsent(entry.key, entry);
return true;
}
public synchronized boolean add(Object key, Cacheable clientObject)
{
return this.add(key, clientObject, clientObject.getSizeInBytes());
}
/**
* Remove the object reference by key from the cache. If no object with the corresponding key is found, this method
* returns immediately.
*
* @param key the key of the object to be removed
* @throws IllegalArgumentException if <code>key</code> is null
*/
public synchronized void remove(Object key)
{
if (key == null)
{
Logging.logger().finer("nullValue.KeyIsNull");
return;
}
CacheEntry entry = this.entries.get(key);
if (entry != null)
this.removeEntry(entry);
}
/**
* Obtain the object referenced by key without removing it. Apart from adding an object, this is the only way to
* mark an object as recently used.
*
* @param key The key for the object to be found.
* @return the object referenced by key if it is present, null otherwise.
* @throws IllegalArgumentException if <code>key</code> is null
*/
public synchronized Object getObject(Object key)
{
if (key == null)
{
Logging.logger().finer("nullValue.KeyIsNull");
return null;
}
CacheEntry entry = this.entries.get(key);
if (entry == null)
return null;
entry.lastUsed = System.nanoTime(); // nanoTime overflows once every 292 years
// which will result in a slowing of the cache
// until ww is restarted or the cache is cleared.
return entry.clientObject;
}
/**
* Obtain a list of all the keys in the cache.
*
* @return a <code>Set</code> of all keys in the cache.
*/
public java.util.Set<Object> getKeySet()
{
return this.entries.keySet();
}
/**
* Empties the cache.
*/
public synchronized void clear()
{
for (CacheEntry entry : this.entries.values())
{
this.removeEntry(entry);
}
}
/**
* Removes <code>entry</code> from the cache. To remove an entry using its key, use <code>remove()</code>
*
* @param entry The entry (as opposed to key) of the item to be removed
*/
private synchronized void removeEntry(CacheEntry entry)
{
// all removal passes through this function,
// so the reduction in "currentUsedCapacity" and listener notification is done here
if (this.entries.remove(entry.key) != null) // returns null if entry does not exist
{
this.currentUsedCapacity -= entry.clientObjectSize;
for (MemoryCache.CacheListener listener : this.listeners)
{
listener.entryRemoved(entry.key, entry.clientObject);
}
}
}
/**
* Makes at least <code>spaceRequired</code> space in the cache. If spaceRequired is less than (capacity-lowWater),
* makes more space. Does nothing if capacity is less than spaceRequired.
*
* @param spaceRequired the amount of space required.
*/
private void makeSpace(long spaceRequired)
{
if (spaceRequired > this.capacityInBytes || spaceRequired < 0)
return;
CacheEntry[] timeOrderedEntries = new CacheEntry[this.entries.size()];
java.util.Arrays.sort(this.entries.values().toArray(timeOrderedEntries));
int i = 0;
while (this.getFreeCapacity() < spaceRequired || this.getUsedCapacity() > this.lowWater)
{
if (i < timeOrderedEntries.length)
{
this.removeEntry(timeOrderedEntries[i++]);
}
}
}
/**
* a <code>String</code> representation of this object is returned. This representation consists of maximum
* size, current used capacity and number of currently cached items.
*
* @return a <code>String</code> representation of this object
*/
@Override
public synchronized String toString()
{
return "MemoryCache " + this.name + " max size = " + this.getCapacity() + " current size = " + this
.currentUsedCapacity + " number of items: " + this.getNumObjects();
}
@Override
protected void finalize() throws Throwable
{
try
{
// clear doesn't throw any checked exceptions
// but this is in case of an unchecked exception
// basically, we don't want to exit without calling super.finalize
this.clear();
}
finally
{
super.finalize();
}
}
}