// Copyright 2010 Andreas Saudemont (andreas.saudemont@gmail.com)
//
// 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.
using System;
using System.Collections.Generic;
using System.IO;
using System.IO.IsolatedStorage;
using System.Net;
using System.Security.Cryptography;
using System.Text;
using System.Threading;
using System.Windows;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using Kawagoe.Threading;
namespace Kawagoe.Storage
{
///
/// Implements an on top of the isolated storage.
///
public class PersistentImageCache : ImageCache
{
///
/// The default value of .
///
public static readonly TimeSpan DefaultExpirationDelay = TimeSpan.FromDays(1);
///
/// The default value of .
///
public const int DefaultMemoryCacheCapacity = 100;
private const string ImageDataExtension = "data";
private const string ImageTimestampExtension = "tstamp";
private TimeSpan _expirationDelay = DefaultExpirationDelay;
///
/// Initializes a new instance with the specified name.
///
public PersistentImageCache(string name)
: base(name)
{
}
///
/// The delay after which an image, once downloaded, is consider expired and is deleted
/// from the cache.
///
public TimeSpan ExpirationDelay
{
get
{
return _expirationDelay;
}
set
{
if (value.TotalMinutes < 1)
{
throw new ArgumentOutOfRangeException();
}
_expirationDelay = value;
RequestCachePruning();
}
}
///
/// Implements .
///
protected override ImageSource GetInternal(Uri imageUri)
{
BitmapImage imageSource = new BitmapImage();
string imageKey = GetImageKey(imageUri);
Stream imageDataStream = LoadImageFromMemoryCache(imageKey);
if (imageDataStream != null)
{
imageSource.SetSource(imageDataStream);
return imageSource;
}
WeakReference imageSourceRef = new WeakReference(imageSource);
ThreadPool.QueueUserWorkItem((state) =>
{
LoadImageSource(imageUri, imageSourceRef);
});
return imageSource;
}
private void LoadImageSource(Uri imageUri, WeakReference imageSourceRef)
{
BitmapImage imageSource = imageSourceRef.Target as BitmapImage;
if (imageSource == null)
{
return;
}
string imageKey = GetImageKey(imageUri);
Stream imageDataStream = LoadImageFromMemoryCache(imageKey);
if (imageDataStream == null)
{
imageDataStream = ReadImageDataFromCache(imageKey);
}
if (imageDataStream != null)
{
Deployment.Current.Dispatcher.BeginInvoke(() =>
{
imageSource.SetSource(imageDataStream);
});
}
else
{
RequestImageDownload(imageUri, imageSourceRef);
}
}
///
/// Implements .
///
public override void Clear()
{
lock (_storeLock)
lock (_memoryCacheLock)
{
ClearMemoryCache();
DeleteAllImagesFromStore();
}
}
#region Image Downloads
private readonly Dictionary _pendingRequests = new Dictionary();
private void RequestImageDownload(Uri imageUri, WeakReference imageSourceRef)
{
if (imageUri == null || imageSourceRef == null || imageSourceRef.Target == null)
{
return;
}
lock (_pendingRequests)
{
PrunePendingRequests();
if (_pendingRequests.ContainsKey(imageUri))
{
ImageRequest request = _pendingRequests[imageUri];
lock (request)
{
_pendingRequests[imageUri].SourceRefs.Add(imageSourceRef);
}
}
else
{
ImageRequest request = new ImageRequest(imageUri);
request.Completed += OnImageRequestCompleted;
request.SourceRefs.Add(imageSourceRef);
_pendingRequests[imageUri] = request;
try
{
request.Start();
}
catch (Exception)
{
_pendingRequests.Remove(imageUri);
}
}
}
}
private void OnImageRequestCompleted(object sender, EventArgs e)
{
ImageRequest request = sender as ImageRequest;
if (request == null)
{
return;
}
lock (_pendingRequests)
{
PrunePendingRequests();
if (!_pendingRequests.ContainsKey(request.ImageUri))
{
return;
}
_pendingRequests.Remove(request.ImageUri);
if (request.ImageData == null || request.ImageData.Length == 0)
{
return;
}
string imageKey = GetImageKey(request.ImageUri);
WriteImageToCache(imageKey, request.ImageData);
WriteImageToMemoryCache(imageKey, request.ImageData);
foreach (WeakReference sourceRef in request.SourceRefs)
{
BitmapSource imageSource = sourceRef.Target as BitmapSource;
if (imageSource != null)
{
Stream imageDataStream = new MemoryStream(request.ImageData);
Deployment.Current.Dispatcher.BeginInvoke(() =>
{
imageSource.SetSource(imageDataStream);
});
}
}
}
}
private void PrunePendingRequests()
{
lock (_pendingRequests)
{
List obsoleteUris = null;
foreach (Uri imageUri in _pendingRequests.Keys)
{
ImageRequest request = _pendingRequests[imageUri];
bool hasSources = false;
foreach (WeakReference sourceRef in request.SourceRefs)
{
if (sourceRef.Target != null)
{
hasSources = true;
break;
}
}
if (!hasSources)
{
if (obsoleteUris == null)
{
obsoleteUris = new List();
}
obsoleteUris.Add(imageUri);
}
}
if (obsoleteUris != null)
{
foreach (Uri obsoleteUri in obsoleteUris)
{
ImageRequest request = _pendingRequests[obsoleteUri];
_pendingRequests.Remove(obsoleteUri);
request.Cancel();
}
}
}
}
private class ImageRequest
{
private bool _started = false;
private HttpWebRequest _webRequest = null;
private Stream _responseInputStream = null;
private byte[] _responseBuffer = new byte[4096];
private MemoryStream _responseDataStream = new MemoryStream();
public ImageRequest(Uri imageUri)
{
ImageUri = imageUri;
ImageData = null;
SourceRefs = new List();
}
public Uri ImageUri
{
get;
private set;
}
public byte[] ImageData
{
get;
private set;
}
public IList SourceRefs
{
get;
private set;
}
public void Start()
{
lock (this)
{
if (_started)
{
return;
}
_started = true;
_webRequest = (HttpWebRequest)HttpWebRequest.Create(ImageUri);
_webRequest.BeginGetResponse(OnGotResponse, null);
}
}
public void Cancel()
{
lock (this)
{
if (!_started)
{
return;
}
HttpWebRequest webRequest = _webRequest;
ReleaseResources();
if (webRequest != null)
{
try
{
webRequest.Abort();
}
catch (Exception) { }
}
}
}
public event EventHandler Completed;
private void OnGotResponse(IAsyncResult asyncResult)
{
lock (this)
{
if (_webRequest == null)
{
return;
}
try
{
HttpWebResponse webResponse = (HttpWebResponse)_webRequest.EndGetResponse(asyncResult);
_responseInputStream = webResponse.GetResponseStream();
_responseInputStream.BeginRead(_responseBuffer, 0, _responseBuffer.Length, OnReadResponseCompleted, null);
}
catch (Exception)
{
NotifyCompletion();
}
}
}
private void OnReadResponseCompleted(IAsyncResult asyncResult)
{
lock (this)
{
if (_responseInputStream == null)
{
return;
}
try
{
int readCount = _responseInputStream.EndRead(asyncResult);
if (readCount > 0)
{
_responseDataStream.Write(_responseBuffer, 0, readCount);
_responseInputStream.BeginRead(_responseBuffer, 0, _responseBuffer.Length, OnReadResponseCompleted, null);
}
else
{
if (_responseDataStream.Length > 0)
{
ImageData = _responseDataStream.ToArray();
}
NotifyCompletion();
}
}
catch (Exception)
{
NotifyCompletion();
}
}
}
private void NotifyCompletion()
{
lock (this)
{
ReleaseResources();
ThreadPool.QueueUserWorkItem((state) =>
{
if (Completed == null)
{
return;
}
try
{
Completed(this, EventArgs.Empty);
}
catch (Exception) { }
});
}
}
private void ReleaseResources()
{
lock (this)
{
_responseBuffer = null;
_responseDataStream = null;
if (_responseInputStream != null)
{
try { _responseInputStream.Dispose(); }
catch (Exception) { }
_responseInputStream = null;
}
_webRequest = null;
}
}
}
#endregion
#region Store Access
private readonly object _storeLock = new object();
private IsolatedStorageFile _store = null;
private readonly SHA1 _hasher = new SHA1Managed();
///
/// The name of directory in isolated storage that contains the files of this image cache.
///
private string StoreDirectoryName
{
get
{
return "ImageCache_" + Name;
}
}
///
/// The isolated storage file used by the cache.
///
private IsolatedStorageFile Store
{
get
{
lock (_storeLock)
{
if (_store == null)
{
_store = IsolatedStorageFile.GetUserStoreForApplication();
if (!_store.DirectoryExists(StoreDirectoryName))
{
_store.CreateDirectory(StoreDirectoryName);
}
}
return _store;
}
}
}
private string GetImageKey(Uri imageUri)
{
byte[] imageUriBytes = Encoding.UTF8.GetBytes(imageUri.ToString());
byte[] hash;
lock (_hasher)
{
hash = _hasher.ComputeHash(imageUriBytes);
}
return BitConverter.ToString(hash).Replace("-", "");
}
private string GetImageFilePath(string imageKey)
{
return Path.Combine(StoreDirectoryName, imageKey) + "." + ImageDataExtension;
}
private string GetTimestampFilePath(string imageKey)
{
return Path.Combine(StoreDirectoryName, imageKey) + "." + ImageTimestampExtension;
}
private Stream ReadImageDataFromCache(string imageKey)
{
RequestCachePruning();
MemoryStream dataStream = null;
try
{
string imageFilePath = GetImageFilePath(imageKey);
lock (_storeLock)
{
if (!Store.FileExists(imageFilePath))
{
return null;
}
if (GetImageTimestamp(imageKey).Add(ExpirationDelay) < DateTime.UtcNow)
{
DeleteImageFromCache(imageKey);
return null;
}
using (IsolatedStorageFileStream fileStream = Store.OpenFile(imageFilePath, FileMode.Open, FileAccess.Read))
{
if (fileStream.Length > int.MaxValue)
{
return null;
}
dataStream = new MemoryStream((int)fileStream.Length);
byte[] buffer = new byte[4096];
while (dataStream.Length < fileStream.Length)
{
int readCount = fileStream.Read(buffer, 0, Math.Min(buffer.Length, (int)(fileStream.Length - dataStream.Length)));
if (readCount <= 0)
{
throw new NotSupportedException();
}
dataStream.Write(buffer, 0, readCount);
}
}
WriteImageToMemoryCache(imageKey, dataStream.ToArray());
return dataStream;
}
}
catch (Exception)
{
if (dataStream != null)
{
try { dataStream.Dispose(); }
catch (Exception) { }
}
}
return null;
}
private void WriteImageToCache(string imageKey, byte[] imageData)
{
RequestCachePruning();
string imageFilePath = GetImageFilePath(imageKey);
try
{
lock (_storeLock)
{
IsolatedStorageFileStream fileStream;
if (Store.FileExists(imageFilePath))
{
fileStream = Store.OpenFile(imageFilePath, FileMode.Create, FileAccess.Write);
}
else
{
fileStream = Store.OpenFile(imageFilePath, FileMode.CreateNew, FileAccess.Write);
}
using (fileStream)
{
fileStream.Seek(0, SeekOrigin.Begin);
while (fileStream.Position < imageData.Length)
{
fileStream.Write(imageData, (int)fileStream.Position, (int)(imageData.Length - fileStream.Position));
}
}
SetImageTimestamp(imageKey, DateTime.UtcNow);
}
}
catch (Exception)
{
try
{
Store.DeleteFile(imageFilePath);
}
catch (Exception) { }
}
}
private void PrunePersistentCache()
{
try
{
lock (_storeLock)
{
string searchPattern = Path.Combine(StoreDirectoryName, string.Format("*.{0}", ImageDataExtension));
string[] fileNames = Store.GetFileNames(searchPattern);
foreach (string fileName in fileNames)
{
if (!fileName.EndsWith("." + ImageDataExtension))
{
continue;
}
string imageKey = fileName.Remove(Math.Max(fileName.Length - ImageDataExtension.Length - 1, 0));
if (GetImageTimestamp(imageKey).Add(ExpirationDelay) < DateTime.UtcNow)
{
DeleteImageFromCache(imageKey);
}
}
}
}
catch (Exception) { }
}
private void DeleteImageFromCache(string imageKey)
{
string imageFilePath = GetImageFilePath(imageKey);
string timestampFilePath = GetTimestampFilePath(imageKey);
lock (_storeLock)
{
try
{
if (Store.FileExists(imageFilePath))
{
Store.DeleteFile(imageFilePath);
}
}
catch (Exception) { }
try
{
if (Store.FileExists(timestampFilePath))
{
Store.DeleteFile(timestampFilePath);
}
}
catch (Exception) { }
}
}
private void DeleteAllImagesFromStore()
{
lock (_storeLock)
{
string searchPattern = Path.Combine(StoreDirectoryName, "*.*");
try
{
string[] fileNames = Store.GetFileNames(searchPattern);
foreach (string fileName in fileNames)
{
string filePath = Path.Combine(StoreDirectoryName, fileName);
try
{
Store.DeleteFile(filePath);
}
catch (Exception) { }
}
}
catch (Exception) { }
}
}
private DateTime GetImageTimestamp(string imageKey)
{
string timestampFilePath = GetTimestampFilePath(imageKey);
try
{
lock (_storeLock)
{
if (!Store.FileExists(timestampFilePath))
{
return DateTime.MinValue;
}
using (IsolatedStorageFileStream fileStream = Store.OpenFile(timestampFilePath, FileMode.Open, FileAccess.Read))
using (StreamReader fileStreamReader = new StreamReader(fileStream, Encoding.UTF8))
{
string timestampString = fileStreamReader.ReadToEnd();
return DateTime.Parse(timestampString).ToUniversalTime();
}
}
}
catch (Exception)
{
return DateTime.MinValue;
}
}
private void SetImageTimestamp(string imageKey, DateTime timestamp)
{
string timestampFilePath = GetTimestampFilePath(imageKey);
try
{
lock (_storeLock)
{
IsolatedStorageFileStream fileStream;
if (Store.FileExists(timestampFilePath))
{
fileStream = Store.OpenFile(timestampFilePath, FileMode.Create, FileAccess.Write);
}
else
{
fileStream = Store.OpenFile(timestampFilePath, FileMode.CreateNew, FileAccess.Write);
}
using (fileStream)
using (StreamWriter fileStreamWriter = new StreamWriter(fileStream, Encoding.UTF8))
{
fileStreamWriter.Write(timestamp.ToUniversalTime().ToString("u"));
}
}
}
catch (Exception) { }
}
#endregion
#region Cache Pruning
private static readonly TimeSpan CachePruningInterval = TimeSpan.FromMinutes(1);
private static readonly TimeSpan CachePruningTimerDuration = TimeSpan.FromSeconds(5);
private DateTime _cachePruningTimestamp = DateTime.MinValue;
private OneShotDispatcherTimer _cachePruningTimer = null;
private void RequestCachePruning()
{
lock (this)
{
if (_cachePruningTimer != null || _cachePruningTimestamp.Add(CachePruningInterval) >= DateTime.UtcNow)
{
return;
}
Deployment.Current.Dispatcher.BeginInvoke(() =>
{
if (_cachePruningTimer != null)
{
return;
}
_cachePruningTimer = OneShotDispatcherTimer.CreateAndStart(CachePruningTimerDuration, OnCachePruningTimerFired);
});
}
}
private void OnCachePruningTimerFired(object sender, EventArgs e)
{
if (sender != _cachePruningTimer)
{
return;
}
_cachePruningTimer = null;
_cachePruningTimestamp = DateTime.UtcNow;
ThreadPool.QueueUserWorkItem((state) => { PruneCache(); });
}
private void PruneCache()
{
PrunePersistentCache();
PruneMemoryCache();
}
#endregion
#region Memory Cache
private readonly object _memoryCacheLock = new object();
private int _memoryCacheCapacity = DefaultMemoryCacheCapacity;
private Dictionary> _memoryCacheNodes = new Dictionary>(DefaultMemoryCacheCapacity);
private LinkedList _memoryCacheList = new LinkedList();
///
/// The capacity of the in-memory cache.
/// If set to zero, the in-memory cache is disabled.
///
public int MemoryCacheCapacity
{
get
{
return _memoryCacheCapacity;
}
set
{
if (value < 0)
{
throw new ArgumentOutOfRangeException();
}
lock (_memoryCacheLock)
{
_memoryCacheCapacity = value;
PruneMemoryCache();
}
}
}
private Stream LoadImageFromMemoryCache(string imageKey)
{
lock (_memoryCacheLock)
{
if (_memoryCacheCapacity == 0)
{
return null;
}
if (!_memoryCacheNodes.ContainsKey(imageKey))
{
return null;
}
LinkedListNode node = _memoryCacheNodes[imageKey];
if (node.List == _memoryCacheList)
{
_memoryCacheList.Remove(node);
}
_memoryCacheList.AddLast(node.Value);
PruneMemoryCache();
return new MemoryStream(node.Value);
}
}
private void WriteImageToMemoryCache(string imageKey, byte[] imageData)
{
if (string.IsNullOrEmpty(imageKey) || imageData == null || imageData.Length == 0)
{
return;
}
lock (_memoryCacheLock)
{
if (_memoryCacheCapacity == 0)
{
return;
}
if (_memoryCacheNodes.ContainsKey(imageKey))
{
_memoryCacheList.Remove(_memoryCacheNodes[imageKey]);
}
LinkedListNode newNode = _memoryCacheList.AddLast(imageData);
PruneMemoryCache();
_memoryCacheNodes[imageKey] = newNode;
}
}
private void PruneMemoryCache()
{
lock (_memoryCacheLock)
{
if (_memoryCacheCapacity == 0)
{
ClearMemoryCache();
return;
}
while (_memoryCacheList.Count > _memoryCacheCapacity)
{
DeleteFirstMemoryCacheNode();
}
}
}
private void DeleteFirstMemoryCacheNode()
{
lock (_memoryCacheLock)
{
LinkedListNode node = _memoryCacheList.First;
if (node == null)
{
return;
}
_memoryCacheList.Remove(node);
foreach (string imageKey in _memoryCacheNodes.Keys)
{
if (_memoryCacheNodes[imageKey] == node)
{
_memoryCacheNodes.Remove(imageKey);
break;
}
}
}
}
private void ClearMemoryCache()
{
lock (_memoryCacheLock)
{
_memoryCacheNodes.Clear();
_memoryCacheList.Clear();
}
}
#endregion
public override string ToString()
{
return string.Format("PersistentImageCache({0})", Name);
}
}
}