using System; using System.Collections.Generic; using System.Text; using ScrewTurn.Wiki.PluginFramework; namespace ScrewTurn.Wiki { /// /// Implements a local cache provider. All instance members are thread-safe. /// 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 _pseudoCache; // Contains the page contents private Dictionary _pageContentCache; // Contains the partially-formatted page content private Dictionary _formattedContentCache; // Records, for each page, how many times a page has been requested, // limited to page contents (not formatted content) private Dictionary _pageCacheUsage; private int _onlineUsers = 0; private List _sessions; // Key is lowercase, invariant culture private Dictionary _redirections; /// /// Initializes the Storage Provider. /// /// The Host of the Component. /// The Configuration data, if any. /// If host or config are null. /// If config is not valid or is incorrect. 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(10); // Initialize page content cache _pageContentCache = new Dictionary(s); _pageCacheUsage = new Dictionary(s); // Initialize formatted page content cache _formattedContentCache = new Dictionary(s); _sessions = new List(50); _redirections = new Dictionary(50); } /// /// Method invoked on shutdown. /// /// This method might not be invoked in some cases. public void Shutdown() { ClearPseudoCache(); ClearPageContentCache(); } /// /// Gets or sets the number of users online. /// public int OnlineUsers { get { lock(this) { return _onlineUsers; } } set { lock(this) { _onlineUsers = value; } } } /// /// Gets the Information about the Provider. /// public ComponentInformation Information { get { return _info; } } /// /// Gets a brief summary of the configuration string format, in HTML. Returns null if no configuration is needed. /// public string ConfigHelpHtml { get { return null; } } /// /// Gets the value of a Pseudo-cache item, previously stored in the cache. /// /// The name of the item being requested. /// The value of the item, or null if the item is not found. /// If name is null. /// If name is empty. 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; } /// /// Sets the value of a Pseudo-cache item. /// /// The name of the item being stored. /// The value of the item. If the value is null, then the item should be removed from the cache. /// If name is null. /// If name is empty. 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; } } /// /// Gets the Content of a Page, previously stored in cache. /// /// The Page Info object related to the Content being requested. /// The Page Content object, or null if the item is not found. /// If pageInfo is null. 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; } /// /// Sets the Content of a Page. /// /// The Page Info object related to the Content being stored. /// The Content of the Page. /// If pageInfo or content are null. 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; } } } /// /// Gets the partially-formatted content (text) of a Page, previously stored in the cache. /// /// The Page Info object related to the content being requested. /// The partially-formatted content, or null if the item is not found. /// If pageInfo is null. public string GetFormattedPageContent(PageInfo pageInfo) { if(pageInfo == null) throw new ArgumentNullException("pageInfo"); string value = null; lock(_formattedContentCache) { _formattedContentCache.TryGetValue(pageInfo, out value); } return value; } /// /// Sets the partially-preformatted content (text) of a Page. /// /// The Page Info object related to the content being stored. /// The partially-preformatted content. /// If pageInfo or content are null. 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; } } /// /// Removes a Page from the cache. /// /// The Page Info object related to the Page that has to be removed. /// If pageInfo is null. 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); } } /// /// Clears the Page Content cache. /// public void ClearPageContentCache() { // In this order lock(_formattedContentCache) { _formattedContentCache.Clear(); } lock(_pageContentCache) { _pageContentCache.Clear(); } lock(_pageCacheUsage) { _pageCacheUsage.Clear(); } } /// /// Clears the Pseudo-Cache. /// public void ClearPseudoCache() { lock(_pseudoCache) { _pseudoCache.Clear(); } } /// /// Reduces the size of the Page Content cache, removing the least-recently used items. /// /// The number of Pages to remove. /// If cutSize is less than or equal to zero. 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); } } } /// /// Gets the number of Pages whose content is currently stored in the cache. /// public int PageCacheUsage { get { lock(_pageContentCache) { return _pageContentCache.Count; } } } /// /// Gets the numer of Pages whose formatted content is currently stored in the cache. /// public int FormatterPageCacheUsage { get { lock(_formattedContentCache) { return _formattedContentCache.Count; } } } /// /// Adds or updates an editing session. /// /// The edited Page. /// The User who is editing the Page. /// If page or user are null. /// If page or user are empty. 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)); } } /// /// Cancels an editing session. /// /// The Page. /// The User. /// If page or user are null. /// If page or user are empty. 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; } } } } /// /// Finds whether a Page is being edited by a different user. /// /// The Page. /// The User who is requesting the status of the Page. /// True if the Page is being edited by another User. /// If page or currentUser are null. /// If page or currentUser are empty. 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; } /// /// Gets the username of the user who's editing a page. /// /// The page. /// The username. /// If page is null. /// If page is empty. 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 ""; } /// /// Adds the redirection information for a page (overwrites the previous value, if any). /// /// The source page. /// The destination page. /// If source or destination are null. /// If source or destination are empty. 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; } } /// /// Gets the destination of a redirection. /// /// The source page. /// The destination page, if any, null otherwise. /// If source is null. /// If source is empty. 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; } /// /// Removes a pge from both sources and destinations. /// /// The name of the page. /// If name is null. /// If name is empty. 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 keysToRemove = new List(10); foreach(KeyValuePair pair in _redirections) { if(pair.Value.ToLowerInvariant() == name) keysToRemove.Add(pair.Key); } foreach(string key in keysToRemove) { _redirections.Remove(key); } } } /// /// Clears all the redirections information. /// public void ClearRedirections() { lock(_redirections) { _redirections.Clear(); } } } /// /// Represents an Editing Session. /// public class EditingSession { private string page; private string user; private DateTime lastContact; /// /// Initializes a new instance of the EditingSession class. /// /// The edited Page. /// The User who is editing the Page. public EditingSession(string page, string user) { this.page = page; this.user = user; lastContact = DateTime.Now; } /// /// Gets the edited Page. /// public string Page { get { return page; } } /// /// Gets the User. /// public string User { get { return user; } } /// /// Sets the Last Contact to now. /// public void Renew() { lastContact = DateTime.Now; } /// /// Gets the Last Contact Date/Time. /// public DateTime LastContact { get { return lastContact; } } } }