package winterwell.utils.containers;
import java.io.BufferedWriter;
import java.io.File;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Set;
import winterwell.utils.io.FileUtils;
import winterwell.utils.time.Dt;
import winterwell.utils.time.RateCounter;
import winterwell.utils.time.Time;
import winterwell.utils.web.XStreamUtils;
/**
* A thread-safe in-memory cache which keeps the most recently used values.
* <p>
* Note: This is mostly a convenience wrapper for using {@link LinkedHashMap}
* with synchronized + RateCounters + crude persistence if you want it.
*
* @author daniel
* @testedby {@link CacheTest}
* @param <Key>
* @param <Value>
*/
public class Cache<Key, Value> extends AbstractMap2<Key, Value> {
private final Map<Key, Value> backing;
private RateCounter hitCounter;
private RateCounter missCounter;
private File persist;
private Time persistAt;
private Dt persistEvery;
/**
* Create a cache with the given capacity
*
* @param capacity
*/
public Cache(final int capacity) {
assert capacity > 0;
// Hm: could we use ConcurrentHashMap somehow -- wouldn't it be faster?
// 0.75 is the default load factor
LinkedHashMap<Key, Value> map = new LinkedHashMap<Key, Value>(
capacity + 1, .75F, true) {
private static final long serialVersionUID = 1L;
@Override
public boolean removeEldestEntry(Map.Entry<Key, Value> eldest) {
if (size() <= capacity)
return false;
boolean ok = preRemovalCheck(eldest.getKey(), eldest.getValue());
return ok;
}
};
backing = Collections.synchronizedMap(map);
}
/**
* Convert the key into a canonical form. E.g. you might trim and lower-case
* strings. The semantics of this are: If canonical(a) = canonical(b) then
* get(a) = get(b).
* <p>
* This does nothing by default - override this as needed.
*
* @param key
* @return canonical form of key.
*/
public Key canonical(Key key) {
return key;
}
/**
* Drop everything from the cache.
*/
@Override
public final void clear() {
backing.clear();
}
/**
* @return the currently cached key => value mappings.
*/
@Override
public Set<Map.Entry<Key, Value>> entrySet() {
return backing.entrySet();
}
/**
* @param key
* @return cached value or null
*/
@Override
public Value get(Object key) {
Key k = canonical((Key) key);
Value v = backing.get(k);
if (v == null) {
if (missCounter != null) {
missCounter.plus(1);
}
} else {
if (hitCounter != null) {
hitCounter.plus(1);
}
}
maybePersist();
return v;
}
/**
* Provides direct access to the backing map. For low-level convenience
* only.
*/
@Deprecated
public Map<Key, Value> getBacking() {
return backing;
}
/**
* @see #setRateCounters(RateCounter, RateCounter)
* @return null by default
*/
public RateCounter getHitCounter() {
return hitCounter;
}
/**
* @see #setRateCounters(RateCounter, RateCounter)
* @return null by default
*/
public RateCounter getMissCounter() {
return missCounter;
}
/**
* @return the currently cached keys.
*/
@Override
public Set<Key> keySet() {
return backing.keySet();
}
protected void maybePersist() {
if (persist == null)
return;
if (System.currentTimeMillis() < persistAt.getTime())
return;
BufferedWriter out = FileUtils.getWriter(persist);
XStreamUtils.serialiseToXml(out, new HashMap(backing));
FileUtils.close(out);
persistAt = persistAt.plus(persistEvery);
}
/**
* Invoked when removing an entry due to capacity issues. This can be
* over-ridden to implement custom behaviour. This is NOT called by
* {@link #remove(Object)}.
* <p>
* NB1: preRemovalCheck must not poke at the cache itself -- in general
* "work" should be done elsewhere. This is quite easy to do, as we found
* out, if, for example, you have a cache inside a depot and want to
* save-on-cache-removal.
* <p>
* NB2: Returning false will lead to the cache growing beyond its prescribed
* capacity!
*
* @param key
* @param value
* @return true if the removal should go ahead.
*
*/
protected boolean preRemovalCheck(Key key, Value value) {
return true;
}
@Override
public final Value put(Key k, Value v) {
Value old = backing.put(canonical(k), v);
maybePersist();
return old;
}
@Override
public final Value remove(Object key) {
Key k = canonical((Key) key);
return backing.remove(k);
}
/**
* Adds crude persistence support to the cache. Every so often (provided
* get() or put() is being called), it will save the current mapping using
* XStream.
* <p>
* If this is set while the cache is empty, then the persistence file will
* be loaded, if it exists.
*
* @param persistHere
* @param persistEvery
* @return this
*/
public Cache<Key, Value> setPersistence(File persistHere, Dt persistEvery) {
assert persistHere.getParentFile().isDirectory() : persistHere;
this.persist = persistHere;
this.persistEvery = persistEvery;
persistAt = new Time().plus(persistEvery);
// load now?
if (backing.isEmpty() && persist.exists()) {
try {
Map map = XStreamUtils
.serialiseFromXml(FileUtils.read(persist));
backing.putAll(map);
} catch (Exception e) {
// oh well -- it will be over-written by the next save
}
}
return this;
}
/**
* Counters are null be default. If set, they will count hits (requests that
* are in cache) and misses (request we don't hold).
*
* @param hitCounter
* @param missCounter
* @return
*/
public Cache<Key, Value> setRateCounters(RateCounter hitCounter,
RateCounter missCounter) {
this.hitCounter = hitCounter;
this.missCounter = missCounter;
return this;
}
/**
* @return the current number of cached objects
*/
@Override
public final int size() {
return backing.size();
}
/**
* @return the currently cached values
*/
@Override
public Collection<Value> values() {
return backing.values();
}
}