org.gdg.frisbee.android.cache.ModelCache.java Source code

Java tutorial

Introduction

Here is the source code for org.gdg.frisbee.android.cache.ModelCache.java

Source

/*
 * Copyright 2013 The GDG Frisbee Project
 *
 * 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 org.gdg.frisbee.android.cache;

import android.content.Context;
import android.content.res.Resources;
import android.os.AsyncTask;
import android.os.Looper;
import android.support.v4.util.LruCache;
import android.util.Log;
import com.google.api.client.json.JsonFactory;
import com.google.api.client.json.gson.GsonFactory;
import com.google.gson.Gson;
import android.os.Process;
import com.google.gson.GsonBuilder;
import com.google.gson.reflect.TypeToken;
import com.jakewharton.DiskLruCache;
import org.gdg.frisbee.android.api.deserializer.DateTimeDeserializer;
import org.gdg.frisbee.android.api.deserializer.DateTimeSerializer;
import org.gdg.frisbee.android.utils.Utils;
import org.joda.time.DateTime;
import java.io.*;
import java.lang.reflect.Type;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;

/**
 * GDG Aachen
 * org.gdg.frisbee.android.cache
 * <p/>
 * User: maui
 * Date: 27.05.13
 * Time: 23:51
 */
public class ModelCache {

    public static class Builder {

        static final int MEGABYTE = 1024 * 1024;

        static final int DEFAULT_MEM_CACHE_MAX_SIZE = 32;

        static final int DEFAULT_DISK_CACHE_MAX_SIZE_MB = 10;

        private static long getHeapSize() {
            return Runtime.getRuntime().maxMemory();
        }

        private Context mContext;

        private boolean mDiskCacheEnabled;

        private File mDiskCacheLocation;

        private long mDiskCacheMaxSize;

        private boolean mMemoryCacheEnabled;

        private int mMemoryCacheMaxSize;

        public Builder() {
            this(null);
        }

        public Builder(Context context) {
            mContext = context;

            // Disk Cache is disabled by default, but it's default size is set
            mDiskCacheMaxSize = DEFAULT_DISK_CACHE_MAX_SIZE_MB * MEGABYTE;

            // Memory Cache is enabled by default, with a small maximum size
            mMemoryCacheEnabled = true;
            mMemoryCacheMaxSize = DEFAULT_MEM_CACHE_MAX_SIZE;
        }

        public ModelCache build() {
            final ModelCache cache = new ModelCache(mContext);

            if (isValidOptionsForMemoryCache()) {
                cache.setMemoryCache(new LruCache<String, CacheItem>(mMemoryCacheMaxSize));
            }

            if (isValidOptionsForDiskCache()) {
                new AsyncTask<Void, Void, DiskLruCache>() {

                    @Override
                    protected DiskLruCache doInBackground(Void... params) {
                        try {
                            DiskLruCache c = DiskLruCache.open(mDiskCacheLocation, 0, 2, mDiskCacheMaxSize);
                            cache.setDiskCache(c);
                            return c;
                        } catch (IOException e) {
                            e.printStackTrace();
                            return null;
                        }
                    }

                    @Override
                    protected void onPostExecute(DiskLruCache result) {
                        //cache.setDiskCache(result);
                    }

                }.execute();
            }

            return cache;
        }

        /**
         * Set whether the Disk Cache should be enabled. Defaults to {@code false}.
         *
         * @return This Builder object to allow for chaining of calls to set methods.
         */
        public Builder setDiskCacheEnabled(boolean enabled) {
            mDiskCacheEnabled = enabled;
            return this;
        }

        /**
         * Set the Disk Cache location. This location should be read-writeable.
         *
         * @return This Builder object to allow for chaining of calls to set methods.
         */
        public Builder setDiskCacheLocation(File location) {
            mDiskCacheLocation = location;
            return this;
        }

        /**
         * Set the maximum number of bytes the Disk Cache should use to store values. Defaults to
         * {@value #DEFAULT_DISK_CACHE_MAX_SIZE_MB}MB.
         *
         * @return This Builder object to allow for chaining of calls to set methods.
         */
        public Builder setDiskCacheMaxSize(long maxSize) {
            mDiskCacheMaxSize = maxSize;
            return this;
        }

        /**
         * Set whether the Memory Cache should be enabled. Defaults to {@code true}.
         *
         * @return This Builder object to allow for chaining of calls to set methods.
         */
        public Builder setMemoryCacheEnabled(boolean enabled) {
            mMemoryCacheEnabled = enabled;
            return this;
        }

        /**
         * Set the maximum number of bytes the Memory Cache should use to store values. Defaults to
         * {@value #DEFAULT_MEM_CACHE_MAX_SIZE}MB.
         *
         * @return This Builder object to allow for chaining of calls to set methods.
         */
        public Builder setMemoryCacheMaxSize(int size) {
            mMemoryCacheMaxSize = size;
            return this;
        }

        private boolean isValidOptionsForDiskCache() {
            if (mDiskCacheEnabled) {
                if (null == mDiskCacheLocation) {
                    return false;
                } else if (!mDiskCacheLocation.canWrite()) {
                    throw new IllegalArgumentException("Disk Cache Location is not write-able");
                }

                return true;
            }
            return false;
        }

        private boolean isValidOptionsForMemoryCache() {
            return mMemoryCacheEnabled && mMemoryCacheMaxSize > 0;
        }
    }

    static final int DISK_CACHE_FLUSH_DELAY_SECS = 5;

    private File mTempDir;
    private Resources mResources;

    private static final String LOG_TAG = "GDG-ModelCache";

    final JsonFactory mJsonFactory = new GsonFactory();

    private Gson mGson;

    private LruCache<String, CacheItem> mMemoryCache;

    private DiskLruCache mDiskCache;

    // Variables which are only used when the Disk Cache is enabled
    private HashMap<String, ReentrantLock> mDiskCacheEditLocks;

    private ScheduledThreadPoolExecutor mDiskCacheFlusherExecutor;

    private DiskCacheFlushRunnable mDiskCacheFlusherRunnable;

    // Transient
    private ScheduledFuture<?> mDiskCacheFuture;

    ModelCache(Context context) {
        if (context != null) {
            // Make sure we have the application context
            context = context.getApplicationContext();

            mTempDir = context.getCacheDir();
            mResources = context.getResources();
        }

        mGson = new GsonBuilder().registerTypeAdapter(DateTime.class, new DateTimeDeserializer())
                .registerTypeAdapter(DateTime.class, new DateTimeSerializer()).create();
    }

    public boolean contains(String url) {
        return containsInMemoryCache(url) || containsInDiskCache(url);
    }

    public boolean containsInDiskCache(String url) {
        if (null != mDiskCache) {
            checkNotOnMainThread();

            try {
                return null != mDiskCache.get(transformUrlForDiskCacheKey(url));
            } catch (IOException e) {
                e.printStackTrace();
            }
        }

        return false;
    }

    public boolean containsInMemoryCache(String url) {
        return null != mMemoryCache && null != mMemoryCache.get(url);
    }

    public void getAsync(final String url, final CacheListener mListener) {
        getAsync(url, true, mListener);
    }

    public void getAsync(final String url, final boolean checkExpiration, final CacheListener mListener) {
        new AsyncTask<Void, Void, Object>() {

            @Override
            protected Object doInBackground(Void... voids) {
                return ModelCache.this.get(url, checkExpiration);
            }

            @Override
            protected void onPostExecute(Object o) {
                if (o != null)
                    mListener.onGet(o);
                else
                    mListener.onNotFound(url);
            }
        }.execute();
    }

    public void getAsync(final String url, final boolean checkExpiration, final boolean forceFetch,
            final CacheListener mListener) {
        new AsyncTask<Void, Void, Object>() {

            @Override
            protected Object doInBackground(Void... voids) {
                if (!forceFetch)
                    return ModelCache.this.get(url, checkExpiration);
                else
                    return null;
            }

            @Override
            protected void onPostExecute(Object o) {
                if (o != null)
                    mListener.onGet(o);
                else
                    mListener.onNotFound(url);
            }
        }.execute();
    }

    public Object get(String url) {
        return get(url, true);
    }

    public Object get(String url, boolean checkExpiration) {
        Log.d(LOG_TAG, String.format("get(%s)", url));
        CacheItem result;

        // First try Memory Cache
        result = getFromMemoryCache(url, checkExpiration);

        if (null == result) {
            // Memory Cache failed, so try Disk Cache
            result = getFromDiskCache(url, checkExpiration);
        }

        if (result != null)
            return result.getValue();
        else
            return null;
    }

    public CacheItem getFromDiskCache(final String url, boolean checkExpiration) {
        CacheItem result = null;

        if (null != mDiskCache) {
            checkNotOnMainThread();

            try {
                final String key = transformUrlForDiskCacheKey(url);
                DiskLruCache.Snapshot snapshot = mDiskCache.get(key);
                if (null != snapshot) {

                    Object value = readValueFromDisk(snapshot.getInputStream(0));
                    DateTime expiresAt = new DateTime(readExpirationFromDisk(snapshot.getInputStream(1)));

                    if (value != null && expiresAt != null) {

                        if (checkExpiration && expiresAt.isBeforeNow()) {
                            mDiskCache.remove(key);
                            scheduleDiskCacheFlush();
                        } else {
                            result = new CacheItem(value, expiresAt);
                            if (null != mMemoryCache) {
                                mMemoryCache.put(url, result);
                            }
                        }
                    } else {
                        // If we get here, the file in the cache can't be
                        // decoded. Remove it and schedule a flush.
                        mDiskCache.remove(key);
                        scheduleDiskCacheFlush();
                    }
                }
            } catch (IOException e) {
                Log.e(LOG_TAG, "getFromDiskCache failed.", e);
                e.printStackTrace();
            }
        }

        return result;
    }

    public CacheItem getFromMemoryCache(final String url, boolean checkExpiration) {
        CacheItem result = null;

        if (null != mMemoryCache) {
            synchronized (mMemoryCache) {
                result = mMemoryCache.get(url);

                // If we get a value, that has expired
                if (null != result && result.getExpiresAt().isBeforeNow() && checkExpiration) {
                    mMemoryCache.remove(url);
                    result = null;
                }
            }
        }

        return result;
    }

    public boolean isDiskCacheEnabled() {
        return null != mDiskCache;
    }

    public boolean isMemoryCacheEnabled() {
        return null != mMemoryCache;
    }

    public CacheItem put(final String url, final Object obj) {
        return put(url, obj, new DateTime(0));
    }

    public void putAsync(final String url, final Object obj, final CachePutListener onDoneListener) {
        putAsync(url, obj, new DateTime(0), onDoneListener);
    }

    public void putAsync(final String url, final Object obj, final DateTime expiresAt,
            final CachePutListener onDoneListener) {
        new AsyncTask<Void, Void, Void>() {

            @Override
            protected Void doInBackground(Void... voids) {
                put(url, obj, expiresAt);
                return null;
            }

            @Override
            protected void onPostExecute(Void aVoid) {
                super.onPostExecute(aVoid);
                if (onDoneListener != null) {
                    onDoneListener.onPutIntoCache();
                }
            }
        }.execute();
    }

    public CacheItem put(final String url, final Object obj, DateTime expiresAt) {

        if (obj == null)
            return null;

        Log.d(LOG_TAG, String.format("put(%s)", url));
        CacheItem d = new CacheItem(obj, expiresAt);

        if (null != mMemoryCache) {
            mMemoryCache.put(url, d);
        }

        if (null != mDiskCache) {
            checkNotOnMainThread();

            final String key = transformUrlForDiskCacheKey(url);
            final ReentrantLock lock = getLockForDiskCacheEdit(key);
            lock.lock();
            try {
                DiskLruCache.Editor editor = mDiskCache.edit(key);
                writeValueToDisk(editor.newOutputStream(0), obj);
                writeExpirationToDisk(editor.newOutputStream(1), expiresAt);
                editor.commit();
            } catch (IOException e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
                scheduleDiskCacheFlush();
            }
        }

        return d;
    }

    public void remove(String url) {
        if (null != mMemoryCache) {
            mMemoryCache.remove(url);
        }

        if (null != mDiskCache) {
            checkNotOnMainThread();

            try {
                mDiskCache.remove(transformUrlForDiskCacheKey(url));
                scheduleDiskCacheFlush();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    synchronized void setDiskCache(DiskLruCache diskCache) {
        mDiskCache = diskCache;

        if (null != diskCache) {
            mDiskCacheEditLocks = new HashMap<String, ReentrantLock>();
            mDiskCacheFlusherExecutor = new ScheduledThreadPoolExecutor(1);
            mDiskCacheFlusherRunnable = new DiskCacheFlushRunnable(diskCache);
        }
    }

    void setMemoryCache(LruCache<String, CacheItem> memoryCache) {
        mMemoryCache = memoryCache;
    }

    private ReentrantLock getLockForDiskCacheEdit(String url) {
        synchronized (mDiskCacheEditLocks) {
            ReentrantLock lock = mDiskCacheEditLocks.get(url);
            if (null == lock) {
                lock = new ReentrantLock();
                mDiskCacheEditLocks.put(url, lock);
            }
            return lock;
        }
    }

    private void scheduleDiskCacheFlush() {
        // If we already have a flush scheduled, cancel it
        if (null != mDiskCacheFuture) {
            mDiskCacheFuture.cancel(false);
        }

        // Schedule a flush
        mDiskCacheFuture = mDiskCacheFlusherExecutor.schedule(mDiskCacheFlusherRunnable,
                DISK_CACHE_FLUSH_DELAY_SECS, TimeUnit.SECONDS);
    }

    private void writeExpirationToDisk(OutputStream os, DateTime expiresAt) throws IOException {

        DataOutputStream out = new DataOutputStream(os);

        out.writeLong(expiresAt.getMillis());

        out.close();
    }

    private void writeValueToDisk(OutputStream os, Object o) throws IOException {

        BufferedWriter out = new BufferedWriter(new OutputStreamWriter(os));

        String className = o.getClass().getCanonicalName();

        if (o instanceof ArrayList) {
            ArrayList d = (ArrayList) o;
            if (d.size() > 0)
                className = className + "<" + d.get(0).getClass().getCanonicalName() + ">";
        }

        out.write(className + "\n");

        if (className.contains("google")) {
            mJsonFactory.createJsonGenerator(out).serialize(o);
        } else {
            String json = mGson.toJson(o);
            out.write(json);
        }

        out.close();
    }

    private long readExpirationFromDisk(InputStream is) throws IOException {
        DataInputStream din = new DataInputStream(is);
        long expiration = din.readLong();
        din.close();
        return expiration;
    }

    private Object readValueFromDisk(InputStream is) throws IOException {
        BufferedReader fss = new BufferedReader(new InputStreamReader(is, "UTF-8"));
        String className = fss.readLine();

        if (className == null) {
            return null;
        }

        Type type = null;
        if (className.contains("ArrayList")) {
            String inner = className.substring("ArrayList<".length(), className.length() - 1);
            Class innerClass = null;
            try {
                innerClass = Class.forName(inner);
                type = TypeToken.get(Utils.createListOfType(innerClass).getClass()).getType();
            } catch (ClassNotFoundException e) {
                e.printStackTrace();
            }
        }

        String line = null;
        String content = "";
        while ((line = fss.readLine()) != null) {
            content += line;
        }

        fss.close();

        Class<?> clazz;
        try {
            clazz = Class.forName(className);

            if (className.contains("google")) {
                return mJsonFactory.createJsonParser(content).parseAndClose(clazz, null);
            } else if (type != null) {
                return mGson.fromJson(content, type);
            } else
                return mGson.fromJson(content, clazz);
        } catch (IllegalArgumentException e) {
            Log.e(LOG_TAG, "Deserializing from disk failed", e);
            return null;
        } catch (ClassNotFoundException e) {
            throw new IOException(e.getMessage());
        }
    }

    private static String transformUrlForDiskCacheKey(String url) {
        try {
            // Create MD5 Hash
            MessageDigest digest = java.security.MessageDigest.getInstance("MD5");
            digest.update(url.getBytes());
            byte messageDigest[] = digest.digest();

            // Create Hex String
            StringBuffer hexString = new StringBuffer();
            for (int i = 0; i < messageDigest.length; i++)
                hexString.append(Integer.toHexString(0xFF & messageDigest[i]));
            return hexString.toString();

        } catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
        }
        return "";
    }

    private static void checkNotOnMainThread() {
        if (Looper.myLooper() == Looper.getMainLooper()) {
            throw new IllegalStateException("This method should not be called from the main/UI thread.");
        }
    }

    public class CacheItem {
        private Object mValue;
        private DateTime mExpiresAt;

        public CacheItem(Object value, DateTime expiresAt) {
            mValue = value;
            mExpiresAt = expiresAt;
        }

        public DateTime getExpiresAt() {
            return mExpiresAt;
        }

        public void setExpiresAt(DateTime mExpiresAt) {
            this.mExpiresAt = mExpiresAt;
        }

        public Object getValue() {
            return mValue;
        }

        public void setValue(Object mValue) {
            this.mValue = mValue;
        }

    }

    public interface CachePutListener {
        public void onPutIntoCache();
    }

    public interface CacheListener {
        public void onGet(Object item);

        public void onNotFound(String key);
    }

    static final class DiskCacheFlushRunnable implements Runnable {

        private final DiskLruCache mDiskCache;

        public DiskCacheFlushRunnable(DiskLruCache cache) {
            mDiskCache = cache;
        }

        public void run() {
            // Make sure we're running with a background priority
            Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);

            try {
                mDiskCache.flush();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}