package com.octo.android.robospice.persistence.file;
import java.io.File;
import java.io.FileFilter;
import java.io.FilenameFilter;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import roboguice.util.temp.Ln;
import android.app.Application;
import com.octo.android.robospice.persistence.DurationInMillis;
import com.octo.android.robospice.persistence.ObjectPersister;
import com.octo.android.robospice.persistence.exception.CacheCreationException;
import com.octo.android.robospice.persistence.exception.CacheLoadingException;
import com.octo.android.robospice.persistence.exception.KeySanitationExcepion;
import com.octo.android.robospice.persistence.keysanitation.KeySanitizer;
/**
* An {@link ObjectPersister} that saves/loads data in a file.
* @author sni
* @param <T>
* the class of the data to load/save.
*/
public abstract class InFileObjectPersister<T> extends ObjectPersister<T> {
// ----------------------------------
// CONSTANTS
// ----------------------------------
/* package private */
static final String CACHE_PREFIX_END = "_";
/* package private */
static final String DEFAULT_ROOT_CACHE_DIR = "robospice-cache";
// ----------------------------------
// ATTRIBUTES
// ----------------------------------
private KeySanitizer keySanitizer;
private File cacheFolder;
private String factoryCachePrefix = "";
// ----------------------------------
// CONSTRUCTOR
// ----------------------------------
public InFileObjectPersister(Application application, Class<T> clazz) throws CacheCreationException {
super(application, clazz);
setCacheFolder(null);
}
public InFileObjectPersister(Application application, Class<T> clazz, File cacheFolder) throws CacheCreationException {
super(application, clazz);
setCacheFolder(cacheFolder);
}
// ----------------------------------
// PUBLIC API
// ----------------------------------
/**
* Set the cacheFolder to use.
* @param cacheFolder
* the new cache folder to use. Can be null, will then default to
* {@link #DEFAULT_ROOT_CACHE_DIR} sub folder in the application
* cache dir.
* @throws CacheCreationException
* if the cache folder doesn't exist or can't be created.
*/
public void setCacheFolder(File cacheFolder) throws CacheCreationException {
if (cacheFolder == null) {
cacheFolder = new File(getApplication().getCacheDir(), DEFAULT_ROOT_CACHE_DIR);
}
synchronized (cacheFolder.getAbsolutePath().intern()) {
if (!cacheFolder.exists() && !cacheFolder.mkdirs()) {
throw new CacheCreationException("The cache folder " + cacheFolder.getAbsolutePath() + " could not be created.");
}
}
this.cacheFolder = cacheFolder;
}
public final File getCacheFolder() {
return cacheFolder;
}
@Override
public long getCreationDateInCache(Object cacheKey) throws CacheLoadingException {
File cacheFile = getCacheFile(cacheKey);
if (cacheFile.exists()) {
return cacheFile.lastModified();
} else {
throw new CacheLoadingException(
"Data could not be found in cache for cacheKey=" + cacheKey);
}
}
@Override
public List<Object> getAllCacheKeys() {
final String prefix = getCachePrefix();
int prefixLength = prefix.length();
String[] cacheFileNameList = getCacheFolder().list(new FilenameFilter() {
@Override
public boolean accept(File dir, String filename) {
// patch from florianmski
return filename.startsWith(prefix);
}
});
if (cacheFileNameList == null) {
return Collections.emptyList();
}
List<Object> result = new ArrayList<Object>(cacheFileNameList.length);
for (String cacheFileName : cacheFileNameList) {
String cacheKey = cacheFileName.substring(prefixLength);
result.add(fromKey(cacheKey));
}
return result;
}
@Override
public List<T> loadAllDataFromCache() throws CacheLoadingException {
List<Object> allCacheKeys = getAllCacheKeys();
List<T> result = new ArrayList<T>(allCacheKeys.size());
for (Object key : allCacheKeys) {
result.add(loadDataFromCache(key, DurationInMillis.ALWAYS_RETURNED));
}
return result;
}
@Override
public boolean removeDataFromCache(Object cacheKey) {
return getCacheFile(cacheKey).delete();
}
@Override
public void removeAllDataFromCache() {
File cacheFolder = getCacheFolder();
File[] cacheFileList = cacheFolder.listFiles(new FileFilter() {
@Override
public boolean accept(File file) {
return file.getName().startsWith(getCachePrefix());
}
});
boolean allDeleted = true;
for (File cacheFile : cacheFileList) {
allDeleted = cacheFile.delete() && allDeleted;
}
if (allDeleted || cacheFileList.length == 0) {
Ln.d("Some file could not be deleted from cache.");
}
}
@Override
public T loadDataFromCache(Object cacheKey, long maxTimeInCache) throws CacheLoadingException {
File file = getCacheFile(cacheKey);
if (isCachedAndNotExpired(file, maxTimeInCache)) {
return readCacheDataFromFile(file);
}
return null;
}
@Override
public boolean isDataInCache(Object cacheKey, long maxTimeInCacheBeforeExpiry) {
File file = getCacheFile(cacheKey);
return isCachedAndNotExpired(file, maxTimeInCacheBeforeExpiry);
}
/**
* @return Whether or not this {@link InFileObjectPersister} uses a
* {@link KeySanitizer}.
*/
public boolean isUsingKeySanitizer() {
return keySanitizer != null;
}
/**
* @param keySanitizer
* the new key sanitizer to be used by this
* {@link InFileObjectPersister}. May be null, in that case no
* key sanitation will be used default). If key sanitation fails
* on a given cache key (by throwing a
* {@link KeySanitationExcepion}, original (unsanitized) cache
* keys will be used directly.
*/
public void setKeySanitizer(KeySanitizer keySanitizer) {
this.keySanitizer = keySanitizer;
}
/**
* @return the key sanitizer used by this {@link InFileObjectPersister}. May
* be null, in that case no key sanitation will be used default).
*/
public KeySanitizer getKeySanitizer() {
return keySanitizer;
}
public final File getCacheFile(Object cacheKey) {
return new File(getCacheFolder(), getCachePrefix() + toKey(cacheKey.toString()));
}
// ----------------------------------
// PROTECTED METHODS
// ----------------------------------
/* package-private */
void setFactoryCachePrefix(String factoryCachePrefix) {
this.factoryCachePrefix = factoryCachePrefix;
}
/**
* Get a key that may be sanitized if a {@link KeySanitizer} is used.
* @param cacheKey
* a non-sanitized cacheKey.
* @return a key that will be sanitized if a {@link KeySanitizer} is used.
*/
protected final String toKey(String cacheKey) {
if (isUsingKeySanitizer()) {
try {
return (String) keySanitizer.sanitizeKey(cacheKey);
} catch (KeySanitationExcepion e) {
Ln.e(e, "Key could not be sanitized, falling back on original key.");
return cacheKey;
}
} else {
return cacheKey;
}
}
/**
* Get a cache key that may be de-sanitized if a {@link KeySanitizer} is
* used.
* @param cacheKey
* a possibly sanitized cacheKey.
* @return a key that will be de-sanitized if a {@link KeySanitizer} is
* used.
*/
protected final String fromKey(String cacheKey) {
if (isUsingKeySanitizer()) {
try {
return (String) keySanitizer.desanitizeKey(cacheKey);
} catch (KeySanitationExcepion e) {
Ln.e(e, "Key could not be desanitized, falling back on original key.");
return cacheKey;
}
} else {
return cacheKey;
}
}
protected abstract T readCacheDataFromFile(File file) throws CacheLoadingException;
protected final String getCachePrefix() {
return factoryCachePrefix + getClass().getSimpleName() + CACHE_PREFIX_END + getHandledClass().getSimpleName() + CACHE_PREFIX_END;
}
protected boolean isCachedAndNotExpired(Object cacheKey, long maxTimeInCacheBeforeExpiry) {
File cacheFile = getCacheFile(cacheKey);
return isCachedAndNotExpired(cacheFile, maxTimeInCacheBeforeExpiry);
}
protected boolean isCachedAndNotExpired(File cacheFile, long maxTimeInCacheBeforeExpiry) {
if (cacheFile.exists()) {
long timeInCache = System.currentTimeMillis() - cacheFile.lastModified();
if (maxTimeInCacheBeforeExpiry == DurationInMillis.ALWAYS_RETURNED || timeInCache <= maxTimeInCacheBeforeExpiry) {
return true;
}
}
return false;
}
}