/**
* Copyright 2008 ThimbleWare Inc.
*
* 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 com.thimbleware.jmemcached;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Set;
/**
* Simple non-thread-safe LRU hash map cache. Thread safety is expected to be provided by the caller.
*/
public final class LRUCache<ID_TYPE, ITEM_TYPE> {
/**
* Map containing the actual storage
*/
private final Map<ID_TYPE, CacheEntry<ITEM_TYPE>> items;
private long size = 0; // current size in bytes
private final long maximumSize; // in bytes
private long ceilingSize;
private int maximumItems;
private static final int INITIAL_TABLE_SIZE = 2048;
final class CacheEntry<ITEM_TYPE> {
long size;
ITEM_TYPE item;
CacheEntry(long size, ITEM_TYPE item) {
this.size = size;
this.item = item;
}
}
/**
* Caches are created as empty, and populated through use.
*
* @param maximumItems maximum number of items allowed in the cache
* @param maximumSize maximum size in bytes of the cache
* @param ceilingSize number of bytes to attempt to leave as ceiling room
*/
public LRUCache(final int maximumItems, final long maximumSize, final long ceilingSize) {
this.maximumItems = maximumItems;
this.maximumSize = maximumSize;
this.ceilingSize = ceilingSize;
/**
* Creates a linked hash map which expels old elements on declared criterion
*/
items = new LinkedHashMap<ID_TYPE, CacheEntry<ITEM_TYPE>>(INITIAL_TABLE_SIZE) {
/**
* Do I need to update serialVersionUID?
* See section 5.6 <cite>Type Changes Affecting Serialization</cite> on page 51 of the
* <a href="http://java.sun.com/j2se/1.4/pdf/serial-spec.pdf">Java Object Serialization Spec</a>
*/
private static final long serialVersionUID = 1L;
protected boolean removeEldestEntry(Map.Entry<ID_TYPE, CacheEntry<ITEM_TYPE>> eldest) {
if ((maximumSize > 0 && (size + ceilingSize > maximumSize)) || (maximumItems > 0 && size() > maximumItems)) {
size -= eldest.getValue().size;
return true;
} else return false;
}
};
}
/**
* Return true only if the corresponding item is in the cache, and has been
* in it for no more that fRefreshInterval milliseconds; if caching is
* disabled, then always return false.
*
* @param aId is non-null.
* @return if the corresponding item is in the cache
* @throws IllegalArgumentException if a param does not comply.
*/
public boolean has(ID_TYPE aId) {
if (aId == null) throw new IllegalArgumentException("Id must not be null.");
return items.containsKey(aId);
}
/**
* Retrieve an existing item from the cache.
*
* @param aId is non-null, and corresponds to an existing item in the cache.
* @return a non-null Object
* @throws IllegalArgumentException if aId is null, or if the item is not in the cache
* @throws IllegalStateException if the item in the cache is null or the cache is disabled
*/
public ITEM_TYPE get(ID_TYPE aId) {
if (aId == null) throw new IllegalArgumentException("Id must not be null.");
ITEM_TYPE result;
if (items.containsKey(aId)) {
result = items.get(aId).item;
if (result == null) {
throw new IllegalStateException("Stored item should not be null. Id:" + aId);
}
} else {
return null;
}
return result;
}
/**
* If the item is already present, then replace it; otherwise, add it.
*
* If the cache is disabled, do nothing.
*
* @param aId is non-null
* @param aItem is non-null
* @param item_size is the size of aItem in bytes
* @throws IllegalArgumentException if param does not comply
*/
public void put(ID_TYPE aId, ITEM_TYPE aItem, long item_size) {
if (aId == null) throw new IllegalArgumentException("Id must not be null.");
if (aItem == null) throw new IllegalArgumentException("Item must not be null.");
// if the item already exists in the cache, subtract its old size
if (items.containsKey(aId)) {
size -= items.get(aId).size;
}
items.put(aId, new CacheEntry<ITEM_TYPE>(item_size, aItem));
size += item_size;
}
/**
* Remove an entry from the cache
* @param key the key for the entry
*/
public void remove(ID_TYPE key) {
CacheEntry<ITEM_TYPE> item = items.get(key);
if (item != null) {
items.remove(key);
size -= item.size;
}
}
/**
* Start from beginning, and remove all items from the cache; if cache is
* disabled, do nothing.
*
* Forces a re-population of all items into the cache.
*/
public void clear() {
items.clear();
size = 0;
}
/**
* @return the set of all keys in the cache
*/
public Set<ID_TYPE> keys() {
return items.keySet();
}
/**
* @return the number of entries in the cache
*/
public long count() {
return items.size();
}
/**
* @return the maximum number of items in the cache
*/
public int getMaximumItems() {
return maximumItems;
}
/**
* @return the size (in bytes) of all entries in the cache.
*/
public long getSize() {
return size;
}
/**
* @return the maximum capacity (in bytes) of the cache
*/
public long getMaximumSize() {
return maximumSize;
}
/**
* @return the reserved headroom/ceiling size (in bytes)
*/
public long getCeilingSize() {
return ceilingSize;
}
}