package cn.trinea.android.common.service.impl;
import java.io.Serializable;
import java.util.Collection;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicLong;
import cn.trinea.android.common.entity.CacheObject;
import cn.trinea.android.common.service.Cache;
import cn.trinea.android.common.service.CacheFullRemoveType;
import cn.trinea.android.common.util.MapUtils;
import cn.trinea.android.common.util.SerializeUtils;
/**
* Simple Cache<br/>
* <ul>
* <strong>Usage</strong>
* <li>Use one of constructors below to construct cache</li>
* <li>{@link #setCacheFullRemoveType(CacheFullRemoveType)} set remove type when cache is full</li>
* <li>{@link #setValidTime(long)} set valid time of elements in cache, in mills</li>
* <li>{@link #put(Object, cn.trinea.android.common.entity.CacheObject)} or {@link #put(Object, Object)} put element</li>
* <li>{@link #get(Object)} get element</li>
* <li>{@link #loadCache(String)} restore cache from file</li>
* <li>{@link #saveCache(String, cn.trinea.android.common.service.impl.SimpleCache)} save cache to file</li>
* </ul>
* <ul>
* <strong>Constructor</strong>
* <li>{@link #SimpleCache()}</li>
* <li>{@link #SimpleCache(int)}</li>
* <li>{@link #loadCache(String)} restore cache from file</li>
* </ul>
* <ul>
* <strong>About hit and miss of the cache</strong>
* <li>{@link #getHitRate()} get hit rate of the cache</li>
* <li>{@link #getHitCount()} get hit count of the cache</li>
* <li>{@link #getMissCount()} get miss count of the cache</li>
* </ul>
* <ul>
* <strong>About size of cache</strong>
* <li>{@link #getMaxSize()} get the maximum capacity of the cache</li>
* <li>{@link #getSize()} get the number of elements in the cache valid</li>
* </ul>
* <ul>
* <strong>Other interfaces same to {@link java.util.Map} </strong>
* </ul>
*
* @author <a href="http://www.trinea.cn" target="_blank">Trinea</a> 2011-12-23
*/
public class SimpleCache<K, V> implements Cache<K, V>, Serializable {
private static final long serialVersionUID = 1L;
/** default maximum capacity of the cache **/
public static final int DEFAULT_MAX_SIZE = 64;
/** maximum size of the cache, if not set, use {@link #DEFAULT_MAX_SIZE} **/
private final int maxSize;
/** valid time of elements in cache, in mills. It means not invalid if less than 0 **/
private long validTime;
/** remove type when cache is full **/
private CacheFullRemoveType<V> cacheFullRemoveType;
/** map to storage element **/
protected Map<K, CacheObject<V>> cache;
/** hit count of cache **/
protected AtomicLong hitCount = new AtomicLong(0);
/** miss count of cache **/
protected AtomicLong missCount = new AtomicLong(0);
/**
* <ul>
* <li>Maximum size of the cache is {@link #DEFAULT_MAX_SIZE}</li>
* <li>Elements of the cache will not invalid, can set by {@link #setValidTime(long)}</li>
* <li>Remove type is {@link RemoveTypeEnterTimeFirst} when cache is full</li>
* </ul>
*/
public SimpleCache() {
this(DEFAULT_MAX_SIZE);
}
/**
* <ul>
* <li>Elements of the cache will not invalid, can set by {@link #setValidTime(long)}</li>
* <li>Remove type is {@link RemoveTypeEnterTimeFirst} when cache is full</li>
* </ul>
*
* @param maxSize maximum size of the cache
*/
public SimpleCache(int maxSize) {
if (maxSize <= 0) {
throw new IllegalArgumentException("The maxSize of cache must be greater than 0.");
}
this.maxSize = maxSize;
this.cacheFullRemoveType = new RemoveTypeEnterTimeFirst<V>();
this.validTime = -1;
this.cache = new ConcurrentHashMap<K, CacheObject<V>>(maxSize);
}
/**
* get the maximum capacity of the cache
*
* @return
*/
public int getMaxSize() {
return maxSize;
}
/**
* get valid time of elements in cache, in mills. It means not invalid if less than 0
*
* @return
*/
public long getValidTime() {
return validTime;
}
/**
* set valid time of elements in cache, in mills
*
* @param validTime valid time of elements in cache, in mills. If less than 0, it will be set to -1 and means not
* invalid. Rule of invalid see {@link #isExpired(cn.trinea.android.common.entity.CacheObject)}
*/
public void setValidTime(long validTime) {
this.validTime = validTime <= 0 ? -1 : validTime;
}
/**
* get remove type when cache is full
*
* @return
*/
public CacheFullRemoveType<V> getCacheFullRemoveType() {
return cacheFullRemoveType;
}
/**
* set remove type when cache is full
*
* @param cacheFullRemoveType the cacheFullRemoveType to set
*/
public void setCacheFullRemoveType(CacheFullRemoveType<V> cacheFullRemoveType) {
if (cacheFullRemoveType == null) {
throw new IllegalArgumentException("The cacheFullRemoveType of cache cannot be null.");
}
this.cacheFullRemoveType = cacheFullRemoveType;
}
/**
* get the number of elements in the cache valid
*
* @return
*/
@Override
public int getSize() {
removeExpired();
return cache.size();
}
/**
* get element
*
* @param key
* @return element if this cache contains the specified key and the element is valid, null otherwise.
*/
@Override
public CacheObject<V> get(K key) {
CacheObject<V> obj = cache.get(key);
if (!isExpired(obj) && obj != null) {
hitCount.incrementAndGet();
setUsedInfo(obj);
return obj;
} else {
missCount.incrementAndGet();
return null;
}
}
/**
* set used info
*
* @param obj
*/
protected synchronized void setUsedInfo(CacheObject<V> obj) {
if (obj != null) {
obj.getAndIncrementUsedCount();
obj.setLastUsedTime(System.currentTimeMillis());
}
}
/**
* put element, key not allowed to be null
*
* @param key key
* @param value data of {@link cn.trinea.android.common.entity.CacheObject}
* @return return null if cache is full and cannot remove one, else return the value be putted
* @see cn.trinea.android.common.service.impl.SimpleCache#put(Object, cn.trinea.android.common.entity.CacheObject)
*/
@Override
public CacheObject<V> put(K key, V value) {
CacheObject<V> obj = new CacheObject<V>();
obj.setData(value);
obj.setForever(validTime == -1);
return put(key, obj);
}
/**
* put element, key and value both not allowed to be null
*
* @param key
* @param value
* @return return null if cache is full and cannot remove one, else return the value be putted
*/
@Override
public synchronized CacheObject<V> put(K key, CacheObject<V> value) {
if (cache.size() >= maxSize) {
if (removeExpired() <= 0) {
if (cacheFullRemoveType instanceof RemoveTypeNotRemove) {
return null;
}
if (fullRemoveOne() == null) {
return null;
}
}
}
value.setEnterTime(System.currentTimeMillis());
cache.put(key, value);
return value;
}
/**
* pull all elements of cache2 to this
*
* @param cache2
*/
@Override
public void putAll(Cache<K, V> cache2) {
for (Entry<K, CacheObject<V>> e : cache2.entrySet()) {
if (e != null) {
put(e.getKey(), e.getValue());
}
}
}
/**
* whether this cache contains the specified key.
*
* @param key
* @return true if this cache contains the specified key and the element is valid, false otherwise.
*/
@Override
public boolean containsKey(K key) {
return cache.containsKey(key) ? !isExpired(key) : false;
}
/**
* whether the element of the specified key has invalided
*
* @param key
* @return
* @see cn.trinea.android.common.service.impl.SimpleCache#isExpired(cn.trinea.android.common.entity.CacheObject)
*/
protected boolean isExpired(K key) {
return validTime == -1 ? false : isExpired(cache.get(key));
}
/**
* remove the specified key from cache, key not allowed to be null
*
* @param key
* @return the value of the removed or null if no mapping for the specified key was found.
*/
@Override
public CacheObject<V> remove(K key) {
return cache.remove(key);
}
/**
* remove a element when cache is full. according to {@link #getCacheFullRemoveType()}
* <ul>
* <li>if {@link #getCacheFullRemoveType()} is instance of {@link RemoveTypeNotRemove} return null, else</li>
* <li>remove a element according to {@link #getCacheFullRemoveType()}</li>
* </ul>
*
* @param key
* @return the value of the removed or null if no element can be remove.
*/
protected CacheObject<V> fullRemoveOne() {
if (MapUtils.isEmpty(cache) || cacheFullRemoveType instanceof RemoveTypeNotRemove) {
return null;
}
K keyToRemove = null;
CacheObject<V> valueToRemove = null;
for (Entry<K, CacheObject<V>> entry : cache.entrySet()) {
if (entry != null) {
if (valueToRemove == null) {
valueToRemove = entry.getValue();
keyToRemove = entry.getKey();
} else {
if (cacheFullRemoveType.compare(entry.getValue(), valueToRemove) < 0) {
valueToRemove = entry.getValue();
keyToRemove = entry.getKey();
}
}
}
}
if (keyToRemove != null) {
cache.remove(keyToRemove);
}
return valueToRemove;
}
/**
* remove invalid elements
*
* @return the count be removed
*/
protected synchronized int removeExpired() {
if (validTime == -1) {
return 0;
}
int count = 0;
// because cache is instance of ConcurrentHashMap, so you can remove when iterator
for (Entry<K, CacheObject<V>> entry : cache.entrySet()) {
if (entry != null && isExpired(entry.getValue())) {
cache.remove(entry.getKey());
count++;
}
}
return count;
}
/**
* Removes all elements from this Map, leaving it empty.
*
* @see java.util.Map#clear()
*/
@Override
public void clear() {
cache.clear();
}
/**
* returns whether the element of the specified key has invalided
* <ul>
* <li>if {@link #getValidTime()} less than 0, return false, else</li>
* <li>if element is null, return true, else</li>
* <li>if {@link cn.trinea.android.common.entity.CacheObject#isExpired()} is true and {@link cn.trinea.android.common.entity.CacheObject#isForever()} is false, return true, else</li>
* <li>if {@link cn.trinea.android.common.entity.CacheObject#getEnterTime()} add {@link #getValidTime()} less than current time, return true</li>
* <li>return false</li>
* </ul>
*
* @param obj
* @return
*/
protected boolean isExpired(CacheObject<V> obj) {
return validTime != -1
&& (obj == null || (obj.isExpired() && !obj.isForever()) || (obj.getEnterTime() + validTime) < System
.currentTimeMillis());
}
/**
* get hit count
**/
public long getHitCount() {
return hitCount.get();
}
/**
* get miss count
**/
public long getMissCount() {
return missCount.get();
}
/**
* get hit rate
*
* @return
*/
@Override
public synchronized double getHitRate() {
long total = hitCount.get() + missCount.get();
return (total == 0 ? 0 : ((double)hitCount.get()) / total);
}
/**
* @return a set of the keys.
* @see java.util.Map#keySet()
*/
@Override
public Set<K> keySet() {
removeExpired();
return cache.keySet();
}
/**
* @return a set of the mappings
* @see java.util.Map#entrySet()
*/
@Override
public Set<Entry<K, CacheObject<V>>> entrySet() {
removeExpired();
return cache.entrySet();
}
/**
* @return a collection of the values contained in this cache.
* @see java.util.Map#values()
*/
@Override
public Collection<CacheObject<V>> values() {
removeExpired();
return cache.values();
}
/**
* restore cache from file
*
* @param filePath
* @return
*/
@SuppressWarnings("unchecked")
public static <K, V> SimpleCache<K, V> loadCache(String filePath) {
return (SimpleCache<K, V>)SerializeUtils.deserialization(filePath);
}
/**
* save cache to file, the data of {@link cn.trinea.android.common.entity.CacheObject} should can be serializabled
*
* @param <K>
* @param <V>
* @param filePath
* @param cache
*/
public static <K, V> void saveCache(String filePath, SimpleCache<K, V> cache) {
SerializeUtils.serialization(filePath, cache);
}
}