/*
* 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 ] );
}
}
}
}