package cn.trinea.android.common.service.impl;
import java.io.Serializable;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import android.content.Context;
import android.net.ConnectivityManager;
import android.net.NetworkInfo;
import cn.trinea.android.common.entity.CacheObject;
import cn.trinea.android.common.service.CacheFullRemoveType;
import cn.trinea.android.common.util.ListUtils;
import cn.trinea.android.common.util.ObjectUtils;
import cn.trinea.android.common.util.SerializeUtils;
import cn.trinea.android.common.util.SystemUtils;
/**
* <strong>Preload data cache</strong>, It a good choice for network application which need to preload data.<br/>
* <br/>
* you can use this cache to preload data, it support preload data backward, forward or both. and you can set preload
* count.<br/>
* <ul>
* <strong>Setting and Usage</strong>
* <li>Use one of constructors below to init cache</li>
* <li>{@link #setOnGetDataListener(cn.trinea.android.common.service.impl.PreloadDataCache.OnGetDataListener)} set how to get data, this cache will get data and preload data
* by it</li>
* <li>{@link cn.trinea.android.common.service.impl.SimpleCache#setCacheFullRemoveType(CacheFullRemoveType)} set remove type when cache is full</li>
* <li>{@link #get(Object, java.util.List)} get object, if list is not null, will preload data auto according to keys in list</li>
* <li>{@link #get(Object)} get object, and not preload data</li>
* <li>{@link #setForwardCacheNumber(int)} set count for preload forward, default is
* {@link #DEFAULT_FORWARD_CACHE_NUMBER}</li>
* <li>{@link #setBackwardCacheNumber(int)} set count for preload backward, default is
* {@link #DEFAULT_BACKWARD_CACHE_NUMBER}</li>
* <li>{@link #setContext(android.content.Context)} and {@link #setAllowedNetworkTypes(int)} restrict the types of networks over which
* this data can get.</li>
* <li>{@link cn.trinea.android.common.service.impl.SimpleCache#setValidTime(long)} set valid time of elements in cache, in mills</li>
* <li>{@link cn.trinea.android.common.service.impl.SimpleCache#saveCache(String, cn.trinea.android.common.service.impl.SimpleCache)} save cache to a file</li>
* </ul>
* <ul>
* <strong>Constructor</strong>
* <li>{@link #PreloadDataCache()}</li>
* <li>{@link #PreloadDataCache(int)}</li>
* <li>{@link #PreloadDataCache(int, int)}</li>
* <li>{@link #loadCache(String)} restore cache from file</li>
* </ul>
*
* @author <a href="http://www.trinea.cn" target="_blank">Trinea</a> 2012-3-4
*/
public class PreloadDataCache<K, V> extends SimpleCache<K, V> {
private static final long serialVersionUID = 1L;
/** count for preload forward, default is {@link #DEFAULT_FORWARD_CACHE_NUMBER} **/
private int forwardCacheNumber = DEFAULT_FORWARD_CACHE_NUMBER;
/** count for preload backward, default is {@link #DEFAULT_BACKWARD_CACHE_NUMBER} **/
private int backwardCacheNumber = DEFAULT_BACKWARD_CACHE_NUMBER;
/** whether to check the network at first when get data **/
private boolean isCheckNetwork = true;
/** allowed network type, default to all network types allowed **/
private int allowedNetworkTypes = ~0;
/** get data listener **/
protected OnGetDataListener<K, V> onGetDataListener;
/**
* restore threads those getting data, to avoid multi threads get the data for same key so that to save network
* traffic
**/
private transient Map<K, GetDataThread> gettingDataThreadMap = new HashMap<K, GetDataThread>();
/** getting data thread pool **/
private ExecutorService threadPool;
private Context context;
private transient ConnectivityManager connectivityManager;
/** default count for preload forward **/
public static final int DEFAULT_FORWARD_CACHE_NUMBER = 3;
/** default count for preload backward **/
public static final int DEFAULT_BACKWARD_CACHE_NUMBER = 1;
/** default getting data thread pool size **/
public static final int DEFAULT_THREAD_POOL_SIZE = SystemUtils.getDefaultThreadPoolSize(8);
/**
* Bit flag for {@link #setAllowedNetworkTypes} corresponding to {@link android.net.ConnectivityManager#TYPE_MOBILE}.
*/
public static final int NETWORK_MOBILE = 1 << 0;
/**
* Bit flag for {@link #setAllowedNetworkTypes} corresponding to {@link android.net.ConnectivityManager#TYPE_WIFI}.
*/
public static final int NETWORK_WIFI = 1 << 1;
/**
* get data synchronous and preload new data asynchronous according to keyList
*
* @param key
* @param keyList key list, if is null, not preload, else preload forward by
* {@link #preloadDataForward(Object, java.util.List, int)}, preload backward by
* {@link #preloadDataBackward(Object, java.util.List, int)}
* @return element if this cache contains the specified key, else get data realtime and wait for it
* @see cn.trinea.android.common.service.impl.PreloadDataCache#get(Object)
*/
public CacheObject<V> get(K key, List<K> keyList) {
if (key == null) {
return null;
}
// if list is not null, preload data
if (!ListUtils.isEmpty(keyList)) {
preloadDataForward(key, keyList, forwardCacheNumber);
preloadDataBackward(key, keyList, backwardCacheNumber);
}
return get(key);
}
/**
* get data synchronous
* <ul>
* <li>if key is null, return null, else</li>
* <li>if key is already in cache, return the element that mapping with the specified key, else</li>
* <li>call {@link cn.trinea.android.common.service.impl.PreloadDataCache.OnGetDataListener#onGetData(Object)} to get data and wait for it finish</li>
* </ul>
*
* @param key
* @return element if this cache contains the specified key, else get data realtime and wait for it
*/
@Override
public CacheObject<V> get(K key) {
if (key == null) {
return null;
}
CacheObject<V> object = super.get(key);
if (object == null && onGetDataListener != null) {
GetDataThread getDataThread = gettingData(key);
// get data synchronous and wait for it
if (getDataThread != null) {
try {
getDataThread.finishGetDataLock.await();
} catch ( InterruptedException e ) {
e.printStackTrace();
}
}
// recalculate hit rate
object = super.get(key);
if (object != null) {
hitCount.decrementAndGet();
} else {
missCount.decrementAndGet();
}
}
return object;
}
/**
* get data from cache
*
* @param key
* @return element if this cache contains the specified key, null otherwise.
*/
CacheObject<V> getFromCache(K key) {
return super.get(key);
}
/**
* get data from cache and preload new data asynchronous according to keyList
*
* @param key
* @param keyList key list, if is null, not preload, else preload forward by
* {@link #preloadDataForward(Object, java.util.List, int)}, preload backward by
* {@link #preloadDataBackward(Object, java.util.List, int)}
* @return element if this cache contains the specified key, null otherwise.
* @see #getFromCache(Object)
*/
CacheObject<V> getFromCache(K key, List<K> keyList) {
if (key == null) {
return null;
}
// if list is not null, preload data
if (!ListUtils.isEmpty(keyList)) {
preloadDataForward(key, keyList, forwardCacheNumber);
preloadDataBackward(key, keyList, backwardCacheNumber);
}
return getFromCache(key);
}
/**
* preload data forward
* <ul>
* <strong>Preload rule below:</strong><br/>
* If key is null or list is empty, not preload, else circle keyList front to back.<br/>
* If entry in the list equals to key, begin preload until to the end of list or preload count has reached
* cacheCount, like this:
* <li>if entry is already in cache or is getting data, continue next entry. else</li>
* <li>new thread to get data and continue next entry</li>
* </ul>
*
* @param key
* @param keyList if is null, not preload
* @param cacheCount count for preload forward
* @return count for getting data, that is cacheCount minus count of keys whose alreadey in cache
*/
protected int preloadDataForward(K key, List<K> keyList, int cacheCount) {
int gettingDataCount = 0;
if (key != null && !ListUtils.isEmpty(keyList) && onGetDataListener != null) {
int cachedCount = 0;
boolean beginCount = false;
for (int i = 0; i < keyList.size() && cachedCount <= cacheCount; i++) {
K k = keyList.get(i);
if (ObjectUtils.isEquals(k, key)) {
beginCount = true;
continue;
}
if (k != null && beginCount) {
cachedCount++;
if (gettingData(k) != null) {
gettingDataCount++;
}
}
}
}
return gettingDataCount;
}
/**
* preload data backward
* <ul>
* <strong>Preload rule below:</strong><br/>
* If key is null or list is empty, not preload, else circle keyList back to front.<br/>
* If entry in the list equals to key, begin preload until to the front of list or preload count has reached
* cacheCount, like this:
* <li>if entry is already in cache or is getting data, continue last entry. else</li>
* <li>new thread to get data and continue last entry</li>
* </ul>
*
* @param key
* @param keyList if is null, not preload
* @param cacheCount count for preload forward
* @return count for getting data, that is cacheCount minus count of keys whose alreadey in cache
*/
protected int preloadDataBackward(K key, List<K> keyList, int cacheCount) {
int gettingDataCount = 0;
if (key != null && !ListUtils.isEmpty(keyList) && onGetDataListener != null) {
int cachedCount = 0;
boolean beginCount = false;
for (int i = keyList.size() - 1; i >= 0 && cachedCount <= cacheCount; i--) {
K k = keyList.get(i);
if (ObjectUtils.isEquals(k, key)) {
beginCount = true;
continue;
}
if (k != null && beginCount) {
cachedCount++;
if (gettingData(k) != null) {
gettingDataCount++;
}
}
}
}
return gettingDataCount;
}
/**
* get getting data thread
* <ul>
* <li>if key is already in cache or net work type is not allowed, return null, else</li>
* <li>if there is a thread which is getting data for the specified key, return thread, else</li>
* <li>new thread to get data and return it</li>
* </ul>
*
* @param key
* @return
*/
private synchronized GetDataThread gettingData(K key) {
if (containsKey(key) || (isCheckNetwork && !checkIsNetworkTypeAllowed())) {
return null;
}
if (isExistGettingDataThread(key)) {
return gettingDataThreadMap.get(key);
}
GetDataThread getDataThread = new GetDataThread(key, onGetDataListener);
gettingDataThreadMap.put(key, getDataThread);
threadPool.execute(getDataThread);
return getDataThread;
}
/**
* whether there is a thread which is getting data for the specified key
*
* @param key
* @return
*/
public synchronized boolean isExistGettingDataThread(K key) {
return gettingDataThreadMap.containsKey(key);
}
/**
* <ul>
* <li>Maximum size of the cache is {@link cn.trinea.android.common.service.impl.SimpleCache#DEFAULT_MAX_SIZE}</li>
* <li>Elements of the cache will not invalid, can set by {@link cn.trinea.android.common.service.impl.SimpleCache#setValidTime(long)}</li>
* <li>Remove type is {@link RemoveTypeEnterTimeFirst} when cache is full</li>
* <li>Size of getting data thread pool is {@link #DEFAULT_THREAD_POOL_SIZE}</li>
* </ul>
*/
public PreloadDataCache() {
this(DEFAULT_MAX_SIZE, DEFAULT_THREAD_POOL_SIZE);
}
/**
* <ul>
* <li>Elements of the cache will not invalid, can set by {@link cn.trinea.android.common.service.impl.SimpleCache#setValidTime(long)}</li>
* <li>Remove type is {@link RemoveTypeEnterTimeFirst} when cache is full</li>
* <li>Size of getting data thread pool is {@link #DEFAULT_THREAD_POOL_SIZE}</li>
* </ul>
*
* @param maxSize maximum size of the cache
*/
public PreloadDataCache(int maxSize) {
this(maxSize, DEFAULT_THREAD_POOL_SIZE);
}
/**
* <ul>
* <li>Elements of the cache will not invalid, can set by {@link cn.trinea.android.common.service.impl.SimpleCache#setValidTime(long)}</li>
* <li>Remove type is {@link RemoveTypeEnterTimeFirst} when cache is full</li>
* </ul>
*
* @param maxSize maximum size of the cache
* @param threadPoolSize getting data thread pool size
*/
public PreloadDataCache(int maxSize, int threadPoolSize) {
super(maxSize);
if (threadPoolSize <= 0) {
throw new IllegalArgumentException("The threadPoolSize of cache must be greater than 0.");
}
this.threadPool = Executors.newFixedThreadPool(threadPoolSize);
}
/**
* get count for preload forward, default is {@link #DEFAULT_FORWARD_CACHE_NUMBER}
*
* @return
*/
public int getForwardCacheNumber() {
return forwardCacheNumber;
}
/**
* set count for preload forward, default is {@link #DEFAULT_FORWARD_CACHE_NUMBER}
*
* @param forwardCacheNumber
*/
public void setForwardCacheNumber(int forwardCacheNumber) {
this.forwardCacheNumber = forwardCacheNumber;
}
/**
* get count for preload backward, default is {@link #DEFAULT_BACKWARD_CACHE_NUMBER}
*
* @return
*/
public int getBackwardCacheNumber() {
return backwardCacheNumber;
}
/**
* set count for preload backward, default is {@link #DEFAULT_BACKWARD_CACHE_NUMBER}
*
* @param backwardCacheNumber
*/
public void setBackwardCacheNumber(int backwardCacheNumber) {
this.backwardCacheNumber = backwardCacheNumber;
}
/**
* get get data listener
*
* @return the onGetDataListener
*/
public OnGetDataListener<K, V> getOnGetDataListener() {
return onGetDataListener;
}
/**
* set get data listener, this cache will get data and preload data by it
*
* @param onGetDataListener
*/
public void setOnGetDataListener(OnGetDataListener<K, V> onGetDataListener) {
this.onGetDataListener = onGetDataListener;
}
/**
* get the types of networks over which this data can get
*
* @return any combination of the NETWORK_* bit flags.
*/
public int getAllowedNetworkTypes() {
return allowedNetworkTypes;
}
/**
* Restrict the types of networks over which this data can get. By default, all network types are allowed.
* <ul>
* <strong>Attentions:</strong>
* <li>To make it effective, you need to ensure that {@link #getContext()} is not null</li>
* </ul>
*
* @param allowedNetworkTypes any combination of the NETWORK_* bit flags.
*/
public void setAllowedNetworkTypes(int allowedNetworkTypes) {
this.allowedNetworkTypes = allowedNetworkTypes;
}
/**
* get whether to check the network at first when get data, used when {@link #checkIsNetworkTypeAllowed()}
*
* @return
*/
public boolean isCheckNetwork() {
return isCheckNetwork;
}
/**
* set whether to check the network at first when get data, used when {@link #checkIsNetworkTypeAllowed()}
*
* @param isCheckNetwork
*/
public void setCheckNetwork(boolean isCheckNetwork) {
this.isCheckNetwork = isCheckNetwork;
}
public Context getContext() {
return context;
}
/**
* used when {@link #checkIsNetworkTypeAllowed()}
*
* @param context
*/
public void setContext(Context context) {
this.context = context;
}
/**
* Check if get data can proceed over the given network type.
*
* @param networkType a constant from ConnectivityManager.TYPE_*.
* @return one of the NETWORK_* constants
* <ul>
* <li>if {@link #getContext()} is null, return true</li>
* <li>if network is not avaliable, return false</li>
* <li>if {@link #getAllowedNetworkTypes()} is not match network, return false</li>
* </ul>
*/
public boolean checkIsNetworkTypeAllowed() {
if (connectivityManager == null && context != null) {
connectivityManager = (ConnectivityManager)context.getSystemService(Context.CONNECTIVITY_SERVICE);
}
if (connectivityManager == null) {
return true;
}
NetworkInfo networkInfo = connectivityManager.getActiveNetworkInfo();
return networkInfo != null
&& (allowedNetworkTypes == ~0 || (translateNetworkTypeToApiFlag(networkInfo.getType()) & allowedNetworkTypes) != 0);
}
/**
* Translate a ConnectivityManager.TYPE_* constant to the corresponding PreloadDataCache.NETWORK_* bit flag.
*/
private int translateNetworkTypeToApiFlag(int networkType) {
switch (networkType) {
case ConnectivityManager.TYPE_MOBILE:
return PreloadDataCache.NETWORK_MOBILE;
case ConnectivityManager.TYPE_WIFI:
return PreloadDataCache.NETWORK_WIFI;
default:
return 0;
}
}
/**
* restore cache from file
*
* @param filePath
* @return
*/
@SuppressWarnings("unchecked")
public static <K, V> PreloadDataCache<K, V> loadCache(String filePath) {
return (PreloadDataCache<K, V>)SerializeUtils.deserialization(filePath);
}
/**
* @see java.util.concurrent.ExecutorService#shutdown()
*/
protected void shutdown() {
threadPool.shutdown();
}
/**
* @see java.util.concurrent.ExecutorService#shutdownNow()
*/
public List<Runnable> shutdownNow() {
return threadPool.shutdownNow();
}
/**
* get data interface, implements this to get data
*
* @author <a href="http://www.trinea.cn" target="_blank">Trinea</a> 2012-3-4
*/
public interface OnGetDataListener<K, V> extends Serializable {
/**
* get data
*
* @param key
* @return the data need to be cached
*/
public CacheObject<V> onGetData(K key);
}
/**
* the thread to get data
*
* @author <a href="http://www.trinea.cn" target="_blank">Trinea</a> 2012-3-4
*/
private class GetDataThread implements Runnable {
private K key;
private OnGetDataListener<K, V> onGetDataListener;
/** get data and cache finish lock, it will be released then **/
public CountDownLatch finishGetDataLock;
/**
* @param key
* @param onGetDataListener
*/
public GetDataThread(K key, OnGetDataListener<K, V> onGetDataListener) {
this.key = key;
this.onGetDataListener = onGetDataListener;
finishGetDataLock = new CountDownLatch(1);
}
public void run() {
if (key != null && onGetDataListener != null) {
CacheObject<V> object = onGetDataListener.onGetData(key);
if (object != null) {
put(key, object);
}
}
// get data success, release lock
finishGetDataLock.countDown();
if (gettingDataThreadMap != null && key != null) {
gettingDataThreadMap.remove(key);
}
}
};
}