package org.jgroups.blocks;
import org.jgroups.util.Util;
import java.util.*;
import java.util.concurrent.ConcurrentMap;
/**
* Cache which doesn't remove elements on remove(), removeAll() or retainAll(), but only removes elements when a
* configurable size limit has been exceeded. In that case, all elements marked as removable and older than a
* configurable time are evicted. Elements are marked as removable by remove(), removeAll() and retainAll(). When
* an elements is marked as removable, but later reinserted, the mark is removed.
* @author Bela Ban
*/
public class LazyRemovalCache<K,V> {
private final ConcurrentMap<K, Entry<V>> map=Util.createConcurrentMap();
/** Max number of elements, if exceeded, we remove all elements marked as removable and older than max_age ms */
private final int max_elements;
private final long max_age;
public interface Printable<K,V> {
String print(K key,V val);
}
public LazyRemovalCache() {
this(200, 5000L);
}
public LazyRemovalCache(int max_elements, long max_age) {
this.max_elements=max_elements;
this.max_age=max_age;
}
public void add(K key, V val) {
if(key != null && val != null)
map.put(key, new Entry<V>(val)); // overwrite existing element (new timestamp, and possible removable mark erased)
checkMaxSizeExceeded();
}
public boolean containsKey(K key) {
return map.containsKey(key);
}
/** Returns true if all of the keys in keys are present. Returns false if one or more of the keys are absent */
public boolean containsKeys(Collection<K> keys) {
for(K key: keys)
if(!map.containsKey(key))
return false;
return true;
}
public V get(K key) {
if(key == null)
return null;
Entry<V> entry=map.get(key);
return entry != null? entry.val : null;
}
public K getByValue(V val) {
if(val == null) return null;
for(Map.Entry<K,Entry<V>> entry: map.entrySet()) {
Entry<V> v=entry.getValue();
if(v.val != null && v.val.equals(val))
return entry.getKey();
}
return null;
}
public void remove(K key) {
remove(key, false);
}
public void remove(K key, boolean force) {
if(key == null)
return;
if(force)
map.remove(key);
else {
Entry<V> entry=map.get(key);
if(entry != null)
entry.removable=true;
}
checkMaxSizeExceeded();
}
public void removeAll(Collection<K> keys) {
removeAll(keys, false);
}
public void removeAll(Collection<K> keys, boolean force) {
if(keys == null || keys.isEmpty())
return;
if(force)
map.keySet().removeAll(keys);
else {
for(K key: keys) {
Entry<V> entry=map.get(key);
if(entry != null)
entry.removable=true;
}
}
checkMaxSizeExceeded();
}
public void clear(boolean force) {
if(force)
map.clear();
else {
for(Map.Entry<K,Entry<V>> entry: map.entrySet()) {
Entry<V> val=entry.getValue();
if(val != null) {
Entry<V> tmp=entry.getValue();
if(tmp != null)
tmp.removable=true;
}
}
}
}
public void retainAll(Collection<K> keys) {
retainAll(keys, false);
}
public void retainAll(Collection<K> keys, boolean force) {
if(keys == null || keys.isEmpty())
return;
if(force)
map.keySet().retainAll(keys);
else {
for(Map.Entry<K,Entry<V>> entry: map.entrySet()) {
if(!keys.contains(entry.getKey())) {
Entry<V> val=entry.getValue();
if(val != null)
val.removable=true;
}
}
}
// now make sure that all elements in keys have removable=false
for(K key: keys) {
Entry<V> val=map.get(key);
if(val != null && val.removable)
val.removable=false;
}
checkMaxSizeExceeded();
}
public Set<V> values() {
Set<V> retval=new HashSet<V>();
for(Entry<V> entry: map.values()) {
retval.add(entry.val);
}
return retval;
}
/**
* Adds all value which have not been marked as removable to the returned set
* @return
*/
public Set<V> nonRemovedValues() {
Set<V> retval=new HashSet<V>();
for(Entry<V> entry: map.values()) {
if(!entry.removable)
retval.add(entry.val);
}
return retval;
}
public Map<K,V> contents() {
Map<K,V> retval=new HashMap<K,V>();
for(Map.Entry<K,Entry<V>> entry: map.entrySet())
retval.put(entry.getKey(), entry.getValue().val);
return retval;
}
public int size() {
return map.size();
}
public String printCache() {
StringBuilder sb=new StringBuilder();
for(Map.Entry<K,Entry<V>> entry: map.entrySet()) {
sb.append(entry.getKey()).append(": ").append(entry.getValue()).append("\n");
}
return sb.toString();
}
public String printCache(Printable print_function) {
StringBuilder sb=new StringBuilder();
for(Map.Entry<K,Entry<V>> entry: map.entrySet()) {
K key=entry.getKey();
V val=entry.getValue().val;
sb.append(print_function.print(key, val));
}
return sb.toString();
}
public String toString() {
return printCache();
}
private void checkMaxSizeExceeded() {
if(map.size() > max_elements) {
removeMarkedElements();
}
}
/**
* Removes elements marked as removable
* @param force If set to true, all elements marked as 'removable' will get removed, regardless of expiration
*/
public void removeMarkedElements(boolean force) {
long curr_time=System.currentTimeMillis();
for(Iterator<Map.Entry<K,Entry<V>>> it=map.entrySet().iterator(); it.hasNext();) {
Map.Entry<K, Entry<V>> entry=it.next();
Entry<V> tmp=entry.getValue();
if(tmp == null)
continue;
if(tmp.removable && ((curr_time - tmp.timestamp) >= max_age || force)) {
it.remove();
}
}
}
/**
* Removes elements marked as removable
*/
public void removeMarkedElements() {
removeMarkedElements(false);
}
private static class Entry<V> {
private final V val;
private final long timestamp=System.currentTimeMillis();
private boolean removable=false;
public Entry(V val) {
this.val=val;
}
public String toString() {
return val + " (" + (System.currentTimeMillis() - timestamp) + "ms old" + (removable? ", removable" : "") + ")";
}
}
}