using System; using System.Collections.Generic; using System.IO; using System.Globalization; using System.Text; using ScrewTurn.Wiki.PluginFramework; using ScrewTurn.Wiki.SearchEngine; namespace ScrewTurn.Wiki { /// <summary> /// Implements a Pages Storage Provider. /// </summary> public class PagesStorageProvider : IPagesStorageProviderV30 { private const string NamespacesFile = "Namespaces.cs"; private const string PagesFile = "Pages.cs"; private const string CategoriesFile = "Categories.cs"; private const string NavigationPathsFile = "NavigationPaths.cs"; private const string DraftsDirectory = "Drafts"; private const string PagesDirectory = "Pages"; private const string MessagesDirectory = "Messages"; private const string SnippetsDirectory = "Snippets"; private const string ContentTemplatesDirectory = "ContentTemplates"; private const string IndexDocumentsFile = "IndexDocuments.cs"; private const string IndexWordsFile = "IndexWords.cs"; private const string IndexMappingsFile = "IndexMappings.cs"; private readonly ComponentInformation info = new ComponentInformation("Local Pages Provider", "ScrewTurn Software", Settings.WikiVersion, "http://www.screwturn.eu", null); private IHostV30 host; // This cache is needed due to performance problems private NamespaceInfo[] namespacesCache = null; private PageInfo[] pagesCache = null; private CategoryInfo[] categoriesCache = null; private IInMemoryIndex index; private IndexStorerBase indexStorer; private string GetFullPath(string filename) { return Path.Combine(host.GetSettingValue(SettingName.PublicDirectory), filename); } private string GetFullPathForPageContent(string filename) { return Path.Combine(Path.Combine(host.GetSettingValue(SettingName.PublicDirectory), PagesDirectory), filename); } private string GetFullPathForPageDrafts(string filename) { return Path.Combine(Path.Combine(host.GetSettingValue(SettingName.PublicDirectory), DraftsDirectory), filename); } private string GetDraftFullPath(LocalPageInfo page) { /*return GetFullPathForPageDrafts(GetNamespacePartialPathForPageContent(NameTools.GetNamespace(page.FullName)) + page.File);*/ return GetFullPathForPageDrafts(page.File); } private string GetFullPathForMessages(string filename) { return Path.Combine(Path.Combine(host.GetSettingValue(SettingName.PublicDirectory), MessagesDirectory), filename); } private string GetFullPathForSnippets(string filename) { return Path.Combine(Path.Combine(host.GetSettingValue(SettingName.PublicDirectory), SnippetsDirectory), filename); } private string GetFullPathForContentTemplate(string filename) { return Path.Combine(Path.Combine(host.GetSettingValue(SettingName.PublicDirectory), ContentTemplatesDirectory), filename); } /// <summary> /// Gets the partial path of the folder that contains page content files for the specified namespace, followed by a directory separator char if appropriate. /// </summary> /// <param name="nspace">The namespace, or <c>null</c>.</param> /// <returns>The correct partial path, such as 'Namespace\' or ''.</returns> private string GetNamespacePartialPathForPageContent(string nspace) { if(nspace == null || nspace.Length == 0) return ""; else return nspace + Path.DirectorySeparatorChar; } /// <summary> /// Initializes the Provider. /// </summary> /// <param name="host">The Host of the Provider.</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"); this.host = host; if(!LocalProvidersTools.CheckWritePermissions(host.GetSettingValue(SettingName.PublicDirectory))) { throw new InvalidConfigurationException("Cannot write into the public directory - check permissions"); } if(!Directory.Exists(Path.Combine(host.GetSettingValue(SettingName.PublicDirectory), PagesDirectory))) { Directory.CreateDirectory(Path.Combine(host.GetSettingValue(SettingName.PublicDirectory), PagesDirectory)); } if(!Directory.Exists(Path.Combine(host.GetSettingValue(SettingName.PublicDirectory), MessagesDirectory))) { Directory.CreateDirectory(Path.Combine(host.GetSettingValue(SettingName.PublicDirectory), MessagesDirectory)); } if(!Directory.Exists(Path.Combine(host.GetSettingValue(SettingName.PublicDirectory), SnippetsDirectory))) { Directory.CreateDirectory(Path.Combine(host.GetSettingValue(SettingName.PublicDirectory), SnippetsDirectory)); } if(!Directory.Exists(Path.Combine(host.GetSettingValue(SettingName.PublicDirectory), ContentTemplatesDirectory))) { Directory.CreateDirectory(Path.Combine(host.GetSettingValue(SettingName.PublicDirectory), ContentTemplatesDirectory)); } if(!Directory.Exists(Path.Combine(host.GetSettingValue(SettingName.PublicDirectory), DraftsDirectory))) { Directory.CreateDirectory(Path.Combine(host.GetSettingValue(SettingName.PublicDirectory), DraftsDirectory)); } bool upgradeNeeded = false; if(!File.Exists(GetFullPath(NamespacesFile))) { File.Create(GetFullPath(NamespacesFile)).Close(); } upgradeNeeded = VerifyIfPagesFileNeedsAnUpgrade(); if(!File.Exists(GetFullPath(PagesFile))) { File.Create(GetFullPath(PagesFile)).Close(); } else if(upgradeNeeded) { VerifyAndPerformUpgradeForPages(); } if(!File.Exists(GetFullPath(CategoriesFile))) { File.Create(GetFullPath(CategoriesFile)).Close(); } else if(upgradeNeeded) { VerifyAndPerformUpgradeForCategories(); } if(!File.Exists(GetFullPath(NavigationPathsFile))) { File.Create(GetFullPath(NavigationPathsFile)).Close(); } else if(upgradeNeeded) { VerifyAndPerformUpgradeForNavigationPaths(); } // Prepare search index index = new StandardIndex(); index.SetBuildDocumentDelegate(BuildDocumentHandler); indexStorer = new IndexStorer(GetFullPath(IndexDocumentsFile), GetFullPath(IndexWordsFile), GetFullPath(IndexMappingsFile), index); indexStorer.LoadIndex(); if(indexStorer.DataCorrupted) { host.LogEntry("Search Engine Index is corrupted and needs to be rebuilt\r\n" + indexStorer.ReasonForDataCorruption.ToString(), LogEntryType.Warning, null, this); } } /// <summary> /// Verifies the need for a data upgrade, and performs it when needed. /// </summary> private void VerifyAndPerformUpgradeForCategories() { // Load file lines, replacing all dots with underscores in category names host.LogEntry("Upgrading categories format from 2.0 to 3.0", LogEntryType.General, null, this); string[] lines = File.ReadAllText(GetFullPath(CategoriesFile)).Replace("\r", "").Split(new char[] { '\n' }, StringSplitOptions.RemoveEmptyEntries); if(lines.Length > 0) { string[] fields; for(int i = 0; i < lines.Length; i++) { fields = lines[i].Split('|'); // Rename category if(fields[0].Contains(".")) { fields[0] = fields[0].Replace(".", "_"); } // Rename all pages for(int f = 1; f < fields.Length; f++) { if(fields[f].Contains(".")) { fields[f] = fields[f].Replace(".", "_"); } } lines[i] = string.Join("|", fields); } string backupFile = GetFullPath(Path.GetFileNameWithoutExtension(CategoriesFile) + "_v2" + Path.GetExtension(CategoriesFile)); File.Copy(GetFullPath(CategoriesFile), backupFile); File.WriteAllLines(GetFullPath(CategoriesFile), lines); } } /// <summary> /// Verifies whether the pages files needs to be upgraded. /// </summary> /// <returns></returns> private bool VerifyIfPagesFileNeedsAnUpgrade() { string file = GetFullPath(PagesFile); if(!File.Exists(file)) return false; string[] lines = File.ReadAllText(file).Replace("\r", "").Split(new char[] { '\n' }, StringSplitOptions.RemoveEmptyEntries); string[] fields; foreach(string line in lines) { fields = line.Split('|'); // Field count has never been 3 except in version 3.0 if(fields.Length == 3) return false; // Version 1.0 if(fields.Length == 2) return true; // Version 2.0 if(fields.Length == 4) return true; } return false; } /// <summary> /// Verifies the need for a data upgrade, and performs it when needed. /// </summary> private void VerifyAndPerformUpgradeForPages() { // Load file lines // Parse first line (if any) with old (v2) algorithm // If parsing is successful, then the file must be converted // Conversion consists in removing the 'Status' field and properly modifying permissions of pages host.LogEntry("Upgrading pages format from 2.0 to 3.0", LogEntryType.General, null, this); //string[] lines = File.ReadAllLines(GetFullPath(PagesFile)); string[] lines = File.ReadAllText(GetFullPath(PagesFile)).Replace("\r", "").Split(new char[] { '\n' }, StringSplitOptions.RemoveEmptyEntries); if(lines.Length > 0) { LocalPageInfo[] pages = new LocalPageInfo[lines.Length]; char[] oldStylePermissions = new char[lines.Length]; char[] splitter = new char[] { '|' }; for(int i = 0; i < lines.Length; i++) { string[] fields = lines[i].Split(splitter, StringSplitOptions.RemoveEmptyEntries); // Structure in version 1.0 // PageName|PageFile // Structure in version 2.0 // PageName|PageFile|Status|DateTime // Use default values (status and date/time were not available in earlier versions) DateTime creationDateTime = new DateTime(2000, 1, 1); // Default to Normal oldStylePermissions[i] = 'N'; if(fields.Length == 2) { // Version 1.0 // Use the Date/Time of the file FileInfo fi = new FileInfo(GetFullPathForPageContent(fields[1])); creationDateTime = fi.CreationTime; } if(fields.Length >= 3) { // Might be version 2.0 switch(fields[2].ToLowerInvariant()) { case "locked": oldStylePermissions[i] = 'L'; break; case "public": oldStylePermissions[i] = 'P'; break; case "normal": oldStylePermissions[i] = 'N'; break; default: try { // If this succeeded, then it's Version 3.0, not 2.0 (at least for this line) creationDateTime = DateTime.Parse(fields[2]); } catch { // Use the Date/Time of the file FileInfo fi = new FileInfo(GetFullPathForPageContent(fields[1])); creationDateTime = fi.CreationTime; } break; } if(fields.Length == 4) { // Version 2.0 creationDateTime = DateTime.Parse(fields[3]); } } pages[i] = new LocalPageInfo(fields[0], this, creationDateTime, fields[1]); pages[i].FullName = pages[i].FullName.Replace(".", "_"); // TODO: host.UpdateContentForPageRename(oldName, newName); } // Setup permissions for single pages for(int i = 0; i < oldStylePermissions.Length; i++) { if(oldStylePermissions[i] != 'N') { // Need to set permissions emulating old-style behavior host.UpgradePageStatusToAcl(pages[i], oldStylePermissions[i]); } } string backupFile = GetFullPath(Path.GetFileNameWithoutExtension(PagesFile) + "_v2" + Path.GetExtension(PagesFile)); File.Copy(GetFullPath(PagesFile), backupFile); // Re-dump pages so that old format data is discarded DumpPages(pages); } } /// <summary> /// Verifies the need for a data upgrade, and performs it when needed. /// </summary> private void VerifyAndPerformUpgradeForNavigationPaths() { // Load file lines, replacing all dots with underscores in category names host.LogEntry("Upgrading navigation paths format from 2.0 to 3.0", LogEntryType.General, null, this); string[] lines = File.ReadAllText(GetFullPath(NavigationPathsFile)).Replace("\r", "").Split(new char[] { '\n' }, StringSplitOptions.RemoveEmptyEntries); if(lines.Length > 0) { string[] fields; for(int i = 0; i < lines.Length; i++) { fields = lines[i].Split('|'); // Rename navigation path if(fields[0].Contains(".")) { fields[0] = fields[0].Replace(".", "_"); } // Rename all pages for(int f = 1; f < fields.Length; f++) { if(fields[f].Contains(".")) { fields[f] = fields[f].Replace(".", "_"); } } lines[i] = string.Join("|", fields); } string backupFile = GetFullPath(Path.GetFileNameWithoutExtension(NavigationPathsFile) + "_v2" + Path.GetExtension(NavigationPathsFile)); File.Copy(GetFullPath(NavigationPathsFile), backupFile); File.WriteAllLines(GetFullPath(NavigationPathsFile), lines); } } /// <summary> /// Method invoked on shutdown. /// </summary> /// <remarks>This method might not be invoked in some cases.</remarks> public void Shutdown() { lock(this) { indexStorer.Dispose(); } } /// <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 a value specifying whether the provider is read-only, i.e. it can only provide data and not store it. /// </summary> public bool ReadOnly { get { return false; } } /// <summary> /// Finds a page. /// </summary> /// <param name="nspace">The namespace that contains the page.</param> /// <param name="name">The name of the page to find.</param> /// <param name="pages">The pages array.</param> /// <returns>The found page, or <c>null</c>.</returns> private PageInfo FindPage(string nspace, string name, PageInfo[] pages) { if(name == null) return null; PageNameComparer comp = new PageNameComparer(); PageInfo target = new PageInfo(NameTools.GetFullName(nspace, name), this, DateTime.Now); PageInfo result = Array.Find(pages, delegate(PageInfo p) { return comp.Compare(p, target) == 0; }); return result; } /// <summary> /// Gets a namespace. /// </summary> /// <param name="name">The name of the namespace.</param> /// <returns>The <see cref="T:NamespaceInfo" />, or <c>null</c> if no namespace is 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 NamespaceInfo GetNamespace(string name) { if(name == null) throw new ArgumentNullException("name"); if(name.Length == 0) throw new ArgumentException("Name cannot be empty", "name"); lock(this) { return FindNamespace(name, GetNamespaces()); } } /// <summary> /// Gets all the sub-namespaces. /// </summary> /// <returns>The sub-namespaces, sorted by name.</returns> public NamespaceInfo[] GetNamespaces() { lock(this) { // Namespaces must be loaded from disk if(namespacesCache == null) { string[] lines = File.ReadAllText(GetFullPath(NamespacesFile)).Replace("\r", "").Split(new char[] { '\n' }, StringSplitOptions.RemoveEmptyEntries); List<NamespaceInfo> result = new List<NamespaceInfo>(lines.Length); PageInfo[] allPages = GetAllPages(); Array.Sort(allPages, new PageNameComparer()); // Line format // Name[|Name.DefaultPage] string[] fields; char[] delimiters = new char[] { '|' }; string name = null, defaultPage = null; foreach(string line in lines) { fields = line.Split(delimiters, StringSplitOptions.RemoveEmptyEntries); if(fields.Length >= 1) { name = fields[0]; } else continue; // Skip entry if(fields.Length == 2) { defaultPage = fields[1]; } result.Add(new NamespaceInfo(name, this, FindPage(name, NameTools.GetLocalName(defaultPage), allPages))); } result.Sort(new NamespaceComparer()); namespacesCache = result.ToArray(); } return namespacesCache; } } /// <summary> /// Finds a namespace. /// </summary> /// <param name="name">The name of the namespace to find.</param> /// <param name="namespaces">The namespaces array.</param> /// <returns>The found namespace, or <c>null</c>.</returns> private NamespaceInfo FindNamespace(string name, NamespaceInfo[] namespaces) { if(name == null) return null; NamespaceInfo target = new NamespaceInfo(name, this, null); NamespaceComparer comp = new NamespaceComparer(); NamespaceInfo result = Array.Find(namespaces, delegate(NamespaceInfo n) { return comp.Compare(n, target) == 0; }); return result; } /// <summary> /// Determines whether a namespace exists. /// </summary> /// <param name="name">The name to check.</param> /// <returns><c>true</c> if the namespace exists or <b>name</b> is <c>null</c> (indicating the root), <c>false</c> otherwise.</returns> private bool NamespaceExists(string name) { if(name == null) return true; NamespaceInfo[] allNamespaces = GetNamespaces(); Array.Sort(allNamespaces, new NamespaceComparer()); if(FindNamespace(name, allNamespaces) != null) return true; else return false; } /// <summary> /// Adds a new namespace. /// </summary> /// <param name="name">The name of the namespace.</param> /// <returns>The correct <see cref="T:NamespaceInfo" /> object.</returns> /// <exception cref="ArgumentNullException">If <b>name</b> is <c>null</c>.</exception> /// <exception cref="ArgumentException">If <b>name</b> is empty.</exception> public NamespaceInfo AddNamespace(string name) { if(name == null) throw new ArgumentNullException("name"); if(name.Length == 0) throw new ArgumentException("Name cannot be empty", "name"); lock(this) { if(NamespaceExists(name)) return null; // Append a line to the namespaces file File.AppendAllText(GetFullPath(NamespacesFile), name + "\r\n"); // Create folder for page content files Directory.CreateDirectory(GetFullPathForPageContent(GetNamespacePartialPathForPageContent(name))); // Create folder for messages files Directory.CreateDirectory(GetFullPathForMessages(GetNamespacePartialPathForPageContent(name))); namespacesCache = null; return new NamespaceInfo(name, this, null); } } /// <summary> /// Dumps namespaces on disk. /// </summary> /// <param name="namespaces">The namespaces to dump.</param> private void DumpNamespaces(NamespaceInfo[] namespaces) { StringBuilder sb = new StringBuilder(namespaces.Length * 20); foreach(NamespaceInfo ns in namespaces) { sb.Append(ns.Name); sb.Append("|"); sb.Append(ns.DefaultPage != null ? ns.DefaultPage.FullName : ""); sb.Append("\r\n"); } File.WriteAllText(GetFullPath(NamespacesFile), sb.ToString()); } /// <summary> /// Renames a namespace. /// </summary> /// <param name="nspace">The namespace to rename.</param> /// <param name="newName">The new name of the namespace.</param> /// <returns>The correct <see cref="T:NamespaceInfo" /> object.</returns> /// <exception cref="ArgumentNullException">If <b>nspace</b> or <b>newName</b> are <c>null</c>.</exception> /// <exception cref="ArgumentException">If <b>newName</b> is empty.</exception> public NamespaceInfo RenameNamespace(NamespaceInfo nspace, string newName) { if(nspace == null) throw new ArgumentNullException("nspace"); if(newName == null) throw new ArgumentNullException("newName"); if(newName.Length == 0) throw new ArgumentException("New Name cannot be empty", "newName"); lock(this) { if(NamespaceExists(newName)) return null; string oldName = nspace.Name; // Remove all pages and their messages from search engine index foreach(PageInfo page in GetPages(nspace)) { PageContent content = GetContent(page); UnindexPage(content); Message[] messages = GetMessages(page); foreach(Message msg in messages) { UnindexMessageTree(page, msg); } } // Load namespace list and change name NamespaceInfo[] allNamespaces = GetNamespaces(); NamespaceComparer comp = new NamespaceComparer(); NamespaceInfo result = FindNamespace(nspace.Name, allNamespaces); if(result == null) return null; result.Name = newName; // Change default page full name if(result.DefaultPage != null) { result.DefaultPage = new LocalPageInfo(NameTools.GetFullName(newName, NameTools.GetLocalName(result.DefaultPage.FullName)), this, result.DefaultPage.CreationDateTime, GetNamespacePartialPathForPageContent(newName) + Path.GetFileName(((LocalPageInfo)result.DefaultPage).File)); } DumpNamespaces(allNamespaces); // Update Category list with new namespace name CategoryInfo[] allCategories = GetAllCategories(); for(int k = 0; k < allCategories.Length; k++) { CategoryInfo category = allCategories[k]; string catNamespace = NameTools.GetNamespace(category.FullName); if(catNamespace != null && StringComparer.OrdinalIgnoreCase.Compare(catNamespace, oldName) == 0) { category.FullName = NameTools.GetFullName(newName, NameTools.GetLocalName(category.FullName)); for(int i = 0; i < category.Pages.Length; i++) { category.Pages[i] = NameTools.GetFullName(newName, NameTools.GetLocalName(category.Pages[i])); } } } DumpCategories(allCategories); // Rename namespace folder Directory.Move(GetFullPathForPageContent(GetNamespacePartialPathForPageContent(oldName)), GetFullPathForPageContent(GetNamespacePartialPathForPageContent(newName))); // Rename drafts folder string oldDraftsFullPath = GetFullPathForPageDrafts(nspace.Name); if(Directory.Exists(oldDraftsFullPath)) { string newDraftsFullPath = GetFullPathForPageDrafts(newName); Directory.Move(oldDraftsFullPath, newDraftsFullPath); } // Rename messages folder Directory.Move(GetFullPathForMessages(GetNamespacePartialPathForPageContent(oldName)), GetFullPathForMessages(GetNamespacePartialPathForPageContent(newName))); // Update Page list with new namespace name and file PageInfo[] allPages = GetAllPages(); foreach(PageInfo page in allPages) { string pageNamespace = NameTools.GetNamespace(page.FullName); if(pageNamespace != null && StringComparer.OrdinalIgnoreCase.Compare(pageNamespace, oldName) == 0) { LocalPageInfo local = (LocalPageInfo)page; local.FullName = NameTools.GetFullName(newName, NameTools.GetLocalName(local.FullName)); local.File = GetNamespacePartialPathForPageContent(newName) + Path.GetFileName(local.File); } } DumpPages(allPages); namespacesCache = null; pagesCache = null; categoriesCache = null; // Re-add all pages and their messages to the search engine index foreach(PageInfo page in GetPages(result)) { // result contains the new name PageContent content = GetContent(page); IndexPage(content); Message[] messages = GetMessages(page); foreach(Message msg in messages) { IndexMessageTree(page, msg); } } return result; } } /// <summary> /// Sets the default page of a namespace. /// </summary> /// <param name="nspace">The namespace of which to set the default page.</param> /// <param name="page">The page to use as default page, or <c>null</c>.</param> /// <returns>The correct <see cref="T:NamespaceInfo" /> object.</returns> /// <exception cref="ArgumentNullException">If <b>nspace</b> is <c>null</c>.</exception> public NamespaceInfo SetNamespaceDefaultPage(NamespaceInfo nspace, PageInfo page) { if(nspace == null) throw new ArgumentNullException("nspace"); lock(this) { // Find requested namespace and page: if they don't exist, return null NamespaceInfo[] allNamespaces = GetNamespaces(); NamespaceInfo targetNamespace = FindNamespace(nspace.Name, allNamespaces); if(targetNamespace == null) return null; LocalPageInfo localPage = null; if(page != null) { localPage = LoadLocalPageInfo(page); if(localPage == null) return null; } targetNamespace.DefaultPage = localPage; DumpNamespaces(allNamespaces); return new NamespaceInfo(targetNamespace.Name, this, targetNamespace.DefaultPage); } } /// <summary> /// Removes a namespace. /// </summary> /// <param name="nspace">The namespace to remove.</param> /// <returns><c>true</c> if the namespace is removed, <c>false</c> otherwise.</returns> /// <exception cref="ArgumentNullException">If <b>nspace</b> is <c>null</c>.</exception> public bool RemoveNamespace(NamespaceInfo nspace) { if(nspace == null) throw new ArgumentNullException("nspace"); lock(this) { // Load all namespaces and remove the one to remove List<NamespaceInfo> allNamespaces = new List<NamespaceInfo>(GetNamespaces()); NamespaceComparer comp = new NamespaceComparer(); int index = allNamespaces.FindIndex(delegate(NamespaceInfo x) { return comp.Compare(x, nspace) == 0; }); if(index >= 0) { // Delete all categories foreach(CategoryInfo cat in GetCategories(nspace)) { RemoveCategory(cat); } // Delete all pages in the namespace (RemovePage removes the page from the search engine index) nspace.DefaultPage = null; // TODO: Remove this trick (needed in order to delete the default page) foreach(PageInfo page in GetPages(nspace)) { RemovePage(page); } // Update namespaces file allNamespaces.RemoveAt(index); DumpNamespaces(allNamespaces.ToArray()); // Remove namespace folder Directory.Delete(GetFullPathForPageContent(GetNamespacePartialPathForPageContent(nspace.Name)), true); // Remove drafts folder string oldDraftsFullPath = GetFullPathForPageDrafts(nspace.Name); if(Directory.Exists(oldDraftsFullPath)) { Directory.Delete(oldDraftsFullPath, true); } // Remove messages folder Directory.Delete(GetFullPathForMessages(GetNamespacePartialPathForPageContent(nspace.Name)), true); namespacesCache = null; pagesCache = null; categoriesCache = null; return true; } else return false; } } /// <summary> /// Moves a page from its namespace into another. /// </summary> /// <param name="page">The page to move.</param> /// <param name="destination">The destination namespace (null for the root).</param> /// <param name="copyCategories">A value indicating whether to copy the page categories in the destination /// namespace, if not already available.</param> /// <returns>The correct instance of <see cref="T:PageInfo" />.</returns> /// <exception cref="ArgumentNullException">If <b>page</b> is <c>null</c>.</exception> public PageInfo MovePage(PageInfo page, NamespaceInfo destination, bool copyCategories) { if(page == null) throw new ArgumentNullException("page"); string destinationName = destination != null ? destination.Name : null; NamespaceInfo currentNs = FindNamespace(NameTools.GetNamespace(page.FullName), GetNamespaces()); NamespaceComparer nsComp = new NamespaceComparer(); if((currentNs == null && destination == null) || nsComp.Compare(currentNs, destination) == 0) return null; if(PageExists(new PageInfo(NameTools.GetFullName(destinationName, NameTools.GetLocalName(page.FullName)), this, page.CreationDateTime))) return null; if(!NamespaceExists(destinationName)) return null; if(currentNs != null && currentNs.DefaultPage != null) { // Cannot move the default page if(new PageNameComparer().Compare(currentNs.DefaultPage, page) == 0) return null; } // Store categories for copying them, if needed CategoryInfo[] pageCategories = GetCategoriesForPage(page); // Update categories names with new namespace (don't modify the same instance because it's actually the cache!) for(int i = 0; i < pageCategories.Length; i++) { string[] pages = pageCategories[i].Pages; pageCategories[i] = new CategoryInfo(NameTools.GetFullName(destinationName, NameTools.GetLocalName(pageCategories[i].FullName)), this); pageCategories[i].Pages = new string[pages.Length]; for(int k = 0; k < pages.Length; k++) { pageCategories[i].Pages[k] = NameTools.GetFullName(destinationName, NameTools.GetLocalName(pages[k])); } } // Delete category bindings RebindPage(page, new string[0]); // Change namespace and file PageInfo[] allPages = GetAllPages(); PageNameComparer comp = new PageNameComparer(); PageInfo newPage = null; foreach(PageInfo current in allPages) { if(comp.Compare(current, page) == 0) { // Page found, update data // Change namespace and file LocalPageInfo local = (LocalPageInfo)current; // Update search engine index PageContent oldPageContent = GetContent(local); UnindexPage(oldPageContent); foreach(Message msg in GetMessages(local)) { UnindexMessageTree(local, msg); } // Move backups in new folder MoveBackups(page, destination); string newFile = GetNamespacePartialPathForPageContent(destinationName) + Path.GetFileName(local.File); // Move data file File.Move(GetFullPathForPageContent(local.File), GetFullPathForPageContent(newFile)); // Move messages file string messagesFullPath = GetFullPathForMessages(local.File); if(File.Exists(messagesFullPath)) { File.Move(messagesFullPath, GetFullPathForMessages(newFile)); } // Move draft file string draftFullPath = GetFullPathForPageDrafts(local.File); if(File.Exists(draftFullPath)) { string newDraftFullPath = GetFullPathForPageDrafts(newFile); if(!Directory.Exists(Path.GetDirectoryName(newDraftFullPath))) { Directory.CreateDirectory(Path.GetDirectoryName(newDraftFullPath)); } File.Move(draftFullPath, newDraftFullPath); } //local.Namespace = destinationName; local.FullName = NameTools.GetFullName(destinationName, NameTools.GetLocalName(local.FullName)); local.File = newFile; newPage = local; DumpPages(allPages); // Update search engine index IndexPage(new PageContent(newPage, oldPageContent.Title, oldPageContent.User, oldPageContent.LastModified, oldPageContent.Comment, oldPageContent.Content, oldPageContent.Keywords, oldPageContent.Description)); foreach(Message msg in GetMessages(local)) { IndexMessageTree(newPage, msg); } break; } } // Rebind page, if needed if(copyCategories) { // Foreach previously bound category, verify that is present in the destination namespace, if not then create it List<string> newCategories = new List<string>(pageCategories.Length); foreach(CategoryInfo oldCategory in pageCategories) { if(!CategoryExists(new CategoryInfo(oldCategory.FullName, this))) { AddCategory(destination != null ? destination.Name : null, NameTools.GetLocalName(oldCategory.FullName)); } newCategories.Add(oldCategory.FullName); } RebindPage(newPage, newCategories.ToArray()); } namespacesCache = null; pagesCache = null; categoriesCache = null; return newPage; } /// <summary> /// Moves the backups of a page into a new namespace. /// </summary> /// <param name="page">The page that is being moved.</param> /// <param name="destination">The destination namespace (<c>null</c> for the root).</param> /// <remarks>This method should be invoked <b>before</b> moving the corresponding page.</remarks> private void MoveBackups(PageInfo page, NamespaceInfo destination) { lock(this) { int[] backups = GetBackups(page); if(backups == null) return; // Page does not exist LocalPageInfo local = (LocalPageInfo)page; string extension = Path.GetExtension(local.File); string currDir = GetNamespacePartialPathForPageContent(NameTools.GetNamespace(page.FullName)); string newDir = GetNamespacePartialPathForPageContent(destination != null ? destination.Name : null); string currPartialName = currDir + Path.GetFileNameWithoutExtension(local.File) + "."; string newPartialName = newDir + NameTools.GetLocalName(page.FullName) + "."; for(int i = 0; i < backups.Length; i++) { File.Move(GetFullPathForPageContent(currPartialName + Tools.GetVersionString(backups[i]) + extension), GetFullPathForPageContent(newPartialName + Tools.GetVersionString(backups[i]) + extension)); } } } /// <summary> /// Extracts an instance of <see cref="T:CategoryInfo" /> from a line contained in the categories file. /// </summary> /// <param name="fileLine">The line to process.</param> /// <returns>The instance of <see cref="T:CategoryInfo" />.</returns> private CategoryInfo BuildCategoryInfo(string fileLine) { string[] fields = fileLine.Split('|'); // Structure // Namespace.Cat|Namespace.Page1|Namespace.Page2|... // First field can be 'Cat' or 'Namespace.Cat' string nspace, name; NameTools.ExpandFullName(fields[0], out nspace, out name); CategoryInfo result = new CategoryInfo(fields[0], this); List<string> pages = new List<string>(fields.Length); for(int k = 0; k < fields.Length - 1; k++) { if(PageExists(new PageInfo(fields[k + 1], this, DateTime.Now))) { pages.Add(fields[k + 1]); } } result.Pages = pages.ToArray(); return result; } /// <summary> /// Gets a category. /// </summary> /// <param name="fullName">The full name of the category.</param> /// <returns>The <see cref="T:CategoryInfo" />, or <c>null</c> if no category is found.</returns> /// <exception cref="ArgumentNullException">If <paramref name="fullName" /> is <c>null</c>.</exception> /// <exception cref="ArgumentException">If <paramref name="fullName" /> is empty.</exception> public CategoryInfo GetCategory(string fullName) { if(fullName == null) throw new ArgumentNullException("fullName"); if(fullName.Length == 0) throw new ArgumentException("Full Name cannot be empty", "fullName"); lock(this) { CategoryInfo[] categories = GetAllCategories(); StringComparer comp = StringComparer.OrdinalIgnoreCase; foreach(CategoryInfo cat in categories) { if(comp.Compare(cat.FullName, fullName) == 0) return cat; } return null; } } /// <summary> /// Gets all the Categories in a namespace. /// </summary> /// <param name="nspace">The namespace.</param> /// <returns>All the Categories in the namespace, sorted by name.</returns> public CategoryInfo[] GetCategories(NamespaceInfo nspace) { lock(this) { CategoryInfo[] allCategories = GetAllCategories(); // Sorted // Preallocate assuming that there are 4 namespaces and that they are distributed evenly among them: // categories might be a few dozens at most, so preallocating a smaller number of items is not a problem List<CategoryInfo> selectedCategories = new List<CategoryInfo>(allCategories.Length / 4); // Select categories that have the same namespace as the requested one, // either null-null or same name foreach(CategoryInfo cat in allCategories) { string catNamespace = NameTools.GetNamespace(cat.FullName); if(nspace == null && catNamespace == null) selectedCategories.Add(cat); else if(nspace != null && catNamespace != null && StringComparer.OrdinalIgnoreCase.Compare(nspace.Name, catNamespace) == 0) selectedCategories.Add(cat); } return selectedCategories.ToArray(); } } /// <summary> /// Gets all the Categories. /// </summary> /// <returns>The Categories.</returns> private CategoryInfo[] GetAllCategories() { lock(this) { if(categoriesCache == null) { string tmp = File.ReadAllText(GetFullPath(CategoriesFile)).Replace("\r", ""); string[] lines = tmp.Split(new char[] { '\n' }, StringSplitOptions.RemoveEmptyEntries); CategoryInfo[] result = new CategoryInfo[lines.Length]; for(int i = 0; i < lines.Length; i++) { result[i] = BuildCategoryInfo(lines[i]); } Array.Sort(result, new CategoryNameComparer()); categoriesCache = result; } return categoriesCache; } } /// <summary> /// Gets all the categories of a page. /// </summary> /// <param name="page">The page.</param> /// <returns>The categories, sorted by name.</returns> /// <exception cref="ArgumentNullException">If <paramref name="page"/> is <c>null</c>.</exception> public CategoryInfo[] GetCategoriesForPage(PageInfo page) { if(page == null) throw new ArgumentNullException("page"); string pageNamespace = NameTools.GetNamespace(page.FullName); CategoryInfo[] categories = GetCategories(FindNamespace(pageNamespace, GetNamespaces())); // Sorted List<CategoryInfo> result = new List<CategoryInfo>(10); PageNameComparer comp = new PageNameComparer(); foreach(CategoryInfo cat in categories) { foreach(string p in cat.Pages) { if(comp.Compare(page, new PageInfo(p, this, DateTime.Now)) == 0) { result.Add(cat); break; } } } return result.ToArray(); } /// <summary> /// Determines whether a category exists. /// </summary> /// <param name="category">The category to check.</param> /// <returns><c>true</c> if the category exists, <c>false</c> otherwise.</returns> private bool CategoryExists(CategoryInfo category) { lock(this) { CategoryInfo[] cats = GetCategories(FindNamespace(NameTools.GetNamespace(category.FullName), GetNamespaces())); CategoryNameComparer comp = new CategoryNameComparer(); for(int i = 0; i < cats.Length; i++) { if(comp.Compare(cats[i], category) == 0) return true; } } return false; } /// <summary> /// Adds a new Category. /// </summary> /// <param name="nspace">The target namespace (<c>null</c> for the root).</param> /// <param name="name">The Category name.</param> /// <returns>The correct CategoryInfo object.</returns> /// <exception cref="ArgumentNullException">If <paramref name="name"/> is <c>null</c>.</exception> /// <exception cref="ArgumentException">If <paramref name="name"/> is empty.</exception> public CategoryInfo AddCategory(string nspace, string name) { if(name == null) throw new ArgumentNullException("name"); if(name.Length == 0) throw new ArgumentException("Name cannot be empty", "name"); lock(this) { CategoryInfo result = new CategoryInfo(NameTools.GetFullName(nspace, name), this); if(CategoryExists(result)) return null; // Structure // Namespace.Category|Page1|Page2|... File.AppendAllText(GetFullPath(CategoriesFile), "\r\n" + result.FullName); result.Pages = new string[0]; categoriesCache = null; return result; } } /// <summary> /// Renames a Category. /// </summary> /// <param name="category">The Category to rename.</param> /// <param name="newName">The new Name.</param> /// <returns>The correct CategoryInfo object.</returns> /// <exception cref="ArgumentNullException">If <paramref name="category"/> or <paramref name="newName"/> are <c>null</c>.</exception> /// <exception cref="ArgumentException">If <paramref name="newName"/> is empty.</exception> public CategoryInfo RenameCategory(CategoryInfo category, string newName) { if(category == null) throw new ArgumentNullException("category"); if(newName == null) throw new ArgumentNullException("newName"); if(newName.Length == 0) throw new ArgumentException("New Name cannot be empty", "newName"); lock(this) { CategoryInfo result = new CategoryInfo(NameTools.GetFullName(NameTools.GetNamespace(category.FullName), newName), this); if(CategoryExists(result)) return null; CategoryInfo[] cats = GetAllCategories(); CategoryNameComparer comp = new CategoryNameComparer(); for(int i = 0; i < cats.Length; i++) { if(comp.Compare(cats[i], category) == 0) { result.Pages = cats[i].Pages; cats[i] = result; DumpCategories(cats); categoriesCache = null; return result; } } } return null; } /// <summary> /// Removes a Category. /// </summary> /// <param name="category">The Category to remove.</param> /// <returns>True if the Category has been removed successfully.</returns> /// <exception cref="ArgumentNullException">If <paramref name="category"/> is <c>null</c>.</exception> public bool RemoveCategory(CategoryInfo category) { if(category == null) throw new ArgumentNullException("category"); lock(this) { CategoryInfo[] cats = GetAllCategories(); CategoryNameComparer comp = new CategoryNameComparer(); for(int i = 0; i < cats.Length; i++) { if(comp.Compare(cats[i], category) == 0) { List<CategoryInfo> tmp = new List<CategoryInfo>(cats); tmp.Remove(tmp[i]); DumpCategories(tmp.ToArray()); categoriesCache = null; return true; } } } return false; } /// <summary> /// Merges two Categories. /// </summary> /// <param name="source">The source Category.</param> /// <param name="destination">The destination Category.</param> /// <returns>True if the Categories have been merged successfully.</returns> /// <remarks>The destination Category remains, while the source Category is deleted, and all its Pages re-bound in the destination Category.</remarks> /// <exception cref="ArgumentNullException">If <paramref name="source"/> or <paramref name="destination"/> are <c>null</c>.</exception> public CategoryInfo MergeCategories(CategoryInfo source, CategoryInfo destination) { if(source == null) throw new ArgumentNullException("source"); if(destination == null) throw new ArgumentNullException("destination"); lock(this) { NamespaceInfo[] allNamespaces = GetNamespaces(); NamespaceInfo sourceNs = FindNamespace(NameTools.GetNamespace(source.FullName), allNamespaces); NamespaceInfo destinationNs = FindNamespace(NameTools.GetNamespace(destination.FullName), allNamespaces); NamespaceComparer nsComp = new NamespaceComparer(); if(!(sourceNs == null && destinationNs == null) && nsComp.Compare(sourceNs, destinationNs) != 0) { // Different namespaces return null; } CategoryInfo[] cats = GetAllCategories(); int idxSource = -1, idxDest = -1; CategoryNameComparer comp = new CategoryNameComparer(); for(int i = 0; i < cats.Length; i++) { if(comp.Compare(cats[i], source) == 0) idxSource = i; if(comp.Compare(cats[i], destination) == 0) idxDest = i; if(idxSource != -1 && idxDest != -1) break; } if(idxSource == -1 || idxDest == -1) return null; List<CategoryInfo> tmp = new List<CategoryInfo>(cats); List<string> newPages = new List<string>(cats[idxDest].Pages); for(int i = 0; i < cats[idxSource].Pages.Length; i++) { bool found = false; for(int k = 0; k < newPages.Count; k++) { if(StringComparer.OrdinalIgnoreCase.Compare(newPages[k], cats[idxSource].Pages[i]) == 0) { found = true; break; } } if(!found) { newPages.Add(cats[idxSource].Pages[i]); } } tmp[idxDest].Pages = newPages.ToArray(); tmp.Remove(tmp[idxSource]); DumpCategories(tmp.ToArray()); CategoryInfo newCat = new CategoryInfo(destination.FullName, this); newCat.Pages = newPages.ToArray(); categoriesCache = null; return newCat; } } /// <summary> /// Handles the construction of an <see cref="T:IDocument" /> for the search engine. /// </summary> /// <param name="dumpedDocument">The input dumped document.</param> /// <returns>The resulting <see cref="T:IDocument" />.</returns> private IDocument BuildDocumentHandler(DumpedDocument dumpedDocument) { if(dumpedDocument.TypeTag == PageDocument.StandardTypeTag) { string pageName = PageDocument.GetPageName(dumpedDocument.Name); PageInfo page = FindPage(NameTools.GetNamespace(pageName), NameTools.GetLocalName(pageName), GetAllPages()); if(page == null) return null; else return new PageDocument(page, dumpedDocument, TokenizeContent); } else if(dumpedDocument.TypeTag == MessageDocument.StandardTypeTag) { string pageFullName; int id; MessageDocument.GetMessageDetails(dumpedDocument.Name, out pageFullName, out id); PageInfo page = FindPage(NameTools.GetNamespace(pageFullName), NameTools.GetLocalName(pageFullName), GetAllPages()); if(page == null) return null; else return new MessageDocument(page, id, dumpedDocument, TokenizeContent); } else return null; } /// <summary> /// Tokenizes page content. /// </summary> /// <param name="content">The content to tokenize.</param> /// <returns>The tokenized words.</returns> private static WordInfo[] TokenizeContent(string content) { WordInfo[] words = SearchEngine.Tools.Tokenize(content); return words; } /// <summary> /// Indexes a page. /// </summary> /// <param name="content">The content of the page.</param> /// <returns>The number of indexed words, including duplicates.</returns> private int IndexPage(PageContent content) { lock(this) { string documentName = PageDocument.GetDocumentName(content.PageInfo); DumpedDocument ddoc = new DumpedDocument(0, documentName, host.PrepareTitleForIndexing(content.PageInfo, content.Title), PageDocument.StandardTypeTag, content.LastModified); // Store the document // The content should always be prepared using IHost.PrepareForSearchEngineIndexing() int count = index.StoreDocument(new PageDocument(content.PageInfo, ddoc, TokenizeContent), content.Keywords, host.PrepareContentForIndexing(content.PageInfo, content.Content), null); if(count == 0 && content.Content.Length > 0) { host.LogEntry("Indexed 0 words for page " + content.PageInfo.FullName + ": possible index corruption. Please report this error to the developers", LogEntryType.Warning, null, this); } return count; } } /// <summary> /// Removes a page from the search engine index. /// </summary> /// <param name="content">The content of the page to remove.</param> private void UnindexPage(PageContent content) { lock(this) { string documentName = PageDocument.GetDocumentName(content.PageInfo); DumpedDocument ddoc = new DumpedDocument(0, documentName, host.PrepareTitleForIndexing(content.PageInfo, content.Title), PageDocument.StandardTypeTag, content.LastModified); index.RemoveDocument(new PageDocument(content.PageInfo, ddoc, TokenizeContent), null); } } /// <summary> /// Performs a search in the index. /// </summary> /// <param name="parameters">The search parameters.</param> /// <returns>The results.</returns> /// <exception cref="ArgumentNullException">If <paramref name="parameters"/> is <c>null</c>.</exception> public SearchResultCollection PerformSearch(SearchParameters parameters) { if(parameters == null) throw new ArgumentNullException("parameters"); lock(this) { return index.Search(parameters); } } /// <summary> /// Rebuilds the search index. /// </summary> public void RebuildIndex() { lock(this) { index.Clear(null); foreach(PageInfo page in GetAllPages()) { IndexPage(GetContent(page)); foreach(Message msg in GetMessages(page)) { IndexMessageTree(page, msg); } } } } /// <summary> /// Gets some statistics about the search engine index. /// </summary> /// <param name="documentCount">The total number of documents.</param> /// <param name="wordCount">The total number of unique words.</param> /// <param name="occurrenceCount">The total number of word-document occurrences.</param> /// <param name="size">The approximated size, in bytes, of the search engine index.</param> public void GetIndexStats(out int documentCount, out int wordCount, out int occurrenceCount, out long size) { lock(this) { documentCount = index.TotalDocuments; wordCount = index.TotalWords; occurrenceCount = index.TotalOccurrences; size = indexStorer.Size; } } /// <summary> /// Gets a value indicating whether the search engine index is corrupted and needs to be rebuilt. /// </summary> public bool IsIndexCorrupted { get { lock(this) { return indexStorer.DataCorrupted; } } } /// <summary> /// Extracts an instance of <see cref="T:LocalPageInfo" /> from a line of the pages file. /// </summary> /// <param name="fileLine">The line to process.</param> /// <returns>The instance of <see cref="T:LocalPageInfo" />.</returns> private LocalPageInfo BuildLocalPageInfo(string fileLine) { string[] fields = fileLine.Split(new char[] { '|' }, StringSplitOptions.RemoveEmptyEntries); // Structure (file format already converted from earlier versions) // Namespace.PageName|PageFile|DateTime if(fields.Length == 3) { return new LocalPageInfo(fields[0], this, DateTime.Parse(fields[2]), fields[1]); } else { throw new ArgumentException("Unsupported data format", "fileLine"); } } /// <summary> /// Gets all the Pages. /// </summary> /// <returns>All the Pages.</returns> private PageInfo[] GetAllPages() { lock(this) { if(pagesCache == null) { string tmp = File.ReadAllText(GetFullPath(PagesFile)).Replace("\r", ""); string[] lines = tmp.Split(new char[] { '\n' }, StringSplitOptions.RemoveEmptyEntries); PageInfo[] result = new PageInfo[lines.Length]; for(int i = 0; i < lines.Length; i++) { result[i] = BuildLocalPageInfo(lines[i]); } pagesCache = result; } return pagesCache; } } /// <summary> /// Gets a page. /// </summary> /// <param name="fullName">The full name of the page.</param> /// <returns>The <see cref="T:PageInfo" />, or <c>null</c> if no page is found.</returns> /// <exception cref="ArgumentNullException">If <paramref name="fullName"/> is <c>null</c>.</exception> /// <exception cref="ArgumentException">If <paramref name="fullName"/> is empty.</exception> public PageInfo GetPage(string fullName) { if(fullName == null) throw new ArgumentNullException("fullName"); if(fullName.Length == 0) throw new ArgumentException("Full Name cannot be empty", "fullName"); lock(this) { string nspace, name; NameTools.ExpandFullName(fullName, out nspace, out name); return FindPage(nspace, name, GetAllPages()); } } /// <summary> /// Gets all the Pages in a namespace. /// </summary> /// <param name="nspace">The namespace (<c>null</c> for the root).</param> /// <returns>All the Pages in the namespace. The array is not sorted.</returns> public PageInfo[] GetPages(NamespaceInfo nspace) { lock(this) { PageInfo[] allPages = GetAllPages(); // Preallocate assuming that there are 2 namespaces and that they are evenly distributed across them: // pages can be as much as many thousands, so preallocating a smaller number can cause a performance loss List<PageInfo> selectedPages = new List<PageInfo>(allPages.Length / 2); // Select pages that have the same namespace as the requested one, // either null-null or same name foreach(PageInfo page in allPages) { string pageNamespace = NameTools.GetNamespace(page.FullName); if(nspace == null && pageNamespace == null) selectedPages.Add(page); if(nspace != null && pageNamespace != null && StringComparer.OrdinalIgnoreCase.Compare(nspace.Name, pageNamespace) == 0) selectedPages.Add(page); } return selectedPages.ToArray(); } } /// <summary> /// Gets all the pages in a namespace that are bound to zero categories. /// </summary> /// <param name="nspace">The namespace (<c>null</c> for the root).</param> /// <returns>The pages, sorted by name.</returns> public PageInfo[] GetUncategorizedPages(NamespaceInfo nspace) { lock(this) { PageInfo[] pages = GetPages(nspace); CategoryInfo[] categories = GetCategories(nspace); List<PageInfo> result = new List<PageInfo>(pages.Length); foreach(PageInfo p in pages) { bool found = false; foreach(CategoryInfo c in categories) { foreach(string name in c.Pages) { if(StringComparer.OrdinalIgnoreCase.Compare(name, p.FullName) == 0) { found = true; break; } } } if(!found) result.Add(p); } return result.ToArray(); } } /// <summary> /// Finds a corresponding instance of <see cref="T:LocalPageInfo" /> in the available pages. /// </summary> /// <param name="page">The instance of <see cref="T:PageInfo" /> to "convert" to <see cref="T:LocalPageInfo" />.</param> /// <returns>The instance of <see cref="T:LocalPageInfo" />, or <c>null</c>.</returns> private LocalPageInfo LoadLocalPageInfo(PageInfo page) { if(page == null) return null; lock(this) { PageInfo[] pages = GetAllPages(); PageNameComparer comp = new PageNameComparer(); for(int i = 0; i < pages.Length; i++) { if(comp.Compare(pages[i], page) == 0) return pages[i] as LocalPageInfo; } } return null; } /// <summary> /// Determines whether a page exists. /// </summary> /// <param name="page">The instance of <see cref="T:PageInfo" /> to look for.</param> /// <returns><c>true</c> if the page exists, <c>false</c> otherwise.</returns> private bool PageExists(PageInfo page) { lock(this) { PageInfo[] pages = GetAllPages(); PageNameComparer comp = new PageNameComparer(); for(int i = 0; i < pages.Length; i++) { if(comp.Compare(pages[i], page) == 0) return true; } } return false; } /// <summary> /// Adds a Page. /// </summary> /// <param name="nspace">The target namespace (<c>null</c> for the root).</param> /// <param name="name">The Page Name.</param> /// <param name="creationDateTime">The creation Date/Time.</param> /// <returns>The correct PageInfo object or null.</returns> /// <remarks>This method should <b>not</b> create the content of the Page.</remarks> /// <exception cref="ArgumentNullException">If <paramref name="name"/> is <c>null</c>.</exception> /// <exception cref="ArgumentException">If <paramref name="name"/> is empty.</exception> public PageInfo AddPage(string nspace, string name, DateTime creationDateTime) { if(name == null) throw new ArgumentNullException("name"); if(name.Length == 0) throw new ArgumentException("Name cannot be empty", "name"); lock(this) { if(!NamespaceExists(nspace)) return null; if(PageExists(new PageInfo(NameTools.GetFullName(nspace, name), this, DateTime.Now))) return null; LocalPageInfo result = new LocalPageInfo(NameTools.GetFullName(nspace, name), this, creationDateTime, GetNamespacePartialPathForPageContent(nspace) + name + ".cs"); BackupPagesFile(); // Structure // Namespace.Page|File|CreationDateTime File.AppendAllText(GetFullPath(PagesFile), result.FullName + "|" + result.File + "|" + creationDateTime.ToString("yyyy'/'MM'/'dd' 'HH':'mm':'ss") + "\r\n"); //File.Create(GetFullPathForPageContent(result.File)).Close(); // Empty content file might cause problems with backups File.WriteAllText(GetFullPathForPageContent(result.File), "--\r\n--|1900/01/01 0:00:00|\r\n##PAGE##\r\n--"); pagesCache = null; return result; } } /// <summary> /// Gets the Content of a Page. /// </summary> /// <param name="page">The Page.</param> /// <returns>The Page Content object, <c>null</c> if the page does not exist or <paramref name="page"/> is <c>null</c>, /// or an empty instance if the content could not be retrieved (<seealso cref="PageContent.GetEmpty"/>).</returns> public PageContent GetContent(PageInfo page) { if(page == null) return null; lock(this) { LocalPageInfo local = LoadLocalPageInfo(page); if(local == null) return null; string text = null; try { text = File.ReadAllText(GetFullPathForPageContent(local.File)); } catch(Exception ex) { host.LogEntry("Could not load content file (" + local.File + ") for page " + local.FullName + " - returning empty (" + ex.Message + ")", LogEntryType.Error, null, this); return PageContent.GetEmpty(page); } return ExtractContent(text, page); } } private PageContent ExtractContent(string data, PageInfo pageInfo) { if(data == null) return null; // Structure (Keywords and Description are new in v3) // Page Title // Username|DateTime[|Comment][|(((Keyword,Keyword,Keyword)))(((Description)))] --- Comment is optional // ##PAGE## // Content... data = data.Replace("\r", ""); string[] lines = data.Split('\n'); if(lines.Length < 4) { host.LogEntry("Corrupted or malformed page data for page " + pageInfo.FullName + " - returning empty", LogEntryType.Error, null, this); return PageContent.GetEmpty(pageInfo); } string[] fields = lines[1].Split('|'); string comment = null; string[] keywords = null; string description = null; if(fields.Length >= 3 && !fields[2].StartsWith("(((")) comment = Tools.UnescapeString(fields[2]); else comment = ""; string lastField = fields[fields.Length - 1]; if(lastField.StartsWith("(((") && lastField.EndsWith(")))")) { // Keywords and/or description are specified int closedBracketsIndex = lastField.IndexOf(")))"); // This identifies the end of keywords block string keywordsBlock = lastField.Substring(0, closedBracketsIndex).Trim('(', ')'); keywords = keywordsBlock.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries); for(int i = 0; i < keywords.Length; i++) { keywords[i] = Tools.UnescapeString(keywords[i]); } description = Tools.UnescapeString(lastField.Substring(closedBracketsIndex + 3).Trim('(', ')')); if(string.IsNullOrEmpty(description)) description = null; } int nlIndex = data.IndexOf("\n"); // Index of first new-line char // Don't consider page title, since it might contain "##PAGE##" int idx = data.Substring(nlIndex + 1).IndexOf("##PAGE##") + 8 + 1 + nlIndex + 1; return new PageContent(pageInfo, lines[0], fields[0], DateTime.Parse(fields[1]), comment, data.Substring(idx), keywords, description); } /// <summary> /// Backups a Page. /// </summary> /// <param name="page">The Page to backup.</param> /// <returns>True if the Page has been backupped successfully.</returns> private bool Backup(PageInfo page) { if(page == null) throw new ArgumentNullException("page"); lock(this) { LocalPageInfo local = LoadLocalPageInfo(page); if(local == null) return false; int[] backups = GetBackups(page); int rev = (backups.Length > 0 ? backups[backups.Length - 1] + 1 : 0); File.Copy( GetFullPathForPageContent(local.File), GetFullPathForPageContent(GetNamespacePartialPathForPageContent(NameTools.GetNamespace(page.FullName)) + Path.GetFileNameWithoutExtension(local.File) + "." + Tools.GetVersionString(rev) + Path.GetExtension(local.File))); } return true; } /// <summary> /// Gets the Backup/Revision numbers of a Page. /// </summary> /// <param name="page">The Page to get the Backups of.</param> /// <returns>The list of Backup/Revision numbers.</returns> /// <exception cref="ArgumentNullException">If <paramref name="page"/> is <c>null</c>.</exception> public int[] GetBackups(PageInfo page) { if(page == null) throw new ArgumentNullException("page"); lock(this) { LocalPageInfo local = LoadLocalPageInfo(page); if(local == null) return null; // Files in <Public>\Pages\[Namespace\]FileName.NNNNN.cs string dir = GetFullPath(PagesDirectory); string nsDir = GetNamespacePartialPathForPageContent(NameTools.GetNamespace(page.FullName)); if(nsDir.Length > 0) dir = Path.Combine(dir, nsDir); string[] files = Directory.GetFiles(dir, Path.GetFileNameWithoutExtension(local.File) + ".*" + Path.GetExtension(local.File)); List<int> result = new List<int>(30); for(int i = 0; i < files.Length; i++) { string num = Path.GetFileNameWithoutExtension(files[i]).Substring(NameTools.GetLocalName(page.FullName).Length + 1); int bak = -1; if(int.TryParse(num, out bak)) result.Add(bak); } return result.ToArray(); } } /// <summary> /// Gets the Content of a Backup. /// </summary> /// <param name="page">The Page.</param> /// <param name="revision">The revision.</param> /// <returns>The content.</returns> /// <exception cref="ArgumentNullException">If <paramref name="page"/> is <c>null</c>.</exception> /// <exception cref="ArgumentOutOfRangeException">If <paramref name="revision"/> is less than zero.</exception> public PageContent GetBackupContent(PageInfo page, int revision) { if(page == null) throw new ArgumentNullException("page"); if(revision < 0) throw new ArgumentOutOfRangeException("revision", "Invalid Revision"); lock(this) { LocalPageInfo local = LoadLocalPageInfo(page); if(local == null) return null; string filename = Path.GetFileNameWithoutExtension(local.File) + "." + Tools.GetVersionString(revision) + Path.GetExtension(local.File); string path = GetFullPathForPageContent(GetNamespacePartialPathForPageContent(NameTools.GetNamespace(page.FullName)) + filename); if(!File.Exists(path)) return null; else return ExtractContent(File.ReadAllText(path), page); } } /// <summary> /// Forces to overwrite or create a Backup. /// </summary> /// <param name="content">The Backup content.</param> /// <param name="revision">The revision.</param> /// <returns>True if the Backup has been created successfully.</returns> /// <exception cref="ArgumentNullException">If <paramref name="content"/> is <c>null</c>.</exception> /// <exception cref="ArgumentOutOfRangeException">If <paramref name="revision"/> is less than zero.</exception> public bool SetBackupContent(PageContent content, int revision) { if(content == null) throw new ArgumentNullException("content"); if(revision < 0) throw new ArgumentOutOfRangeException("Invalid Revision", "revision"); lock(this) { LocalPageInfo local = LoadLocalPageInfo(content.PageInfo); if(local == null) return false; StringBuilder sb = new StringBuilder(); sb.Append(content.Title); sb.Append("\r\n"); sb.Append(content.User); sb.Append("|"); sb.Append(content.LastModified.ToString("yyyy'/'MM'/'dd' 'HH':'mm':'ss")); if(!string.IsNullOrEmpty(content.Comment)) { sb.Append("|"); sb.Append(Tools.EscapeString(content.Comment)); } sb.Append("\r\n##PAGE##\r\n"); sb.Append(content.Content); string filename = Path.GetFileNameWithoutExtension(local.File) + "." + Tools.GetVersionString(revision) + Path.GetExtension(local.File); File.WriteAllText(GetFullPathForPageContent(GetNamespacePartialPathForPageContent(NameTools.GetNamespace(content.PageInfo.FullName)) + filename), sb.ToString()); } return true; } /// <summary> /// Renames a Page. /// </summary> /// <param name="page">The Page to rename.</param> /// <param name="newName">The new Name.</param> /// <returns>True if the Page has been renamed successfully.</returns> /// <exception cref="ArgumentNullException">If <paramref name="page"/> or <paramref name="newName"/> are <c>null</c>.</exception> /// <exception cref="ArgumentException">If <paramref name="newName"/> is empty.</exception> public PageInfo RenamePage(PageInfo page, string newName) { if(page == null) throw new ArgumentNullException("page"); if(newName == null) throw new ArgumentNullException("newName"); if(newName.Length == 0) throw new ArgumentException("New Name cannot be empty", "newName"); lock(this) { if(PageExists(new PageInfo(NameTools.GetFullName(NameTools.GetNamespace(page.FullName), newName), this, DateTime.Now))) return null; NamespaceInfo currentNs = FindNamespace(NameTools.GetNamespace(page.FullName), GetNamespaces()); if(currentNs != null && currentNs.DefaultPage != null) { // Cannot rename the default page if(new PageNameComparer().Compare(currentNs.DefaultPage, page) == 0) return null; } PageInfo[] pgs = GetAllPages(); PageNameComparer comp = new PageNameComparer(); // Store page's categories for rebinding with new page CategoryInfo[] tmp = GetCategoriesForPage(page); string[] cats = new string[tmp.Length]; for(int i = 0; i < tmp.Length; i++) { cats[i] = tmp[i].FullName; } // Remove all bindings for old page RebindPage(page, new string[0]); // Find page and rename files for(int i = 0; i < pgs.Length; i++) { if(comp.Compare(pgs[i], page) == 0) { LocalPageInfo local = pgs[i] as LocalPageInfo; PageContent oldContent = GetContent(page); Message[] messages = GetMessages(local); // Update search engine index UnindexPage(oldContent); foreach(Message msg in messages) { UnindexMessageTree(local, msg); } string oldFullName = local.FullName; local.FullName = NameTools.GetFullName(NameTools.GetNamespace(local.FullName), newName); string newFile = GetNamespacePartialPathForPageContent(NameTools.GetNamespace(local.FullName)) + newName + Path.GetExtension(local.File); // Rename content file string oldLocalName = local.File; string oldFullPath = GetFullPathForPageContent(local.File); string newFullPath = GetFullPathForPageContent(newFile); File.Move(oldFullPath, newFullPath); // Rename messages file if(File.Exists(GetFullPathForMessages(oldLocalName))) { File.Move(GetFullPathForMessages(oldLocalName), GetFullPathForMessages(newFile)); } // Rename draft file, if any string oldDraftFullPath = GetDraftFullPath(local); if(File.Exists(oldDraftFullPath)) { string newDraftFullPath = GetDraftFullPath(new LocalPageInfo(local.FullName, this, local.CreationDateTime, newFile)); File.Move(oldDraftFullPath, newDraftFullPath); } // Rename all backups, store new page list on disk // and rebind new page with old categories RenameBackups(new LocalPageInfo(oldFullName, this, local.CreationDateTime, oldLocalName), newName); // Set new filename (local references an element in the pgs array) local.File = newFile; DumpPages(pgs); // Clear internal cache categoriesCache = null; pagesCache = null; // Re-bind page with previously saved categories RebindPage(local, cats); // Update search engine index IndexPage(new PageContent(local, oldContent.Title, oldContent.User, oldContent.LastModified, oldContent.Comment, oldContent.Content, oldContent.Keywords, oldContent.Description)); foreach(Message msg in messages) { IndexMessageTree(local, msg); } return local; } } // Page not found, return null return null; } } /// <summary> /// Renames the backups of a page. /// </summary> /// <param name="page">The page that is being renamed.</param> /// <param name="newName">The new name of the page.</param> /// <remarks>This method should be invoked <b>before</b> renaming the corresponding page.</remarks> private void RenameBackups(PageInfo page, string newName) { lock(this) { int[] backups = GetBackups(page); if(backups == null) return; // Page does not exist LocalPageInfo local = (LocalPageInfo)page; string extension = Path.GetExtension(local.File); string nsDir = GetNamespacePartialPathForPageContent(NameTools.GetNamespace(page.FullName)); string partialName = nsDir + Path.GetFileNameWithoutExtension(local.File) + "."; for(int i = 0; i < backups.Length; i++) { File.Move(GetFullPathForPageContent(partialName + Tools.GetVersionString(backups[i]) + extension), GetFullPathForPageContent(nsDir + newName + "." + Tools.GetVersionString(backups[i]) + extension)); } } } /// <summary> /// Modifies the Content of a Page. /// </summary> /// <param name="page">The Page.</param> /// <param name="title">The Title of the Page.</param> /// <param name="username">The Username.</param> /// <param name="dateTime">The Date/Time.</param> /// <param name="comment">The Comment of the editor, about this revision.</param> /// <param name="content">The Page Content.</param> /// <param name="keywords">The keywords, usually used for SEO.</param> /// <param name="description">The description, usually used for SEO.</param> /// <param name="saveMode">The save mode for this modification.</param> /// <returns><c>true</c> if the Page has been modified successfully, <c>false</c> otherwise.</returns> /// <remarks>If <b>saveMode</b> equals <b>Draft</b> and a draft already exists, it is overwritten.</remarks> /// <exception cref="ArgumentNullException">If <paramref name="page"/>, <paramref name="title"/> <paramref name="username"/> or <paramref name="content"/> are <c>null</c>.</exception> /// <exception cref="ArgumentException">If <paramref name="title"/> or <paramref name="username"/> are empty.</exception> public bool ModifyPage(PageInfo page, string title, string username, DateTime dateTime, string comment, string content, string[] keywords, string description, SaveMode saveMode) { if(page == null) throw new ArgumentNullException("page"); if(title == null) throw new ArgumentNullException("title"); if(title.Length == 0) throw new ArgumentException("Title cannot be empty", "title"); if(username == null) throw new ArgumentNullException("username"); if(username.Length == 0) throw new ArgumentException("Username cannot be empty", "username"); if(content == null) throw new ArgumentNullException("content"); // content can be empty lock(this) { LocalPageInfo local = LoadLocalPageInfo(page); if(local == null) return false; if(saveMode == SaveMode.Backup) { Backup(local); } StringBuilder sb = new StringBuilder(); sb.Append(title); sb.Append("\r\n"); sb.Append(username); sb.Append("|"); sb.Append(dateTime.ToString("yyyy'/'MM'/'dd' 'HH':'mm':'ss")); if(!string.IsNullOrEmpty(comment)) { sb.Append("|"); sb.Append(Tools.EscapeString(comment)); } if((keywords != null && keywords.Length > 0) || !string.IsNullOrEmpty(description)) { sb.Append("|((("); if(keywords != null) { for(int i = 0; i < keywords.Length; i++) { sb.Append(Tools.EscapeString(keywords[i])); if(i != keywords.Length - 1) sb.Append(","); } } sb.Append(")))((("); sb.Append(Tools.EscapeString(description)); sb.Append(")))"); } sb.Append("\r\n##PAGE##\r\n"); sb.Append(content); if(saveMode == SaveMode.Draft) { // Create the namespace directory for the draft, if needed // Drafts\NS\Page.cs string targetFileFullPath = GetDraftFullPath(local); if(!Directory.Exists(Path.GetDirectoryName(targetFileFullPath))) { Directory.CreateDirectory(Path.GetDirectoryName(targetFileFullPath)); } File.WriteAllText(targetFileFullPath, sb.ToString()); } else { File.WriteAllText(GetFullPathForPageContent(local.File), sb.ToString()); // Update search engine index PageContent pageContent = new PageContent(page, title, username, dateTime, comment, content, keywords, description); IndexPage(pageContent); } } return true; } /// <summary> /// Gets the content of a draft of a Page. /// </summary> /// <param name="page">The Page.</param> /// <returns>The draft, or <c>null</c> if no draft exists.</returns> /// <exception cref="ArgumentNullException">If <paramref name="page"/> is <c>null</c>.</exception> public PageContent GetDraft(PageInfo page) { if(page == null) throw new ArgumentNullException("page"); lock(this) { LocalPageInfo local = LoadLocalPageInfo(page); if(local == null) return null; string targetFileFullPath = GetDraftFullPath(local); if(!File.Exists(targetFileFullPath)) return null; else return ExtractContent(File.ReadAllText(targetFileFullPath), local); } } /// <summary> /// Deletes a draft of a Page. /// </summary> /// <param name="page">The page.</param> /// <returns><c>true</c> if the draft is deleted, <c>false</c> otherwise.</returns> /// <exception cref="ArgumentNullException">If <paramref name="page"/> is <c>null</c>.</exception> public bool DeleteDraft(PageInfo page) { if(page == null) throw new ArgumentNullException("page"); lock(this) { LocalPageInfo local = LoadLocalPageInfo(page); if(local == null) return false; string targetFileFullPath = GetDraftFullPath(local); if(!File.Exists(targetFileFullPath)) return false; else { File.Delete(targetFileFullPath); // Delete directory if empty if(Directory.GetFiles(Path.GetDirectoryName(targetFileFullPath)).Length == 0) { Directory.Delete(Path.GetDirectoryName(targetFileFullPath)); } return true; } } } /// <summary> /// Performs the rollback of a Page to a specified revision. /// </summary> /// <param name="page">The Page to rollback.</param> /// <param name="revision">The Revision to rollback the Page to.</param> /// <returns><c>true</c> if the rollback succeeded, <c>false</c> otherwise.</returns> /// <exception cref="ArgumentNullException">If <paramref name="page"/> is <c>null</c>.</exception> /// <exception cref="ArgumentOutOfRangeException">If <paramref name="revision"/> is less than zero.</exception> public bool RollbackPage(PageInfo page, int revision) { if(page == null) throw new ArgumentNullException("page"); if(revision < 0) throw new ArgumentOutOfRangeException("Invalid Revision", "revision"); lock(this) { if(!PageExists(page)) return false; // Operations: // - Load specific revision's content // - Modify page with loaded content, performing backup PageContent revisionContent = GetBackupContent(page, revision); if(revisionContent == null) return false; bool done = ModifyPage(page, revisionContent.Title, revisionContent.User, revisionContent.LastModified, revisionContent.Comment, revisionContent.Content, revisionContent.Keywords, revisionContent.Description, SaveMode.Backup); return done; } } /// <summary> /// Deletes the Backups of a Page, up to a specified revision. /// </summary> /// <param name="page">The Page to delete the backups of.</param> /// <param name="revision">The newest revision to delete (newer revision are kept) or -1 to delete all the Backups.</param> /// <returns><c>true</c> if the deletion succeeded, <c>false</c> otherwise.</returns> /// <exception cref="ArgumentNullException">If <paramref name="page"/> is <c>null</c>.</exception> /// <exception cref="ArgumentOutOfRangeException">If <paramref name="revision"/> is less than -1.</exception> public bool DeleteBackups(PageInfo page, int revision) { if(page == null) throw new ArgumentNullException("page"); if(revision < -1) throw new ArgumentOutOfRangeException("Invalid Revision", "revision"); lock(this) { int[] temp = GetBackups(page); if(temp == null) return false; if(temp.Length == 0) return true; List<int> backups = new List<int>(temp); int idx = (revision != -1 ? backups.IndexOf(revision) : backups[backups.Count - 1]); // Operations // - Delete old beckups, from 0 to revision // - Rename newer backups starting from 0 LocalPageInfo local = (LocalPageInfo)page; string extension = Path.GetExtension(local.File); string filenameNoExt = Path.GetFileNameWithoutExtension(local.File); string nsDir = GetNamespacePartialPathForPageContent(NameTools.GetNamespace(page.FullName)); for(int i = 0; i <= idx; i++) { File.Delete(GetFullPathForPageContent(nsDir + filenameNoExt + "." + Tools.GetVersionString(backups[i]) + extension)); } if(revision != -1) { for(int i = revision + 1; i < backups.Count; i++) { File.Move(GetFullPathForPageContent(nsDir + filenameNoExt + "." + Tools.GetVersionString(backups[i]) + extension), GetFullPathForPageContent(nsDir + filenameNoExt + "." + Tools.GetVersionString(backups[i] - revision - 1) + extension)); } } } return true; } /// <summary> /// Removes a Page. /// </summary> /// <param name="page">The Page to remove.</param> /// <returns><c>true</c> if the Page has been removed successfully, <c>false</c> otherwise.</returns> /// <exception cref="ArgumentNullException">If <paramref name="page"/> is <c>null</c>.</exception> public bool RemovePage(PageInfo page) { if(page == null) throw new ArgumentNullException("page"); lock(this) { NamespaceInfo currentNs = FindNamespace(NameTools.GetNamespace(page.FullName), GetNamespaces()); if(currentNs != null && currentNs.DefaultPage != null) { // Cannot remove the default page if(new PageNameComparer().Compare(currentNs.DefaultPage, page) == 0) return false; } List<PageInfo> allPages = new List<PageInfo>(GetAllPages()); PageNameComparer comp = new PageNameComparer(); for(int i = 0; i < allPages.Count; i++) { if(comp.Compare(allPages[i], page) == 0) { PageContent content = GetContent(page); LocalPageInfo local = page as LocalPageInfo; // Update search engine index UnindexPage(content); Message[] messages = GetMessages(local); foreach(Message msg in messages) { UnindexMessageTree(local, msg); } allPages.Remove(allPages[i]); DeleteBackups(page, -1); DumpPages(allPages.ToArray()); try { File.Delete(GetFullPathForPageContent(GetNamespacePartialPathForPageContent(NameTools.GetNamespace(page.FullName)) + ((LocalPageInfo)page).File)); } catch { } try { File.Delete(GetDraftFullPath(local)); } catch { } try { File.Delete(GetFullPathForMessages(local.File)); } catch { } pagesCache = null; categoriesCache = null; return true; } } } return false; } /// <summary> /// Binds a Page with one or more Categories. /// </summary> /// <param name="page">The Page to bind.</param> /// <param name="categories">The Categories to bind the Page with (full name).</param> /// <returns>True if the binding succeeded.</returns> /// <remarks>After a successful operation, the Page is bound with all and only the categories passed as argument.</remarks> /// <exception cref="ArgumentNullException">If <paramref name="page"/> or <paramref name="categories"/> are <c>null</c>.</exception> public bool RebindPage(PageInfo page, string[] categories) { if(page == null) throw new ArgumentNullException("page"); if(categories == null) throw new ArgumentNullException("categories"); lock(this) { if(!PageExists(page)) return false; CategoryInfo[] cats = GetAllCategories(); // Check all categories (they all must exist and be valid) foreach(string cat in categories) { if(cat == null) throw new ArgumentNullException("categories", "A category name cannot be null"); if(cat.Length == 0) throw new ArgumentException("A category name cannot be empty", "categories"); CategoryNameComparer comp = new CategoryNameComparer(); if(Array.Find<CategoryInfo>(cats, delegate(CategoryInfo x) { return comp.Compare(x, new CategoryInfo(cat, this)) == 0; }) == null) return false; } // Operations: // - Remove the Page from every Category // - For each specified category, add (if needed) the Page List<string> pages; CategoryNameComparer catComp = new CategoryNameComparer(); for(int i = 0; i < cats.Length; i++) { pages = new List<string>(cats[i].Pages); int idx = GetIndex(pages, page.FullName); if(idx != -1) pages.Remove(pages[idx]); cats[i].Pages = pages.ToArray(); } for(int i = 0; i < cats.Length; i++) { for(int k = 0; k < categories.Length; k++) { if(catComp.Compare(cats[i], new CategoryInfo(categories[k], this)) == 0) { pages = new List<string>(cats[i].Pages); pages.Add(page.FullName); cats[i].Pages = pages.ToArray(); } } } DumpCategories(cats); pagesCache = null; categoriesCache = null; } return true; } private static int GetIndex(List<string> pages, string page) { for(int i = 0; i < pages.Count; i++) { if(StringComparer.OrdinalIgnoreCase.Compare(pages[i], page) == 0) return i; } return -1; } /// <summary> /// Makes a backup copy of the pages file. /// </summary> private void BackupPagesFile() { lock(this) { File.Copy(GetFullPath(PagesFile), GetFullPath(Path.GetFileNameWithoutExtension(PagesFile) + ".bak" + Path.GetExtension(PagesFile)), true); } } /// <summary> /// Writes all pages in the storage file. /// </summary> /// <param name="pages">The pages to write.</param> private void DumpPages(PageInfo[] pages) { lock(this) { BackupPagesFile(); StringBuilder sb = new StringBuilder(); for(int i = 0; i < pages.Length; i++) { sb.Append(pages[i].FullName); sb.Append("|"); sb.Append(((LocalPageInfo)pages[i]).File); sb.Append("|"); sb.Append(pages[i].CreationDateTime.ToString("yyyy'/'MM'/'dd' 'HH':'mm':'ss")); sb.Append("\r\n"); } File.WriteAllText(GetFullPath(PagesFile), sb.ToString()); } } /// <summary> /// Makes a backup copy of the categories file. /// </summary> private void BackupCategoriesFile() { lock(this) { File.Copy(GetFullPath(CategoriesFile), GetFullPath(Path.GetFileNameWithoutExtension(CategoriesFile) + ".bak" + Path.GetExtension(CategoriesFile)), true); } } /// <summary> /// Writes all categories in the storage file. /// </summary> /// <param name="categories">The categories.</param> private void DumpCategories(CategoryInfo[] categories) { lock(this) { BackupCategoriesFile(); // Format // NS.Category|NS.Page1|NS.Page2 StringBuilder sb = new StringBuilder(10480); for(int i = 0; i < categories.Length; i++) { sb.Append(categories[i].FullName); if(categories[i].Pages.Length > 0) { for(int k = 0; k < categories[i].Pages.Length; k++) { sb.Append("|"); sb.Append(categories[i].Pages[k]); } } sb.Append("\r\n"); } File.WriteAllText(GetFullPath(CategoriesFile), sb.ToString()); } } /// <summary> /// Gets the Page Messages. /// </summary> /// <param name="page">The Page.</param> /// <returns>The list of the <b>first-level</b> Messages, containing the replies properly nested, sorted by date/time.</returns> /// <exception cref="ArgumentNullException">If <paramref name="page"/> is <c>null</c>.</exception> public Message[] GetMessages(PageInfo page) { if(page == null) throw new ArgumentNullException("page"); lock(this) { LocalPageInfo local = LoadLocalPageInfo(page); if(local == null) return null; // Shortcut if(!File.Exists(GetFullPathForMessages(local.File))) return new Message[0]; string data = File.ReadAllText(GetFullPathForMessages(local.File)).Replace("\r", ""); string[] lines = data.Split(new char[] { '\n' }, StringSplitOptions.RemoveEmptyEntries); List<Message> result = new List<Message>(); // Structure // ID|Username|Subject|DateTime|ParentID|Body // This algorithm DOES not handle replies that are stored BEFORE their parent, // so every reply MUST be stored anywhere but AFTER its parent // (the message tree should be stored depht-first; new messages can be appended at the end of the file) string[] fields; int id, parent; string username, subject, body; DateTime dateTime; for(int i = 0; i < lines.Length; i++) { fields = lines[i].Split('|'); id = int.Parse(fields[0]); username = fields[1]; subject = Tools.UnescapeString(fields[2]); dateTime = DateTime.Parse(fields[3]); parent = int.Parse(fields[4]); body = Tools.UnescapeString(fields[5]); if(parent != -1) { // Find parent Message p = FindMessage(result, parent); if(p == null) { // Add as top-level message result.Add(new Message(id, username, subject, dateTime, body)); } else { // Add to parent's replies Message[] newMessages = new Message[p.Replies.Length + 1]; Array.Copy(p.Replies, newMessages, p.Replies.Length); newMessages[newMessages.Length - 1] = new Message(id, username, subject, dateTime, body); p.Replies = newMessages; } } else { // Add as top-level message result.Add(new Message(id, username, subject, dateTime, body)); } } result.Sort((a, b) => { return a.DateTime.CompareTo(b.DateTime); }); return result.ToArray(); } } /// <summary> /// Gets the total number of Messages in a Page Discussion. /// </summary> /// <param name="page">The Page.</param> /// <returns>The number of messages.</returns> /// <exception cref="ArgumentNullException">If <paramref name="page"/> is <c>null</c>.</exception> public int GetMessageCount(PageInfo page) { if(page == null) throw new ArgumentNullException("page"); lock(this) { LocalPageInfo local = LoadLocalPageInfo(page); if(local == null) return -1; if(!File.Exists(GetFullPathForMessages(local.File))) return 0; string data = File.ReadAllText(GetFullPathForMessages(local.File)).Replace("\r", ""); return data.Split(new char[] { '\n' }, StringSplitOptions.RemoveEmptyEntries).Length; } } /// <summary> /// Finds a Message in a Message tree. /// </summary> /// <param name="messages">The Message tree.</param> /// <param name="id">The ID of the Message to find.</param> /// <returns>The Message or null.</returns> /// <remarks>The method is recursive.</remarks> private static Message FindMessage(IEnumerable<Message> messages, int id) { Message result = null; foreach(Message msg in messages) { if(msg.ID == id) { result = msg; } if(result == null) { result = FindMessage(msg.Replies, id); } if(result != null) break; } return result; } /// <summary> /// Removes all messages for a page and stores the new messages. /// </summary> /// <param name="page">The page.</param> /// <param name="messages">The new messages to store.</param> /// <returns><c>true</c> if the messages are stored, <c>false</c> otherwise.</returns> /// <exception cref="ArgumentNullException">If <paramref name="page"/> or <paramref name="messages"/> are <c>null</c>.</exception> public bool BulkStoreMessages(PageInfo page, Message[] messages) { if(page == null) throw new ArgumentNullException("page"); if(messages == null) throw new ArgumentNullException("messages"); LocalPageInfo local = LoadLocalPageInfo(page); if(local == null) return false; // Validate IDs by using a dictionary as a way of validation try { Dictionary<int, byte> ids = new Dictionary<int, byte>(50); foreach(Message msg in messages) { AddAllIds(ids, msg); } } catch(ArgumentException) { return false; } // Be sure to remove all old messages from the search engine index foreach(Message msg in GetMessages(local)) { UnindexMessageTree(local, msg); } // Simply overwrite all messages on disk DumpMessages(local, messages); // Add the new messages to the search engine index foreach(Message msg in messages) { IndexMessageTree(local, msg); } return true; } private static void AddAllIds(Dictionary<int, byte> dictionary, Message msg) { dictionary.Add(msg.ID, 0); foreach(Message m in msg.Replies) { AddAllIds(dictionary, m); } } /// <summary> /// Adds a new Message to a Page. /// </summary> /// <param name="page">The Page.</param> /// <param name="username">The Username.</param> /// <param name="subject">The Subject.</param> /// <param name="dateTime">The Date/Time.</param> /// <param name="body">The Body.</param> /// <param name="parent">The Parent Message ID, or -1.</param> /// <returns>True if the Message has been added successfully.</returns> /// <exception cref="ArgumentNullException">If <paramref name="page"/>, <paramref name="username"/>, <paramref name="subject"/> or <paramref name="body"/> are <c>null</c>.</exception> /// <exception cref="ArgumentException">If <paramref name="username"/> or <paramref name="subject"/> are empty.</exception> /// <exception cref="ArgumentOutOfRangeException">If <paramref name="parent"/> is less than -1.</exception> public bool AddMessage(PageInfo page, string username, string subject, DateTime dateTime, string body, int parent) { if(page == null) throw new ArgumentNullException("page"); if(username == null) throw new ArgumentNullException("username"); if(username.Length == 0) throw new ArgumentException("Username cannot be empty", "username"); if(subject == null) throw new ArgumentNullException("subject"); if(subject.Length == 0) throw new ArgumentException("Subject cannot be empty", "subject"); if(body == null) throw new ArgumentNullException("body"); // body can be empty if(parent < -1) throw new ArgumentOutOfRangeException("parent", "Invalid Parent Message ID"); lock(this) { LocalPageInfo local = LoadLocalPageInfo(page); if(local == null) return false; if(parent != -1) { // Check for existence of parent message Message[] allMessages = GetMessages(page); if(FindMessage(new List<Message>(allMessages), parent) == null) return false; } subject = Tools.EscapeString(subject); body = Tools.EscapeString(body); StringBuilder sb = new StringBuilder(); // Structure // ID|Username|Subject|DateTime|ParentID|Body int messageID = GetFreeMessageID(local); sb.Append(messageID); sb.Append("|"); sb.Append(username); sb.Append("|"); sb.Append(subject); sb.Append("|"); sb.Append(dateTime.ToString("yyyy'/'MM'/'dd' 'HH':'mm':'ss")); sb.Append("|"); sb.Append(parent.ToString()); sb.Append("|"); sb.Append(body); sb.Append("\r\n"); File.AppendAllText(GetFullPathForMessages(local.File), sb.ToString()); // Update search engine index IndexMessage(local, messageID, subject, dateTime, body); } return true; } /// <summary> /// Indexes a message. /// </summary> /// <param name="page">The page.</param> /// <param name="id">The message ID.</param> /// <param name="subject">The subject.</param> /// <param name="dateTime">The date/time.</param> /// <param name="body">The body.</param> /// <returns>The number of indexed words, including duplicates.</returns> private int IndexMessage(PageInfo page, int id, string subject, DateTime dateTime, string body) { lock(this) { // Trim "RE:" to avoid polluting the search engine index if(subject.ToLowerInvariant().StartsWith("re:") && subject.Length > 3) subject = subject.Substring(3).Trim(); string documentName = MessageDocument.GetDocumentName(page, id); DumpedDocument ddoc = new DumpedDocument(0, documentName, host.PrepareTitleForIndexing(null, subject), MessageDocument.StandardTypeTag, dateTime); // Store the document // The content should always be prepared using IHost.PrepareForSearchEngineIndexing() int count = index.StoreDocument(new MessageDocument(page, id, ddoc, TokenizeContent), null, host.PrepareContentForIndexing(null, body), null); if(count == 0 && body.Length > 0) { host.LogEntry("Indexed 0 words for message " + page.FullName + ":" + id.ToString() + ": possible index corruption. Please report this error to the developers", LogEntryType.Warning, null, this); } return count; } } /// <summary> /// Indexes a message tree. /// </summary> /// <param name="page">The page.</param> /// <param name="root">The tree root.</param> private void IndexMessageTree(PageInfo page, Message root) { IndexMessage(page, root.ID, root.Subject, root.DateTime, root.Body); foreach(Message reply in root.Replies) { IndexMessageTree(page, reply); } } /// <summary> /// Removes a message from the search engine index. /// </summary> /// <param name="page">The page.</param> /// <param name="id">The message ID.</param> /// <param name="subject">The subject.</param> /// <param name="dateTime">The date/time.</param> /// <param name="body">The body.</param> /// <returns>The number of indexed words, including duplicates.</returns> private void UnindexMessage(PageInfo page, int id, string subject, DateTime dateTime, string body) { lock(this) { // Trim "RE:" to avoid polluting the search engine index if(subject.ToLowerInvariant().StartsWith("re:") && subject.Length > 3) subject = subject.Substring(3).Trim(); string documentName = MessageDocument.GetDocumentName(page, id); DumpedDocument ddoc = new DumpedDocument(0, documentName, host.PrepareTitleForIndexing(null, subject), MessageDocument.StandardTypeTag, DateTime.Now); index.RemoveDocument(new MessageDocument(page, id, ddoc, TokenizeContent), null); } } /// <summary> /// Removes a message tree from the search engine index. /// </summary> /// <param name="page">The page.</param> /// <param name="root">The tree root.</param> private void UnindexMessageTree(PageInfo page, Message root) { UnindexMessage(page, root.ID, root.Subject, root.DateTime, root.Body); foreach(Message reply in root.Replies) { UnindexMessageTree(page, reply); } } /// <summary> /// Find a free Message ID for a Page. /// </summary> /// <param name="page">The Page.</param> /// <returns>The Message ID.</returns> private int GetFreeMessageID(LocalPageInfo page) { lock(this) { if(!File.Exists(GetFullPathForMessages(page.File))) return 0; int result = 0; string data = File.ReadAllText(GetFullPathForMessages(page.File)).Replace("\r", ""); string[] lines = data.Split(new char[] { '\n' }, StringSplitOptions.RemoveEmptyEntries); int idx, tmp; for(int i = 0; i < lines.Length; i++) { idx = lines[i].IndexOf('|'); tmp = int.Parse(lines[i].Substring(0, idx)); if(tmp > result) result = tmp; } result++; return result; } } /// <summary> /// Removes a Message. /// </summary> /// <param name="page">The Page.</param> /// <param name="id">The ID of the Message to remove.</param> /// <param name="removeReplies">A value specifying whether or not to remove the replies.</param> /// <returns>True if the Message has been removed successfully.</returns> /// <exception cref="ArgumentNullException">If <paramref name="page"/> is <c>null</c>.</exception> /// <exception cref="ArgumentOutOfRangeException">If <paramref name="id"/> is less than zero.</exception> public bool RemoveMessage(PageInfo page, int id, bool removeReplies) { if(page == null) throw new ArgumentNullException("page"); if(id < 0) throw new ArgumentOutOfRangeException("Invalid ID", "id"); lock(this) { LocalPageInfo local = LoadLocalPageInfo(page); if(local == null) return false; Message[] messages = GetMessages(page); Message msg = FindMessage(messages, id); if(msg == null) return false; Message[] replies = new Message[0]; if(!removeReplies) { replies = msg.Replies; } if(!removeReplies && replies.Length > 0) { // Find Message's anchestor Message anchestor = FindAnchestor(messages, msg.ID); if(anchestor != null) { Message[] newReplies = new Message[anchestor.Replies.Length + replies.Length]; Array.Copy(anchestor.Replies, newReplies, anchestor.Replies.Length); Array.Copy(replies, 0, newReplies, anchestor.Replies.Length, replies.Length); anchestor.Replies = newReplies; } else { Message[] newMessages = new Message[messages.Length + replies.Length]; Array.Copy(messages, newMessages, messages.Length); Array.Copy(replies, 0, newMessages, messages.Length, replies.Length); messages = newMessages; } } // Recursively update search engine index if(removeReplies) { UnindexMessageTree(page, msg); } else UnindexMessage(page, msg.ID, msg.Subject, msg.DateTime, msg.Body); List<Message> tempList = new List<Message>(messages); RemoveMessage(tempList, msg); messages = tempList.ToArray(); tempList = null; DumpMessages(page, messages); } return true; } /// <summary> /// Finds the anchestor/parent of a Message. /// </summary> /// <param name="messages">The Messages.</param> /// <param name="id">The Message ID.</param> /// <returns>The anchestor Message or null.</returns> private static Message FindAnchestor(IEnumerable<Message> messages, int id) { Message result = null; foreach(Message msg in messages) { for(int k = 0; k < msg.Replies.Length; k++) { if(msg.Replies[k].ID == id) { result = msg; break; } if(result == null) { result = FindAnchestor(msg.Replies, id); } } if(result != null) break; } return result; } /// <summary> /// Removes a Message from a Message Tree. /// </summary> /// <param name="messages">The Message Tree.</param> /// <param name="msg">The Message to Remove.</param> /// <returns>True if the Message has been removed.</returns> private static bool RemoveMessage(List<Message> messages, Message msg) { for(int i = 0; i < messages.Count; i++) { if(messages.Contains(msg)) { messages.Remove(msg); return true; } List<Message> tempList = new List<Message>(messages[i].Replies); bool done = RemoveMessage(tempList, msg); if(done) { messages[i].Replies = tempList.ToArray(); // Message found and removed return true; } } // Message not found return false; } /// <summary> /// Modifies a Message. /// </summary> /// <param name="page">The Page.</param> /// <param name="id">The ID of the Message to modify.</param> /// <param name="username">The Username.</param> /// <param name="subject">The Subject.</param> /// <param name="dateTime">The Date/Time.</param> /// <param name="body">The Body.</param> /// <returns>True if the Message has been modified successfully.</returns> /// <exception cref="ArgumentNullException">If <paramref name="page"/>, <paramref name="username"/>, <paramref name="subject"/> or <paramref name="body"/> are <c>null</c>.</exception> /// <exception cref="ArgumentOutOfRangeException">If <paramref name="id"/> is less than zero.</exception> /// <exception cref="ArgumentException">If <paramref name="username"/> or <paramref name="subject"/> are empty.</exception> public bool ModifyMessage(PageInfo page, int id, string username, string subject, DateTime dateTime, string body) { if(page == null) throw new ArgumentNullException("page"); if(id < 0) throw new ArgumentOutOfRangeException("Invalid Message ID", "id"); if(username == null) throw new ArgumentNullException("username"); if(username.Length == 0) throw new ArgumentException("Username cannot be empty", "username"); if(subject == null) throw new ArgumentNullException("subject"); if(subject.Length == 0) throw new ArgumentException("Subject cannot be empty", "subject"); if(body == null) throw new ArgumentNullException("body"); // body can be empty lock(this) { if(LoadLocalPageInfo(page) == null) return false; List<Message> messages = new List<Message>(GetMessages(page)); Message msg = FindMessage(messages, id); if(msg == null) return false; // Update search engine index UnindexMessage(page, id, msg.Subject, msg.DateTime, msg.Body); msg.Username = username; msg.Subject = subject; msg.DateTime = dateTime; msg.Body = body; DumpMessages(page, messages); // Update search engine index IndexMessage(page, id, subject, dateTime, body); } return true; } /// <summary> /// Dumps the Message tree of a Page to disk. /// </summary> /// <param name="page">The Page.</param> /// <param name="messages">The Message tree.</param> private void DumpMessages(PageInfo page, IEnumerable<Message> messages) { lock(this) { StringBuilder sb = new StringBuilder(5000); AppendMessages(messages, -1, sb); File.WriteAllText(GetFullPathForMessages(((LocalPageInfo)page).File), sb.ToString()); } } /// <summary> /// Appends to a StringBuilder object the branches and leaves of a Message tree. /// </summary> /// <param name="messages">The Message tree branch to append.</param> /// <param name="parent">The ID of the parent of the Message tree or -1.</param> /// <param name="sb">The StringBuilder.</param> /// <remarks>The methods appends the Messages traversing the tree depht-first, and it is recursive.</remarks> private void AppendMessages(IEnumerable<Message> messages, int parent, StringBuilder sb) { // Depht-first // Structure // ID|Username|Subject|DateTime|ParentID|Body lock(this) { foreach(Message msg in messages) { sb.Append(msg.ID.ToString()); sb.Append("|"); sb.Append(msg.Username); sb.Append("|"); sb.Append(Tools.EscapeString(msg.Subject)); sb.Append("|"); sb.Append(msg.DateTime.ToString("yyyy'/'MM'/'dd' 'HH':'mm':'ss")); sb.Append("|"); sb.Append(parent.ToString()); sb.Append("|"); sb.Append(Tools.EscapeString(msg.Body)); sb.Append("\r\n"); AppendMessages(msg.Replies, msg.ID, sb); } } } /// <summary> /// Extracts an instance of <see cref="T:NavigationPath" /> from a line in the navigation paths file. /// </summary> /// <param name="fileLine">The line to process.</param> /// <returns>The instance of <see cref="T:NavigationPath" /></returns> private NavigationPath BuildNavigationPath(string fileLine) { // Structure // Namespace.PathName|Page1|Page2|... // First field can be 'Namespace.PathName' or 'PathName' string[] fields = fileLine.Split('|'); string[] fullName = fields[0].Split(new char[] { '.' }, StringSplitOptions.RemoveEmptyEntries); string nspace, name; if(fullName.Length == 1) { nspace = null; name = fullName[0]; } else { nspace = fullName[0]; name = fullName[1]; } NavigationPath result = new NavigationPath(NameTools.GetFullName(nspace, name), this); List<string> tempPages = new List<string>(10); for(int k = 1; k < fields.Length; k++) { tempPages.Add(fields[k]); } result.Pages = tempPages.ToArray(); return result; } /// <summary> /// Gets all the Navigation Paths. /// </summary> /// <returns>The Navigation Paths.</returns> private NavigationPath[] GetAllNavigationPaths() { lock(this) { List<NavigationPath> paths = new List<NavigationPath>(10); string[] lines = File.ReadAllText(GetFullPath(NavigationPathsFile)).Replace("\r", "").Split(new char[] { '\n' }, StringSplitOptions.RemoveEmptyEntries); // Structure // Namespace/PathName|Page1|Page2|... // First field can be 'Namespace/PathName', '/PathName' or 'PathName' for(int i = 0; i < lines.Length; i++) { paths.Add(BuildNavigationPath(lines[i])); } return paths.ToArray(); } } /// <summary> /// Gets all the Navigation Paths in a Namespace. /// </summary> /// <param name="nspace">The Namespace.</param> /// <returns>All the Navigation Paths, sorted by name.</returns> public NavigationPath[] GetNavigationPaths(NamespaceInfo nspace) { lock(this) { NavigationPath[] allNavigationPaths = GetAllNavigationPaths(); List<NavigationPath> selectedNavigationPaths = new List<NavigationPath>(allNavigationPaths.Length / 4); foreach(NavigationPath path in allNavigationPaths) { string pathNamespace = NameTools.GetNamespace(path.FullName); if(nspace == null && pathNamespace == null) selectedNavigationPaths.Add(path); if(nspace != null && pathNamespace != null && StringComparer.OrdinalIgnoreCase.Compare(nspace.Name, pathNamespace) == 0) selectedNavigationPaths.Add(path); } selectedNavigationPaths.Sort(new NavigationPathComparer()); return selectedNavigationPaths.ToArray(); } } /// <summary> /// Adds a new Navigation Path. /// </summary> /// <param name="nspace">The target namespace (<c>null</c> for the root).</param> /// <param name="name">The Name of the Path.</param> /// <param name="pages">The Pages array.</param> /// <returns>The correct <see cref="T:NavigationPath" /> object.</returns> /// <exception cref="ArgumentNullException">If <paramref name="name"/> or <paramref name="pages"/> are <c>null</c>.</exception> /// <exception cref="ArgumentException">If <paramref name="name"/> or <paramref name="pages"/> are empty.</exception> public NavigationPath AddNavigationPath(string nspace, string name, PageInfo[] pages) { if(name == null) throw new ArgumentNullException("name"); if(name.Length == 0) throw new ArgumentException("Name cannot be empty", "name"); if(pages == null) throw new ArgumentNullException("pages"); if(pages.Length == 0) throw new ArgumentException("Pages cannot be empty"); lock(this) { NavigationPathComparer comp = new NavigationPathComparer(); NavigationPath temp = new NavigationPath(NameTools.GetFullName(nspace, name), this); if(Array.Find(GetAllNavigationPaths(), delegate(NavigationPath p) { return comp.Compare(p, temp) == 0; }) != null) return null; temp = null; foreach(PageInfo page in pages) { if(page == null) throw new ArgumentNullException("pages", "A page element cannot be null"); if(LoadLocalPageInfo(page) == null) throw new ArgumentException("Page not found", "pages"); } NavigationPath result = new NavigationPath(NameTools.GetFullName(nspace, name), this); List<string> tempPages = new List<string>(pages.Length); StringBuilder sb = new StringBuilder(500); sb.Append("\r\n"); sb.Append(result.FullName); for(int i = 0; i < pages.Length; i++) { if(pages[i].Provider == this) { sb.Append("|"); sb.Append(pages[i].FullName); tempPages.Add(pages[i].FullName); } } result.Pages = tempPages.ToArray(); File.AppendAllText(GetFullPath(NavigationPathsFile), sb.ToString()); return result; } } /// <summary> /// Modifies an existing Navigation Path. /// </summary> /// <param name="path">The Navigation Path to modify.</param> /// <param name="pages">The new Pages array.</param> /// <returns>The correct <see cref="T:NavigationPath" /> object.</returns> /// <exception cref="ArgumentNullException">If <paramref name="path"/> or <paramref name="pages"/> are <c>null</c>.</exception> /// <exception cref="ArgumentException">If <paramref name="pages"/> is empty.</exception> public NavigationPath ModifyNavigationPath(NavigationPath path, PageInfo[] pages) { if(path == null) throw new ArgumentNullException("path"); if(pages == null) throw new ArgumentNullException("pages"); if(pages.Length == 0) throw new ArgumentException("Pages cannot be empty"); lock(this) { foreach(PageInfo page in pages) { if(page == null) throw new ArgumentNullException("pages", "A page element cannot be null"); if(LoadLocalPageInfo(page) == null) throw new ArgumentException("Page not found", "pages"); } NavigationPath[] paths = GetAllNavigationPaths(); NavigationPathComparer comp = new NavigationPathComparer(); for(int i = 0; i < paths.Length; i++) { if(comp.Compare(path, paths[i]) == 0) { paths[i].Pages = new string[0]; NavigationPath np = new NavigationPath(path.FullName, this); List<string> tempPages = new List<string>(pages.Length); for(int k = 0; k < pages.Length; k++) { if(pages[i].Provider == this) { tempPages.Add(pages[k].FullName); } } np.Pages = tempPages.ToArray(); paths[i] = np; DumpNavigationPaths(paths); return np; } } } return null; } /// <summary> /// Removes a Navigation Path. /// </summary> /// <param name="path">The Navigation Path to remove.</param> /// <returns>True if the Path is removed successfully.</returns> /// <exception cref="ArgumentNullException">If <paramref name="path"/> is <c>null</c>.</exception> public bool RemoveNavigationPath(NavigationPath path) { if(path == null) throw new ArgumentNullException("path"); lock(this) { List<NavigationPath> paths = new List<NavigationPath>(GetAllNavigationPaths()); NavigationPathComparer comp = new NavigationPathComparer(); for(int i = 0; i < paths.Count; i++) { if(comp.Compare(path, paths[i]) == 0) { paths.Remove(paths[i]); DumpNavigationPaths(paths.ToArray()); return true; } } } return false; } /// <summary> /// Writes an array of Navigation Paths to disk. /// </summary> /// <param name="paths">The array.</param> private void DumpNavigationPaths(NavigationPath[] paths) { lock(this) { StringBuilder sb = new StringBuilder(); for(int i = 0; i < paths.Length; i++) { sb.Append(paths[i].FullName); for(int k = 0; k < paths[i].Pages.Length; k++) { sb.Append("|"); sb.Append(paths[i].Pages[k]); } if(i != paths.Length - 1) sb.Append("\r\n"); } File.WriteAllText(GetFullPath(NavigationPathsFile), sb.ToString()); } } /// <summary> /// Gets all the Snippets. /// </summary> /// <returns>All the Snippets, sorted by name.</returns> public Snippet[] GetSnippets() { lock(this) { string[] files = Directory.GetFiles(GetFullPath(SnippetsDirectory), "*.cs"); Snippet[] snippets = new Snippet[files.Length]; for(int i = 0; i < files.Length; i++) { snippets[i] = new Snippet(Path.GetFileNameWithoutExtension(files[i]), File.ReadAllText(files[i]), this); } Array.Sort(snippets, new SnippetNameComparer()); return snippets; } } /// <summary> /// Adds a new Snippet. /// </summary> /// <param name="name">The Name of the Snippet.</param> /// <param name="content">The Content of the Snippet.</param> /// <returns>The correct Snippet object.</returns> /// <exception cref="ArgumentNullException">If <paramref name="name"/> or <paramref name="content"/> are <c>null</c>.</exception> /// <exception cref="ArgumentNullException">If <paramref name="name"/> is empty.</exception> public Snippet AddSnippet(string name, string content) { if(name == null) throw new ArgumentNullException("name"); if(name.Length == 0) throw new ArgumentException("Name cannot be empty", "name"); if(content == null) throw new ArgumentNullException("content"); // content can be empty lock(this) { SnippetNameComparer comp = new SnippetNameComparer(); Snippet temp = new Snippet(name, content, this); if(Array.Find(GetSnippets(), delegate(Snippet s) { return comp.Compare(s, temp) == 0; }) != null) return null; temp = null; File.WriteAllText(GetFullPathForSnippets(name + ".cs"), content); return new Snippet(name, content, this); } } /// <summary> /// Modifies a new Snippet. /// </summary> /// <param name="name">The Name of the Snippet to modify.</param> /// <param name="content">The Content of the Snippet.</param> /// <returns>The correct Snippet object.</returns> /// <exception cref="ArgumentNullException">If <paramref name="name"/> or <paramref name="content"/> are <c>null</c>.</exception> /// <exception cref="ArgumentNullException">If <paramref name="name"/> is empty.</exception> public Snippet ModifySnippet(string name, string content) { if(name == null) throw new ArgumentNullException("name"); if(name.Length == 0) throw new ArgumentException("Name cannot be empty", "name"); if(content == null) throw new ArgumentNullException("content"); // content can be empty lock(this) { SnippetNameComparer comp = new SnippetNameComparer(); Snippet temp = new Snippet(name, content, this); if(Array.Find(GetSnippets(), delegate(Snippet s) { return comp.Compare(s, temp) == 0; }) == null) return null; temp = null; File.WriteAllText(GetFullPathForSnippets(name + ".cs"), content); return new Snippet(name, content, this); } } /// <summary> /// Removes a new Snippet. /// </summary> /// <param name="name">The Name of the Snippet to remove.</param> /// <returns>True if the Snippet is removed successfully.</returns> /// <exception cref="ArgumentNullException">If <paramref name="name"/> is <c>null</c>.</exception> /// <exception cref="ArgumentNullException">If <paramref name="name"/> is empty.</exception> public bool RemoveSnippet(string name) { if(name == null) throw new ArgumentNullException("name"); if(name.Length == 0) throw new ArgumentException("Name cannot be empty", "name"); lock(this) { SnippetNameComparer comp = new SnippetNameComparer(); Snippet temp = new Snippet(name, "", this); if(Array.Find(GetSnippets(), delegate(Snippet s) { return comp.Compare(s, temp) == 0; }) == null) return false; temp = null; File.Delete(GetFullPathForSnippets(name + ".cs")); } return true; } /// <summary> /// Gets all the content templates. /// </summary> /// <returns>All the content templates, sorted by name.</returns> public ContentTemplate[] GetContentTemplates() { lock(this) { string[] files = Directory.GetFiles(GetFullPath(ContentTemplatesDirectory), "*.cs"); ContentTemplate[] templates = new ContentTemplate[files.Length]; for(int i = 0; i < files.Length; i++) { templates[i] = new ContentTemplate(Path.GetFileNameWithoutExtension(files[i]), File.ReadAllText(files[i]), this); } Array.Sort(templates, new ContentTemplateNameComparer()); return templates; } } /// <summary> /// Adds a new content template. /// </summary> /// <param name="name">The name of template.</param> /// <param name="content">The content of the template.</param> /// <returns>The correct <see cref="T:ContentTemplate" /> object.</returns> /// <exception cref="ArgumentNullException">If <paramref name="name"/> or <paramref name="content"/> are <c>null</c>.</exception> /// <exception cref="ArgumentNullException">If <paramref name="name"/> is empty.</exception> public ContentTemplate AddContentTemplate(string name, string content) { if(name == null) throw new ArgumentNullException("name"); if(name.Length == 0) throw new ArgumentException("Name cannot be empty", "name"); if(content == null) throw new ArgumentNullException("content"); lock(this) { string file = GetFullPathForContentTemplate(name + ".cs"); if(File.Exists(file)) return null; File.WriteAllText(file, content); return new ContentTemplate(name, content, this); } } /// <summary> /// Modifies an existing content template. /// </summary> /// <param name="name">The name of the template to modify.</param> /// <param name="content">The content of the template.</param> /// <returns>The correct <see cref="T:ContentTemplate" /> object.</returns> /// <exception cref="ArgumentNullException">If <paramref name="name"/> or <paramref name="content"/> are <c>null</c>.</exception> /// <exception cref="ArgumentNullException">If <paramref name="name"/> is empty.</exception> public ContentTemplate ModifyContentTemplate(string name, string content) { if(name == null) throw new ArgumentNullException("name"); if(name.Length == 0) throw new ArgumentException("Name cannot be empty", "name"); if(content == null) throw new ArgumentNullException("content"); lock(this) { string file = GetFullPathForContentTemplate(name + ".cs"); if(!File.Exists(file)) return null; File.WriteAllText(file, content); return new ContentTemplate(name, content, this); } } /// <summary> /// Removes a content template. /// </summary> /// <param name="name">The name of the template to remove.</param> /// <returns><c>true</c> if the template is removed, <c>false</c> otherwise.</returns> /// <exception cref="ArgumentNullException">If <paramref name="name"/> is <c>null</c>.</exception> /// <exception cref="ArgumentNullException">If <paramref name="name"/> is empty.</exception> public bool RemoveContentTemplate(string name) { if(name == null) throw new ArgumentNullException("name"); if(name.Length == 0) throw new ArgumentException("Name cannot be empty", "name"); lock(this) { string file = GetFullPathForContentTemplate(name + ".cs"); if(!File.Exists(file)) return false; File.Delete(file); return true; } } } }