blob: b235c44aafc967c3b645e8fe556670c526aa2968 [file] [log] [blame]
////////////////////////////////////////////////////////////////////////////////
//
// Licensed to the Apache Software Foundation (ASF) under one or more
// contributor license agreements. See the NOTICE file distributed with
// this work for additional information regarding copyright ownership.
// The ASF licenses this file to You 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 spark.core
{
import flash.display.Loader;
import flash.display.LoaderInfo;
import flash.events.Event;
import flash.events.EventDispatcher;
import flash.events.IOErrorEvent;
import flash.events.SecurityErrorEvent;
import flash.net.URLRequest;
import flash.system.LoaderContext;
import flash.utils.Dictionary;
import mx.core.mx_internal;
import mx.utils.LinkedList;
import mx.utils.LinkedListNode;
import spark.events.LoaderInvalidationEvent;
use namespace mx_internal;
//--------------------------------------
// Events
//--------------------------------------
/**
* Dispatched when a cache entry is invalidated, generally this
* occurs when the entry is determined to be untrusted while one or
* more outstanding load requests are active for a given cache entry.
* This mechanism allows any outstanding content requests to be reset
* due to the fact that the cache entry has been deemed 'unshareable'.
* Each content request notified then attempts instead re-requests the
* asset.
*
* @eventType spark.events.LoaderInvalidationEvent
*
* @langversion 3.0
* @playerversion Flash 10
* @playerversion AIR 2.0
* @productversion Flex 4.5
*/
[Event(name="invalidateLoader", type="spark.events.LoaderInvalidationEvent")]
/**
* Provides a caching and queuing image content loader suitable for using
* a shared image cache for the BitmapImage and spark Image components.
*
* @langversion 3.0
* @playerversion Flash 10
* @playerversion AIR 1.5
* @productversion Flex 4.5
*/
public class ContentCache extends EventDispatcher implements IContentLoader
{
//--------------------------------------------------------------------------
//
// Constructor
//
//--------------------------------------------------------------------------
/**
* Constructor.
*
* @langversion 3.0
* @playerversion Flash 10
* @playerversion AIR 1.5
* @productversion Flex 4.5
*/
public function ContentCache():void
{
super();
}
//--------------------------------------------------------------------------
//
// Class constants
//
//--------------------------------------------------------------------------
/**
* Value used to mark cached URLs that are detected as being from an
* untrusted source (meaning they will no longer be shareable).
*
* @langversion 3.0
* @playerversion Flash 9
* @playerversion AIR 1.1
* @productversion Flex 4.5
*/
protected static const UNTRUSTED:String = "untrusted";
//--------------------------------------------------------------------------
//
// Variables
//
//--------------------------------------------------------------------------
/**
* Map of source to CacheEntryNode.
*
* @langversion 3.0
* @playerversion Flash 10
* @playerversion AIR 1.5
* @productversion Flex 4.5
*/
protected var cachedData:Dictionary = new Dictionary();
/**
* Ordered (MRU) list of CacheEntryNode instances.
*
* @langversion 3.0
* @playerversion Flash 10
* @playerversion AIR 1.5
* @productversion Flex 4.5
*/
protected var cacheEntries:LinkedList = new LinkedList();
/**
* List of queued CacheEntryNode instances.
*
* @langversion 3.0
* @playerversion Flash 10
* @playerversion AIR 1.5
* @productversion Flex 4.5
*/
protected var requestQueue:LinkedList = new LinkedList();
/**
* List of queued CacheEntryNode instances currently executing.
*
* @langversion 3.0
* @playerversion Flash 10
* @playerversion AIR 1.5
* @productversion Flex 4.5
*/
protected var activeRequests:LinkedList = new LinkedList();
/**
* Identifier of the currently prioritized content grouping.
* @default "_DEFAULT_"
*
* @langversion 3.0
* @playerversion Flash 10
* @playerversion AIR 1.5
* @productversion Flex 4.5
*/
protected var priorityGroup:String = "_DEFAULT_";
//--------------------------------------------------------------------------
//
// Properties
//
//--------------------------------------------------------------------------
//----------------------------------
// enableQueuing
//----------------------------------
/**
* @private
*/
private var _enableCaching:Boolean = true;
/**
* Enables caching behavior and functionality. Applies only to new
* load() requests.
*
* @default true
*
* @langversion 3.0
* @playerversion Flash 10
* @playerversion AIR 1.5
* @productversion Flex 4.5
*/
public function get enableCaching():Boolean
{
return _enableCaching;
}
/**
* @private
*/
public function set enableCaching(value:Boolean):void
{
if (value != _enableCaching)
_enableCaching = value;
}
//----------------------------------
// enableQueuing
//----------------------------------
/**
* @private
*/
private var _enableQueueing:Boolean = false;
/**
* Enables queuing behavior and functionality. Applies only to new
* load() requests.
*
* @default false
*
* @langversion 3.0
* @playerversion Flash 10
* @playerversion AIR 1.5
* @productversion Flex 4.5
*/
public function get enableQueueing():Boolean
{
return _enableQueueing;
}
/**
* @private
*/
public function set enableQueueing(value:Boolean):void
{
if (value != _enableQueueing)
_enableQueueing = value;
}
//----------------------------------
// numCacheEntries
//----------------------------------
/**
* Count of active/in-use cache entries.
*
* @langversion 3.0
* @playerversion Flash 10
* @playerversion AIR 1.5
* @productversion Flex 4.5
*/
public function get numCacheEntries():int
{
return cacheEntries.length;
}
//----------------------------------
// maxActiveRequests
//----------------------------------
/**
* @private
*/
private var _maxActiveRequests:int = 2;
/**
* Maximum simultaneous active requests when queuing is
* enabled.
*
* @default 2
*
* @langversion 3.0
* @playerversion Flash 10
* @playerversion AIR 1.5
* @productversion Flex 4.5
*/
public function get maxActiveRequests():int
{
return _maxActiveRequests;
}
/**
* @private
*/
public function set maxActiveRequests(value:int):void
{
if (value != _maxActiveRequests)
_maxActiveRequests = value;
}
//----------------------------------
// maxCacheEntries
//----------------------------------
/**
* @private
*/
private var _maxCacheEntries:int = 100;
/**
* Maximum size of MRU based cache. When numCacheEntries exceeds
* maxCacheEntries the least recently used are pruned to fit.
*
* @default 100
*
* @langversion 3.0
* @playerversion Flash 10
* @playerversion AIR 1.5
* @productversion Flex 4.5
*/
public function get maxCacheEntries():int
{
return _maxCacheEntries;
}
/**
* @private
*/
public function set maxCacheEntries(value:int):void
{
if (value != _maxCacheEntries)
{
_maxCacheEntries = value;
enforceMaximumCacheEntries();
}
}
//--------------------------------------------------------------------------
//
// Methods
//
//--------------------------------------------------------------------------
/**
* @copy spark.core.IContentLoader#load()
*
* @langversion 3.0
* @playerversion Flash 10
* @playerversion AIR 2.5
* @productversion Flex 4.5
*/
public function load(source:Object, contentLoaderGrouping:String=null):ContentRequest
{
var key:Object = source is URLRequest ? URLRequest(source).url : source;
var cacheEntry:CacheEntryNode = cachedData[key];
var contentRequest:ContentRequest;
if (!cacheEntry || cacheEntry.value == UNTRUSTED || !enableCaching)
{
// No previously cached entry or the entry is marked as
// unshareable (untrusted), we must execute a Loader request
// for the data.
var loader:Loader = new Loader();
// Listen for completion so we can manage our cache entry upon
// failure or if the loaded data is deemed unshareable.
loader.contentLoaderInfo.addEventListener(Event.COMPLETE,
loader_completeHandler, false, 0, true);
loader.contentLoaderInfo.addEventListener(IOErrorEvent.IO_ERROR,
loader_completeHandler, false, 0, true);
loader.contentLoaderInfo.addEventListener(SecurityErrorEvent.SECURITY_ERROR,
loader_completeHandler, false, 0, true);
var urlRequest:URLRequest = source is URLRequest ?
source as URLRequest : new URLRequest(source as String);
// Cache our new LoaderInfo if applicable.
if (!cacheEntry && enableCaching)
{
addCacheEntry(key, loader.contentLoaderInfo);
// Mark entry as complete, we'll mark complete later
// once fully loaded.
var entry:CacheEntryNode = cachedData[key];
if (entry)
entry.complete = false;
}
// Create ContentRequest instance to return to caller.
contentRequest = new ContentRequest(this, loader.contentLoaderInfo);
if (enableQueueing)
{
// Queue load request.
queueRequest(urlRequest, loader, contentLoaderGrouping);
}
else
{
// Execute Loader
var loaderContext:LoaderContext = new LoaderContext();
loaderContext.checkPolicyFile = true;
loader.load(urlRequest, loaderContext);
}
}
else
{
// Found a valid cache entry. Create a ContentRequest instance.
contentRequest = new ContentRequest(this, cacheEntry.value as LoaderInfo,
true, cacheEntry.complete);
// Promote in our MRU list.
var node:LinkedListNode = cacheEntries.remove(cacheEntry);
cacheEntries.unshift(node);
}
return contentRequest;
}
/**
* Obtain an entry for the given key if one exists.
*
* @param source Unique key used to represent the requested content resource.
*
* @return A value being stored by the cache for the provided key. Returns
* null if not found or in the likely case the value was stored as null.
*
* @langversion 3.0
* @playerversion Flash 10
* @playerversion AIR 1.5
* @productversion Flex 4.5
*/
public function getCacheEntry(source:Object):Object
{
var key:Object = source is URLRequest ? URLRequest(source).url : source;
var cacheEntry:CacheEntryNode = cachedData[key];
return cacheEntry ? cacheEntry.value : null;
}
/**
* Resets our cache content to initial empty state.
*
* @langversion 3.0
* @playerversion Flash 10
* @playerversion AIR 1.5
* @productversion Flex 4.5
*/
public function removeAllCacheEntries():void
{
cachedData = new Dictionary();
cacheEntries = new LinkedList();
}
/**
* Remove specific entry from cache.
*
* @param source Unique key for value to remove from cache.
*
* @langversion 3.0
* @playerversion Flash 10
* @playerversion AIR 1.5
* @productversion Flex 4.5
*/
public function removeCacheEntry(source:Object):void
{
var key:Object = source is URLRequest ? URLRequest(source).url : source;
var node:CacheEntryNode = cachedData[key];
if (node)
{
cacheEntries.remove(node);
delete cachedData[key];
}
}
/**
* Adds new entry to cache (or replaces existing entry).
*
* @param source Unique key to associate provided value with in cache.
* @param value Value to cache for given key.
*
* @langversion 3.0
* @playerversion Flash 10
* @playerversion AIR 1.5
* @productversion Flex 4.5
*/
public function addCacheEntry(source:Object, value:Object):void
{
var key:Object = source is URLRequest ? URLRequest(source).url : source;
var node:CacheEntryNode = cachedData[key];
if (node)
cacheEntries.remove(node);
node = new CacheEntryNode(key, value);
cachedData[source] = node;
cacheEntries.unshift(node);
enforceMaximumCacheEntries();
}
/**
* If size of our cache exceeds our maximum, we release the least
* recently used entries necessary to meet our limit.
*
* @private
*/
mx_internal function enforceMaximumCacheEntries():void
{
if (_maxCacheEntries <= 0)
return;
while (cacheEntries.length > _maxCacheEntries)
{
var node:CacheEntryNode = cacheEntries.pop() as CacheEntryNode;
var key:Object = (node.source is URLRequest) ?
URLRequest(node.source).url : node.source;
delete cachedData[key];
}
}
//--------------------------------------------------------------------------
//
// Queueing Methods
//
//--------------------------------------------------------------------------
/**
* Promotes a content grouping to the head of the loading queue.
*
* @param contentLoaderGrouping Name of content grouping to promote
* in the loading queue. All queued requests with matching
* contentLoaderGroup will be shifted to the head of the queue.
*
* @langversion 3.0
* @playerversion Flash 10
* @playerversion AIR 1.5
* @productversion Flex 4.5
*/
public function prioritize(contentLoaderGrouping:String):void
{
priorityGroup = contentLoaderGrouping;
shiftPriority();
processQueue();
}
/**
* Process the request queue and execute any pending requests until we
* reach our maxActiveRequests limit.
*
* @private
*/
mx_internal function queueRequest(source:URLRequest, loader:Loader, queueGroup:String):void
{
var node:QueueEntryNode = new QueueEntryNode(source, loader, queueGroup);
if (queueGroup == priorityGroup)
{
// Our new request matches the current priority group, so insert
// after all currently queued instances of the same priority group.
var current:QueueEntryNode = requestQueue.head as QueueEntryNode;
while (current && current.next && current.queueGroup == priorityGroup)
current = current.next as QueueEntryNode;
if (current)
{
if (current.queueGroup == priorityGroup)
requestQueue.insertAfter(node, current);
else
requestQueue.insertBefore(node, current);
}
else
requestQueue.push(node);
}
else
{
// No active priority group, just push to request queue.
requestQueue.push(node);
}
processQueue();
}
/**
* Process the request queue and execute any pending requests until we
* reach our maxActiveRequests limit.
*
* @private
*/
mx_internal function processQueue():void
{
if (activeRequests.length < maxActiveRequests && requestQueue.length > 0)
{
var node:QueueEntryNode = requestQueue.shift() as QueueEntryNode;
if (node)
{
// Execute load request.
var loaderContext:LoaderContext = new LoaderContext();
loaderContext.checkPolicyFile = true;
var loader:Loader = node.value;
loader.load(node.urlRequest, loaderContext);
// Promote to active list.
activeRequests.push(node);
}
}
}
/**
* Resets the queue to initial empty state. All requests, both active
* and queued, are cancelled. All cache entries associated with canceled
* requests are invalidated.
*
* @langversion 3.0
* @playerversion Flash 10
* @playerversion AIR 1.5
* @productversion Flex 4.5
*/
public function removeAllQueueEntries():void
{
// Cancel any active requests and return to queue.
requeueActive(true);
// Walk queue and invalidate any associated cache entries.
if (enableCaching)
{
var current:QueueEntryNode = requestQueue.head as QueueEntryNode;
while (current)
{
removeCacheEntry(current.urlRequest.url);
current = current.next as QueueEntryNode;
}
}
// Clear request queue.
requestQueue = new LinkedList();
}
/**
* Reorder our request queue giving priorityGroup preference.
* @private
*/
mx_internal function shiftPriority():void
{
var current:QueueEntryNode = requestQueue.tail as QueueEntryNode;
var prioritizedNodes:LinkedList = new LinkedList();
// Requeue
requeueActive();
// Remove all nodes matching current priority queue.
while (current)
{
var candidate:QueueEntryNode = current;
current = current.prev as QueueEntryNode;
if (candidate.queueGroup == priorityGroup)
{
requestQueue.remove(candidate);
prioritizedNodes.push(candidate);
}
}
// Reinsert to head of list in original queued order.
while (prioritizedNodes.length)
{
current = prioritizedNodes.shift() as QueueEntryNode;
requestQueue.unshift(current);
}
}
/**
* Requeues active requests.
*
* @param requeueAll Cancel all active requests if true,
* otherwise cancel and requeue any requests not in the
* active priority group.
*
* @private
*/
mx_internal function requeueActive(requeueAll:Boolean=false):void
{
var current:QueueEntryNode = activeRequests.head as QueueEntryNode;
while (current)
{
var activeNode:QueueEntryNode = current;
current = current.next as QueueEntryNode;
if (activeNode.queueGroup != priorityGroup || requeueAll)
{
// Remove from active list and invoke close() on the Loader.
// We'll reinvoke load() again once the queued request is
// serviced.
activeRequests.remove(activeNode);
try
{
Loader(activeNode.value).close();
}
catch (e:Error) {}
finally
{
requestQueue.unshift(activeNode);
}
}
}
}
//--------------------------------------------------------------------------
//
// Event handlers
//
//--------------------------------------------------------------------------
/**
* Invoked when a request is complete. We detect if our content is
* considered "trusted" and if not, we mark our cache entry to that
* effect so that future requests of the same source don't attempt to
* use a cached value.
*
* @private
*/
private function loader_completeHandler(e:Event):void
{
var loaderInfo:LoaderInfo = e.target as LoaderInfo;
// Lookup our cache entry for this loader. We can't lookup by key since
// loaderInfo.url may have been sanitized/modified by the player (or for
// example converted to a fully qualified form since our initial request).
var cachedRequest:CacheEntryNode = cacheEntries.find(loaderInfo) as CacheEntryNode;
if (e.type == Event.COMPLETE && loaderInfo)
{
// Mark cache entry as complete.
if (cachedRequest)
{
cachedRequest.complete = true;
// Detected that our loader cannot be shared or cached. Mark
// as such and notify and possibly active content requests.
if (!loaderInfo.childAllowsParent)
{
addCacheEntry(cachedRequest.source, UNTRUSTED);
dispatchEvent(new LoaderInvalidationEvent(LoaderInvalidationEvent.INVALIDATE_LOADER, loaderInfo));
}
}
}
else if (e.type == IOErrorEvent.IO_ERROR || e.type == SecurityErrorEvent.SECURITY_ERROR)
{
// Not suitable for caching. Lookup our loader info in our cache since
// the ioError event does not provide us the original url.
if (cachedRequest)
removeCacheEntry(cachedRequest.source);
}
// Remove the related loader from our activeRequests list if applicable.
if (activeRequests.length > 0 || requestQueue.length > 0)
{
var node:LinkedListNode = activeRequests.remove(loaderInfo.loader);
processQueue();
}
// Remove our listeners.
loaderInfo.removeEventListener(Event.COMPLETE, loader_completeHandler);
loaderInfo.removeEventListener(IOErrorEvent.IO_ERROR, loader_completeHandler);
loaderInfo.removeEventListener(SecurityErrorEvent.SECURITY_ERROR, loader_completeHandler);
}
}
}
import mx.utils.LinkedListNode;
import flash.display.Loader;
import flash.net.URLRequest;
/**
* Represents a single cache entry.
* @private
*/
class CacheEntryNode extends LinkedListNode
{
public function CacheEntryNode(source:Object, value:Object, queueGroup:String=null):void
{
super(value);
this.source = source;
}
//----------------------------------
// source
//----------------------------------
/**
* Key into cachedData map for this cache entry.
* @private
*/
public var source:Object;
//----------------------------------
// complete
//----------------------------------
/**
* For loaded content denotes that entry is finished loading.
* @private
*/
public var complete:Boolean = true;
}
/**
* Represents a single queue entry.
* @private
*/
class QueueEntryNode extends LinkedListNode
{
public function QueueEntryNode(urlRequest:URLRequest, loader:Loader, queueGroup:String):void
{
super(loader);
this.urlRequest = urlRequest;
this.queueGroup = queueGroup;
}
//----------------------------------
// source
//----------------------------------
/**
* Key into cachedData map for this cache entry.
* @private
*/
public var urlRequest:URLRequest;
//----------------------------------
// queueGroup
//----------------------------------
/**
* Queue group name used for prioritizing queued cached entry requests.
* @private
*/
public var queueGroup:String;
}