org.appcelerator.titanium.util.TiResponseCache.java Source code

Java tutorial

Introduction

Here is the source code for org.appcelerator.titanium.util.TiResponseCache.java

Source

/**
 * Appcelerator Titanium Mobile
 * Copyright (c) 2009-2012 by Appcelerator, Inc. All Rights Reserved.
 * Licensed under the terms of the Apache Public License
 * Please see the LICENSE included with this distribution for details.
 */
package org.appcelerator.titanium.util;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.FilenameFilter;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.CacheRequest;
import java.net.CacheResponse;
import java.net.ResponseCache;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URLConnection;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

import org.apache.commons.codec.digest.DigestUtils;
import org.appcelerator.kroll.common.Log;
import org.appcelerator.titanium.TiApplication;

public class TiResponseCache extends ResponseCache {
    private static final String TAG = "TiResponseCache";

    private static final String HEADER_SUFFIX = ".hdr";
    private static final String BODY_SUFFIX = ".bdy";
    private static final String CACHE_SIZE_KEY = "ti.android.cache.size.max";
    private static final int DEFAULT_CACHE_SIZE = 25 * 1024 * 1024; // 25MB
    private static final int INITIAL_DELAY = 10000;
    private static final int CLEANUP_DELAY = 60000;
    private static HashMap<String, ArrayList<CompleteListener>> completeListeners = new HashMap<String, ArrayList<CompleteListener>>();
    private static long maxCacheSize = 0;

    private static ScheduledExecutorService cleanupExecutor = null;

    public static interface CompleteListener {
        public void cacheCompleted(URI uri);
    }

    private static class TiCacheCleanup implements Runnable {
        private File cacheDir;
        private long maxSize;

        public TiCacheCleanup(File cacheDir, long maxSize) {
            this.cacheDir = cacheDir;
            this.maxSize = maxSize;
        }

        // TODO @Override
        public void run() {
            // Build up a list of access times
            HashMap<Long, File> lastTime = new HashMap<Long, File>();
            for (File hdrFile : cacheDir.listFiles(new FilenameFilter() {
                // TODO @Override
                public boolean accept(File dir, String name) {
                    return name.endsWith(HEADER_SUFFIX);
                }
            })) {
                lastTime.put(hdrFile.lastModified(), hdrFile);
            }

            // Ensure that the cache is under the required size
            List<Long> sz = new ArrayList<Long>(lastTime.keySet());
            Collections.sort(sz);
            Collections.reverse(sz);
            long cacheSize = 0;
            for (Long last : sz) {
                File hdrFile = lastTime.get(last);
                String h = hdrFile.getName().substring(0, hdrFile.getName().lastIndexOf('.')); // Hash
                File bdyFile = new File(cacheDir, h + BODY_SUFFIX);

                cacheSize += hdrFile.length();
                cacheSize += bdyFile.length();
                if (cacheSize > this.maxSize) {
                    hdrFile.delete();
                    bdyFile.delete();
                }
            }
        }

    }

    private static class TiCacheResponse extends CacheResponse {
        private Map<String, List<String>> headers;
        private InputStream istream;

        public TiCacheResponse(Map<String, List<String>> hdrs, InputStream istr) {
            super();
            headers = hdrs;
            istream = istr;
        }

        @Override
        public Map<String, List<String>> getHeaders() throws IOException {
            return headers;
        }

        @Override
        public InputStream getBody() throws IOException {
            return istream;
        }
    }

    private static class TiCacheOutputStream extends FileOutputStream {
        private URI uri;

        public TiCacheOutputStream(URI uri, File file) throws FileNotFoundException {
            super(file);
            this.uri = uri;
        }

        @Override
        public void close() throws IOException {
            super.close();
            fireCacheCompleted(uri);
        }
    }

    private static class TiCacheRequest extends CacheRequest {
        private URI uri;
        private File bFile, hFile;
        private long contentLength;

        public TiCacheRequest(URI uri, File bFile, File hFile, long contentLength) {
            super();
            this.uri = uri;
            this.bFile = bFile;
            this.hFile = hFile;
            this.contentLength = contentLength;
        }

        @Override
        public OutputStream getBody() throws IOException {
            return new TiCacheOutputStream(uri, bFile);
        }

        @Override
        public void abort() {
            // Only truly abort if we didn't write the whole length
            // This works around a bug where Android calls abort()
            // whenever the file is closed, successful writes or not
            if (bFile.length() != this.contentLength) {
                Log.e(TAG, "Failed to add item to the cache!");
                if (bFile.exists())
                    bFile.delete();
                if (hFile.exists())
                    hFile.delete();
            }
        }
    }

    public static boolean peek(URI uri) {
        TiResponseCache rc = (TiResponseCache) TiResponseCache.getDefault();
        if (rc == null)
            return false;
        if (rc.cacheDir == null)
            return false;

        String hash = DigestUtils.shaHex(uri.toString());
        File hFile = new File(rc.cacheDir, hash + HEADER_SUFFIX);
        File bFile = new File(rc.cacheDir, hash + BODY_SUFFIX);
        if (!bFile.exists() || !hFile.exists())
            return false;
        return true;
    }

    public static InputStream openCachedStream(URI uri) {
        TiResponseCache rc = (TiResponseCache) TiResponseCache.getDefault();
        if (rc == null) {
            return null;
        }

        if (rc.cacheDir == null) {
            return null;
        }

        String hash = DigestUtils.shaHex(uri.toString());
        File hFile = new File(rc.cacheDir, hash + HEADER_SUFFIX);
        File bFile = new File(rc.cacheDir, hash + BODY_SUFFIX);

        if (!bFile.exists() || !hFile.exists()) {
            return null;
        }

        try {
            return new FileInputStream(bFile);
        } catch (FileNotFoundException e) {
            // Fallback to URL download?
            return null;
        }
    }

    public static void addCompleteListener(URI uri, CompleteListener listener) {
        synchronized (completeListeners) {
            String hash = DigestUtils.shaHex(uri.toString());
            if (!completeListeners.containsKey(hash)) {
                completeListeners.put(hash, new ArrayList<CompleteListener>());
            }
            completeListeners.get(hash).add(listener);
        }
    }

    private File cacheDir = null;

    public TiResponseCache(File cachedir, TiApplication tiApp) {
        super();
        assert cachedir.isDirectory() : "cachedir MUST be a directory";
        cacheDir = cachedir;

        maxCacheSize = tiApp.getSystemProperties().getInt(CACHE_SIZE_KEY, DEFAULT_CACHE_SIZE) * 1024;
        Log.d(TAG, "max cache size is:" + maxCacheSize, Log.DEBUG_MODE);

        cleanupExecutor = Executors.newSingleThreadScheduledExecutor();
        TiCacheCleanup command = new TiCacheCleanup(cacheDir, maxCacheSize);
        cleanupExecutor.scheduleWithFixedDelay(command, INITIAL_DELAY, CLEANUP_DELAY, TimeUnit.MILLISECONDS);
    }

    @Override
    public CacheResponse get(URI uri, String rqstMethod, Map<String, List<String>> rqstHeaders) throws IOException {
        if (uri == null || cacheDir == null)
            return null;

        // Get our key, which is a hash of the URI
        String hash = DigestUtils.shaHex(uri.toString());

        // Make our cache files
        File hFile = new File(cacheDir, hash + HEADER_SUFFIX);
        File bFile = new File(cacheDir, hash + BODY_SUFFIX);

        if (!bFile.exists() || !hFile.exists()) {
            return null;
        }

        // Read in the headers
        Map<String, List<String>> headers = new HashMap<String, List<String>>();
        BufferedReader rdr = new BufferedReader(new FileReader(hFile), 1024);
        for (String line = rdr.readLine(); line != null; line = rdr.readLine()) {
            String keyval[] = line.split("=", 2);
            if (!headers.containsKey(keyval[0])) {
                headers.put(keyval[0], new ArrayList<String>());
            }
            headers.get(keyval[0]).add(keyval[1]);
        }
        rdr.close();

        // Update the access log
        hFile.setLastModified(System.currentTimeMillis());

        // Respond with the cache
        return new TiCacheResponse(headers, new FileInputStream(bFile));
    }

    protected String getHeader(Map<String, List<String>> headers, String header) {
        List<String> values = headers.get(header);
        if (values == null || values.size() == 0) {
            return null;
        }
        return values.get(values.size() - 1);
    }

    protected int getHeaderInt(Map<String, List<String>> headers, String header, int defaultValue) {
        String value = getHeader(headers, header);
        if (value == null) {
            return defaultValue;
        }
        try {
            return Integer.parseInt(value);
        } catch (NumberFormatException e) {
            return defaultValue;
        }
    }

    private Map<String, List<String>> makeLowerCaseHeaders(Map<String, List<String>> origHeaders) {
        Map<String, List<String>> headers = new HashMap<String, List<String>>(origHeaders.size());
        for (String key : origHeaders.keySet()) {
            if (key != null) {
                headers.put(key.toLowerCase(), origHeaders.get(key));
            }
        }
        return headers;
    }

    @Override
    public CacheRequest put(URI uri, URLConnection conn) throws IOException {
        if (cacheDir == null)
            return null;

        // Make sure the cacheDir exists, in case user clears cache while app is running
        if (!cacheDir.exists()) {
            cacheDir.mkdirs();
        }

        // Gingerbread 2.3 bug: getHeaderField tries re-opening the InputStream
        // getHeaderFields() just checks the response itself
        Map<String, List<String>> headers = makeLowerCaseHeaders(conn.getHeaderFields());
        String cacheControl = getHeader(headers, "cache-control");
        if (cacheControl != null && cacheControl.matches("^.*(no-cache|no-store|must-revalidate).*")) {
            return null; // See RFC-2616
        }

        boolean skipTransferEncodingHeader = false;
        String tEncoding = getHeader(headers, "transfer-encoding");
        if (tEncoding != null && tEncoding.toLowerCase().equals("chunked")) {
            skipTransferEncodingHeader = true; // don't put "chunked" transfer-encoding into our header file, else the http connection object that gets our header information will think the data starts with a chunk length specification
        }

        // Form the headers and generate the content length
        String newl = System.getProperty("line.separator");
        long contentLength = getHeaderInt(headers, "content-length", 0);
        StringBuilder sb = new StringBuilder();
        for (String hdr : headers.keySet()) {
            if (!skipTransferEncodingHeader || !hdr.equals("transfer-encoding")) {
                for (String val : headers.get(hdr)) {
                    sb.append(hdr);
                    sb.append("=");
                    sb.append(val);
                    sb.append(newl);
                }
            }
        }
        if (contentLength + sb.length() > maxCacheSize) {
            return null;
        }

        // Work around an android bug which gives us the wrong URI
        try {
            uri = conn.getURL().toURI();
        } catch (URISyntaxException e) {
        }

        // Get our key, which is a hash of the URI
        String hash = DigestUtils.shaHex(uri.toString());

        // Make our cache files
        File hFile = new File(cacheDir, hash + HEADER_SUFFIX);
        File bFile = new File(cacheDir, hash + BODY_SUFFIX);

        // Write headers synchronously
        FileWriter hWriter = new FileWriter(hFile);
        try {
            hWriter.write(sb.toString());
        } finally {
            hWriter.close();
        }

        synchronized (this) {
            // Don't add it to the cache if its already being written
            if (!bFile.createNewFile()) {
                return null;
            }
            return new TiCacheRequest(uri, bFile, hFile, contentLength);
        }
    }

    public void setCacheDir(File dir) {
        cacheDir = dir;
    }

    private static final void fireCacheCompleted(URI uri) {
        synchronized (completeListeners) {
            String hash = DigestUtils.shaHex(uri.toString());
            if (completeListeners.containsKey(hash)) {
                for (CompleteListener listener : completeListeners.get(hash)) {
                    listener.cacheCompleted(uri);
                }
                completeListeners.remove(hash);
            }
        }
    }
}