Package blackberry.web.widget.caching

Source Code of blackberry.web.widget.caching.CacheManager

/*
* Copyright 2010-2011 Research In Motion Limited.
*
* 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 blackberry.web.widget.caching;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.Date;
import java.util.Enumeration;
import java.util.Hashtable;
import java.util.Vector;

import javax.microedition.io.HttpConnection;
import javax.microedition.io.InputConnection;

import net.rim.device.api.browser.field2.BrowserFieldRequest;
import net.rim.device.api.browser.field2.BrowserFieldResponse;
import net.rim.device.api.compress.GZIPOutputStream;
import net.rim.device.api.io.MalformedURIException;
import net.rim.device.api.io.URI;
import net.rim.device.api.io.http.HttpHeaders;
import net.rim.device.api.io.http.HttpProtocolConstants;
import net.rim.device.api.system.CodeSigningKey;
import net.rim.device.api.system.ControlledAccess;
import net.rim.device.api.system.ControlledAccessException;
import net.rim.device.api.system.NonPersistableObjectException;
import net.rim.device.api.system.PersistentContentException;
import net.rim.device.api.system.PersistentObject;
import net.rim.device.api.system.PersistentStore;

import blackberry.web.widget.impl.WidgetConfigImpl;

public class CacheManager implements HttpProtocolConstants {
    // Constants
    private static String    CRLF                        = "\r\n";
    private static String    HTTP_HEADER_FIELD_SEPARATOR = ":";
    // private static String HTTP_HEADER_VALUE_SEPARATOR = ","; // Not currently used
    private static String    HTTP_HEADER_SINGLE_SPACE    = " ";

    private static String    WIDGETCACHEROOT             = "file:///store/home/user/cache/";
    private static final int MAX_STANDARD_CACHE_AGE      = 2592000;

    private String           _cacheStoreRoot;
    private WidgetConfigImpl _widgetConfigImpl;
    private Hashtable        _cacheTable;
    private long             _storeKey;

    public CacheManager( WidgetConfigImpl widgetConfigImpl ) {
        _widgetConfigImpl = widgetConfigImpl;
        String author = ( widgetConfigImpl.getAuthor() == null ) ? "" : widgetConfigImpl.getAuthor();
        _cacheStoreRoot = WIDGETCACHEROOT + Integer.toHexString( widgetConfigImpl.getName().hashCode() )
                + Integer.toHexString( widgetConfigImpl.getVersion().hashCode() ) + Integer.toHexString( author.hashCode() )
                + "/";
        _cacheTable = new Hashtable();
        populateCacheTable();
    }

    private void populateCacheTable() {
        ( new Date() ).getTime();
        try {
            // Generate store key for this app.
            _storeKey = generateStoreKeyFromPackageName();

            // Check Persistent Store for existing cacheTable data.
            PersistentObject cacheTableStore = PersistentStore.getPersistentObject( _storeKey );

            // Get the code signing key associated with this BlackBerry WebWorks Application.
            CodeSigningKey codeSigningKey = CodeSigningKey.get( this );
            Object cacheTableObj = cacheTableStore.getContents( codeSigningKey );

            // If we find an entry in the Persistent store.
            if( cacheTableObj != null ) {

                if( cacheTableObj instanceof Hashtable ) {
                    // Set the cache table using the stored value.
                    _cacheTable = (Hashtable) cacheTableObj;

                    // Ensure that expired entries are cleaned out.
                    cleanExpiredCache();
                }
            }
            // Otherwise, create the cacheTable entry in persistent store.
            else {
                synchronized( cacheTableStore ) {
                    cacheTableStore.setContents( new ControlledAccess( _cacheTable, codeSigningKey ) );
                    cacheTableStore.commit();
                }
            }

        } catch( Exception e ) {
        }
    }

    private void addCacheItem( CacheItem ci ) {
        if( ci != null ) {
            synchronized( _cacheTable ) {
                _cacheTable.put( ci.getUrl(), ci );

                int totalSize = getTotalCacheSize();
                if( totalSize > _widgetConfigImpl.getMaxCacheSize() ) {
                    refreshCache( totalSize - _widgetConfigImpl.getMaxCacheSize() );
                }

            }

            // Update cache table in persistent store.
            PersistentObject cacheTableStore = PersistentStore.getPersistentObject( _storeKey );
            CodeSigningKey codeSigningKey = CodeSigningKey.get( this );
            synchronized( cacheTableStore ) {
                cacheTableStore.setContents( new ControlledAccess( _cacheTable, codeSigningKey ) );
                cacheTableStore.commit();
            }
        }
    }

    private void refreshCache( int spaceToFree ) {
        Vector itemsFoundSorted = new Vector();
        int sizeFound = 0;
        synchronized( _cacheTable ) {
            Enumeration e = _cacheTable.elements();
            while( e.hasMoreElements() ) {
                CacheItem ci = (CacheItem) e.nextElement();

                boolean bFound = false;
                int size = itemsFoundSorted.size();
                for( int i = 0; i < size; i++ ) {
                    CacheItem ciFound = (CacheItem) itemsFoundSorted.elementAt( i );
                    if( ciFound.getExpires() > ci.getExpires() ) {
                        itemsFoundSorted.insertElementAt( ci, i );
                        bFound = true;
                        break;
                    } else if( ciFound.getExpires() == ci.getExpires() ) {
                        if( ciFound.getSize() < ci.getSize() ) {
                            itemsFoundSorted.insertElementAt( ci, i );
                            bFound = true;
                            break;
                        }
                    }
                }

                if( !bFound && sizeFound >= spaceToFree ) {
                } else {
                    if( !bFound ) {
                        itemsFoundSorted.addElement( ci );
                    }

                    sizeFound = 0;
                    size = itemsFoundSorted.size();
                    for( int i = 0; i < size; i++ ) {
                        CacheItem ciFound = (CacheItem) itemsFoundSorted.elementAt( i );
                        sizeFound += ciFound.getSize();
                        if( sizeFound >= spaceToFree ) {
                            itemsFoundSorted.setSize( i + 1 );
                            break;
                        }
                    }
                }
            }

            for( int i = 0; i < itemsFoundSorted.size(); i++ ) {
                clearCache( ( (CacheItem) itemsFoundSorted.elementAt( i ) ).getUrl() );
            }
        }
    }

    private void removeCacheFile( long storeKey ) {

        // Check Persistent Store for existing cacheTable data.
        PersistentObject cacheItemStore = PersistentStore.getPersistentObject( storeKey );

        // Get the code signing key associated with this BlackBerry WebWorks Application.
        CodeSigningKey codeSigningKey = CodeSigningKey.get( this );

        // If we find an entry in the Persistent store.
        if( cacheItemStore != null ) {
            Object cacheItemObj = cacheItemStore.getContents( codeSigningKey );
            if( cacheItemObj instanceof ByteVectorWrapper ) {
                // Remove the entry.
                synchronized( cacheItemStore ) {
                    cacheItemStore.setContents( null );
                    cacheItemStore.commit();
                }
            }
        }

    }

    private CacheItem writeCacheFile( String url, long expires, final byte[] data,
            final HttpHeaders headers ) throws IOException {
       
        // ByteVector is used to bypass persistent storage object size limits
        ByteVectorWrapper pDataStore = new ByteVectorWrapper();

        // Write URL
        writeToByteVector( url.getBytes(), pDataStore );
        writeToByteVector( CRLF.getBytes(), pDataStore );

        // Write expires
        writeToByteVector( ( new Long( expires ) ).toString().getBytes(), pDataStore );
        writeToByteVector( CRLF.getBytes(), pDataStore );

        // Write size
        writeToByteVector( ( new Integer( data.length ) ).toString().getBytes(), pDataStore );
        writeToByteVector( CRLF.getBytes(), pDataStore );

        // Write headers
        int xPropertiesSize = headers.size();
        for( int i = 0; i < xPropertiesSize; i++ ) {
            writeToByteVector( headers.getPropertyKey( i ).getBytes(), pDataStore );
            writeToByteVector( HTTP_HEADER_FIELD_SEPARATOR.getBytes(), pDataStore );
            writeToByteVector( HTTP_HEADER_SINGLE_SPACE.getBytes(), pDataStore );
            writeToByteVector( headers.getPropertyValue( i ).getBytes(), pDataStore );
            writeToByteVector( CRLF.getBytes(), pDataStore );
        }

        writeToByteVector( CRLF.getBytes(), pDataStore );

        // Write data.
        writeDataToStore( pDataStore, data );
       
        String filePath = _cacheStoreRoot + Integer.toHexString( url.hashCode() )
                + Integer.toHexString( data.length );
        long storeKey = generateCacheItemStoreKey( filePath );

        // Open persistent store. Use key to access store.
        PersistentObject persistentObject = PersistentStore.getPersistentObject( storeKey );

        // Save to Pstore.
        synchronized( persistentObject ) {
            try {
                // Get the code signing key associated with this BlackBerry WebWorks Application.
                CodeSigningKey codeSigningKey = CodeSigningKey.get( this );
                persistentObject.setContents( new ControlledAccess( pDataStore, codeSigningKey ) );
                persistentObject.commit();
            } catch ( ControlledAccessException cae ) {
                throw new IOException();
            } catch ( NonPersistableObjectException npoe ) {
                throw new IOException();
            } catch ( PersistentContentException pce ) {
                throw new IOException();
            }
        }

        // Create cache item using store key.
        return new CacheItem( storeKey, url, expires, data.length, pDataStore.size() );
    }

    private void writeDataToStore( ByteVectorWrapper dataVector, byte[] data ) throws IOException {
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        GZIPOutputStream gzipStream = new GZIPOutputStream( baos, 6, GZIPOutputStream.MAX_LOG2_WINDOW_LENGTH );
        gzipStream.write( data );
        gzipStream.close();
        byte[] compressedData = baos.toByteArray();

        // Write compressed size
        int compressedSize = compressedData.length;
        int originalSize = data.length;

        if( compressedSize < originalSize * 0.95 ) {
            // If the compression ratio is greater than 95%
            writeToByteVector( ( new Integer( 1 ) ).toString().getBytes(), dataVector );
            writeToByteVector( CRLF.getBytes(), dataVector );
            writeToByteVector( ( new Integer( compressedSize ) ).toString().getBytes(), dataVector );
            writeToByteVector( CRLF.getBytes(), dataVector );
            writeToByteVector( compressedData, dataVector );
        } else {
            // Compression ratio is not satisfactory, so store the original data
            writeToByteVector( ( new Integer( 0 ) ).toString().getBytes(), dataVector );
            writeToByteVector( CRLF.getBytes(), dataVector );
            writeToByteVector( ( new Integer( originalSize ) ).toString().getBytes(), dataVector );
            writeToByteVector( CRLF.getBytes(), dataVector );
            writeToByteVector( data, dataVector );
        }
    }

    private boolean isAggressivelyCaching() {
        return _widgetConfigImpl.getAggressivelyCaching();
    }

    public boolean isRequestCacheable( BrowserFieldRequest request ) {
        // Only http request is cacheable.
        if( !request.getProtocol().equals( "http" ) ) {
            return false;
        }

        // Don't cache the request whose method is not "GET".
        if( request instanceof HttpConnection ) {
            if( !( (HttpConnection) request ).getRequestMethod().equals( "GET" ) ) {
                return false;
            }
        }

        // Don't cache the request with post data.
        if( request.getPostData() != null ) {
            return false;
        }

        // Don't cache authentication request.
        if( request.getHeaders().getPropertyValue( "Authorization" ) != null ) {
            return false;
        }

        // Check URI file types from config.xml.
        if( !isUriCacheable( request.getURL(), _widgetConfigImpl.getAllowedUriTypes() ) ) {
            return false;
        }

        return true;
    }

    public boolean isResponseCacheable( HttpConnection response ) {
        try {
            if( response.getResponseCode() != 200 ) {
                return false;
            }
        } catch( IOException ioe ) {
            return false;
        }

        if( !response.getRequestMethod().equals( "GET" ) ) {
            return false;
        }

        if( containsPragmaNoCache( response ) ) {
            return false;
        }

        if( isExpired( response ) ) {
            return false;
        }

        if( containsCacheControlNoCache( response ) ) {
            return false;
        }

        if( containsNoContentLength( response ) ) {
            return false;
        }

        // Bypass size check if -1
        long size = getDataSize( response );
        long maxCacheable = _widgetConfigImpl.getMaxCacheable();
        long maxCacheSize = _widgetConfigImpl.getMaxCacheSize();

        if( maxCacheable != -1 && ( size > maxCacheable || size > maxCacheSize ) ) {
            return false;
        }

        long expires = getResponseExpires( response );
        if( expires <= 0 ) {
            if( !isAggressivelyCaching() ) {
                return false;
            }
        }

        return true;
    }

    private boolean containsPragmaNoCache( HttpConnection response ) {
        try {
            if( response.getHeaderField( "pragma" ) != null
                    && response.getHeaderField( "pragma" ).toLowerCase().indexOf( "no-cache" ) >= 0 ) {
                return true;
            }

            return false;
        } catch( IOException ioe ) {
            return true;
        }
    }

    private boolean isExpired( HttpConnection response ) {
        long expires = getResponseExpires( response ); // getExpiration() returns 0 if not known
        if( expires > 0 && expires <= ( new Date() ).getTime() ) {
            return true;
        }

        return false;
    }

    private boolean containsCacheControlNoCache( HttpConnection response ) {
        try {
            String cacheControl = response.getHeaderField( "cache-control" );
            if( cacheControl != null ) {
                cacheControl = removeSpace( cacheControl.toLowerCase() );
                if( cacheControl.indexOf( "no-cache" ) >= 0 || cacheControl.indexOf( "no-store" ) >= 0
                        || cacheControl.indexOf( "private" ) >= 0 || cacheControl.indexOf( "max-age=0" ) >= 0 ) {
                    return true;
                }

                long maxAge = parseMaxAge( cacheControl );
                if( maxAge > 0 && response.getDate() > 0 ) {
                    long date = response.getDate();
                    long now = ( new Date() ).getTime();
                    if( now > date + maxAge ) {
                        // Already expired
                        return true;
                    }
                }
            }

            return false;
        } catch( IOException ioe ) {
            return true;
        }
    }

    private boolean containsNoContentLength( HttpConnection response ) {
        return ( response.getLength() <= 0 );
    }

    private long getDataSize( HttpConnection response ) {
        return response.getLength();
    }

    private long getResponseExpires( HttpConnection response ) {
        try {
            // Calculate expires from "max-age" and "date"
            if( response.getHeaderField( "cache-control" ) != null ) {
                String cacheControl = removeSpace( response.getHeaderField( "cache-control" ).toLowerCase() );
                long maxAge = parseMaxAge( cacheControl );
                long date = response.getDate();

                if( maxAge > 0 && date > 0 ) {
                    return ( date + maxAge );
                }
            }
           
            // Calculate expires from "expires"
            long expires = response.getExpiration();
            if( expires > 0 ) {
                return expires;
            }
        } catch( IOException ioe ) {
        }

        return 0;
    }

    private long parseMaxAge( String cacheControl ) {
        if( cacheControl == null ) {
            return 0;
        }

        long maxAge = 0;
        if( cacheControl.indexOf( "max-age=" ) >= 0 ) {
            int maxAgeStart = cacheControl.indexOf( "max-age=" ) + 8;
            int maxAgeEnd = cacheControl.indexOf( ',', maxAgeStart );
            if( maxAgeEnd < 0 ) {
                maxAgeEnd = cacheControl.length();
            }

            try {
                maxAge = Long.parseLong( cacheControl.substring( maxAgeStart, maxAgeEnd ) );
            } catch( NumberFormatException nfe ) {
            }
        }

        // Multiply maxAge by 1000 to convert seconds to milliseconds
        maxAge *= 1000L;
        return maxAge;
    }

    public InputConnection createCache( String url, HttpConnection response ) {
        System.out.println( "WEBWORKS ==> createCache: " + url );

        // Calculate expires
        long expires = calculateCacheExpires( response );

        // Copy headers
        HttpHeaders headers = copyResponseHeaders( response );

        // Read data
        byte[] data = null;
        InputStream is = null;
        try {
            int len = (int) response.getLength();
            if( len > 0 ) {
                is = response.openInputStream();
                int actual = 0;
                int bytesread = 0;
                data = new byte[ len ];
                while( ( bytesread != len ) && ( actual != -1 ) ) {
                    actual = is.read( data, bytesread, len - bytesread );
                    bytesread += actual;
                }
            }
        } catch( IOException ioe ) {
            data = null;
        } finally {
            if( is != null ) {
                try {
                    is.close();
                } catch( IOException ioe ) {
                }
            }
            if( response != null ) {
                try {
                    response.close();
                } catch( IOException ioe ) {
                }
            }
        }

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

        // Store the cache copy and create in-memory cache item
        CacheItem ci = null;
        try {
            ci = writeCacheFile( url, expires, data, headers );
        } catch ( IOException ignore ) {
            // ci remains null
        }
        if( ci != null ) {
            System.out.println( "WEBWORKS ==> cache created: " + url + " at " + ci.getStoreKey() );
            addCacheItem( ci );
        } else {
            System.out.println( "WEBWORKS ==> cache not created: " + url );
        }

        return new BrowserFieldResponse( url, data, headers );
    }

    private long calculateCacheExpires( HttpConnection response ) {
        long date = 0;
        try {
            date = response.getDate();
        } catch( IOException ioe ) {
        }

        if( date == 0 ) {
            date = ( new Date() ).getTime();
        }

        long expires = getResponseExpires( response );
        if( expires <= 0 ) {
            // Calculate the aggressive cache's expires based on AggressiveCacheAge
            /*
             * Multiply the cache age value by 1000 to convert the value from seconds to milliseconds
             */
            expires = date + ( _widgetConfigImpl.getAggressiveCacheAge() * 1000L );
        } else {
            // Check whether the cache's fresh age is overridden
            if( _widgetConfigImpl.getOverrodeAge() > 0 && expires > date + _widgetConfigImpl.getOverrodeAge() ) {
                expires = date + _widgetConfigImpl.getOverrodeAge();
            }
        }

        // Do not allow the expires value to exceed the max allowed
        expires = Math.min( date + ( MAX_STANDARD_CACHE_AGE * 1000L ), expires );

        return expires;
    }

    private HttpHeaders copyResponseHeaders( HttpConnection response ) {
        HttpHeaders headers = new HttpHeaders();
        try {
            int index = 0;
            while( response.getHeaderFieldKey( index ) != null ) {
                headers.addProperty( response.getHeaderFieldKey( index ), response.getHeaderField( index ) );
                index++;
            }
        } catch( IOException ioe ) {
        }

        return headers;
    }

    public boolean hasCache( String url ) {
        boolean ret;
        synchronized( _cacheTable ) {
            ret = _cacheTable.containsKey( url );
        }

        return ret;
    }

    public void clearCache( String url ) {
        Object o;
        synchronized( _cacheTable ) {
            o = _cacheTable.get( url );
            _cacheTable.remove( url );
        }

        if( o instanceof CacheItem ) {
            removeCacheFile( ( (CacheItem) o ).getStoreKey() );
        }
    }

    public void clearAll() {
        Vector files = new Vector();
        CacheItem nextElement = null;
        synchronized( _cacheTable ) {
            Enumeration e = _cacheTable.elements();
            while( e.hasMoreElements() ) {
                nextElement = (CacheItem) e.nextElement();
                files.addElement( new Long( nextElement.getStoreKey() ) );
            }

            _cacheTable.clear();
        }

        // File deletion doesn't require synchronization
        for( int i = 0; i < files.size(); i++ ) {
            removeCacheFile( ( (Long) files.elementAt( i ) ).longValue() );
        }
    }

    public int getTotalCacheSize() {
        int size = 0;
        synchronized( _cacheTable ) {
            Enumeration e = _cacheTable.elements();
            while( e.hasMoreElements() ) {
                size += ( (CacheItem) e.nextElement() ).getSize();
            }
        }

        return size;
    }

    public ScriptableCacheItem[] getScriptableCacheItems() {
        int count = 0;
        ScriptableCacheItem[] items = null;

        synchronized( _cacheTable ) {
            items = new ScriptableCacheItem[ _cacheTable.size() ];
            Enumeration e = _cacheTable.elements();
            while( e.hasMoreElements() ) {
                CacheItem ci = (CacheItem) e.nextElement();
                items[ count ] = new ScriptableCacheItem( ci.getUrl(), ci.getSize(), ci.getExpires() );
                count++;
            }
        }

        return items;
    }

    public boolean hasCacheExpired( String url ) {
        Object o;
        synchronized( _cacheTable ) {
            o = _cacheTable.get( url );
        }

        if( o instanceof CacheItem ) {
            CacheItem ci = (CacheItem) o;
            long date = ( new Date() ).getTime();
            if( ci.getExpires() > date ) {
                return false;
            } else {
                // Remove the expired cache item
                clearCache( url );
            }
        }

        return true;
    }

    public InputConnection getCache( String url ) {
        Object o;
        synchronized( _cacheTable ) {
            o = _cacheTable.get( url );
        }

        if( o instanceof CacheItem ) {
            CacheItem ci = (CacheItem) o;
            HttpHeaders headers = ci.getHeaders();
            byte[] data = ci.getData();
            return new BrowserFieldResponse( url, data, headers );
        }

        return null;
    }

    public static String receiveLine( InputStream inputStream ) {
        int value;
        boolean shouldContinue = true;
        StringBuffer result = new StringBuffer();
        while( shouldContinue ) {
            try {
                value = inputStream.read();
                switch( ( value ) ) {
                    case -1:
                    case 10:
                        shouldContinue = false;
                        break;
                    case 13:
                        break;
                    default:
                        result.append( (char) value );
                }
            } catch( IOException e ) {
                return "";
            }
        }
        return ( result.toString().trim() );
    }

    private static String removeSpace( String s ) {
        StringBuffer result = new StringBuffer();
        int count = s.length();
        for( int i = 0; i < count; i++ ) {
            char c = s.charAt( i );
            if( c != ' ' ) {
                result.append( c );
            }
        }

        return result.toString();
    }

    private boolean isUriCacheable( String url, Hashtable filters ) {
        if( filters == null ) {
            return true;
        }

        URI uri = null;
        try {
            uri = URI.create( url.trim() );
        } catch( MalformedURIException mue ) {
            return false;
        }

        if( uri == null ) {
            return false;
        }

        String file = URI.getFile( uri.getPath() );
        int lastDot = file.lastIndexOf( '.' );
        if( lastDot < 0 ) {
            return false;
        } else {
            String ext = file.substring( lastDot + 1 );
            if( filters.containsKey( ext ) ) {
                return true;
            }
        }

        return false;
    }

    private long generateStoreKeyFromPackageName() {
        String packageName = this.getClass().getName();
        int hashcodeInt = packageName.hashCode();
        return Long.parseLong( Integer.toString( hashcodeInt ) );
    }

    private long generateCacheItemStoreKey( String filepath ) {
        int hashcodeInt = filepath.hashCode();
        return Long.parseLong( Integer.toString( hashcodeInt ) );
    }

    private void cleanExpiredCache() {
        // Determine current time
        long now = ( new Date() ).getTime();

        // Go through all the elements in the cache table
        synchronized( _cacheTable ) {
            Enumeration e = _cacheTable.elements();
            while( e.hasMoreElements() ) {
                CacheItem ci = (CacheItem) e.nextElement();
             // Remove cache file if expired
                if( ci != null && ci.getExpires() <= now ) {
                    removeCacheFile( ci.getStoreKey() );
                }
            }
        }
    }

    private void writeToByteVector( final byte[] dataToWrite, final ByteVectorWrapper vector ) {
        if( dataToWrite != null ) {
            for( int i = 0; i < dataToWrite.length; i++ ) {
                vector.addElement( dataToWrite[ i ] );
            }
        }
    }

}
TOP

Related Classes of blackberry.web.widget.caching.CacheManager

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.