@Beta
public enum AccessPolicy {

    /**
     * Normal content fetching: passes through all levels of cache, if there is a cache miss,
     * download the content from the server.
     */
    NORMAL,

    /**
     * Attempt a cache only content fetching.<br> If there is a total cache miss, do nothing.
     */
    CACHE_ONLY,

    /**
     * Pre-fetch data that could be needed soon.<br> If the data is not available in cache, schedule
     * a content download.
     */
    PRE_FETCH,

    /**
     * Refresh the cache data, and return the updated content if any.
     */
    REFRESH;

} @Beta
public class ContentLruCache<K, V> extends LruCache<K, V> implements MemoryCache<K, V> {

    @SuppressWarnings("unused")
    private static final String TAG = ContentLruCache.class.getSimpleName();

    public ContentLruCache(@IntRange(from = 1) int maxSize) {
        super(maxSize);
    }

    /**
     * If the specified key is not already associated with a value, associate it with the given
     * value.
     *
     * @param key   key with which the specified value is to be associated
     * @param value value to be associated with the specified key
     * @return the previous value associated with the specified key, or null if there was no mapping
     * for the key. (A null return can also indicate that the map previously associated null with
     * the key, if the implementation supports null values.)
     */
    @Nullable
    public synchronized V putIfAbsent(@NonNull K key, V value) {
        V old = null;
        if ((old = get(key)) == null) {
            return put(key, value);
        } else {
            return old;
        }
    }

    /**
     * Remove a cache entry if the key is currently associated with the passed object.
     *
     * @param key
     * @param value
     * @return true if the element was removed, false otherwise
     */
    public boolean remove(@NonNull K key, V value) {
        if (value == null) {
            return false;
        }
        synchronized (this) {
            V oldValue = get(key);
            if (oldValue != null && oldValue.equals(value)) {
                remove(key);
                return true;
            }
            return false;
        }
    }

    @Override
    @CallSuper
    public void clear() {
        evictAll();
    }

    /**
     * @throws UnsupportedOperationException
     */
    @Override
    public void setOnEntryRemovedListener(OnEntryRemovedListener<K, V> listener) {
        throw new UnsupportedOperationException();
    }

} @Beta
@NotThreadSafe
public class ModelDiskCache<V> extends SimpleDiskCache<V> {

    private static final String TAG = ModelDiskCache.class.getSimpleName();

    private static final String PATH = "model";

    private static final long PURGE_AFTER = MIN_EXPIRE_IN_SEC * 2;

    private final Class<V> mModelClass;
    private final ObjectMapper mObjectMapper;

    /**
     * Builds a {@link ModelDiskCache} that stores POJO/DTO objects as (plain) text into the passed
     * sub-folder, using the passed {@link ObjectParser}.
     *
     * Note that the {@link CacheLocation#INTERNAL} cache is always used. If no internal caches are
     * present, it falls back to the external one.
     *
     * TODO: allow choosing whether to store in ext storage for failover?
     *
     * @param context    The {@link Context} to use
     * @param mapper     The {@link ObjectMapper} to use for model de/serialization
     * @param subFolder  The relative path to the cache folder where to store the cache (if it
     *                   doesn't exist, the folder is created)
     * @param modelClass The POJO object to store class type
     * @throws IOException
     */
    public ModelDiskCache(@NonNull Context context, @NonNull ObjectMapper mapper,
                          @NonNull String subFolder, @NonNull Class<V> modelClass) throws IOException {
        super(context, CacheLocation.INTERNAL, PATH + File.separator + subFolder, true);
        mModelClass = modelClass;
        mObjectMapper = mapper;
        if (DroidConfig.DEBUG) {
            Log.d(TAG, "Model disk cache created at: " + mCacheLocation.getAbsolutePath());
        }
    }

    /**
     * Gets an item from the disk cache
     *
     * @param key The cache item key (must be not null)
     * @return The cached item, null if not present or an error occurred
     */
    @Nullable
    public V get(@NonNull String key) {
        File jsonFile = new File(mCacheLocation, key);
        if (!jsonFile.exists()) {
            return null;
        }
        try {
            return mObjectMapper.readValue(jsonFile, mModelClass);
        } catch (IOException e) {
            LogUtils.logException(TAG, "Exception when reading " + key, e);
            jsonFile.delete(); // try to delete damaged file
            return null;
        }
    }

    /**
     * Gets an item from the disk cache only if it is not expired.
     *
     * <b>Note on expiration:</b> This method uses the {@link File#lastModified()} method to check
     * if the cache item is expired. Sometimes in Android this value is rounded to the second, so
     * using very small expiration times can result in a cache miss even if the item would be
     * valid.
     *
     * @param key        The cache item key (must be not null)
     * @param expiration The validity of the item from its modification in milliseconds, or {@link
     *                   Long#MAX_VALUE} for no expiration
     * @return The cached item, null if not present, expired or an error occurred
     */
    @Nullable
    public V get(@NonNull String key, long expiration) {
        File jsonFile = new File(mCacheLocation, key);
        if (!jsonFile.exists()) {
            return null;
        }
        final boolean noExpire = expiration == Long.MAX_VALUE;
        if (noExpire || (jsonFile.lastModified() + expiration) > System.currentTimeMillis()) {
            try { // the item is still valid, try parsing it
                return mObjectMapper.readValue(jsonFile, mModelClass);
            } catch (IOException e) { // something wrong happened
                LogUtils.logException(TAG, "Exception when reading " + key, e);
                jsonFile.delete(); // try to delete damaged file
                return null;
            }
        }
        return null;
    }

    /**
     * Puts an item into the disk cache.
     *
     * @param key   The cache item key (must be not null)
     * @param model The value to put (must be not null)
     * @return true if successful, false otherwise (IO error while saving the stream)
     */
    public boolean put(@NonNull String key, @NonNull V model) {
        Preconditions.checkNotNull(key);
        Preconditions.checkNotNull(model);
        // don't care if the file already exist, it will be replaced
        File jsonFile = new File(mCacheLocation, key);
        try {
            mObjectMapper.writeValue(jsonFile, model);
        } catch (IOException e) {
            LogUtils.logException(TAG, "Exception when writing " + key, e);
            return false;
        }
        return true;
    }

    /**
     * Deletes an item from the disk cache.
     *
     * @param key The cache item key to remove (must be not null)
     * @return true if successful, false otherwise
     */
    public boolean remove(@NonNull String key) {
        Preconditions.checkNotNull(key);
        File jsonFile = new File(mCacheLocation, key);
        return jsonFile.delete();
    }

    @Override
    @NotForUIThread
    public final void clearOld() {
        purge(PURGE_AFTER);
    }

} @Beta
@ThreadSafe
public class SimpleDiskCache<V> implements SecondLevelCache<String, V> {

    /**
     * Minimum expiration that can be set to a disk cache entry before it can get deleted (used to
     * speed up setting of last modification date by avoiding calling I/O write OS methods when
     * unnecessary).
     */
    public static final long MIN_EXPIRE_IN_SEC = DroidUtils.HOUR * 2;
    protected static final long MIN_EXPIRE_IN_MS = MIN_EXPIRE_IN_SEC * 1000;
    /**
     * Default expiration time for the {@link SimpleDiskCache} subclasses (in seconds)
     */
    public static final long DEFAULT_EXPIRE_IN_SEC = DroidUtils.DAY * 2;

    protected static final ExecutorService PURGE_EXECUTOR = Executors
            .newSingleThreadExecutor(new PriorityThreadFactory(
                    "SimpleDiskCache purge executor thread", Process.THREAD_PRIORITY_BACKGROUND));

    @NonNull
    protected final File mCacheLocation;

    /**
     * Constructor for a disk cache that performs directory creation and initialization of the
     * passed location. Client code using this must handle any I/O failure while creating the cache
     * or inform callers.
     *
     * @param context               The context to retrieve the cache location
     * @param location              The preferred cache location
     * @param subFolder             The relative path to the cache folder where to store the cache
     *                              (if it doesn't exist, the folder is created)
     * @param allowLocationFallback Whether the disk cache can fallback to another location if the
     *                              selected one is not available
     * @throws IOException if the cache cannot be created
     */
    protected SimpleDiskCache(@NonNull Context context, @NonNull CacheLocation location,
                              @NonNull String subFolder, boolean allowLocationFallback) throws IOException {
        final File cacheRoot = StorageUtils.getAppCacheDir(context, location, true);
        if (cacheRoot != null) {
            mCacheLocation = new File(cacheRoot.getAbsolutePath() + File.separator + subFolder);
            // setup cache directory
            if (!mCacheLocation.exists() && !mCacheLocation.mkdirs()) {
                throw new IOException("Disk cache location cannot be created");
            }
        } else {
            throw new IOException("Disk cache location cannot be found");
        }
    }

    @Override
    @NotForUIThread
    public synchronized void clear() {
        cleanCacheDir();
    }

    /**
     * Purge old elements from this disk cache, according to the expiration policy of the
     * implementation.
     *
     * The definition of "old" can be overridden by subclasses by passing an expiration time to
     * {@link #cleanCacheDir(long)} or implementing a custom policy.
     */
    public void clearOld() {
        cleanCacheDir(DEFAULT_EXPIRE_IN_SEC);
    }

    /**
     * Asynchronously executes a purge of all contents on this disk cache.
     */
    public void scheduleClear() {
        PURGE_EXECUTOR.execute(new Runnable() {
            @Override
            public void run() {
                cleanCacheDir();
            }
        });
    }

    @Override
    @NotForUIThread
    public void clear(@NonNull ClearMode mode) {
        switch (mode) {
            case ALL:
                clear();
                break;
            case EVICT_OLD:
                clearOld();
                break;
        }
    }

    /**
     * Execute a purge of all contents on this disk cache which are older than the passed value.
     *
     * @param olderThanSec The maximum "age", in seconds, of the elements to keep in cache. This
     *                     value must be, in any case, equals or higher than {@value
     *                     #MIN_EXPIRE_IN_SEC}
     * @throws IllegalArgumentException if olderThan is less than {@value #MIN_EXPIRE_IN_SEC}
     */
    @NotForUIThread
    protected void purge(long olderThanSec) {
        Preconditions.checkArgument(olderThanSec >= MIN_EXPIRE_IN_SEC, "olderThanSec too short");
        cleanCacheDir(olderThanSec);
    }

    /**
     * Asynchronously execute a purge of all contents on this disk cache which are older than the
     * passed value.
     *
     * @param olderThan The maximum "age", in seconds, of the elements to keep in cache. This value
     *                  must be, in any case, equals or higher than {@value #MIN_EXPIRE_IN_SEC}
     * @throws IllegalArgumentException if olderThan is less than {@value #MIN_EXPIRE_IN_SEC}
     */
    protected void schedulePurge(final long olderThan) {
        if (olderThan < MIN_EXPIRE_IN_SEC) {
            throw new IllegalArgumentException("olderThan too short");
        } else {
            PURGE_EXECUTOR.execute(new Runnable() {
                @Override
                public void run() {
                    cleanCacheDir(olderThan);
                }
            });
        }
    }

    /**
     * Delete all files in the given directory (ignoring sub-directories).<br> Do NOT call from the
     * UI thread.
     *
     * @param dir The directory to clean up
     */
    @NotForUIThread
    @VisibleForTesting
    synchronized final boolean cleanCacheDir() {
        boolean success = true;
        if (mCacheLocation.exists()) {
            final File[] files = mCacheLocation.listFiles();
            if (files != null) {
                // iterate over files in the directory
                for (File f : files) {
                    if (f.isFile()) { // ignore sub dirs
                        success = success && f.delete();
                    }
                }
            }
        }
        return success;
    }

    /**
     * Delete files older than the given parameter in the given directory (ignoring
     * sub-directories).
     *
     * Note: this operation is time-consuming and should be done from a dedicated thread, if
     * possible when the application is not running.
     *
     * @param dir          The directory to clean
     * @param olderThanSec The max "age" from the current time for the file to be kept (in seconds)
     */
    @NotForUIThread
    @VisibleForTesting
    synchronized final void cleanCacheDir(@IntRange(from = 0) long olderThanSec) {
        final long now = System.currentTimeMillis();
        final long expirationMs = olderThanSec * 1000;
        final File[] files = mCacheLocation.listFiles();
        if (files != null) {
            for (File f : files) { // iterate over files
                if (f.isFile()) { // ignore sub dirs
                    deleteIfExpired(f, now, expirationMs);
                }
            }
        }
    }

    /**
     * Implements the policy for marking a file to avoid purging it. The method currently "touches"
     * the file by setting its modification date only if the file is older than the MIN_EXPIRE_IN
     * value (it is useless and time consuming to set it at every access).
     *
     * See the note regarding {@link File#setLastModified(long)} in the class documentation.
     *
     * @param file The {@link File} to "touch" if necessary
     * @return true if the file has been "touched", false otherwise
     */
    @NotForUIThread
    protected static synchronized final boolean touchFile(@NonNull File file) {
        final long now = System.currentTimeMillis();
        if (file.lastModified() < (now - MIN_EXPIRE_IN_MS)) {
            return file.setLastModified(now);
        }
        return false;
    }

    /**
     * Deletes a file if its last modified date is older than the expiration.
     *
     * @return true if the file is expired and it has been deleted, false otherwise
     */
    @VisibleForTesting
    static boolean deleteIfExpired(@NonNull File file, long now, @IntRange(from = 0) long expirationMs) {
        final long lastMod = file.lastModified();
        if (lastMod < (now - expirationMs)) {
            // delete if older than expiration
            return file.delete();
        }
        return false;
    }

} @Beta
public interface ContentLoader<R extends CacheableRequest<D>, D> {

    /**
     * Starts the content retrieval.
     *
     * @param action   The {@link AccessPolicy} to use
     * @param request  The {@link CacheableRequest} to retrieve the content
     * @param callback An optional {@link ContentUpdateCallback}
     * @return The retrieved data model, or null if unsuccessful.
     * @throws Exception if an exception was thrown during the retrieval.
     */
    public D load(AccessPolicy action, @NonNull final R request, ContentUpdateCallback<D> callback)
            throws Exception;

    /**
     * Interface for custom handlers that a {@link ContentLoader} uses to validate and execute an
     * {@link CacheableRequest}.
     *
     * The purpose is to abstract as much as possible the request execution, allowing callers to
     * inject custom actions when the content loader asks for a request execution.
     *
     * @param <R> The type of request (can be a subclass of {@link CacheableRequest})
     */
    @Beta
    public static interface RequestHandler {

        /**
         * Validates a request before execution. This can be used to perform customizations or set
         * values that may affect the request hash (or URL) in a non-immutable request prior to
         * passing them through the {@link ContentLoader} and being executed.
         *
         * See {@link AbstractModelRequest#setRequestUrl(String)} for more information on how a
         * request hash might change.
         *
         * @param request The {@link AbstractModelRequest} to validate
         * @return true if the passed request has been modified, false otherwise
         */
        public boolean validateRequest(@NonNull CacheableRequest<?> request);

        /**
         * Executes the passed request with the handler.
         *
         * @param request The {@link AbstractModelRequest} to execute
         * @return The retrieved {@link JsonModel} or null
         * @throws Exception if the request threw an exception
         */
        public JsonModel execRequest(@NonNull CacheableRequest<?> request) throws Exception;
    }

    /**
     * Interface that {@link ContentProxy} can implement to pass themselves to a task and be
     * notified when a {@link JsonModel} content has been refreshed from the remote location, in
     * order to perform specific "cache maintenance" operations.
     *
     * Sometimes, for example, a model update from the server can cause other existing
     * wrapped/dependent model items in cache to be invalidated. By passing this callback to a
     * {@link ContentProxy} it can handle this update transparently.
     *
     * @param <DATA> The data updated
     */
    public static interface ContentUpdateCallback<DATA> {

        /**
         * Called when a new content has been updated from outside the local cache.
         *
         * @param newContent The new {@link JsonModel} content
         */
        public void onContentUpdated(@NonNull DATA newContent);
    }

} public static interface ContentUpdateCallback<DATA> {

    /**
     * Called when a new content has been updated from outside the local cache.
     *
     * @param newContent The new {@link JsonModel} content
     */
    public void onContentUpdated(@NonNull DATA newContent);
} @Beta
@ThreadSafe
public final class DiskContentLoader<D> implements ContentLoader<CacheableRequest<D>, D> {

    private static final String TAG = DiskContentLoader.class.getSimpleName();

    private final ContentLruCache<String, ExpirableFutureTask<D>> mMemCache;
    @Nullable
    private final ModelDiskCache<D> mDiskCache;
    private final long mExpiration;
    @Nullable
    private final RequestHandler mRequestHandler;
    @Nullable
    private final ConnectionMonitor mConnMonitor;

    /**
     * Instantiates a new {@link DiskContentLoader}.
     *
     * @param memCache       The {@link ModelLruCache} to use
     * @param diskCache      The (optional) {@link ModelDiskCache} to use
     * @param expiration     Expiration offset time to use for {@link AccessPolicy}s that require
     *                       it
     * @param requestHandler The (optional) {@link RequestHandler} for this loader
     * @param connMonitor    The (optional) {@link ConnectionMonitor} for this loader
     */
    public DiskContentLoader(@NonNull ContentLruCache<String, ExpirableFutureTask<D>> memCache,
                             @Nullable ModelDiskCache<D> diskCache, long expiration,
                             @Nullable RequestHandler requestHandler, @Nullable ConnectionMonitor connMonitor) {
        mMemCache = memCache;
        mDiskCache = diskCache;
        mExpiration = expiration;
        mRequestHandler = requestHandler;
        mConnMonitor = connMonitor;
    }

    /**
     * @see ContentLoader#load(AccessPolicy, CacheableRequest, ContentUpdateCallback)
     */
    @Override
    public D load(@Nullable AccessPolicy action, @NonNull CacheableRequest<D> request,
                  @Nullable ContentUpdateCallback<D> callback) throws Exception {
        // TODO: improve this? It's almost procedural

        // if network is not active, turn every action to CACHE_ONLY
        boolean networkActive = (mConnMonitor != null) ? mConnMonitor.isNetworkActive() : true;
        action = (networkActive) ? ((action != null) ? action : AccessPolicy.NORMAL)
                : AccessPolicy.CACHE_ONLY;

        if (mRequestHandler != null) { // request handler validation
            mRequestHandler.validateRequest(request);
        }

        final String key = request.hash();
        // we try to retrieve item from our task cache
        ExpirableFutureTask<D> oldFutureTask = null;
        ExpirableFutureTask<D> future = oldFutureTask = mMemCache.get(key);
        /*
		 * we try to get a new task either if there is no previous one, if the
		 * action is REFRESH or the previous is expired and this is not a
		 * CACHE_ONLY action.
		 */
        final boolean isExpired = future != null && future.isExpired()
                && action != AccessPolicy.CACHE_ONLY;

        if (future == null || action == AccessPolicy.REFRESH || isExpired) {

            /** cache debugging */
            if (DroidConfig.DEBUG) {
                final String url = request.getRequestUrl();
                if (future == null) {
                    Log.v(TAG, "Memory cache miss for " + url);
                } else if (action == AccessPolicy.REFRESH) {
                    Log.v(TAG, "Refreshing cache for " + url);
                } else if (isExpired) {
                    Log.v(TAG, "Memory cache expired for " + url);
                }
            }
            /** cache debugging - END */

            // build the new loading task
            final ExpirableFutureTask<D> newFutureTask = new ExpirableFutureTask<D>(
                    new IOContentLoader(action, request, callback), mExpiration);

            if (action == AccessPolicy.REFRESH || isExpired) {
                // invalidate cache item if any to start new task
                mMemCache.remove(key, future);
            }

            future = mMemCache.putIfAbsent(key, newFutureTask);
            if (future == null) {
                // no tasks inserted in the meantime, execute it
                future = newFutureTask;
                newFutureTask.run();
            }
        }
        try {
            // wait for the task execution completion
            D model = future.get();
            if (model == null) {
                revertOnFailure(key, oldFutureTask, future, isExpired);
            }
            return model;
        } catch (CancellationException e) {
            revertOnFailure(key, oldFutureTask, future, isExpired);
        } catch (ExecutionException e) {
            // we don't want the cache to be polluted with failed attempts
            revertOnFailure(key, oldFutureTask, future, isExpired);
            throw Memoizer.launderThrowable(e.getCause());
        }
        return null;
    }

    /**
     * Fallback method to handle the case when a task isn't successful and needs to be removed from
     * the cache to avoid pollution. For some {@link AccessPolicy}s, it is also needed to replace
     * the failed new task with the old one (if any).
     *
     * @param key     The cache key string
     * @param oldTask The old, removed future task
     * @param newTask The new, failed future task to be removed
     * @param putOld  true to synchronously put the old model in the cache again if no other new
     *                tasks have been added, false otherwise
     */
    private void revertOnFailure(String key, ExpirableFutureTask<D> oldTask,
                                 ExpirableFutureTask<D> newTask, boolean putOld) {
        mMemCache.remove(key, newTask);
        if (oldTask != null && putOld) {
            mMemCache.putIfAbsent(key, oldTask);
        }
    }

    /**
     * Callable that executes loading of data from disk or network
     */
    private class IOContentLoader implements Callable<D> {

        private final AccessPolicy mAction;
        private final CacheableRequest<D> mRequest;
        @Nullable
        private final ContentUpdateCallback<D> mUpdateCallback;

        public IOContentLoader(@NonNull AccessPolicy action, @NonNull CacheableRequest<D> request,
                               @Nullable ContentUpdateCallback<D> callback) {
            mAction = action;
            mRequest = request;
            mUpdateCallback = callback;
        }

        @Override
        @SuppressWarnings("unchecked")
        public D call() throws Exception {

            final String key = mRequest.hash();
            D model = null;

            /** Disk cache access */
            if (mDiskCache != null) {
                if (mAction == AccessPolicy.CACHE_ONLY) {
                    // do not take care of cache expiration
                    model = mDiskCache.get(key);

                    /** cache debugging */
                    if (DroidConfig.DEBUG) {
                        final String url = mRequest.getRequestUrl();
                        if (model != null) {
                            Log.v(TAG, "CACHE_ONLY: Disk cache hit for " + url);
                        } else {
                            Log.v(TAG, "CACHE_ONLY: Disk cache miss for " + url);
                        }
                    }
                    /** cache debugging - END */

                    // caches retrieval failed, don't attempt a request
                    return model;
                } else if (mAction == AccessPolicy.REFRESH) {
                    mDiskCache.remove(key);
                } else {
                    // check disk cache (and verify item expiration)
                    if ((model = mDiskCache.get(key, mExpiration)) != null) {
                        return model;
                    } else {
                        /** cache debugging */
                        if (DroidConfig.DEBUG) {
                            Log.v(TAG, "Disk cache miss or expired for " + mRequest.getRequestUrl());
                        }
                        /** cache debugging - END */
                    }
                }
            }

            /** execute GET request to the server */
            if (mRequestHandler != null) {
                model = (D) mRequestHandler.execRequest(mRequest);
            } else {
                model = mRequest.execute();
            }
            if (model != null) { // update caches
                if (mDiskCache != null) {
                    mDiskCache.put(key, model);
                }
                if (mUpdateCallback != null) {
                    mUpdateCallback.onContentUpdated(model);
                }
            } else if (mAction == AccessPolicy.NORMAL) {
                // fallback when request failed
                if (mDiskCache != null) {
                    // set model to any stale data we might find
                    model = mDiskCache.get(key);
                }
                /** cache debugging */
                if (DroidConfig.DEBUG) {
                    final String url = mRequest.getRequestUrl();
                    if (model != null) {
                        Log.d(TAG, "Fallback: Disk cache hit for " + url);
                    } else {
                        Log.d(TAG, "Fallback: Disk cache miss for " + url);
                    }
                }
                /** cache debugging - END */
            }
            // FIXME: should we throw an exception rather than returning null?
            return model;
        }
    }

} @Beta
public interface ModelDiskContentLoaderFactory<R extends CacheableRequest<M>, M extends JsonModel> {

    /**
     * Builds a {@link ContentLoader}.
     *
     * @param memoryCache The {@link ContentLruCache} that the content loader will use.
     * @param diskCache   The {@link ModelDiskCache} that the content loader will use.
     * @return The built {@link ContentLoader}
     */
    public ContentLoader<R, M> getContentLoader(
            ContentLruCache<String, ExpirableFutureTask<M>> memoryCache, ModelDiskCache<M> diskCache);

} @Beta
public interface ContentProxy {

    /**
     * Completely clears the memory cache of this content manager.
     */
    public void clearMemoryCache();

    /**
     * Synchronously clears permanent storage (DB or flash disk) cache items of this content proxy
     * according to the passed {@link SimpleDiskCache.ClearMode}
     */
    public void clearDiskCache(SimpleDiskCache.ClearMode mode);

    /**
     * Completely wipes any permanent storage (DB or flash disk) cache for this content proxy.
     *
     * Note: implementations must be asynchronous and non-blocking.
     */
    public void scheduleClearDiskCache();

    /**
     * Synchronously wipe any kind of cache for this content proxy.
     */
    public void clearCache();

} @Beta
public interface CacheableRequest<E> extends Callable<E> {

    /**
     * Returns the request's URL. Override this in subclasses if the request URL cannot be generated
     * at object instantiation.
     */
    @Nullable
    public abstract String getRequestUrl();

    /**
     * Synchronously executes the request.
     *
     * @return The response object if it can be retrieved or null
     * @throws Exception If something went wrong
     */
    @Nullable
    public abstract E execute() throws Exception;

    /**
     * Synchronously executes the request using the passed {@link HttpRequestsManager}
     *
     * @see {@link #execute()}
     */
    @Nullable
    public abstract E execute(@NonNull HttpRequestsManager connManager) throws Exception;

    /**
     * Returns a 128-bit unique hash code string representation for this request to be used as a key
     * in caches.
     *
     * @return The String representation of the hash key
     */
    @NonNull
    public abstract String hash();

} @Beta
public class LogUtils {

    @SuppressWarnings("unused")
    private static final String TAG = LogUtils.class.getSimpleName();

    private LogUtils() {
        // hidden constructor, no instantiation needed
    }

    /**
     * Logs a message in LogCat, only if library debugging is active.
     *
     * @param logLevel   The log level to use: must be one of the level constants provided by the
     *                   {@link Log} class.
     * @param logTag
     * @param logMessage
     */
    public static void log(int logLevel, String logTag, String logMessage) {
        if (DroidConfig.DEBUG) {
            switch (logLevel) {
                case Log.ASSERT:
                case Log.VERBOSE:
                    Log.v(logTag, logMessage);
                    break;
                case Log.DEBUG:
                    Log.d(logTag, logMessage);
                    break;
                case Log.INFO:
                    Log.i(logTag, logMessage);
                    break;
                case Log.WARN:
                    Log.w(logTag, logMessage);
                    break;
                case Log.ERROR:
                    Log.e(logTag, logMessage);
                    break;
            }
        }
    }

    /**
     * Prints on LogCat a stack track of an exception, only if library debugging is active.
     *
     * @param t The {@link Throwable} to get the stack trace from
     */
    public static void logException(Throwable t) {
        if (DroidConfig.DEBUG) {
            if (t != null) {
                t.printStackTrace();
            }
        }
    }

    /**
     * Prints on LogCat a log warning message with the exception stack, only if library debugging is
     * active.
     *
     * @param logTag
     * @param logMessage
     * @param t          The {@link Throwable} to get the stack trace from
     */
    public static void logException(String logTag, String logMessage, Throwable t) {
        if (DroidConfig.DEBUG) {
            if (t != null) {
                Log.w(logTag, logMessage, t);
            }
        }
    }

    /**
     * Prints on LogCat a log warning message with the exception's message string, only if library
     * debugging is active.
     *
     * @param t The {@link Throwable} to get the message from
     */
    public static void logExceptionMessage(String logTag, String logMessage, Throwable t) {
        if (DroidConfig.DEBUG) {
            if (t != null) {
                Log.w(logTag,
                        logMessage + "\n" + t.getClass().getSimpleName() + " message: "
                                + t.getMessage());
            }
        }
    }

} @Immutable
public class ExpirableFutureTask<E> extends SettableFutureTask<E> {

    private final long mExpirationMs;

    /**
     * Instantiate an {@link ExpirableFutureTask} from a {@link Callable}
     *
     * @param callable   The content {@link Future} computation
     * @param expiration The expiration time, in milliseconds, or {@link Long#MAX_VALUE} for no
     *                   expiration.
     */
    public ExpirableFutureTask(@NonNull Callable<E> callable, @IntRange(from = 0) long expiration) {
        super(callable);
        if (expiration == Long.MAX_VALUE) {
            // force task to never expire
            mExpirationMs = Long.MAX_VALUE;
        } else {
            mExpirationMs = System.currentTimeMillis() + expiration;
        }
    }

    /**
     * Instantiate an {@link ExpirableFutureTask} from a {@link Runnable}
     *
     * @param runnable   The {@link Runnable} that executes the task
     * @param result     The result of the computation
     * @param expiration The expiration time, in milliseconds, or {@link Long#MAX_VALUE} for no
     *                   expiration.
     */
    public ExpirableFutureTask(@NonNull Runnable runnable, @Nullable E result,
                               @IntRange(from = 0) long expiration) {
        super(runnable, result);
        if (expiration == Long.MAX_VALUE) {
            // force task to never expire
            mExpirationMs = Long.MAX_VALUE;
        } else {
            mExpirationMs = System.currentTimeMillis() + expiration;
        }
    }

    /**
     * Returns whether the held computation result is expired or not.
     */
    public boolean isExpired() {
        return System.currentTimeMillis() > mExpirationMs;
    }

} @Beta
@Immutable
@SuppressWarnings("deprecation")
public abstract class JsonModel {

    /**
     * Empty list global instance that can be used as a safer replacement of null when a
     * deserialized list is null in a model. Using a raw type is acceptable because the list itself
     * will never contain an object.
     */
    @SuppressWarnings("rawtypes")
    protected static final ImmutableList EMPTY_LIST = ImmutableList.of();

    /**
     * Empty map global instance that can be used as a safer replacement of null when a deserialized
     * list is null in a model. Using a raw type is acceptable because the map itself will never
     * contain an object.
     */
    @SuppressWarnings("rawtypes")
    protected static final ImmutableMap EMPTY_MAP = ImmutableMap.of();

    private static final ObjectMapper JSON_MAPPER = JacksonJsonManager.getObjectMapper();

    /**
     * Interface to implement in {@link Filter} objects that filter on model objects.
     *
     * By implementing it for a specific model you can provide custom filtering and use the method
     * {@link #filterModel(CharSequence, JsonModel)} for a {@link Filterable}
     */
    public static interface JsonModelFilter<M> {
        public abstract boolean filterModel(CharSequence constraint, M model);
    }

    /**
     * <p> <b>Note:</b> This method is not overridden anymore. The default {@link Object#toString()}
     * implementation is returned. Use {@link #toJsonString()} instead. </p>
     *
     * {@inheritDoc}
     */
    @Override
    public String toString() {
        return super.toString();
    }

    /**
     * Returns a Json representation of this instance.<br> See {@link ObjectMapper#writeValueAsString(Object)}
     * for details.<br>
     *
     * Note that this implementation performs an actual serialization of a model object, avoid using
     * this method inside production code for logging purposes only.
     */
    @Nullable
    public String toJsonString() {
        return JacksonJsonManager.toJsonString(this);
    }

    /**
     * Creates a {@link JsonModel} instance by parsing its JSON string representation with the
     * default {@link ObjectMapper}.
     *
     * @param profileJson The model object string representation (can be null)
     * @param type        The type of the object to parse (use TypeClass[].class for an array of
     *                    models)
     * @return The instance of the given type or null if the string could not be parsed
     */
    @Nullable
    public static <E> E parseFromString(@Nullable String modelJson, Class<E> type) {
        E model = null;
        if (modelJson != null) {
            try {
                model = parseFromStringOrThrow(modelJson, type);
            } catch (IOException e) {
                // we ignore this
            }
        }
        return model;
    }

    /**
     * Creates a {@link JsonModel} instance by parsing its JSON string representation with the
     * default {@link ObjectMapper}, or throw an exception if not successful.
     *
     * @param profileJson The model object string representation (can be null)
     * @param type        The type of the object to parse (use TypeClass[].class for an array of
     *                    models)
     * @return The instance of the given type or null if the string could not be parsed
     * @throws IOException
     * @throws JsonMappingException
     * @throws JsonParseException
     */
    @Nullable
    public static <E> E parseFromStringOrThrow(@Nullable String modelJson, Class<E> type)
            throws JsonParseException, JsonMappingException, IOException {
        E model = null;
        if (modelJson != null) {
            model = JSON_MAPPER.readValue(modelJson, type);
        }
        return model;
    }

    /**
     * Creates a {@link List} of {@link JsonModel} instances by parsing its JSON string
     * representation with the default {@link ObjectMapper}, ignoring all exceptions.
     *
     * TODO: unit tests
     *
     * @param profileJson The model object string representation (can be null)
     * @param type        The type of the object to parse
     * @return The list of models or null if the string could not be parsed
     */
    @Nullable
    public static <E extends JsonModel> List<E> parseListFromString(@Nullable String modelJson,
                                                                    Class<E> type) {
        List<E> list = null;
        if (modelJson != null) {
            try {
                list = parseListFromStringOrThrow(modelJson, type);
            } catch (IOException e) {
                // we ignore this
            }
        }
        return list;
    }

    /**
     * Creates a {@link List} of {@link JsonModel} instances by parsing its JSON string
     * representation with the default {@link ObjectMapper}, or throw an exception if not
     * successful.
     *
     * TODO: unit tests
     *
     * @param profileJson The model object string representation (can be null)
     * @param type        The type of the object to parse
     * @return The list of models or null if the string could not be parsed
     * @throws IOException
     * @throws JsonMappingException
     * @throws JsonParseException
     */
    @Nullable
    public static <E extends JsonModel> List<E> parseListFromStringOrThrow(
            @Nullable String modelJson, Class<E> type) throws JsonParseException,
            JsonMappingException, IOException {
        ArrayList<E> list = null;
        if (modelJson != null) {
            list = JSON_MAPPER.readValue(modelJson, new TypeReference<List<E>>() {
            });
        }
        return list;
    }

}
import android.content.Context;
import android.support.annotation.CallSuper;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.util.Log;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.github.marcosalis.kraken.cache.AccessPolicy;
import com.github.marcosalis.kraken.cache.ContentLruCache;
import com.github.marcosalis.kraken.cache.ModelDiskCache;
import com.github.marcosalis.kraken.cache.SimpleDiskCache;
import com.github.marcosalis.kraken.cache.internal.loaders.ContentLoader;
import com.github.marcosalis.kraken.cache.internal.loaders.ContentLoader.ContentUpdateCallback;
import com.github.marcosalis.kraken.cache.internal.loaders.DiskContentLoader;
import com.github.marcosalis.kraken.cache.internal.loaders.ModelDiskContentLoaderFactory;
import com.github.marcosalis.kraken.cache.proxies.ContentProxy;
import com.github.marcosalis.kraken.cache.requests.CacheableRequest;
import com.github.marcosalis.kraken.utils.android.LogUtils;
import com.github.marcosalis.kraken.utils.annotations.NotForUIThread;
import com.github.marcosalis.kraken.utils.concurrent.ExpirableFutureTask;
import com.github.marcosalis.kraken.utils.json.JsonModel;
import com.google.common.annotations.Beta;
import java.io.IOException;
import java.util.concurrent.Callable;
import javax.annotation.concurrent.ThreadSafe;
/*
 * Copyright 2013 Luluvise Ltd
 * Copyright 2013 Marco Salis - fast3r(at)gmail.com
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *    http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.github.marcosalis.kraken.cache.internal;





/**
 * Generic, abstract implementation of {@link ContentProxy} that handles the retrieval of {@link
 * JsonModel}s using a 2-levels cache ( {@link ContentLruCache} and {@link ModelDiskCache}) before
 * retrieving content from the network.
 *
 * It is backed by a {@link ContentLoader} for the content retrieval.
 *
 * It provides methods to load and put models from and to the content proxy, and perform common
 * maintenance operations to the content proxy caches.
 *
 * @author Marco Salis
 * @since 1.0
 */
@Beta
@ThreadSafe
public abstract class AbstractDiskModelContentProxy<MODEL extends JsonModel> extends
        ModelContentProxy<MODEL> {

    private static final String TAG = AbstractDiskModelContentProxy.class.getSimpleName();

    private final ContentLruCache<String, ExpirableFutureTask<MODEL>> mModelCache;