screwturn-4/Core/CacheProvider.cs
2009-09-30 13:47:13 +00:00

539 lines
18 KiB
C#

using System;
using System.Collections.Generic;
using System.Text;
using ScrewTurn.Wiki.PluginFramework;
namespace ScrewTurn.Wiki {
/// <summary>
/// Implements a local cache provider. All instance members are thread-safe.
/// </summary>
public class CacheProvider : ICacheProviderV30 {
private readonly ComponentInformation _info =
new ComponentInformation("Local Cache Provider", "ScrewTurn Software", Settings.WikiVersion, "http://www.screwturn.eu", null);
private IHostV30 _host;
// Implements the pseudo cache
private Dictionary<string, string> _pseudoCache;
// Contains the page contents
private Dictionary<PageInfo, PageContent> _pageContentCache;
// Contains the partially-formatted page content
private Dictionary<PageInfo, string> _formattedContentCache;
// Records, for each page, how many times a page has been requested,
// limited to page contents (not formatted content)
private Dictionary<PageInfo, int> _pageCacheUsage;
private int _onlineUsers = 0;
private List<EditingSession> _sessions;
// Key is lowercase, invariant culture
private Dictionary<string, string> _redirections;
/// <summary>
/// Initializes the Storage Provider.
/// </summary>
/// <param name="host">The Host of the Component.</param>
/// <param name="config">The Configuration data, if any.</param>
/// <exception cref="ArgumentNullException">If <b>host</b> or <b>config</b> are <c>null</c>.</exception>
/// <exception cref="InvalidConfigurationException">If <b>config</b> is not valid or is incorrect.</exception>
public void Init(IHostV30 host, string config) {
if(host == null) throw new ArgumentNullException("host");
if(config == null) throw new ArgumentNullException("config");
_host = host;
int s = int.Parse(host.GetSettingValue(SettingName.CacheSize));
// Initialize pseudo cache
_pseudoCache = new Dictionary<string, string>(10);
// Initialize page content cache
_pageContentCache = new Dictionary<PageInfo, PageContent>(s);
_pageCacheUsage = new Dictionary<PageInfo, int>(s);
// Initialize formatted page content cache
_formattedContentCache = new Dictionary<PageInfo, string>(s);
_sessions = new List<EditingSession>(50);
_redirections = new Dictionary<string, string>(50);
}
/// <summary>
/// Method invoked on shutdown.
/// </summary>
/// <remarks>This method might not be invoked in some cases.</remarks>
public void Shutdown() {
ClearPseudoCache();
ClearPageContentCache();
}
/// <summary>
/// Gets or sets the number of users online.
/// </summary>
public int OnlineUsers {
get {
lock(this) {
return _onlineUsers;
}
}
set {
lock(this) {
_onlineUsers = value;
}
}
}
/// <summary>
/// Gets the Information about the Provider.
/// </summary>
public ComponentInformation Information {
get { return _info; }
}
/// <summary>
/// Gets a brief summary of the configuration string format, in HTML. Returns <c>null</c> if no configuration is needed.
/// </summary>
public string ConfigHelpHtml {
get { return null; }
}
/// <summary>
/// Gets the value of a Pseudo-cache item, previously stored in the cache.
/// </summary>
/// <param name="name">The name of the item being requested.</param>
/// <returns>The value of the item, or <c>null</c> if the item is not found.</returns>
/// <exception cref="ArgumentNullException">If <b>name</b> is <c>null</c>.</exception>
/// <exception cref="ArgumentException">If <b>name</b> is empty.</exception>
public string GetPseudoCacheValue(string name) {
if(name == null) throw new ArgumentNullException("name");
if(name.Length == 0) throw new ArgumentException("Name cannot be empty", "name");
string value = null;
lock(_pseudoCache) {
_pseudoCache.TryGetValue(name, out value);
}
return value;
}
/// <summary>
/// Sets the value of a Pseudo-cache item.
/// </summary>
/// <param name="name">The name of the item being stored.</param>
/// <param name="value">The value of the item. If the value is <c>null</c>, then the item should be removed from the cache.</param>
/// <exception cref="ArgumentNullException">If <b>name</b> is <c>null</c>.</exception>
/// <exception cref="ArgumentException">If <b>name</b> is empty.</exception>
public void SetPseudoCacheValue(string name, string value) {
if(name == null) throw new ArgumentNullException("name");
if(name.Length == 0) throw new ArgumentException("Name cannot be empty", "name");
lock(_pseudoCache) {
if(value == null) _pseudoCache.Remove(name);
else _pseudoCache[name] = value;
}
}
/// <summary>
/// Gets the Content of a Page, previously stored in cache.
/// </summary>
/// <param name="pageInfo">The Page Info object related to the Content being requested.</param>
/// <returns>The Page Content object, or <c>null</c> if the item is not found.</returns>
/// <exception cref="ArgumentNullException">If <b>pageInfo</b> is <c>null</c>.</exception>
public PageContent GetPageContent(PageInfo pageInfo) {
if(pageInfo == null) throw new ArgumentNullException("pageInfo");
PageContent value = null;
lock(_pageContentCache) {
if(_pageContentCache.TryGetValue(pageInfo, out value)) {
_pageCacheUsage[pageInfo]++;
}
}
return value;
}
/// <summary>
/// Sets the Content of a Page.
/// </summary>
/// <param name="pageInfo">The Page Info object related to the Content being stored.</param>
/// <param name="content">The Content of the Page.</param>
/// <exception cref="ArgumentNullException">If <b>pageInfo</b> or <b>content</b> are <c>null</c>.</exception>
public void SetPageContent(PageInfo pageInfo, PageContent content) {
if(pageInfo == null) throw new ArgumentNullException("pageInfo");
if(content == null) throw new ArgumentNullException("content");
lock(_pageContentCache) {
_pageContentCache[pageInfo] = content;
lock(_pageCacheUsage) {
_pageCacheUsage[pageInfo] = 0;
}
}
}
/// <summary>
/// Gets the partially-formatted content (text) of a Page, previously stored in the cache.
/// </summary>
/// <param name="pageInfo">The Page Info object related to the content being requested.</param>
/// <returns>The partially-formatted content, or <c>null</c> if the item is not found.</returns>
/// <exception cref="ArgumentNullException">If <b>pageInfo</b> is <c>null</c>.</exception>
public string GetFormattedPageContent(PageInfo pageInfo) {
if(pageInfo == null) throw new ArgumentNullException("pageInfo");
string value = null;
lock(_formattedContentCache) {
_formattedContentCache.TryGetValue(pageInfo, out value);
}
return value;
}
/// <summary>
/// Sets the partially-preformatted content (text) of a Page.
/// </summary>
/// <param name="pageInfo">The Page Info object related to the content being stored.</param>
/// <param name="content">The partially-preformatted content.</param>
/// <exception cref="ArgumentNullException">If <b>pageInfo</b> or <b>content</b> are <c>null</c>.</exception>
public void SetFormattedPageContent(PageInfo pageInfo, string content) {
if(pageInfo == null) throw new ArgumentNullException("pageInfo");
if(content == null) throw new ArgumentNullException("content");
lock(_formattedContentCache) {
_formattedContentCache[pageInfo] = content;
}
}
/// <summary>
/// Removes a Page from the cache.
/// </summary>
/// <param name="pageInfo">The Page Info object related to the Page that has to be removed.</param>
/// <exception cref="ArgumentNullException">If <b>pageInfo</b> is <c>null</c>.</exception>
public void RemovePage(PageInfo pageInfo) {
if(pageInfo == null) throw new ArgumentNullException("pageInfo");
// In this order
lock(_formattedContentCache) {
_formattedContentCache.Remove(pageInfo);
}
lock(_pageContentCache) {
_pageContentCache.Remove(pageInfo);
}
lock(_pageCacheUsage) {
_pageCacheUsage.Remove(pageInfo);
}
}
/// <summary>
/// Clears the Page Content cache.
/// </summary>
public void ClearPageContentCache() {
// In this order
lock(_formattedContentCache) {
_formattedContentCache.Clear();
}
lock(_pageContentCache) {
_pageContentCache.Clear();
}
lock(_pageCacheUsage) {
_pageCacheUsage.Clear();
}
}
/// <summary>
/// Clears the Pseudo-Cache.
/// </summary>
public void ClearPseudoCache() {
lock(_pseudoCache) {
_pseudoCache.Clear();
}
}
/// <summary>
/// Reduces the size of the Page Content cache, removing the least-recently used items.
/// </summary>
/// <param name="cutSize">The number of Pages to remove.</param>
/// <exception cref="ArgumentOutOfRangeException">If <b>cutSize</b> is less than or equal to zero.</exception>
public void CutCache(int cutSize) {
if(cutSize <= 0) throw new ArgumentOutOfRangeException("cutSize", "Cut Size should be greater than zero");
lock(_pageContentCache) {
// TODO: improve performance - now the operation is O(cache_size^2)
for(int i = 0; i < cutSize; i++) {
PageInfo key = null;
int min = int.MaxValue;
// Find the page that has been requested the least times
foreach(PageInfo p in _pageCacheUsage.Keys) {
if(_pageCacheUsage[p] < min) {
key = p;
min = _pageCacheUsage[p];
}
}
// This is necessary to avoid infinite loops
if(key == null) {
break;
}
// Remove the page from cache
RemovePage(key);
}
}
}
/// <summary>
/// Gets the number of Pages whose content is currently stored in the cache.
/// </summary>
public int PageCacheUsage {
get {
lock(_pageContentCache) {
return _pageContentCache.Count;
}
}
}
/// <summary>
/// Gets the numer of Pages whose formatted content is currently stored in the cache.
/// </summary>
public int FormatterPageCacheUsage {
get {
lock(_formattedContentCache) {
return _formattedContentCache.Count;
}
}
}
/// <summary>
/// Adds or updates an editing session.
/// </summary>
/// <param name="page">The edited Page.</param>
/// <param name="user">The User who is editing the Page.</param>
/// <exception cref="ArgumentNullException">If <b>page</b> or <b>user</b> are <c>null</c>.</exception>
/// <exception cref="ArgumentException">If <b>page</b> or <b>user</b> are empty.</exception>
public void RenewEditingSession(string page, string user) {
if(page == null) throw new ArgumentNullException("page");
if(page.Length == 0) throw new ArgumentException("Page cannot be empty", "page");
if(user == null) throw new ArgumentNullException("user");
if(user.Length == 0) throw new ArgumentException("User cannot be empty", "user");
lock(this) {
bool found = false;
for(int i = 0; i < _sessions.Count; i++) {
if(_sessions[i].Page == page && _sessions[i].User.Equals(user)) {
_sessions[i].Renew();
found = true;
break;
}
}
if(!found) _sessions.Add(new EditingSession(page, user));
}
}
/// <summary>
/// Cancels an editing session.
/// </summary>
/// <param name="page">The Page.</param>
/// <param name="user">The User.</param>
/// <exception cref="ArgumentNullException">If <b>page</b> or <b>user</b> are <c>null</c>.</exception>
/// <exception cref="ArgumentException">If <b>page</b> or <b>user</b> are empty.</exception>
public void CancelEditingSession(string page, string user) {
if(page == null) throw new ArgumentNullException("page");
if(page.Length == 0) throw new ArgumentException("Page cannot be empty", "page");
if(user == null) throw new ArgumentNullException("user");
if(user.Length == 0) throw new ArgumentException("User cannot be empty", "user");
lock(this) {
for(int i = 0; i < _sessions.Count; i++) {
if(_sessions[i].Page == page && _sessions[i].User.Equals(user)) {
_sessions.Remove(_sessions[i]);
break;
}
}
}
}
/// <summary>
/// Finds whether a Page is being edited by a different user.
/// </summary>
/// <param name="page">The Page.</param>
/// <param name="currentUser">The User who is requesting the status of the Page.</param>
/// <returns>True if the Page is being edited by another User.</returns>
/// <exception cref="ArgumentNullException">If <b>page</b> or <b>currentUser</b> are <c>null</c>.</exception>
/// <exception cref="ArgumentException">If <b>page</b> or <b>currentUser</b> are empty.</exception>
public bool IsPageBeingEdited(string page, string currentUser) {
if(page == null) throw new ArgumentNullException("page");
if(page.Length == 0) throw new ArgumentException("Page cannot be empty", "page");
if(currentUser == null) throw new ArgumentNullException("currentUser");
if(currentUser.Length == 0) throw new ArgumentException("Current User cannot be empty", "currentUser");
lock(this) {
int timeout = int.Parse(_host.GetSettingValue(SettingName.EditingSessionTimeout));
DateTime limit = DateTime.Now.AddSeconds(-(timeout + 5)); // Allow 5 seconds for network delays
for(int i = 0; i < _sessions.Count; i++) {
if(_sessions[i].Page == page && !_sessions[i].User.Equals(currentUser)) {
if(_sessions[i].LastContact.CompareTo(limit) >= 0) {
// Page is being edited
return true;
}
else {
// Lost contact
_sessions.Remove(_sessions[i]);
return false;
}
}
}
}
return false;
}
/// <summary>
/// Gets the username of the user who's editing a page.
/// </summary>
/// <param name="page">The page.</param>
/// <returns>The username.</returns>
/// <exception cref="ArgumentNullException">If <b>page</b> is <c>null</c>.</exception>
/// <exception cref="ArgumentException">If <b>page</b> is empty.</exception>
public string WhosEditing(string page) {
if(page == null) throw new ArgumentNullException("page");
if(page.Length == 0) throw new ArgumentException("Page cannot be empty", "page");
lock(this) {
foreach(EditingSession s in _sessions) {
if(s.Page == page) return s.User;
}
}
return "";
}
/// <summary>
/// Adds the redirection information for a page (overwrites the previous value, if any).
/// </summary>
/// <param name="source">The source page.</param>
/// <param name="destination">The destination page.</param>
/// <exception cref="ArgumentNullException">If <b>source</b> or <b>destination</b> are <c>null</c>.</exception>
/// <exception cref="ArgumentException">If <b>source</b> or <b>destination</b> are empty.</exception>
public void AddRedirection(string source, string destination) {
if(source == null) throw new ArgumentNullException("source");
if(source.Length == 0) throw new ArgumentException("Source cannot be empty", "source");
if(destination == null) throw new ArgumentNullException("destination");
if(destination.Length == 0) throw new ArgumentException("Destination cannot be empty", "destination");
lock(_redirections) {
_redirections[source.ToLowerInvariant()] = destination;
}
}
/// <summary>
/// Gets the destination of a redirection.
/// </summary>
/// <param name="source">The source page.</param>
/// <returns>The destination page, if any, <c>null</c> otherwise.</returns>
/// <exception cref="ArgumentNullException">If <b>source</b> is <c>null</c>.</exception>
/// <exception cref="ArgumentException">If <b>source</b> is empty.</exception>
public string GetRedirectionDestination(string source) {
if(source == null) throw new ArgumentNullException("source");
if(source.Length == 0) throw new ArgumentException("Source cannot be empty", "source");
string dest = null;
lock(_redirections) {
_redirections.TryGetValue(source.ToLowerInvariant(), out dest);
}
return dest;
}
/// <summary>
/// Removes a pge from both sources and destinations.
/// </summary>
/// <param name="name">The name of the page.</param>
/// <exception cref="ArgumentNullException">If <b>name</b> is <c>null</c>.</exception>
/// <exception cref="ArgumentException">If <b>name</b> is empty.</exception>
public void RemovePageFromRedirections(string name) {
if(name == null) throw new ArgumentNullException("name");
if(name.Length == 0) throw new ArgumentException("Name cannot be empty", "name");
name = name.ToLowerInvariant();
lock(_redirections) {
_redirections.Remove(name);
List<string> keysToRemove = new List<string>(10);
foreach(KeyValuePair<string, string> pair in _redirections) {
if(pair.Value.ToLowerInvariant() == name) keysToRemove.Add(pair.Key);
}
foreach(string key in keysToRemove) {
_redirections.Remove(key);
}
}
}
/// <summary>
/// Clears all the redirections information.
/// </summary>
public void ClearRedirections() {
lock(_redirections) {
_redirections.Clear();
}
}
}
/// <summary>
/// Represents an Editing Session.
/// </summary>
public class EditingSession {
private string page;
private string user;
private DateTime lastContact;
/// <summary>
/// Initializes a new instance of the <b>EditingSession</b> class.
/// </summary>
/// <param name="page">The edited Page.</param>
/// <param name="user">The User who is editing the Page.</param>
public EditingSession(string page, string user) {
this.page = page;
this.user = user;
lastContact = DateTime.Now;
}
/// <summary>
/// Gets the edited Page.
/// </summary>
public string Page {
get { return page; }
}
/// <summary>
/// Gets the User.
/// </summary>
public string User {
get { return user; }
}
/// <summary>
/// Sets the Last Contact to now.
/// </summary>
public void Renew() {
lastContact = DateTime.Now;
}
/// <summary>
/// Gets the Last Contact Date/Time.
/// </summary>
public DateTime LastContact {
get { return lastContact; }
}
}
}