im.r_c.android.fusioncache.DiskCache.java Source code

Java tutorial

Introduction

Here is the source code for im.r_c.android.fusioncache.DiskCache.java

Source

/*
 * Copyright (c) 2016 Richard Chien
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy of
 * this software and associated documentation files (the "Software"), to deal in
 * the Software without restriction, including without limitation the rights to
 * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
 * the Software, and to permit persons to whom the Software is furnished to do so,
 * subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
 * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
 * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
 * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
 * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 */

package im.r_c.android.fusioncache;

import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.drawable.Drawable;

import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;

import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.FileReader;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.PrintWriter;
import java.io.Serializable;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;

import im.r_c.android.fusioncache.util.BitmapUtils;
import im.r_c.android.fusioncache.util.FileUtils;

/**
 * FusionCache
 * Created by richard on 6/12/16.
 * <p>
 * A thread-safe class that provides disk cache functions.
 * <p>
 * Methods of this class may block while doing IO things.
 *
 * @author Richard Chien
 */
public class DiskCache extends AbstractCache {

    /**
     * Pretend to store the key-value entry in a LRU cache.
     * The actual objects are in the disk in fact.
     * <p>
     * Keys in this cache wrapper are all hashed keys,
     * same as the cache file name.
     */
    private LruCacheWrapper<String, ValueWrapper> mCacheWrapper;

    /**
     * The directory that stores cache files.
     * <p>
     * Typically a sub directory inside the app's cache dir.
     */
    private File mCacheDir;

    public DiskCache(File cacheDir, long maxCacheSize) {
        if (cacheDir.exists() && cacheDir.isFile()) {
            throw new IllegalArgumentException("cacheDir is not a directory.");
        } else if (!cacheDir.exists()) {
            if (!cacheDir.mkdirs()) {
                // Failed to make dirs
                throw new RuntimeException("Cannot create cache directory.");
            }
        }

        mCacheDir = cacheDir;
        mCacheWrapper = new LruCacheWrapper<>(maxCacheSize, new LruCacheDelegate(mCacheDir));

        // Try to restore journal, aka the state of mCacheWrapper when last used
        List<LruCacheWrapper.Entry<String, ValueWrapper>> entryList = restoreJournal();
        if (entryList != null && entryList.size() > 0) {
            for (LruCacheWrapper.Entry<String, ValueWrapper> entry : entryList) {
                // Sizes of entries in the restore list are all fit current max cache size
                // so just put it in
                mCacheWrapper.put(entry.key, entry.value);
            }
        }
    }

    @Override
    public void put(String key, String value) {
        put(key, value.getBytes());
    }

    @Override
    public void put(String key, JSONObject value) {
        put(key, value.toString());
    }

    @Override
    public void put(String key, JSONArray value) {
        put(key, value.toString());
    }

    /**
     * The ultimate {@code put} method.
     * <p>
     * Any other {@code put} methods will finally call this one
     * to actually store byte array into the disk.
     */
    @Override
    public synchronized void put(String key, byte[] value) {
        int valueSize = value.length;
        if (valueSize > maxSize()) {
            // Value size is bigger than max cache size
            return;
        }

        // Get the hash value of the key
        // Never use the parameter "key" below
        String hashKey = hashKeyForDisk(key);

        File file = getCacheFile(mCacheDir, hashKey);
        FileOutputStream fos = null;
        try {
            fos = new FileOutputStream(file);
            fos.write(value);
            fos.flush();
            mCacheWrapper.put(hashKey, new ValueWrapper(valueSize));

            // Save journal file
            saveJournal();
        } catch (java.io.IOException e) {
            e.printStackTrace();
        } finally {
            if (fos != null) {
                try {
                    fos.close();
                } catch (IOException ignored) {
                }
            }
        }
    }

    @Override
    public void put(String key, Bitmap value) {
        put(key, BitmapUtils.bitmapToBytes(value));
    }

    @Override
    public void put(String key, Drawable value) {
        put(key, BitmapUtils.drawableToBitmap(value));
    }

    @Override
    public void put(String key, Serializable value) {
        ByteArrayOutputStream baos = null;
        ObjectOutputStream oos = null;
        try {
            baos = new ByteArrayOutputStream();
            oos = new ObjectOutputStream(baos);
            oos.writeObject(value);
            byte[] byteArray = baos.toByteArray();
            put(key, byteArray);
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (baos != null) {
                try {
                    baos.close();
                } catch (IOException ignored) {
                }
            }
            if (oos != null) {
                try {
                    oos.close();
                } catch (IOException ignored) {
                }
            }
        }
    }

    @Override
    public String getString(String key) {
        byte[] byteArray = getBytes(key);
        if (byteArray == null) {
            return null;
        }
        return new String(byteArray);
    }

    @Override
    public JSONObject getJSONObject(String key) {
        String jsonString = getString(key);
        if (jsonString == null) {
            return null;
        }
        JSONObject result = null;
        try {
            result = new JSONObject(jsonString);
        } catch (JSONException e) {
            e.printStackTrace();
        }
        return result;
    }

    @Override
    public JSONArray getJSONArray(String key) {
        String jsonString = getString(key);
        if (jsonString == null) {
            return null;
        }
        JSONArray result = null;
        try {
            result = new JSONArray(jsonString);
        } catch (JSONException e) {
            e.printStackTrace();
        }
        return result;
    }

    /**
     * The ultimate {@code get} method.
     * <p>
     * Any other {@code get} methods will first call this one
     * to get the primitive byte array and then convert to the type needed.
     */
    @Override
    public synchronized byte[] getBytes(String key) {
        // Get the hash value of the key
        // Never use the parameter "key" below
        String hashKey = hashKeyForDisk(key);

        File file = getCacheFile(mCacheDir, hashKey);
        if (!file.exists()) {
            remove(key); // Cache file missed, so remove it
            return null;
        }

        FileInputStream fis = null;
        byte[] byteArray = new byte[(int) file.length()];
        try {
            fis = new FileInputStream(file);
            if (fis.read(byteArray) < 0) {
                // Didn't read anything actually
                byteArray = null;
            }
            mCacheWrapper.get(hashKey);

            // Save journal file
            saveJournal();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (fis != null) {
                try {
                    fis.close();
                } catch (IOException ignored) {
                }
            }
        }

        return byteArray;
    }

    @Override
    public Bitmap getBitmap(String key) {
        return BitmapUtils.bytesToBitmap(getBytes(key));
    }

    /**
     * @deprecated Use {@code getDrawable(String, Resources)} instead.
     */
    @Deprecated
    @Override
    public Drawable getDrawable(String key) {
        return BitmapUtils.bitmapToDrawable(getBitmap(key), null);
    }

    public Drawable getDrawable(String key, Resources res) {
        return BitmapUtils.bitmapToDrawable(getBitmap(key), res);
    }

    @Override
    public Serializable getSerializable(String key) {
        byte[] byteArray = getBytes(key);
        if (byteArray == null || byteArray.length == 0) {
            return null;
        }

        ByteArrayInputStream bais = null;
        ObjectInputStream ois = null;
        Object result = null;
        try {
            bais = new ByteArrayInputStream(byteArray);
            ois = new ObjectInputStream(bais);
            result = ois.readObject();
        } catch (IOException | ClassNotFoundException e) {
            e.printStackTrace();
        } finally {
            if (bais != null) {
                try {
                    bais.close();
                } catch (IOException ignored) {
                }
            }
            if (ois != null) {
                try {
                    ois.close();
                } catch (IOException ignored) {
                }
            }
        }

        if (result == null || !(result instanceof Serializable)) {
            return null;
        } else {
            return (Serializable) result;
        }
    }

    /**
     * Always returns null, because for disk cache,
     * any action that gets a value must specify the type of the value.
     */
    @Override
    public synchronized Object remove(String key) {
        // Get the hash value of the key
        // Never use the parameter "key" below
        String hashKey = hashKeyForDisk(key);

        File file = getCacheFile(mCacheDir, hashKey);
        //noinspection ResultOfMethodCallIgnored
        file.delete();

        mCacheWrapper.remove(hashKey);

        // Save journal file
        saveJournal();

        return null;
    }

    @SuppressWarnings("ResultOfMethodCallIgnored")
    @Override
    public synchronized void clear() {
        FileUtils.deleteFile(mCacheDir);
    }

    @Override
    public synchronized long size() {
        return mCacheWrapper.size();
    }

    @Override
    public synchronized long maxSize() {
        return mCacheWrapper.maxSize();
    }

    synchronized Map<String, ValueWrapper> snapshot() {
        return mCacheWrapper.snapshot();
    }

    /**
     * Save snapshot to journal file.
     */
    synchronized void saveJournal() {
        final Map<String, ValueWrapper> snapshot = snapshot();
        File file = getJournalFile();
        PrintWriter pw = null;
        try {
            pw = new PrintWriter(file);
            for (String hashKey : snapshot.keySet()) {
                pw.println(hashKey);
            }
            pw.flush();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (pw != null) {
                pw.close();
            }
        }
    }

    /**
     * Restore from journal file if exists.
     * <p>
     * Typically called in constructor.
     * <p>
     * Note: Only restore caches that fit current max cache size,
     * which means the ones whose sizes are bigger than current max size will be skipped,
     * and the corresponding file will be deleted.
     *
     * @return List of entries.
     */
    synchronized List<LruCacheWrapper.Entry<String, ValueWrapper>> restoreJournal() {
        File journalFile = getJournalFile();
        if (!journalFile.exists()) {
            return null;
        }

        List<LruCacheWrapper.Entry<String, ValueWrapper>> list = new ArrayList<>();
        BufferedReader br = null;
        try {
            br = new BufferedReader(new FileReader(journalFile));
            String hashKey;
            while ((hashKey = br.readLine()) != null) {
                File cacheFile = getCacheFile(mCacheDir, hashKey);
                if (!cacheFile.exists()) {
                    continue;
                } else if (cacheFile.length() > maxSize()) {
                    // The cache file does not fit in the current cache
                    // so delete it
                    //noinspection ResultOfMethodCallIgnored
                    cacheFile.delete();
                    continue;
                }
                list.add(new LruCacheWrapper.Entry<>(hashKey, new ValueWrapper((int) cacheFile.length())));
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (br != null) {
                try {
                    br.close();
                } catch (IOException ignored) {
                }
            }
        }

        return list;
    }

    /**
     * Returns a {@code File} object refers to the journal file.
     */
    private File getJournalFile() {
        //noinspection SpellCheckingInspection
        return new File(mCacheDir, "fusioncache.journal");
    }

    private static File getCacheFile(File cacheDir, String hashKey) {
        // Use ".0" suffix for compatibility with DiskLruCache
        return new File(cacheDir, hashKey + ".0");
    }

    /**
     * A hashing method that changes a string (like a URL) into a hash
     * suitable for using as a disk filename.
     */
    private static String hashKeyForDisk(String key) {
        String cacheKey;
        try {
            final MessageDigest mDigest = MessageDigest.getInstance("MD5");
            mDigest.update(key.getBytes());
            cacheKey = bytesToHexString(mDigest.digest());
        } catch (NoSuchAlgorithmException e) {
            cacheKey = String.valueOf(key.hashCode());
        }
        return cacheKey;
    }

    /**
     * Convert bytes to hex string,
     * working for {@link #hashKeyForDisk(java.lang.String)}
     */
    private static String bytesToHexString(byte[] bytes) {
        // http://stackoverflow.com/questions/332079
        StringBuilder sb = new StringBuilder();
        for (byte aByte : bytes) {
            String hex = Integer.toHexString(0xFF & aByte);
            if (hex.length() == 1) {
                sb.append('0');
            }
            sb.append(hex);
        }
        return sb.toString();
    }

    /**
     * A value wrapper for disk cache items,
     * used to keep sizes of cache files.
     */
    static class ValueWrapper {
        int size;

        public ValueWrapper(int size) {
            this.size = size;
        }

        @Override
        public String toString() {
            return "ValueWrapper{" + "size=" + size + '}';
        }
    }

    /**
     * Implements some delegate methods of {@code LruCache}.
     */
    private static class LruCacheDelegate implements LruCacheWrapper.Delegate<String, ValueWrapper> {
        private File mCacheDir;

        public LruCacheDelegate(File cacheDir) {
            mCacheDir = cacheDir;
        }

        @Override
        public int sizeOf(String key, ValueWrapper valueWrapper) {
            return valueWrapper.size;
        }

        @Override
        public void entryRemoved(boolean evicted, String hashKey, ValueWrapper oldValue, ValueWrapper newValue) {
            if (evicted) {
                File file = getCacheFile(mCacheDir, hashKey);
                //noinspection ResultOfMethodCallIgnored
                file.delete();
            }
        }
    }
}