3073 lines
117 KiB
C#
3073 lines
117 KiB
C#
|
|
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);
|
|
}
|
|
|
|
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);
|
|
}
|
|
|
|
// Set new filename (local references an element in the pgs array)
|
|
local.File = newFile;
|
|
|
|
// Rename all backups, store new page list on disk
|
|
// and rebind new page with old categories
|
|
RenameBackups(page, newName);
|
|
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;
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
}
|