using System; using System.Collections.Generic; using System.Data.Common; using System.Text; using ScrewTurn.Wiki.PluginFramework; using ScrewTurn.Wiki.SearchEngine; namespace ScrewTurn.Wiki.Plugins.SqlCommon { /// /// Implements a base class for a SQL pages storage provider. /// public abstract class SqlPagesStorageProviderBase : SqlStorageProviderBase, IPagesStorageProviderV30 { private const int MaxStatementsInBatch = 20; private const int FirstRevision = 0; private const int CurrentRevision = -1; private const int DraftRevision = -100; private IIndex index; private bool alwaysGenerateDocument = false; /// /// Initializes the Storage Provider. /// /// The Host of the Component. /// The Configuration data, if any. /// If the configuration string is not valid, the methoud should throw a . public new void Init(IHostV30 host, string config) { base.Init(host, config); index = new SqlIndex(new IndexConnector(GetWordFetcher, GetSize, GetCount, ClearIndex, DeleteDataForDocument, SaveDataForDocument, TryFindWord)); } #region Index and Search Engine /// /// Sets test flags (to be used only for tests). /// /// A value indicating whether to always generate a result when resolving a document, /// even when the page does not exist. public void SetFlags(bool alwaysGenerateDocument) { this.alwaysGenerateDocument = alwaysGenerateDocument; } /// /// Gets a word fetcher. /// /// The word fetcher. private IWordFetcher GetWordFetcher() { return new SqlWordFetcher(GetCommandBuilder().GetConnection(connString), TryFindWord); } /// /// Gets the search index (only used for testing purposes). /// public IIndex Index { get { return index; } } /// /// Performs a search in the index. /// /// The search parameters. /// The results. /// If is null. public SearchResultCollection PerformSearch(SearchParameters parameters) { if(parameters == null) throw new ArgumentNullException("parameters"); return index.Search(parameters); } /// /// Rebuilds the search index. /// public void RebuildIndex() { index.Clear(null); ICommandBuilder builder = GetCommandBuilder(); DbConnection connection = builder.GetConnection(connString); DbTransaction transaction = BeginTransaction(connection); List allNamespaces = new List(GetNamespaces()); allNamespaces.Add(null); int indexedElements = 0; foreach(NamespaceInfo nspace in allNamespaces) { foreach(PageInfo page in GetPages(transaction, nspace)) { IndexPage(GetContent(page), transaction); indexedElements++; foreach(Message msg in GetMessages(transaction, page)) { IndexMessageTree(page, msg, transaction); indexedElements++; } // Every 10 indexed documents, commit the transaction to // reduce the number of database locks if(indexedElements >= 10) { CommitTransaction(transaction); indexedElements = 0; connection = builder.GetConnection(connString); transaction = BeginTransaction(connection); } } } CommitTransaction(transaction); } /// /// Gets a value indicating whether the search engine index is corrupted and needs to be rebuilt. /// public bool IsIndexCorrupted { get { return false; } } /// /// Handles the construction of an for the search engine. /// /// The input dumped document. /// The resulting . private IDocument BuildDocument(DumpedDocument dumpedDocument) { if(alwaysGenerateDocument) { return new DummyDocument() { ID = dumpedDocument.ID, Name = dumpedDocument.Name, Title = dumpedDocument.Title, TypeTag = dumpedDocument.TypeTag, DateTime = dumpedDocument.DateTime }; } if(dumpedDocument.TypeTag == PageDocument.StandardTypeTag) { string pageName = PageDocument.GetPageName(dumpedDocument.Name); PageInfo page = GetPage(pageName); 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 = GetPage(pageFullName); if(page == null) return null; else return new MessageDocument(page, id, dumpedDocument, TokenizeContent); } else return null; } // Extremely dirty way for testing the search engine in combination with alwaysGenerateDocument private class DummyDocument : IDocument { public uint ID { get; set; } public string Name { get; set; } public string Title { get; set; } public string TypeTag { get; set; } public DateTime DateTime { get; set; } public WordInfo[] Tokenize(string content) { return ScrewTurn.Wiki.SearchEngine.Tools.Tokenize(content); } } /// /// Gets some statistics about the search engine index. /// /// The total number of documents. /// The total number of unique words. /// The total number of word-document occurrences. /// The approximated size, in bytes, of the search engine index. public void GetIndexStats(out int documentCount, out int wordCount, out int occurrenceCount, out long size) { documentCount = index.TotalDocuments; wordCount = index.TotalWords; occurrenceCount = index.TotalOccurrences; size = GetSize(); } /// /// Gets the approximate size, in bytes, of the search engine index. /// private long GetSize() { // 1. Size of documents: 8 + 2*20 + 2*30 + 2*1 + 8 = 118 bytes // 2. Size of words: 8 + 2*8 = 24 bytes // 3. Size of mappings: 8 + 8 + 2 + 2 + 1 = 21 bytes // 4. Size = Size * 2 ICommandBuilder builder = GetCommandBuilder(); DbConnection connection = builder.GetConnection(connString); QueryBuilder queryBuilder = new QueryBuilder(builder); long size = 0; string query = queryBuilder.SelectCountFrom("IndexDocument"); DbCommand command = builder.GetCommand(connection, query, new List()); int rows = ExecuteScalar(command, -1, false); if(rows == -1) return 0; size += rows * 118; query = queryBuilder.SelectCountFrom("IndexWord"); command = builder.GetCommand(connection, query, new List()); rows = ExecuteScalar(command, -1, false); if(rows == -1) return 0; size += rows * 24; query = queryBuilder.SelectCountFrom("IndexWordMapping"); command = builder.GetCommand(connection, query, new List()); rows = ExecuteScalar(command, -1, false); if(rows == -1) return 0; size += rows * 21; CloseConnection(connection); return size * 2; } /// /// Gets the number of elements in the index. /// /// The type of elements. /// The number of elements. private int GetCount(IndexElementType element) { ICommandBuilder builder = GetCommandBuilder(); DbConnection connection = builder.GetConnection(connString); QueryBuilder queryBuilder = new QueryBuilder(builder); int count = 0; string elemName = ""; if(element == IndexElementType.Documents) elemName = "IndexDocument"; else if(element == IndexElementType.Words) elemName = "IndexWord"; else if(element == IndexElementType.Occurrences) elemName = "IndexWordMapping"; else throw new NotSupportedException("Unsupported element type"); string query = queryBuilder.SelectCountFrom(elemName); DbCommand command = builder.GetCommand(connection, query, new List()); count = ExecuteScalar(command, -1, true); return count; } /// /// Deletes all data associated to a document. /// /// The document. /// A state object passed from the index (can be null or a ). private void DeleteDataForDocument(IDocument document, object state) { // 1. Delete all data related to a document // 2. Delete all words that have no more mappings ICommandBuilder builder = GetCommandBuilder(); QueryBuilder queryBuilder = new QueryBuilder(builder); string query = queryBuilder.DeleteFrom("IndexDocument"); query = queryBuilder.Where(query, "Name", WhereOperator.Equals, "DocName"); List parameters = new List(1); parameters.Add(new Parameter(ParameterType.String, "DocName", document.Name)); string subQuery = queryBuilder.SelectFrom("IndexWordMapping", new string[] { "Word" }); subQuery = queryBuilder.GroupBy(subQuery, new string[] { "Word" }); string query2 = queryBuilder.DeleteFrom("IndexWord"); query2 = queryBuilder.WhereNotInSubquery(query2, "IndexWord", "Id", subQuery); query = queryBuilder.AppendForBatch(query, query2); DbCommand command = null; if(state != null) command = builder.GetCommand((DbTransaction)state, query, parameters); else command = builder.GetCommand(connString, query, parameters); // Close only if state is null ExecuteNonQuery(command, state == null); } /// /// Saves data for a new document. /// /// The document. /// The content words. /// The title words. /// The keywords. /// A state object passed from the index (can be null or a ). /// The number of stored occurrences. private int SaveDataForDocument(IDocument document, WordInfo[] content, WordInfo[] title, WordInfo[] keywords, object state) { // 1. Insert document // 2. Insert all new words // 3. Load all word IDs // 4. Insert mappings // On error, return without rolling back if state != null, rollback otherwise // On completion, commit if state == null ICommandBuilder builder = GetCommandBuilder(); QueryBuilder queryBuilder = new QueryBuilder(builder); DbTransaction transaction = null; if(state != null) transaction = (DbTransaction)state; else { DbConnection connection = builder.GetConnection(connString); transaction = BeginTransaction(connection); } uint freeDocumentId = GetFreeElementId(IndexElementType.Documents, transaction); uint freeWordId = GetFreeElementId(IndexElementType.Words, transaction); // Insert the document string query = queryBuilder.InsertInto("IndexDocument", new string[] { "Id", "Name", "Title", "TypeTag", "DateTime" }, new string[] { "Id", "Name", "Title", "TypeTag", "DateTime" }); List parameters = new List(5); parameters.Add(new Parameter(ParameterType.Int32, "Id", (int)freeDocumentId)); parameters.Add(new Parameter(ParameterType.String, "Name", document.Name)); parameters.Add(new Parameter(ParameterType.String, "Title", document.Title)); parameters.Add(new Parameter(ParameterType.String, "TypeTag", document.TypeTag)); parameters.Add(new Parameter(ParameterType.DateTime, "DateTime", document.DateTime)); DbCommand command = builder.GetCommand(transaction, query, parameters); if(ExecuteNonQuery(command, false) != 1) { if(state == null) RollbackTransaction(transaction); return -1; } document.ID = freeDocumentId; List allWords = new List(content.Length + title.Length + keywords.Length); allWords.AddRange(content); allWords.AddRange(title); allWords.AddRange(keywords); List existingWords = new List(allWords.Count / 2); Dictionary wordIds = new Dictionary(1024); // Try to blindly insert all words (assumed to be lowercase and clean from diacritics) query = queryBuilder.InsertInto("IndexWord", new string[] { "Id", "Text" }, new string[] { "Id", "Text" }); parameters = new List(2); parameters.Add(new Parameter(ParameterType.Int32, "Id", 0)); parameters.Add(new Parameter(ParameterType.String, "Text", "")); foreach(WordInfo word in allWords) { parameters[0].Value = (int)freeWordId; parameters[1].Value = word.Text; command = builder.GetCommand(transaction, query, parameters); if(ExecuteNonQuery(command, false, false) == 1) { wordIds.Add(word.Text, freeWordId); freeWordId++; } else { existingWords.Add(word); } } // Load IDs of all existing words query = queryBuilder.SelectFrom("IndexWord", new string[] { "Id" }); query = queryBuilder.Where(query, "Text", WhereOperator.Equals, "Text"); parameters = new List(1); parameters.Add(new Parameter(ParameterType.String, "Text", "")); foreach(WordInfo word in existingWords) { parameters[0].Value = word.Text; command = builder.GetCommand(transaction, query, parameters); int id = ExecuteScalar(command, -1, false); if(id == -1) { if(state == null) RollbackTransaction(transaction); return -1; } if(!wordIds.ContainsKey(word.Text)) { wordIds.Add(word.Text, (uint)id); } else if(wordIds[word.Text] != (uint)id) throw new InvalidOperationException("Word ID mismatch"); } // Insert all mappings query = queryBuilder.InsertInto("IndexWordMapping", new string[] { "Word", "Document", "FirstCharIndex", "WordIndex", "Location" }, new string[] { "Word", "Document", "FirstCharIndex", "WordIndex", "Location" }); parameters = new List(5); parameters.Add(new Parameter(ParameterType.Int32, "Word", 0)); parameters.Add(new Parameter(ParameterType.Int32, "Document", (int)freeDocumentId)); parameters.Add(new Parameter(ParameterType.Int16, "FirstCharIndex", 0)); parameters.Add(new Parameter(ParameterType.Int16, "WordIndex", 0)); parameters.Add(new Parameter(ParameterType.Byte, "Location", 0)); foreach(WordInfo word in allWords) { parameters[0].Value = (int)wordIds[word.Text]; parameters[1].Value = (int)freeDocumentId; parameters[2].Value = (short)word.FirstCharIndex; parameters[3].Value = (short)word.WordIndex; parameters[4].Value = word.Location.Location; command = builder.GetCommand(transaction, query, parameters); if(ExecuteNonQuery(command, false) != 1) { if(state == null) RollbackTransaction(transaction); return -1; } } if(state == null) CommitTransaction(transaction); return allWords.Count; } /// /// Gets a free element ID from the database. /// /// The element type. /// The current database transaction. /// The free element ID. private uint GetFreeElementId(IndexElementType element, DbTransaction transaction) { if(element == IndexElementType.Occurrences) throw new ArgumentException("Element cannot be Occurrences", "element"); string table = element == IndexElementType.Documents ? "IndexDocument" : "IndexWord"; ICommandBuilder builder = GetCommandBuilder(); QueryBuilder queryBuilder = new QueryBuilder(builder); string query = queryBuilder.SelectFrom(table, new string[] { "Id" }); query = queryBuilder.OrderBy(query, new string[] { "Id" }, new Ordering[] { Ordering.Desc }); DbCommand command = builder.GetCommand(transaction, query, new List()); int id = ExecuteScalar(command, -1, false); if(id == -1) return 0; else return (uint)id + 1; } /// /// Tries to load all data related to a word from the database. /// /// The word text. /// The returned word. /// An open database connection. /// true if the word is found, false otherwise. private bool TryFindWord(string text, out Word word, DbConnection connection) { // 1. Find word - if not found, return // 2. Read all raw word mappings // 3. Read all documents (unique) // 4. Build result data structure ICommandBuilder builder = GetCommandBuilder(); QueryBuilder queryBuilder = new QueryBuilder(builder); string query = queryBuilder.SelectFrom("IndexWord", new string[] { "Id" }); query = queryBuilder.Where(query, "Text", WhereOperator.Equals, "Text"); List parameters = new List(1); parameters.Add(new Parameter(ParameterType.String, "Text", text)); DbCommand command = builder.GetCommand(connection, query, parameters); int wordId = ExecuteScalar(command, -1, false); if(wordId == -1) { word = null; return false; } // Read all raw mappings query = queryBuilder.SelectFrom("IndexWordMapping"); query = queryBuilder.Where(query, "Word", WhereOperator.Equals, "WordId"); parameters = new List(1); parameters.Add(new Parameter(ParameterType.Int32, "WordId", wordId)); command = builder.GetCommand(connection, query, parameters); DbDataReader reader = ExecuteReader(command, false); List mappings = new List(2048); while(reader != null && reader.Read()) { mappings.Add(new DumpedWordMapping((uint)wordId, (uint)(int)reader["Document"], (ushort)(short)reader["FirstCharIndex"], (ushort)(short)reader["WordIndex"], (byte)reader["Location"])); } CloseReader(reader); if(mappings.Count == 0) { word = null; return false; } // Find all documents query = queryBuilder.SelectFrom("IndexDocument"); query = queryBuilder.Where(query, "Id", WhereOperator.Equals, "DocId"); parameters = new List(1); parameters.Add(new Parameter(ParameterType.Int32, "DocId", 0)); Dictionary documents = new Dictionary(64); foreach(DumpedWordMapping map in mappings) { uint docId = map.DocumentID; if(documents.ContainsKey(docId)) continue; parameters[0].Value = (int)docId; command = builder.GetCommand(connection, query, parameters); reader = ExecuteReader(command, false); if(reader != null && reader.Read()) { DumpedDocument dumpedDoc = new DumpedDocument(docId, reader["Name"] as string, reader["Title"] as string, reader["TypeTag"] as string, (DateTime)reader["DateTime"]); IDocument document = BuildDocument(dumpedDoc); if(document != null) documents.Add(docId, document); } CloseReader(reader); } OccurrenceDictionary occurrences = new OccurrenceDictionary(mappings.Count); foreach(DumpedWordMapping map in mappings) { if(!occurrences.ContainsKey(documents[map.DocumentID])) { occurrences.Add(documents[map.DocumentID], new SortedBasicWordInfoSet(2)); } occurrences[documents[map.DocumentID]].Add(new BasicWordInfo( map.FirstCharIndex, map.WordIndex, WordLocation.GetInstance(map.Location))); } word = new Word((uint)wordId, text, occurrences); return true; } /// /// Clears the index. /// /// A state object passed from the index. private void ClearIndex(object state) { // state can be null, depending on when the method is called ICommandBuilder builder = GetCommandBuilder(); QueryBuilder queryBuilder = new QueryBuilder(builder); string query = queryBuilder.DeleteFrom("IndexWordMapping"); query = queryBuilder.AppendForBatch(query, queryBuilder.DeleteFrom("IndexWord")); query = queryBuilder.AppendForBatch(query, queryBuilder.DeleteFrom("IndexDocument")); DbCommand command = null; if(state == null) command = builder.GetCommand(connString, query, new List()); else command = builder.GetCommand((DbTransaction)state, query, new List()); ExecuteNonQuery(command, state == null); } /// /// Tokenizes page content. /// /// The content to tokenize. /// The tokenized words. private static WordInfo[] TokenizeContent(string content) { WordInfo[] words = SearchEngine.Tools.Tokenize(content); return words; } /// /// Indexes a page. /// /// The page content. /// The current transaction. /// The number of indexed words, including duplicates. private int IndexPage(PageContent content, DbTransaction transaction) { 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), transaction); 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; } /// /// Removes a page from the search engine index. /// /// The content of the page to remove. /// The current transaction. private void UnindexPage(PageContent content, DbTransaction transaction) { 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), transaction); } /// /// Indexes a message tree. /// /// The page. /// The root message. /// The current transaction. private void IndexMessageTree(PageInfo page, Message root, DbTransaction transaction) { IndexMessage(page, root.ID, root.Subject, root.DateTime, root.Body, transaction); foreach(Message reply in root.Replies) { IndexMessageTree(page, reply, transaction); } } /// /// Indexes a message. /// /// The page. /// The message ID. /// The subject. /// The date/time. /// The body. /// The current transaction. /// The number of indexed words, including duplicates. private int IndexMessage(PageInfo page, int id, string subject, DateTime dateTime, string body, DbTransaction transaction) { // 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), transaction); 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; } /// /// Removes a message tree from the search engine index. /// /// The page. /// The tree root. /// The current transaction. private void UnindexMessageTree(PageInfo page, Message root, DbTransaction transaction) { UnindexMessage(page, root.ID, root.Subject, root.DateTime, root.Body, transaction); foreach(Message reply in root.Replies) { UnindexMessageTree(page, reply, transaction); } } /// /// Removes a message from the search engine index. /// /// The page. /// The message ID. /// The subject. /// The date/time. /// The body. /// The current transaction. /// The number of indexed words, including duplicates. private void UnindexMessage(PageInfo page, int id, string subject, DateTime dateTime, string body, DbTransaction transaction) { // 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), transaction); } #endregion #region IPagesStorageProvider Members /// /// Gets a namespace. /// /// A database transaction. /// The name of the namespace (cannot be null or empty). /// The , or null if no namespace is found. private NamespaceInfo GetNamespace(DbTransaction transaction, string name) { ICommandBuilder builder = GetCommandBuilder(); QueryBuilder queryBuilder = new QueryBuilder(builder); // select ... from Namespace left join Page on Namespace.DefaultPage = Page.Name where Namespace.Name = and (Namespace.DefaultPage is null or Page.Namespace = ) string query = queryBuilder.SelectFrom("Namespace", "Page", "DefaultPage", "Name", Join.LeftJoin, new string[] { "Name", "DefaultPage" }, new string[] { "CreationDateTime" }); query = queryBuilder.Where(query, "Namespace", "Name", WhereOperator.Equals, "Name1"); query = queryBuilder.AndWhere(query, "Namespace", "DefaultPage", WhereOperator.IsNull, null, true, false); query = queryBuilder.OrWhere(query, "Page", "Namespace", WhereOperator.Equals, "Name2", false, true); List parameters = new List(2); parameters.Add(new Parameter(ParameterType.String, "Name1", name)); parameters.Add(new Parameter(ParameterType.String, "Name2", name)); DbCommand command = builder.GetCommand(transaction, query, parameters); DbDataReader reader = ExecuteReader(command); if(reader != null) { NamespaceInfo result = null; if(reader.Read()) { string realName = reader["Namespace_Name"] as string; string page = GetNullableColumn(reader, "Namespace_DefaultPage", null); PageInfo defaultPage = string.IsNullOrEmpty(page) ? null : new PageInfo(NameTools.GetFullName(realName, page), this, (DateTime)reader["Page_CreationDateTime"]); result = new NamespaceInfo(realName, this, defaultPage); } CloseReader(reader); return result; } else return null; } /// /// Gets a namespace. /// /// A database connection. /// The name of the namespace (cannot be null or empty). /// The , or null if no namespace is found. private NamespaceInfo GetNamespace(DbConnection connection, string name) { ICommandBuilder builder = GetCommandBuilder(); QueryBuilder queryBuilder = new QueryBuilder(builder); // select ... from Namespace left join Page on Namespace.DefaultPage = Page.Name where Namespace.Name = and (Namespace.DefaultPage is null or Page.Namespace = ) string query = queryBuilder.SelectFrom("Namespace", "Page", "DefaultPage", "Name", Join.LeftJoin, new string[] { "Name", "DefaultPage" }, new string[] { "CreationDateTime" }); query = queryBuilder.Where(query, "Namespace", "Name", WhereOperator.Equals, "Name1"); query = queryBuilder.AndWhere(query, "Namespace", "DefaultPage", WhereOperator.IsNull, null, true, false); query = queryBuilder.OrWhere(query, "Page", "Namespace", WhereOperator.Equals, "Name2", false, true); List parameters = new List(2); parameters.Add(new Parameter(ParameterType.String, "Name1", name)); parameters.Add(new Parameter(ParameterType.String, "Name2", name)); DbCommand command = builder.GetCommand(connection, query, parameters); DbDataReader reader = ExecuteReader(command); if(reader != null) { NamespaceInfo result = null; if(reader.Read()) { string realName = reader["Namespace_Name"] as string; string page = GetNullableColumn(reader, "Namespace_DefaultPage", null); PageInfo defaultPage = string.IsNullOrEmpty(page) ? null : new PageInfo(NameTools.GetFullName(realName, page), this, (DateTime)reader["Page_CreationDateTime"]); result = new NamespaceInfo(realName, this, defaultPage); } CloseReader(reader); return result; } else return null; } /// /// Gets a namespace. /// /// The name of the namespace (cannot be null or empty). /// The , or null if no namespace is found. /// If is null. /// If is empty. public NamespaceInfo GetNamespace(string name) { if(name == null) throw new ArgumentNullException("name"); if(name.Length == 0) throw new ArgumentException("Name cannot be empty"); ICommandBuilder builder = GetCommandBuilder(); DbConnection connection = builder.GetConnection(connString); NamespaceInfo nspace = GetNamespace(connection, name); CloseConnection(connection); return nspace; } /// /// Gets all the sub-namespaces. /// /// The sub-namespaces, sorted by name. public NamespaceInfo[] GetNamespaces() { ICommandBuilder builder = GetCommandBuilder(); QueryBuilder queryBuilder = new QueryBuilder(builder); // select ... from Namespace left join Page on Namespace.DefaultPage = Page.Name where Namespace.Name <> '' and (Namespace.DefaultPage is null or Page.Namespace <> '') string query = queryBuilder.SelectFrom("Namespace", "Page", "DefaultPage", "Name", Join.LeftJoin, new string[] { "Name", "DefaultPage" }, new string[] { "CreationDateTime" }); query = queryBuilder.Where(query, "Namespace", "Name", WhereOperator.NotEquals, "Empty1"); query = queryBuilder.AndWhere(query, "Namespace", "DefaultPage", WhereOperator.IsNull, null, true, false); query = queryBuilder.OrWhere(query, "Page", "Namespace", WhereOperator.NotEquals, "Empty2", false, true); query = queryBuilder.OrderBy(query, new[] { "Namespace_Name" }, new[] { Ordering.Asc }); List parameters = new List(2); parameters.Add(new Parameter(ParameterType.String, "Empty1", "")); parameters.Add(new Parameter(ParameterType.String, "Empty2", "")); DbCommand command = builder.GetCommand(connString, query, parameters); DbDataReader reader = ExecuteReader(command); if(reader != null) { List result = new List(10); while(reader.Read()) { string realName = reader["Namespace_Name"] as string; string page = GetNullableColumn(reader, "Namespace_DefaultPage", null); PageInfo defaultPage = string.IsNullOrEmpty(page) ? null : new PageInfo(NameTools.GetFullName(realName, page), this, (DateTime)reader["Page_CreationDateTime"]); // The query returns duplicate entries if the main page of two or more namespaces have the same name if(result.Find(n => { return n.Name.Equals(realName); }) == null) { result.Add(new NamespaceInfo(realName, this, defaultPage)); } } CloseReader(command, reader); return result.ToArray(); } else return null; } /// /// Adds a new namespace. /// /// The name of the namespace. /// The correct object. /// If is null. /// If is empty. public NamespaceInfo AddNamespace(string name) { if(name == null) throw new ArgumentNullException("name"); if(name.Length == 0) throw new ArgumentException("Name cannot be empty", "name"); ICommandBuilder builder = GetCommandBuilder(); QueryBuilder queryBuilder = new QueryBuilder(builder); string query = queryBuilder.InsertInto("Namespace", new string[] { "Name" }, new string[] { "Name" }); List parameters = new List(1); parameters.Add(new Parameter(ParameterType.String, "Name", name)); DbCommand command = builder.GetCommand(connString, query, parameters); int rows = ExecuteNonQuery(command); if(rows == 1) return new NamespaceInfo(name, this, null); else return null; } /// /// Renames a namespace. /// /// The namespace to rename. /// The new name of the namespace. /// The correct object. /// If or are null. /// If is empty. 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"); ICommandBuilder builder = GetCommandBuilder(); DbConnection connection = builder.GetConnection(connString); DbTransaction transaction = BeginTransaction(connection); if(GetNamespace(transaction, nspace.Name) == null) { RollbackTransaction(transaction); return null; } foreach(PageInfo page in GetPages(transaction, nspace)) { PageContent content = GetContent(transaction, page, CurrentRevision); if(content != null) { UnindexPage(content, transaction); } Message[] messages = GetMessages(transaction, page); if(messages != null) { foreach(Message msg in messages) { UnindexMessageTree(page, msg, transaction); } } } QueryBuilder queryBuilder = new QueryBuilder(builder); string query = queryBuilder.Update("Namespace", new string[] { "Name" }, new string[] { "NewName" }); query = queryBuilder.Where(query, "Name", WhereOperator.Equals, "OldName"); List parameters = new List(2); parameters.Add(new Parameter(ParameterType.String, "NewName", newName)); parameters.Add(new Parameter(ParameterType.String, "OldName", nspace.Name)); DbCommand command = builder.GetCommand(transaction, query, parameters); int rows = ExecuteNonQuery(command, false); if(rows > 0) { NamespaceInfo result = GetNamespace(transaction, newName); foreach(PageInfo page in GetPages(transaction, result)) { PageContent content = GetContent(transaction, page, CurrentRevision); if(content != null) { IndexPage(content, transaction); } Message[] messages = GetMessages(transaction, page); if(messages != null) { foreach(Message msg in messages) { IndexMessageTree(page, msg, transaction); } } } CommitTransaction(transaction); return result; } else { RollbackTransaction(transaction); return null; } } /// /// Sets the default page of a namespace. /// /// The namespace of which to set the default page. /// The page to use as default page, or null. /// The correct object. /// If is null. public NamespaceInfo SetNamespaceDefaultPage(NamespaceInfo nspace, PageInfo page) { if(nspace == null) throw new ArgumentNullException("nspace"); // Namespace existence is verified by the affected rows (should be 1) ICommandBuilder builder = GetCommandBuilder(); DbConnection connection = builder.GetConnection(connString); DbTransaction transaction = BeginTransaction(connection); if(page != null && GetPage(transaction, page.FullName) == null) { RollbackTransaction(transaction); return null; } QueryBuilder queryBuilder = new QueryBuilder(builder); string query = queryBuilder.Update("Namespace", new string[] { "DefaultPage" }, new string[] { "DefaultPage" }); query = queryBuilder.Where(query, "Name", WhereOperator.Equals, "Name"); List parameters = new List(2); if(page == null) parameters.Add(new Parameter(ParameterType.String, "DefaultPage", DBNull.Value)); else parameters.Add(new Parameter(ParameterType.String, "DefaultPage", NameTools.GetLocalName(page.FullName))); parameters.Add(new Parameter(ParameterType.String, "Name", nspace.Name)); DbCommand command = builder.GetCommand(transaction, query, parameters); int rows = ExecuteNonQuery(command, false); if(rows == 1) { CommitTransaction(transaction); return new NamespaceInfo(nspace.Name, this, page); } else { RollbackTransaction(transaction); return null; } } /// /// Removes a namespace. /// /// The namespace to remove. /// true if the namespace is removed, false otherwise. /// If is null. public bool RemoveNamespace(NamespaceInfo nspace) { if(nspace == null) throw new ArgumentNullException("nspace"); ICommandBuilder builder = GetCommandBuilder(); DbConnection connection = builder.GetConnection(connString); DbTransaction transaction = BeginTransaction(connection); foreach(PageInfo page in GetPages(transaction, nspace)) { PageContent content = GetContent(transaction, page, CurrentRevision); UnindexPage(content, transaction); foreach(Message msg in GetMessages(transaction, page)) { UnindexMessageTree(page, msg, transaction); } } QueryBuilder queryBuilder = new QueryBuilder(builder); string query = queryBuilder.DeleteFrom("Namespace"); query = queryBuilder.Where(query, "Name", WhereOperator.Equals, "Name"); List parameters = new List(1); parameters.Add(new Parameter(ParameterType.String, "Name", nspace.Name)); DbCommand command = builder.GetCommand(transaction, query, parameters); int rows = ExecuteNonQuery(command, false); if(rows > 0) CommitTransaction(transaction); else RollbackTransaction(transaction); return rows > 0; } /// /// Determines whether a page is the default page of its namespace. /// /// A database transaction. /// The page. /// true if the page is the default page, false otherwise. private bool IsDefaultPage(DbTransaction transaction, PageInfo page) { string nspaceName = NameTools.GetNamespace(page.FullName); if(string.IsNullOrEmpty(nspaceName)) return false; NamespaceInfo nspace = GetNamespace(transaction, nspaceName); if(nspace == null) return false; else { if(nspace.DefaultPage != null) return new PageNameComparer().Compare(nspace.DefaultPage, page) == 0; else return false; } } /// /// Moves a page from its namespace into another. /// /// The page to move. /// The destination namespace (null for the root). /// A value indicating whether to copy the page categories in the destination /// namespace, if not already available. /// The correct instance of . /// If is null. public PageInfo MovePage(PageInfo page, NamespaceInfo destination, bool copyCategories) { if(page == null) throw new ArgumentNullException("page"); // Check: // 1. Same namespace - ROOT, SUB (explicit check) // 2. Destination existence (update query affects 0 rows because it would break a FK) // 3. Page existence in target (update query affects 0 rows because it would break a FK) // 4. Page is default page of its namespace (explicit check) string destinationName = destination != null ? destination.Name : ""; string sourceName = null; string pageName = null; NameTools.ExpandFullName(page.FullName, out sourceName, out pageName); if(sourceName == null) sourceName = ""; ICommandBuilder builder = GetCommandBuilder(); DbConnection connection = builder.GetConnection(connString); DbTransaction transaction = BeginTransaction(connection); if(destinationName.ToLowerInvariant() == sourceName.ToLowerInvariant()) return null; if(IsDefaultPage(transaction, page)) { RollbackTransaction(transaction); return null; } PageContent currentContent = GetContent(transaction, page, CurrentRevision); if(currentContent != null) { UnindexPage(currentContent, transaction); foreach(Message msg in GetMessages(transaction, page)) { UnindexMessageTree(page, msg, transaction); } } CategoryInfo[] currCategories = GetCategories(transaction, sourceName == "" ? null : GetNamespace(transaction, sourceName)); // Remove bindings RebindPage(transaction, page, new string[0]); string[] newCategories = new string[0]; if(copyCategories) { // Retrieve categories for page // Copy missing ones in destination string lowerPageName = page.FullName.ToLowerInvariant(); List pageCategories = new List(10); foreach(CategoryInfo cat in currCategories) { if(Array.Find(cat.Pages, (s) => { return s.ToLowerInvariant() == lowerPageName; }) != null) { pageCategories.Add(NameTools.GetLocalName(cat.FullName)); } } // Create categories into destination without checking existence (AddCategory will return null) string tempName = destinationName == "" ? null : destinationName; newCategories = new string[pageCategories.Count]; for(int i = 0; i < pageCategories.Count; i++) { if(GetCategory(transaction, NameTools.GetFullName(tempName, pageCategories[i])) == null) { CategoryInfo added = AddCategory(tempName, pageCategories[i]); newCategories[i] = added.FullName; } } } QueryBuilder queryBuilder = new QueryBuilder(builder); string query = queryBuilder.Update("Page", new string[] { "Namespace" }, new string[] { "Destination" }); query = queryBuilder.Where(query, "Name", WhereOperator.Equals, "Name"); query = queryBuilder.AndWhere(query, "Namespace", WhereOperator.Equals, "Source"); List parameters = new List(3); parameters.Add(new Parameter(ParameterType.String, "Destination", destinationName)); parameters.Add(new Parameter(ParameterType.String, "Name", pageName)); parameters.Add(new Parameter(ParameterType.String, "Source", sourceName)); DbCommand command = builder.GetCommand(transaction, query, parameters); int rows = ExecuteNonQuery(command, false); if(rows > 0) { PageInfo result = new PageInfo(NameTools.GetFullName(destinationName, pageName), this, page.CreationDateTime); // Re-bind categories if(copyCategories) { bool rebound = RebindPage(transaction, result, newCategories); if(!rebound) { RollbackTransaction(transaction); return null; } } PageContent newContent = GetContent(transaction, result, CurrentRevision); IndexPage(newContent, transaction); foreach(Message msg in GetMessages(transaction, result)) { IndexMessageTree(result, msg, transaction); } CommitTransaction(transaction); return result; } else { RollbackTransaction(transaction); return null; } } /// /// Gets a category. /// /// A database transaction. /// The full name of the category. /// The , or null if no category is found. private CategoryInfo GetCategory(DbTransaction transaction, string fullName) { string nspace = null; string name = null; NameTools.ExpandFullName(fullName, out nspace, out name); if(nspace == null) nspace = ""; ICommandBuilder builder = GetCommandBuilder(); QueryBuilder queryBuilder = new QueryBuilder(builder); string query = queryBuilder.SelectFrom("Category", "CategoryBinding", new string[] { "Name", "Namespace" }, new string[] { "Category", "Namespace" }, Join.LeftJoin, new string[] { "Name", "Namespace" }, new string[] { "Page" }); query = queryBuilder.Where(query, "Category", "Namespace", WhereOperator.Equals, "Namespace"); query = queryBuilder.AndWhere(query, "Category", "Name", WhereOperator.Equals, "Name"); List parameters = new List(3); parameters.Add(new Parameter(ParameterType.String, "Namespace", nspace)); parameters.Add(new Parameter(ParameterType.String, "Name", name)); DbCommand command = builder.GetCommand(transaction, query, parameters); DbDataReader reader = ExecuteReader(command); if(reader != null) { CategoryInfo result = null; List pages = new List(50); while(reader.Read()) { if(result == null) result = new CategoryInfo(NameTools.GetFullName(reader["Category_Namespace"] as string, reader["Category_Name"] as string), this); if(!IsDBNull(reader, "CategoryBinding_Page")) { pages.Add(NameTools.GetFullName(reader["Category_Namespace"] as string, reader["CategoryBinding_Page"] as string)); } } CloseReader(reader); if(result != null) result.Pages = pages.ToArray(); return result; } else return null; } /// /// Gets a category. /// /// A database connection. /// The full name of the category. /// The , or null if no category is found. private CategoryInfo GetCategory(DbConnection connection, string fullName) { string nspace = null; string name = null; NameTools.ExpandFullName(fullName, out nspace, out name); if(nspace == null) nspace = ""; ICommandBuilder builder = GetCommandBuilder(); QueryBuilder queryBuilder = new QueryBuilder(builder); string query = queryBuilder.SelectFrom("Category", "CategoryBinding", new string[] { "Name", "Namespace" }, new string[] { "Category", "Namespace" }, Join.LeftJoin, new string[] { "Name", "Namespace" }, new string[] { "Page" }); query = queryBuilder.Where(query, "Category", "Namespace", WhereOperator.Equals, "Namespace"); query = queryBuilder.AndWhere(query, "Category", "Name", WhereOperator.Equals, "Name"); List parameters = new List(3); parameters.Add(new Parameter(ParameterType.String, "Namespace", nspace)); parameters.Add(new Parameter(ParameterType.String, "Name", name)); DbCommand command = builder.GetCommand(connection, query, parameters); DbDataReader reader = ExecuteReader(command); if(reader != null) { CategoryInfo result = null; List pages = new List(50); while(reader.Read()) { if(result == null) result = new CategoryInfo(NameTools.GetFullName(reader["Category_Namespace"] as string, reader["Category_Name"] as string), this); if(!IsDBNull(reader, "CategoryBinding_Page")) { pages.Add(NameTools.GetFullName(reader["Category_Namespace"] as string, reader["CategoryBinding_Page"] as string)); } } CloseReader(reader); if(result != null) result.Pages = pages.ToArray(); return result; } else return null; } /// /// Gets a category. /// /// The full name of the category. /// The , or null if no category is found. /// If is null. /// If is empty. 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"); ICommandBuilder builder = GetCommandBuilder(); DbConnection connection = builder.GetConnection(connString); CategoryInfo category = GetCategory(connection, fullName); CloseConnection(connection); return category; } /// /// Gets all the Categories in a namespace. /// /// A database transaction. /// The namespace. /// All the Categories in the namespace. The array is not sorted. private CategoryInfo[] GetCategories(DbTransaction transaction, NamespaceInfo nspace) { string nspaceName = nspace != null ? nspace.Name : ""; ICommandBuilder builder = GetCommandBuilder(); QueryBuilder queryBuilder = new QueryBuilder(builder); string query = queryBuilder.SelectFrom("Category", "CategoryBinding", new string[] { "Name", "Namespace" }, new string[] { "Category", "Namespace" }, Join.LeftJoin, new string[] { "Name", "Namespace" }, new string[] { "Page" }); query = queryBuilder.Where(query, "Category", "Namespace", WhereOperator.Equals, "Namespace"); query = queryBuilder.OrderBy(query, new string[] { "Category_Name", "CategoryBinding_Page" }, new Ordering[] { Ordering.Asc, Ordering.Asc }); List parameters = new List(1); parameters.Add(new Parameter(ParameterType.String, "Namespace", nspaceName)); DbCommand command = builder.GetCommand(transaction, query, parameters); DbDataReader reader = ExecuteReader(command); if(reader != null) { List result = new List(20); List pages = new List(50); string prevName = "|||"; string name = null; while(reader.Read()) { name = reader["Category_Name"] as string; if(name != prevName) { if(prevName != "|||") { result[result.Count - 1].Pages = pages.ToArray(); pages.Clear(); } result.Add(new CategoryInfo(NameTools.GetFullName(reader["Category_Namespace"] as string, name), this)); } prevName = name; if(!IsDBNull(reader, "CategoryBinding_Page")) { pages.Add(NameTools.GetFullName(reader["Category_Namespace"] as string, reader["CategoryBinding_Page"] as string)); } } CloseReader(reader); if(result.Count > 0) result[result.Count - 1].Pages = pages.ToArray(); return result.ToArray(); } else return null; } /// /// Gets all the Categories in a namespace. /// /// A database connection. /// The namespace. /// All the Categories in the namespace. The array is not sorted. private CategoryInfo[] GetCategories(DbConnection connection, NamespaceInfo nspace) { string nspaceName = nspace != null ? nspace.Name : ""; ICommandBuilder builder = GetCommandBuilder(); QueryBuilder queryBuilder = new QueryBuilder(builder); string query = queryBuilder.SelectFrom("Category", "CategoryBinding", new string[] { "Name", "Namespace" }, new string[] { "Category", "Namespace" }, Join.LeftJoin, new string[] { "Name", "Namespace" }, new string[] { "Page" }); query = queryBuilder.Where(query, "Category", "Namespace", WhereOperator.Equals, "Namespace"); query = queryBuilder.OrderBy(query, new string[] { "Category_Name", "CategoryBinding_Page" }, new Ordering[] { Ordering.Asc, Ordering.Asc }); List parameters = new List(1); parameters.Add(new Parameter(ParameterType.String, "Namespace", nspaceName)); DbCommand command = builder.GetCommand(connection, query, parameters); DbDataReader reader = ExecuteReader(command); if(reader != null) { List result = new List(20); List pages = new List(50); string prevName = "|||"; string name = null; while(reader.Read()) { name = reader["Category_Name"] as string; if(name != prevName) { if(prevName != "|||") { result[result.Count - 1].Pages = pages.ToArray(); pages.Clear(); } result.Add(new CategoryInfo(NameTools.GetFullName(reader["Category_Namespace"] as string, name), this)); } prevName = name; if(!IsDBNull(reader, "CategoryBinding_Page")) { pages.Add(NameTools.GetFullName(reader["Category_Namespace"] as string, reader["CategoryBinding_Page"] as string)); } } CloseReader(reader); if(result.Count > 0) result[result.Count - 1].Pages = pages.ToArray(); return result.ToArray(); } else return null; } /// /// Gets all the Categories in a namespace. /// /// The namespace. /// All the Categories in the namespace, sorted by name. public CategoryInfo[] GetCategories(NamespaceInfo nspace) { ICommandBuilder builder = GetCommandBuilder(); DbConnection connection = builder.GetConnection(connString); CategoryInfo[] categories = GetCategories(connection, nspace); CloseConnection(connection); return categories; } /// /// Gets all the categories of a page. /// /// The page. /// The categories, sorted by name. /// If is null. public CategoryInfo[] GetCategoriesForPage(PageInfo page) { if(page == null) throw new ArgumentNullException("page"); ICommandBuilder builder = GetCommandBuilder(); QueryBuilder queryBuilder = new QueryBuilder(builder); string nspace, pageName; NameTools.ExpandFullName(page.FullName, out nspace, out pageName); if(nspace == null) nspace = ""; string query = queryBuilder.SelectFrom("Category", "CategoryBinding", new string[] { "Name", "Namespace" }, new string[] { "Category", "Namespace" }, Join.LeftJoin, new string[] { "Name", "Namespace" }, new string[] { "Page" }); query = queryBuilder.Where(query, "CategoryBinding", "Namespace", WhereOperator.Equals, "Namespace"); query = queryBuilder.AndWhere(query, "CategoryBinding", "Page", WhereOperator.Equals, "Page"); query = queryBuilder.OrderBy(query, new[] { "Category_Name" }, new[] { Ordering.Asc }); List parameters = new List(2); parameters.Add(new Parameter(ParameterType.String, "Namespace", nspace)); parameters.Add(new Parameter(ParameterType.String, "Page", pageName)); DbCommand command = builder.GetCommand(connString, query, parameters); DbDataReader reader = ExecuteReader(command); if(reader != null) { List result = new List(20); List pages = new List(50); string prevName = "|||"; string name = null; while(reader.Read()) { name = reader["Category_Name"] as string; if(name != prevName) { if(prevName != "|||") { result[result.Count - 1].Pages = pages.ToArray(); pages.Clear(); } result.Add(new CategoryInfo(NameTools.GetFullName(reader["Category_Namespace"] as string, name), this)); } prevName = name; if(!IsDBNull(reader, "CategoryBinding_Page")) { pages.Add(NameTools.GetFullName(reader["Category_Namespace"] as string, reader["CategoryBinding_Page"] as string)); } } CloseReader(command, reader); if(result.Count > 0) result[result.Count - 1].Pages = pages.ToArray(); return result.ToArray(); } else return null; } /// /// Adds a Category. /// /// The target namespace (null for the root). /// The Category name. /// The correct CategoryInfo object. /// The method should set category's Pages to an empty array. /// If is null. /// If is empty. 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"); if(nspace == null) nspace = ""; ICommandBuilder builder = GetCommandBuilder(); string query = QueryBuilder.NewQuery(builder).InsertInto("Category", new string[] { "Name", "Namespace" }, new string[] { "Name", "Namespace" }); List parameters = new List(2); parameters.Add(new Parameter(ParameterType.String, "Name", name)); parameters.Add(new Parameter(ParameterType.String, "Namespace", nspace)); DbCommand command = builder.GetCommand(connString, query, parameters); int rows = ExecuteNonQuery(command); if(rows == 1) return new CategoryInfo(NameTools.GetFullName(nspace, name), this); else return null; } /// /// Renames a Category. /// /// The Category to rename. /// The new Name. /// The correct CategoryInfo object. /// If or are null. /// If is empty. 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"); string nspace = null; string name = null; NameTools.ExpandFullName(category.FullName, out nspace, out name); if(nspace == null) nspace = ""; ICommandBuilder builder = GetCommandBuilder(); DbConnection connection = builder.GetConnection(connString); DbTransaction transaction = BeginTransaction(connection); QueryBuilder queryBuilder = new QueryBuilder(builder); string query = queryBuilder.Update("Category", new string[] { "Name" }, new string[] { "NewName" }); query = queryBuilder.Where(query, "Name", WhereOperator.Equals, "OldName"); query = queryBuilder.AndWhere(query, "Namespace", WhereOperator.Equals, "Namespace"); List parameters = new List(3); parameters.Add(new Parameter(ParameterType.String, "NewName", newName)); parameters.Add(new Parameter(ParameterType.String, "OldName", name)); parameters.Add(new Parameter(ParameterType.String, "Namespace", nspace)); DbCommand command = builder.GetCommand(transaction, query, parameters); int rows = ExecuteNonQuery(command, false); if(rows > 0) { CategoryInfo result = GetCategory(transaction, NameTools.GetFullName(nspace, newName)); CommitTransaction(transaction); return result; } else { RollbackTransaction(transaction); return null; } } /// /// Removes a Category. /// /// A database transaction. /// The Category to remove. /// True if the Category has been removed successfully. private bool RemoveCategory(DbTransaction transaction, CategoryInfo category) { string nspace = null; string name = null; NameTools.ExpandFullName(category.FullName, out nspace, out name); if(nspace == null) nspace = ""; ICommandBuilder builder = GetCommandBuilder(); QueryBuilder queryBuilder = new QueryBuilder(builder); string query = queryBuilder.DeleteFrom("Category"); query = queryBuilder.Where(query, "Name", WhereOperator.Equals, "Name"); query = queryBuilder.AndWhere(query, "Namespace", WhereOperator.Equals, "Namespace"); List parameters = new List(2); parameters.Add(new Parameter(ParameterType.String, "Name", name)); parameters.Add(new Parameter(ParameterType.String, "Namespace", nspace)); DbCommand command = builder.GetCommand(transaction, query, parameters); int rows = ExecuteNonQuery(command, false); return rows > 0; } /// /// Removes a Category. /// /// A database connection. /// The Category to remove. /// True if the Category has been removed successfully. private bool RemoveCategory(DbConnection connection, CategoryInfo category) { string nspace = null; string name = null; NameTools.ExpandFullName(category.FullName, out nspace, out name); if(nspace == null) nspace = ""; ICommandBuilder builder = GetCommandBuilder(); QueryBuilder queryBuilder = new QueryBuilder(builder); string query = queryBuilder.DeleteFrom("Category"); query = queryBuilder.Where(query, "Name", WhereOperator.Equals, "Name"); query = queryBuilder.AndWhere(query, "Namespace", WhereOperator.Equals, "Namespace"); List parameters = new List(2); parameters.Add(new Parameter(ParameterType.String, "Name", name)); parameters.Add(new Parameter(ParameterType.String, "Namespace", nspace)); DbCommand command = builder.GetCommand(connection, query, parameters); int rows = ExecuteNonQuery(command, false); return rows > 0; } /// /// Removes a Category. /// /// The Category to remove. /// True if the Category has been removed successfully. /// If is null. public bool RemoveCategory(CategoryInfo category) { if(category == null) throw new ArgumentNullException("category"); ICommandBuilder builder = GetCommandBuilder(); DbConnection connection = builder.GetConnection(connString); bool removed = RemoveCategory(connection, category); CloseConnection(connection); return removed; } /// /// Merges two Categories. /// /// The source Category. /// The destination Category. /// The correct object. /// The destination Category remains, while the source Category is deleted, and all its Pages re-bound /// in the destination Category. /// If or are null. public CategoryInfo MergeCategories(CategoryInfo source, CategoryInfo destination) { if(source == null) throw new ArgumentNullException("source"); if(destination == null) throw new ArgumentNullException("destination"); // 1. Check for same namespace // 2. Load all pages in source // 3. Load all pages in destination // 4. Merge lists in memory // 5. Delete all destination bindings // 6. Delete source cat // 7. Insert new bindings stored in memory string sourceNs = NameTools.GetNamespace(source.FullName); string destinationNs = NameTools.GetNamespace(destination.FullName); // If one is null and the other not null, fail if(sourceNs == null && destinationNs != null || sourceNs != null && destinationNs == null) return null; else { // Both non-null or both null if(sourceNs != null) { // Both non-null, check names NamespaceInfo tempSource = new NamespaceInfo(sourceNs, this, null); NamespaceInfo tempDest = new NamespaceInfo(destinationNs, this, null); // Different names, fail if(new NamespaceComparer().Compare(tempSource, tempDest) != 0) return null; } // else both null, OK } string nspace = sourceNs != null ? sourceNs : ""; ICommandBuilder builder = GetCommandBuilder(); DbConnection connection = builder.GetConnection(connString); DbTransaction transaction = BeginTransaction(connection); CategoryInfo actualSource = GetCategory(transaction, source.FullName); CategoryInfo actualDestination = GetCategory(transaction, destination.FullName); if(actualSource == null) { RollbackTransaction(transaction); return null; } if(actualDestination == null) { RollbackTransaction(transaction); return null; } string destinationName = NameTools.GetLocalName(actualDestination.FullName); string[] mergedPages = MergeArrays(actualSource.Pages, actualDestination.Pages); QueryBuilder queryBuilder = new QueryBuilder(builder); string query = queryBuilder.DeleteFrom("CategoryBinding"); query = queryBuilder.Where(query, "Namespace", WhereOperator.Equals, "Namespace"); query = queryBuilder.AndWhere(query, "Category", WhereOperator.Equals, "Category"); List parameters = new List(2); parameters.Add(new Parameter(ParameterType.String, "Namespace", nspace)); parameters.Add(new Parameter(ParameterType.String, "Category", destinationName)); DbCommand command = builder.GetCommand(transaction, query, parameters); int rows = ExecuteNonQuery(command, false); if(rows == -1) { RollbackTransaction(transaction); return null; } if(!RemoveCategory(transaction, source)) { RollbackTransaction(transaction); return null; } string finalQuery = ""; parameters = new List(MaxStatementsInBatch * 3); rows = 0; int count = 1; string countString; foreach(string page in mergedPages) { // This batch is executed in small chunks (MaxStatementsInBatch) to avoid exceeding DB's max batch length/size countString = count.ToString(); query = queryBuilder.InsertInto("CategoryBinding", new string[] { "Namespace", "Category", "Page" }, new string[] { "Namespace" + countString, "Category" + countString, "Page" + countString }); finalQuery = queryBuilder.AppendForBatch(finalQuery, query); parameters.Add(new Parameter(ParameterType.String, "Namespace" + countString, nspace)); parameters.Add(new Parameter(ParameterType.String, "Category" + countString, destinationName)); parameters.Add(new Parameter(ParameterType.String, "Page" + countString, NameTools.GetLocalName(page))); count++; if(count == MaxStatementsInBatch) { // Batch is complete -> execute command = builder.GetCommand(transaction, finalQuery, parameters); rows += ExecuteNonQuery(command, false); count = 1; finalQuery = ""; parameters.Clear(); } } if(finalQuery.Length > 0) { // Execute remaining queries, if any command = builder.GetCommand(transaction, finalQuery, parameters); rows += ExecuteNonQuery(command, false); } if(rows == mergedPages.Length) { CommitTransaction(transaction); CategoryInfo result = new CategoryInfo(actualDestination.FullName, this); result.Pages = mergedPages; return result; } else { RollbackTransaction(transaction); return null; } } /// /// Merges two arrays of strings. /// /// The first array. /// The second array. /// The merged array. private static string[] MergeArrays(string[] array1, string[] array2) { List result = new List(array1.Length + array2.Length); // A) BinarySearch is O(log n), but Insert is O(n) (+ QuickSort which is O(n*log n)) // B) A linear search is O(n), and Add is O(1) (given that the list is already big enough) // --> B is faster, even when result approaches a size of array1.Length + array2.Length StringComparer comp = StringComparer.OrdinalIgnoreCase; result.AddRange(array1); foreach(string value in array2) { if(result.Find(x => { return comp.Compare(x, value) == 0; }) == null) { result.Add(value); } } return result.ToArray(); } /// /// Gets a page. /// /// A database transaction. /// The full name of the page. /// The , or null if no page is found. private PageInfo GetPage(DbTransaction transaction, string fullName) { string nspace, name; NameTools.ExpandFullName(fullName, out nspace, out name); if(nspace == null) nspace = ""; ICommandBuilder builder = GetCommandBuilder(); QueryBuilder queryBuilder = new QueryBuilder(builder); string query = queryBuilder.SelectFrom("Page"); query = queryBuilder.Where(query, "Name", WhereOperator.Equals, "Name"); query = queryBuilder.AndWhere(query, "Namespace", WhereOperator.Equals, "Namespace"); List parameters = new List(2); parameters.Add(new Parameter(ParameterType.String, "Name", name)); parameters.Add(new Parameter(ParameterType.String, "Namespace", nspace)); DbCommand command = builder.GetCommand(transaction, query, parameters); DbDataReader reader = ExecuteReader(command); if(reader != null) { PageInfo result = null; if(reader.Read()) { result = new PageInfo(NameTools.GetFullName(reader["Namespace"] as string, reader["Name"] as string), this, (DateTime)reader["CreationDateTime"]); } CloseReader(reader); return result; } else return null; } /// /// Gets a page. /// /// A database connection. /// The full name of the page. /// The , or null if no page is found. private PageInfo GetPage(DbConnection connection, string fullName) { string nspace, name; NameTools.ExpandFullName(fullName, out nspace, out name); if(nspace == null) nspace = ""; ICommandBuilder builder = GetCommandBuilder(); QueryBuilder queryBuilder = new QueryBuilder(builder); string query = queryBuilder.SelectFrom("Page"); query = queryBuilder.Where(query, "Name", WhereOperator.Equals, "Name"); query = queryBuilder.AndWhere(query, "Namespace", WhereOperator.Equals, "Namespace"); List parameters = new List(2); parameters.Add(new Parameter(ParameterType.String, "Name", name)); parameters.Add(new Parameter(ParameterType.String, "Namespace", nspace)); DbCommand command = builder.GetCommand(connection, query, parameters); DbDataReader reader = ExecuteReader(command); if(reader != null) { PageInfo result = null; if(reader.Read()) { result = new PageInfo(NameTools.GetFullName(reader["Namespace"] as string, reader["Name"] as string), this, (DateTime)reader["CreationDateTime"]); } CloseReader(reader); return result; } else return null; } /// /// Gets a page. /// /// The full name of the page. /// The , or null if no page is found. /// If is null. /// If is empty. 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"); ICommandBuilder builder = GetCommandBuilder(); DbConnection connection = builder.GetConnection(connString); PageInfo page = GetPage(connection, fullName); CloseConnection(connection); return page; } /// /// Gets all the Pages in a namespace. /// /// A database transaction. /// The namespace (null for the root). /// All the Pages in the namespace. The array is not sorted. private PageInfo[] GetPages(DbTransaction transaction, NamespaceInfo nspace) { string nspaceName = nspace != null ? nspace.Name : ""; ICommandBuilder builder = GetCommandBuilder(); QueryBuilder queryBuilder = new QueryBuilder(builder); string query = queryBuilder.SelectFrom("Page"); query = queryBuilder.Where(query, "Namespace", WhereOperator.Equals, "Namespace"); query = queryBuilder.OrderBy(query, new[] { "Name" }, new[] { Ordering.Asc }); List parameters = new List(1); parameters.Add(new Parameter(ParameterType.String, "Namespace", nspaceName)); DbCommand command = builder.GetCommand(transaction, query, parameters); DbDataReader reader = ExecuteReader(command); if(reader != null) { List result = new List(100); while(reader.Read()) { result.Add(new PageInfo(NameTools.GetFullName(reader["Namespace"] as string, reader["Name"] as string), this, (DateTime)reader["CreationDateTime"])); } CloseReader(reader); return result.ToArray(); } else return null; } /// /// Gets all the Pages in a namespace. /// /// A database connection. /// The namespace (null for the root). /// All the Pages in the namespace. The array is not sorted. private PageInfo[] GetPages(DbConnection connection, NamespaceInfo nspace) { string nspaceName = nspace != null ? nspace.Name : ""; ICommandBuilder builder = GetCommandBuilder(); QueryBuilder queryBuilder = new QueryBuilder(builder); string query = queryBuilder.SelectFrom("Page"); query = queryBuilder.Where(query, "Namespace", WhereOperator.Equals, "Namespace"); List parameters = new List(1); parameters.Add(new Parameter(ParameterType.String, "Namespace", nspaceName)); query = queryBuilder.OrderBy(query, new[] { "Name" }, new[] { Ordering.Asc }); DbCommand command = builder.GetCommand(connection, query, parameters); DbDataReader reader = ExecuteReader(command); if(reader != null) { List result = new List(100); while(reader.Read()) { result.Add(new PageInfo(NameTools.GetFullName(reader["Namespace"] as string, reader["Name"] as string), this, (DateTime)reader["CreationDateTime"])); } CloseReader(reader); return result.ToArray(); } else return null; } /// /// Gets all the Pages in a namespace. /// /// The namespace (null for the root). /// All the Pages in the namespace. The array is not sorted. public PageInfo[] GetPages(NamespaceInfo nspace) { ICommandBuilder builder = GetCommandBuilder(); DbConnection connection = builder.GetConnection(connString); PageInfo[] pages = GetPages(connection, nspace); CloseConnection(connection); return pages; } /// /// Gets all the pages in a namespace that are bound to zero categories. /// /// The namespace (null for the root). /// The pages, sorted by name. public PageInfo[] GetUncategorizedPages(NamespaceInfo nspace) { string nspaceName = nspace != null ? nspace.Name : ""; ICommandBuilder builder = GetCommandBuilder(); QueryBuilder queryBuilder = new QueryBuilder(builder); string query = queryBuilder.SelectFrom("Page", "CategoryBinding", "Name", "Page", Join.LeftJoin); query = queryBuilder.Where(query, "CategoryBinding", "Category", WhereOperator.IsNull, null); query = queryBuilder.AndWhere(query, "Page", "Namespace", WhereOperator.Equals, "Namespace"); query = queryBuilder.OrderBy(query, new[] { "Name" }, new[] { Ordering.Asc }); List parameters = new List(1); parameters.Add(new Parameter(ParameterType.String, "Namespace", nspaceName)); DbCommand command = builder.GetCommand(connString, query, parameters); DbDataReader reader = ExecuteReader(command); if(reader != null) { List result = new List(100); while(reader.Read()) { result.Add(new PageInfo(NameTools.GetFullName(reader["Namespace"] as string, reader["Name"] as string), this, (DateTime)reader["CreationDateTime"])); } CloseReader(command, reader); return result.ToArray(); } else return null; } /// /// Gets the content of a specific revision of a page. /// /// A database connection. /// The page. /// The revision. /// The content. private PageContent GetContent(DbConnection connection, PageInfo page, int revision) { // Internal version to work with GetContent, GetBackupContent, GetDraft ICommandBuilder builder = GetCommandBuilder(); QueryBuilder queryBuilder = new QueryBuilder(builder); string name, nspace; NameTools.ExpandFullName(page.FullName, out nspace, out name); if(nspace == null) nspace = ""; string query = queryBuilder.SelectFrom("PageContent", "PageKeyword", new string[] { "Page", "Namespace", "Revision" }, new string[] { "Page", "Namespace", "Revision" }, Join.LeftJoin, new string [] { "Title", "User", "LastModified", "Comment", "Content", "Description" }, new string[] { "Keyword" }); query = queryBuilder.Where(query, "PageContent", "Page", WhereOperator.Equals, "Page"); query = queryBuilder.AndWhere(query, "PageContent", "Namespace", WhereOperator.Equals, "Namespace"); query = queryBuilder.AndWhere(query, "PageContent", "Revision", WhereOperator.Equals, "Revision"); List parameters = new List(3); parameters.Add(new Parameter(ParameterType.String, "Page", name)); parameters.Add(new Parameter(ParameterType.String, "Namespace", nspace)); parameters.Add(new Parameter(ParameterType.Int16, "Revision", (short)revision)); DbCommand command = builder.GetCommand(connection, query, parameters); DbDataReader reader = ExecuteReader(command); if(reader != null) { PageContent result = null; string title = null, user = null, comment = null, content = null, description = null; DateTime dateTime = DateTime.MinValue; List keywords = new List(10); while(reader.Read()) { if(title == null) { title = reader["PageContent_Title"] as string; user = reader["PageContent_User"] as string; dateTime = (DateTime)reader["PageContent_LastModified"]; comment = GetNullableColumn(reader, "PageContent_Comment", ""); content = reader["PageContent_Content"] as string; description = GetNullableColumn(reader, "PageContent_Description", null); } if(!IsDBNull(reader, "PageKeyword_Keyword")) { keywords.Add(reader["PageKeyword_Keyword"] as string); } } if(title != null) { result = new PageContent(page, title, user, dateTime, comment, content, keywords.ToArray(), description); } CloseReader(reader); return result; } else return null; } /// /// Gets the content of a specific revision of a page. /// /// A database transaction. /// The page. /// The revision. /// The content. private PageContent GetContent(DbTransaction transaction, PageInfo page, int revision) { // Internal version to work with GetContent, GetBackupContent, GetDraft ICommandBuilder builder = GetCommandBuilder(); QueryBuilder queryBuilder = new QueryBuilder(builder); string name, nspace; NameTools.ExpandFullName(page.FullName, out nspace, out name); if(nspace == null) nspace = ""; string query = queryBuilder.SelectFrom("PageContent", "PageKeyword", new string[] { "Page", "Namespace", "Revision" }, new string[] { "Page", "Namespace", "Revision" }, Join.LeftJoin, new string[] { "Title", "User", "LastModified", "Comment", "Content", "Description" }, new string[] { "Keyword" }); query = queryBuilder.Where(query, "PageContent", "Page", WhereOperator.Equals, "Page"); query = queryBuilder.AndWhere(query, "PageContent", "Namespace", WhereOperator.Equals, "Namespace"); query = queryBuilder.AndWhere(query, "PageContent", "Revision", WhereOperator.Equals, "Revision"); List parameters = new List(3); parameters.Add(new Parameter(ParameterType.String, "Page", name)); parameters.Add(new Parameter(ParameterType.String, "Namespace", nspace)); parameters.Add(new Parameter(ParameterType.Int16, "Revision", (short)revision)); DbCommand command = builder.GetCommand(transaction, query, parameters); DbDataReader reader = ExecuteReader(command); if(reader != null) { PageContent result = null; string title = null, user = null, comment = null, content = null, description = null; DateTime dateTime = DateTime.MinValue; List keywords = new List(10); while(reader.Read()) { if(title == null) { title = reader["PageContent_Title"] as string; user = reader["PageContent_User"] as string; dateTime = (DateTime)reader["PageContent_LastModified"]; comment = GetNullableColumn(reader, "PageContent_Comment", ""); content = reader["PageContent_Content"] as string; description = GetNullableColumn(reader, "PageContent_Description", null); } if(!IsDBNull(reader, "PageKeyword_Keyword")) { keywords.Add(reader["PageKeyword_Keyword"] as string); } } if(title != null) { result = new PageContent(page, title, user, dateTime, comment, content, keywords.ToArray(), description); } CloseReader(reader); return result; } else return null; } /// /// Gets the Content of a Page. /// /// The Page. /// The Page Content object, null if the page does not exist or is null, /// or an empty instance if the content could not be retrieved (). public PageContent GetContent(PageInfo page) { if(page == null) return null; ICommandBuilder builder = GetCommandBuilder(); DbConnection connection = builder.GetConnection(connString); PageContent content = GetContent(connection, page, CurrentRevision); CloseConnection(connection); return content; } /// /// Gets the content of a draft of a Page. /// /// The Page. /// The draft, or null if no draft exists. /// If is null. public PageContent GetDraft(PageInfo page) { if(page == null) throw new ArgumentNullException("page"); ICommandBuilder builder = GetCommandBuilder(); DbConnection connection = builder.GetConnection(connString); PageContent content = GetContent(connection, page, DraftRevision); CloseConnection(connection); return content; } /// /// Deletes a draft of a Page. /// /// The page. /// true if the draft is deleted, false otherwise. /// If is null. public bool DeleteDraft(PageInfo page) { if(page == null) throw new ArgumentNullException("page"); ICommandBuilder builder = GetCommandBuilder(); DbConnection connection = builder.GetConnection(connString); bool deleted = DeleteContent(connection, page, DraftRevision); CloseConnection(connection); return deleted; } /// /// Gets the Backup/Revision numbers of a Page. /// /// A database transaction. /// The Page to get the Backups of. /// The Backup/Revision numbers. private int[] GetBackups(DbTransaction transaction, PageInfo page) { if(GetPage(transaction, page.FullName) == null) { return null; } string name, nspace; NameTools.ExpandFullName(page.FullName, out nspace, out name); if(nspace == null) nspace = ""; ICommandBuilder builder = GetCommandBuilder(); QueryBuilder queryBuilder = new QueryBuilder(builder); string query = queryBuilder.SelectFrom("PageContent", new string[] { "Revision" }); query = queryBuilder.Where(query, "Page", WhereOperator.Equals, "Page"); query = queryBuilder.AndWhere(query, "Namespace", WhereOperator.Equals, "Namespace"); query = queryBuilder.AndWhere(query, "Revision", WhereOperator.GreaterThanOrEqualTo, "Revision"); query = queryBuilder.OrderBy(query, new[] { "Revision" }, new[] { Ordering.Asc }); List parameters = new List(3); parameters.Add(new Parameter(ParameterType.String, "Page", name)); parameters.Add(new Parameter(ParameterType.String, "Namespace", nspace)); parameters.Add(new Parameter(ParameterType.Int16, "Revision", FirstRevision)); DbCommand command = builder.GetCommand(transaction, query, parameters); DbDataReader reader = ExecuteReader(command); if(reader != null) { List result = new List(100); while(reader.Read()) { result.Add((short)reader["Revision"]); } CloseReader(reader); return result.ToArray(); } else return null; } /// /// Gets the Backup/Revision numbers of a Page. /// /// A database connection. /// The Page to get the Backups of. /// The Backup/Revision numbers. private int[] GetBackups(DbConnection connection, PageInfo page) { if(GetPage(connection, page.FullName) == null) { return null; } string name, nspace; NameTools.ExpandFullName(page.FullName, out nspace, out name); if(nspace == null) nspace = ""; ICommandBuilder builder = GetCommandBuilder(); QueryBuilder queryBuilder = new QueryBuilder(builder); string query = queryBuilder.SelectFrom("PageContent", new string[] { "Revision" }); query = queryBuilder.Where(query, "Page", WhereOperator.Equals, "Page"); query = queryBuilder.AndWhere(query, "Namespace", WhereOperator.Equals, "Namespace"); query = queryBuilder.AndWhere(query, "Revision", WhereOperator.GreaterThanOrEqualTo, "Revision"); query = queryBuilder.OrderBy(query, new[] { "Revision" }, new[] { Ordering.Asc }); List parameters = new List(3); parameters.Add(new Parameter(ParameterType.String, "Page", name)); parameters.Add(new Parameter(ParameterType.String, "Namespace", nspace)); parameters.Add(new Parameter(ParameterType.Int16, "Revision", FirstRevision)); DbCommand command = builder.GetCommand(connection, query, parameters); DbDataReader reader = ExecuteReader(command); if(reader != null) { List result = new List(100); while(reader.Read()) { result.Add((short)reader["Revision"]); } CloseReader(reader); return result.ToArray(); } else return null; } /// /// Gets the Backup/Revision numbers of a Page. /// /// The Page to get the Backups of. /// The Backup/Revision numbers. /// If is null. public int[] GetBackups(PageInfo page) { if(page == null) throw new ArgumentNullException("page"); ICommandBuilder builder = GetCommandBuilder(); DbConnection connection = builder.GetConnection(connString); int[] revisions = GetBackups(connection, page); CloseConnection(connection); return revisions; } /// /// Gets the Content of a Backup of a Page. /// /// The Page to get the backup of. /// The Backup/Revision number. /// The Page Backup. /// If is null. /// If is less than zero. public PageContent GetBackupContent(PageInfo page, int revision) { if(page == null) throw new ArgumentNullException("page"); if(revision < 0) throw new ArgumentOutOfRangeException("revision", "Invalid Revision"); ICommandBuilder builder = GetCommandBuilder(); DbConnection connection = builder.GetConnection(connString); PageContent content = GetContent(connection, page, revision); CloseConnection(connection); return content; } /// /// Stores the content for a revision. /// /// A database transaction. /// The content. /// The revision. /// true if the content is stored, false otherwise. private bool SetContent(DbTransaction transaction, PageContent content, int revision) { string name, nspace; NameTools.ExpandFullName(content.PageInfo.FullName, out nspace, out name); if(nspace == null) nspace = ""; ICommandBuilder builder = GetCommandBuilder(); QueryBuilder queryBuilder = new QueryBuilder(builder); string query = queryBuilder.InsertInto("PageContent", new string[] { "Page", "Namespace", "Revision", "Title", "User", "LastModified", "Comment", "Content", "Description" }, new string[] { "Page", "Namespace", "Revision", "Title", "User", "LastModified", "Comment", "Content", "Description" }); List parameters = new List(9); parameters.Add(new Parameter(ParameterType.String, "Page", name)); parameters.Add(new Parameter(ParameterType.String, "Namespace", nspace)); parameters.Add(new Parameter(ParameterType.Int16, "Revision", revision)); parameters.Add(new Parameter(ParameterType.String, "Title", content.Title)); parameters.Add(new Parameter(ParameterType.String, "User", content.User)); parameters.Add(new Parameter(ParameterType.DateTime, "LastModified", content.LastModified)); if(!string.IsNullOrEmpty(content.Comment)) parameters.Add(new Parameter(ParameterType.String, "Comment", content.Comment)); else parameters.Add(new Parameter(ParameterType.String, "Comment", DBNull.Value)); parameters.Add(new Parameter(ParameterType.String, "Content", content.Content)); if(!string.IsNullOrEmpty(content.Description)) parameters.Add(new Parameter(ParameterType.String, "Description", content.Description)); else parameters.Add(new Parameter(ParameterType.String, "Description", DBNull.Value)); DbCommand command = builder.GetCommand(transaction, query, parameters); int rows = ExecuteNonQuery(command, false); if(rows != 1) return false; if(content.Keywords.Length > 0) { parameters = new List(content.Keywords.Length * 4); string fullQuery = ""; int count = 0; string countString; foreach(string kw in content.Keywords) { countString = count.ToString(); query = queryBuilder.InsertInto("PageKeyword", new string[] { "Page", "Namespace", "Revision", "Keyword" }, new string[] { "Page" + countString, "Namespace" + countString, "Revision" + countString, "Keyword" + countString }); fullQuery = queryBuilder.AppendForBatch(fullQuery, query); parameters.Add(new Parameter(ParameterType.String, "Page" + countString, name)); parameters.Add(new Parameter(ParameterType.String, "Namespace" + countString, nspace)); parameters.Add(new Parameter(ParameterType.Int16, "Revision" + countString, revision)); parameters.Add(new Parameter(ParameterType.String, "Keyword" + countString, kw)); count++; } command = builder.GetCommand(transaction, fullQuery, parameters); rows = ExecuteNonQuery(command, false); return rows == content.Keywords.Length; } else return true; } /// /// Deletes a revision of a page content. /// /// A database transaction. /// The page. /// The revision. /// true if the content ir deleted, false otherwise. private bool DeleteContent(DbTransaction transaction, PageInfo page, int revision) { string name, nspace; NameTools.ExpandFullName(page.FullName, out nspace, out name); if(nspace == null) nspace = ""; ICommandBuilder builder = GetCommandBuilder(); QueryBuilder queryBuilder = new QueryBuilder(builder); string query = queryBuilder.DeleteFrom("PageContent"); query = queryBuilder.Where(query, "Page", WhereOperator.Equals, "Page"); query = queryBuilder.AndWhere(query, "Namespace", WhereOperator.Equals, "Namespace"); query = queryBuilder.AndWhere(query, "Revision", WhereOperator.Equals, "Revision"); List parameters = new List(3); parameters.Add(new Parameter(ParameterType.String, "Page", name)); parameters.Add(new Parameter(ParameterType.String, "Namespace", nspace)); parameters.Add(new Parameter(ParameterType.String, "Revision", revision)); DbCommand command = builder.GetCommand(transaction, query, parameters); int rows = ExecuteNonQuery(command, false); return rows > 0; } /// /// Deletes a revision of a page content. /// /// A database connection. /// The page. /// The revision. /// true if the content ir deleted, false otherwise. private bool DeleteContent(DbConnection connection, PageInfo page, int revision) { string name, nspace; NameTools.ExpandFullName(page.FullName, out nspace, out name); if(nspace == null) nspace = ""; ICommandBuilder builder = GetCommandBuilder(); QueryBuilder queryBuilder = new QueryBuilder(builder); string query = queryBuilder.DeleteFrom("PageContent"); query = queryBuilder.Where(query, "Page", WhereOperator.Equals, "Page"); query = queryBuilder.AndWhere(query, "Namespace", WhereOperator.Equals, "Namespace"); query = queryBuilder.AndWhere(query, "Revision", WhereOperator.Equals, "Revision"); List parameters = new List(3); parameters.Add(new Parameter(ParameterType.String, "Page", name)); parameters.Add(new Parameter(ParameterType.String, "Namespace", nspace)); parameters.Add(new Parameter(ParameterType.String, "Revision", revision)); DbCommand command = builder.GetCommand(connection, query, parameters); int rows = ExecuteNonQuery(command, false); return rows > 0; } /// /// Forces to overwrite or create a Backup. /// /// The Backup content. /// The revision. /// True if the Backup has been created successfully. /// If is null. /// If is less than zero. public bool SetBackupContent(PageContent content, int revision) { if(content == null) throw new ArgumentNullException("content"); if(revision < 0) throw new ArgumentOutOfRangeException("revision", "Invalid Revision"); // 1. DeletebBackup, if any // 2. Set new content ICommandBuilder builder = GetCommandBuilder(); DbConnection connection = builder.GetConnection(connString); DbTransaction transaction = BeginTransaction(connection); DeleteContent(transaction, content.PageInfo, revision); bool set = SetContent(transaction, content, revision); if(set) CommitTransaction(transaction); else RollbackTransaction(transaction); return set; } /// /// Adds a Page. /// /// The target namespace (null for the root). /// The Page Name. /// The creation Date/Time. /// The correct PageInfo object or null. /// This method should not create the content of the Page. /// If is null. /// If is empty. 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"); if(nspace == null) nspace = ""; ICommandBuilder builder = GetCommandBuilder(); string query = QueryBuilder.NewQuery(builder).InsertInto("Page", new string[] { "Name", "Namespace", "CreationDateTime" }, new string[] { "Name", "Namespace", "CreationDateTime" }); List parameters = new List(3); parameters.Add(new Parameter(ParameterType.String, "Name", name)); parameters.Add(new Parameter(ParameterType.String, "Namespace", nspace)); parameters.Add(new Parameter(ParameterType.DateTime, "CreationDateTime", creationDateTime)); DbCommand command = builder.GetCommand(connString, query, parameters); int rows = ExecuteNonQuery(command); if(rows == 1) { return new PageInfo(NameTools.GetFullName(nspace, name), this, creationDateTime); } else return null; } /// /// Renames a Page. /// /// The Page to rename. /// The new Name. /// The correct object. /// If or are null. /// If is empty. 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"); // Check // 1. Page is default page of its namespace // 2. New name already exists ICommandBuilder builder = GetCommandBuilder(); DbConnection connection = builder.GetConnection(connString); DbTransaction transaction = BeginTransaction(connection); if(GetPage(transaction, page.FullName) == null) { RollbackTransaction(transaction); return null; } if(IsDefaultPage(transaction, page)) { RollbackTransaction(transaction); return null; } if(GetPage(transaction, NameTools.GetFullName(NameTools.GetNamespace(page.FullName), NameTools.GetLocalName(newName))) != null) { RollbackTransaction(transaction); return null; } PageContent currentContent = GetContent(transaction, page, CurrentRevision); UnindexPage(currentContent, transaction); foreach(Message msg in GetMessages(transaction, page)) { UnindexMessageTree(page, msg, transaction); } string nspace, name; NameTools.ExpandFullName(page.FullName, out nspace, out name); if(nspace == null) nspace = ""; CategoryInfo[] currCategories = GetCategories(transaction, nspace == "" ? null : GetNamespace(transaction, nspace)); string lowerPageName = page.FullName.ToLowerInvariant(); List pageCategories = new List(10); foreach(CategoryInfo cat in currCategories) { if(Array.Find(cat.Pages, (s) => { return s.ToLowerInvariant() == lowerPageName; }) != null) { pageCategories.Add(NameTools.GetLocalName(cat.FullName)); } } RebindPage(transaction, page, new string[0]); QueryBuilder queryBuilder = new QueryBuilder(builder); string query = queryBuilder.Update("Page", new string[] { "Name" }, new string[] { "NewName" }); query = queryBuilder.Where(query, "Name", WhereOperator.Equals, "OldName"); query = queryBuilder.AndWhere(query, "Namespace", WhereOperator.Equals, "Namespace"); List parameters = new List(3); parameters.Add(new Parameter(ParameterType.String, "NewName", newName)); parameters.Add(new Parameter(ParameterType.String, "OldName", name)); parameters.Add(new Parameter(ParameterType.String, "Namespace", nspace)); DbCommand command = builder.GetCommand(transaction, query, parameters); int rows = ExecuteNonQuery(command, false); if(rows > 0) { PageInfo result = new PageInfo(NameTools.GetFullName(nspace, newName), this, page.CreationDateTime); RebindPage(transaction, result, pageCategories.ToArray()); PageContent newContent = GetContent(transaction, result, CurrentRevision); IndexPage(newContent, transaction); foreach(Message msg in GetMessages(transaction, result)) { IndexMessageTree(result, msg, transaction); } CommitTransaction(transaction); return result; } else { RollbackTransaction(transaction); return null; } } /// /// Modifies the Content of a Page. /// /// The Page. /// The Title of the Page. /// The Username. /// The Date/Time. /// The Comment of the editor, about this revision. /// The Page Content. /// The keywords, usually used for SEO. /// The description, usually used for SEO. /// The save mode for this modification. /// true if the Page has been modified successfully, false otherwise. /// If saveMode equals Draft and a draft already exists, it is overwritten. /// If , , or are null. /// If or are empty. 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 ICommandBuilder builder = GetCommandBuilder(); DbConnection connection = builder.GetConnection(connString); DbTransaction transaction = BeginTransaction(connection); PageContent currentContent = GetContent(transaction, page, CurrentRevision); PageContent pageContent = new PageContent(page, title, username, dateTime, comment, content, keywords != null ? keywords : new string[0], description); switch(saveMode) { case SaveMode.Backup: // Do backup (if there is something to backup), delete current version (if any), store new version if(currentContent != null) UnindexPage(currentContent, transaction); Backup(transaction, page); DeleteContent(transaction, page, CurrentRevision); bool done1 = SetContent(transaction, pageContent, CurrentRevision); if(done1) IndexPage(pageContent, transaction); if(done1) CommitTransaction(transaction); else RollbackTransaction(transaction); return done1; case SaveMode.Normal: // Delete current version (if any), store new version if(currentContent != null) UnindexPage(currentContent, transaction); DeleteContent(transaction, page, CurrentRevision); bool done2 = SetContent(transaction, pageContent, CurrentRevision); if(done2) IndexPage(pageContent, transaction); if(done2) CommitTransaction(transaction); else RollbackTransaction(transaction); return done2; case SaveMode.Draft: // Delete current draft (if any), store new draft DeleteContent(transaction, page, DraftRevision); bool done3 = SetContent(transaction, pageContent, DraftRevision); if(done3) CommitTransaction(transaction); else RollbackTransaction(transaction); return done3; default: RollbackTransaction(transaction); throw new NotSupportedException(); } } /// /// Backs up the content of a page. /// /// A database transaction. /// The page. /// true if the backup is performed, false otherwise. private bool Backup(DbTransaction transaction, PageInfo page) { PageContent currentContent = GetContent(transaction, page, CurrentRevision); if(currentContent != null) { // Insert a new revision int[] backups = GetBackups(transaction, page); if(backups == null) return false; int revision = backups.Length > 0 ? backups[backups.Length - 1] + 1 : FirstRevision; bool set = SetContent(transaction, currentContent, revision); return set; } else return false; } /// /// Performs the rollback of a Page to a specified revision. /// /// The Page to rollback. /// The Revision to rollback the Page to. /// true if the rollback succeeded, false otherwise. /// If is null. /// If is less than zero. public bool RollbackPage(PageInfo page, int revision) { if(page == null) throw new ArgumentNullException("page"); if(revision < 0) throw new ArgumentOutOfRangeException("revision", "Invalid Revision"); // 1. Load specific revision's content // 2. Modify page with loaded content, performing backup ICommandBuilder builder = GetCommandBuilder(); DbConnection connection = builder.GetConnection(connString); DbTransaction transaction = BeginTransaction(connection); PageContent targetContent = GetContent(transaction, page, revision); if(targetContent == null) { RollbackTransaction(transaction); return false; } UnindexPage(GetContent(transaction, page, CurrentRevision), transaction); bool done = Backup(transaction, page); if(!done) { RollbackTransaction(transaction); return false; } done = DeleteContent(transaction, page, CurrentRevision); if(!done) { RollbackTransaction(transaction); return false; } done = SetContent(transaction, targetContent, CurrentRevision); if(!done) { RollbackTransaction(transaction); return false; } IndexPage(targetContent, transaction); CommitTransaction(transaction); return true; } /// /// Deletes the Backups of a Page, up to a specified revision. /// /// The Page to delete the backups of. /// The newest revision to delete (newer revision are kept) o -1 to delete all the Backups. /// true if the deletion succeeded, false otherwise. /// If is null. /// If is less than -1. public bool DeleteBackups(PageInfo page, int revision) { if(page == null) throw new ArgumentNullException("page"); if(revision < -1) throw new ArgumentOutOfRangeException("revision", "Invalid Revision"); // 1. Retrieve target content (revision-1 = first kept revision) // 2. Replace the current content (delete, store) // 3. Delete all older revisions up to the specified on (included) "N-m...N" // 4. Re-number remaining revisions starting from FirstRevision (zero) to revision-1 (don't re-number revs -1, -100) ICommandBuilder builder = GetCommandBuilder(); DbConnection connection = builder.GetConnection(connString); DbTransaction transaction = BeginTransaction(connection); if(GetPage(transaction, page.FullName) == null) { RollbackTransaction(transaction); return false; } int[] baks = GetBackups(transaction, page); if(baks.Length > 0 && revision > baks[baks.Length - 1]) { RollbackTransaction(transaction); return true; } string nspace, name; NameTools.ExpandFullName(page.FullName, out nspace, out name); if(nspace == null) nspace = ""; QueryBuilder queryBuilder = new QueryBuilder(builder); string query = queryBuilder.DeleteFrom("PageContent"); query = queryBuilder.Where(query, "Page", WhereOperator.Equals, "Page"); query = queryBuilder.AndWhere(query, "Namespace", WhereOperator.Equals, "Namespace"); if(revision != -1) query = queryBuilder.AndWhere(query, "Revision", WhereOperator.LessThanOrEqualTo, "Revision"); query = queryBuilder.AndWhere(query, "Revision", WhereOperator.GreaterThanOrEqualTo, "FirstRevision"); List parameters = new List(4); parameters.Add(new Parameter(ParameterType.String, "Page", name)); parameters.Add(new Parameter(ParameterType.String, "Namespace", nspace)); if(revision != -1) parameters.Add(new Parameter(ParameterType.Int16, "Revision", revision)); parameters.Add(new Parameter(ParameterType.Int16, "FirstRevision", FirstRevision)); DbCommand command = builder.GetCommand(transaction, query, parameters); int rows = ExecuteNonQuery(command, false); if(rows == -1) { RollbackTransaction(transaction); return false; } if(revision != -1) { int revisionDelta = revision + 1; query = queryBuilder.UpdateIncrement("PageContent", "Revision", -revisionDelta); query = queryBuilder.Where(query, "Page", WhereOperator.Equals, "Page"); query = queryBuilder.AndWhere(query, "Namespace", WhereOperator.Equals, "Namespace"); query = queryBuilder.AndWhere(query, "Revision", WhereOperator.GreaterThanOrEqualTo, "FirstRevision"); parameters = new List(3); parameters.Add(new Parameter(ParameterType.String, "Page", name)); parameters.Add(new Parameter(ParameterType.String, "Namespace", nspace)); parameters.Add(new Parameter(ParameterType.Int16, "FirstRevision", FirstRevision)); command = builder.GetCommand(transaction, query, parameters); rows = ExecuteNonQuery(command, false); if(rows > 0) CommitTransaction(transaction); else RollbackTransaction(transaction); return rows >= 0; } else { CommitTransaction(transaction); return true; } } /// /// Removes a Page. /// /// The Page to remove. /// True if the Page is removed successfully. /// If is null. public bool RemovePage(PageInfo page) { if(page == null) throw new ArgumentNullException("page"); ICommandBuilder builder = GetCommandBuilder(); DbConnection connection = builder.GetConnection(connString); DbTransaction transaction = BeginTransaction(connection); if(IsDefaultPage(transaction, page)) { RollbackTransaction(transaction); return false; } PageContent currentContent = GetContent(transaction, page, CurrentRevision); if(currentContent != null) { UnindexPage(currentContent, transaction); foreach(Message msg in GetMessages(transaction, page)) { UnindexMessageTree(page, msg, transaction); } } RebindPage(transaction, page, new string[0]); string nspace, name; NameTools.ExpandFullName(page.FullName, out nspace, out name); if(nspace == null) nspace = ""; QueryBuilder queryBuilder = new QueryBuilder(builder); string query = queryBuilder.DeleteFrom("Page"); query = queryBuilder.Where(query, "Name", WhereOperator.Equals, "Name"); query = queryBuilder.AndWhere(query, "Namespace", WhereOperator.Equals, "Namespace"); List parameters = new List(2); parameters.Add(new Parameter(ParameterType.String, "Name", name)); parameters.Add(new Parameter(ParameterType.String, "Namespace", nspace)); DbCommand command = builder.GetCommand(transaction, query, parameters); int rows = ExecuteNonQuery(command, false); if(rows > 0) CommitTransaction(transaction); else RollbackTransaction(transaction); return rows > 0; } /// /// Binds a Page with one or more Categories. /// /// A database transaction. /// The Page to bind. /// The Categories to bind the Page with. /// True if the binding succeeded. /// After a successful operation, the Page is bound with all and only the categories passed as argument. private bool RebindPage(DbTransaction transaction, PageInfo page, string[] categories) { string nspace, name; NameTools.ExpandFullName(page.FullName, out nspace, out name); if(nspace == null) nspace = ""; ICommandBuilder builder = GetCommandBuilder(); QueryBuilder queryBuilder = new QueryBuilder(builder); string query = queryBuilder.DeleteFrom("CategoryBinding"); query = queryBuilder.Where(query, "Page", WhereOperator.Equals, "Page"); query = queryBuilder.AndWhere(query, "Namespace", WhereOperator.Equals, "Namespace"); List parameters = new List(2); parameters.Add(new Parameter(ParameterType.String, "Page", name)); parameters.Add(new Parameter(ParameterType.String, "Namespace", nspace)); DbCommand command = builder.GetCommand(transaction, query, parameters); int rows = ExecuteNonQuery(command, false); if(rows < 0) return false; if(categories.Length > 0) { string finalQuery = ""; parameters = new List(categories.Length * 3); int count = 0; string countString; foreach(string cat in categories) { countString = count.ToString(); query = queryBuilder.InsertInto("CategoryBinding", new string[] { "Namespace", "Category", "Page" }, new string[] { "Namespace" + countString, "Category" + countString, "Page" + countString }); finalQuery = queryBuilder.AppendForBatch(finalQuery, query); parameters.Add(new Parameter(ParameterType.String, "Namespace" + countString, nspace)); parameters.Add(new Parameter(ParameterType.String, "Category" + countString, NameTools.GetLocalName(cat))); parameters.Add(new Parameter(ParameterType.String, "Page" + countString, name)); count++; } command = builder.GetCommand(transaction, finalQuery, parameters); rows = ExecuteNonQuery(command, false); return rows == categories.Length; } else return true; } /// /// Binds a Page with one or more Categories. /// /// A database connection. /// The Page to bind. /// The Categories to bind the Page with. /// True if the binding succeeded. /// After a successful operation, the Page is bound with all and only the categories passed as argument. private bool RebindPage(DbConnection connection, PageInfo page, string[] categories) { string nspace, name; NameTools.ExpandFullName(page.FullName, out nspace, out name); if(nspace == null) nspace = ""; ICommandBuilder builder = GetCommandBuilder(); QueryBuilder queryBuilder = new QueryBuilder(builder); string query = queryBuilder.DeleteFrom("CategoryBinding"); query = queryBuilder.Where(query, "Page", WhereOperator.Equals, "Page"); query = queryBuilder.AndWhere(query, "Namespace", WhereOperator.Equals, "Namespace"); List parameters = new List(2); parameters.Add(new Parameter(ParameterType.String, "Page", name)); parameters.Add(new Parameter(ParameterType.String, "Namespace", nspace)); DbCommand command = builder.GetCommand(connection, query, parameters); int rows = ExecuteNonQuery(command, false); if(rows < 0) return false; if(categories.Length > 0) { string finalQuery = ""; parameters = new List(categories.Length * 3); int count = 0; string countString; foreach(string cat in categories) { countString = count.ToString(); query = queryBuilder.InsertInto("CategoryBinding", new string[] { "Namespace", "Category", "Page" }, new string[] { "Namespace" + countString, "Category" + countString, "Page" + countString }); finalQuery = queryBuilder.AppendForBatch(finalQuery, query); parameters.Add(new Parameter(ParameterType.String, "Namespace" + countString, nspace)); parameters.Add(new Parameter(ParameterType.String, "Category" + countString, NameTools.GetLocalName(cat))); parameters.Add(new Parameter(ParameterType.String, "Page" + countString, name)); count++; } command = builder.GetCommand(connection, finalQuery, parameters); rows = ExecuteNonQuery(command, false); return rows == categories.Length; } else return true; } /// /// Binds a Page with one or more Categories. /// /// The Page to bind. /// The Categories to bind the Page with. /// True if the binding succeeded. /// After a successful operation, the Page is bound with all and only the categories passed as argument. /// If or are null. public bool RebindPage(PageInfo page, string[] categories) { if(page == null) throw new ArgumentNullException("page"); if(categories == null) throw new ArgumentNullException("categories"); foreach(string cat in categories) { if(cat == null) throw new ArgumentNullException("categories"); if(cat.Length == 0) throw new ArgumentException("Category item cannot be empty", "categories"); } // 1. Delete old bindings // 2. Store new bindings ICommandBuilder builder = GetCommandBuilder(); DbConnection connection = builder.GetConnection(connString); bool rebound = RebindPage(connection, page, categories); CloseConnection(connection); return rebound; } /// /// Gets the Page Messages. /// /// A database transaction. /// The Page. /// The list of the first-level Messages, containing the replies properly nested, sorted by date/time. private Message[] GetMessages(DbTransaction transaction, PageInfo page) { if(GetPage(transaction, page.FullName) == null) return null; // 1. Load all messages in memory in a dictionary id->message // 2. Build tree using ParentID string nspace, name; NameTools.ExpandFullName(page.FullName, out nspace, out name); if(nspace == null) nspace = ""; ICommandBuilder builder = GetCommandBuilder(); QueryBuilder queryBuilder = new QueryBuilder(builder); string query = queryBuilder.SelectFrom("Message", new string[] { "Id", "Parent", "Username", "Subject", "DateTime", "Body" }); query = queryBuilder.Where(query, "Page", WhereOperator.Equals, "Page"); query = queryBuilder.AndWhere(query, "Namespace", WhereOperator.Equals, "Namespace"); query = queryBuilder.OrderBy(query, new string[] { "DateTime", "Id" }, new Ordering[] { Ordering.Asc, Ordering.Asc }); List parameters = new List(2); parameters.Add(new Parameter(ParameterType.String, "Page", name)); parameters.Add(new Parameter(ParameterType.String, "Namespace", nspace)); DbCommand command = builder.GetCommand(transaction, query, parameters); DbDataReader reader = ExecuteReader(command); if(reader != null) { Dictionary allMessages = new Dictionary(50); List ids = new List(50); List parents = new List(50); while(reader.Read()) { Message msg = new Message((short)reader["Id"], reader["Username"] as string, reader["Subject"] as string, (DateTime)reader["DateTime"], reader["Body"] as string); ids.Add((short)msg.ID); // Import from V2: parent = -1, otherwise null if(!IsDBNull(reader, "Parent")) { short par = (short)reader["Parent"]; if(par >= 0) parents.Add(par); else parents.Add(null); } else parents.Add(null); allMessages.Add((short)msg.ID, msg); } CloseReader(reader); // Add messages to their parents and build the top-level messages list List result = new List(20); for(int i = 0; i < ids.Count; i++) { short? currentParent = parents[i]; short currentId = ids[i]; if(currentParent.HasValue) { List replies = new List(allMessages[currentParent.Value].Replies); replies.Add(allMessages[currentId]); allMessages[currentParent.Value].Replies = replies.ToArray(); } else result.Add(allMessages[currentId]); } return result.ToArray(); } else return null; } /// /// Gets the Page Messages. /// /// A database connection. /// The Page. /// The list of the first-level Messages, containing the replies properly nested, sorted by date/time. private Message[] GetMessages(DbConnection connection, PageInfo page) { if(GetPage(connection, page.FullName) == null) return null; // 1. Load all messages in memory in a dictionary id->message // 2. Build tree using ParentID string nspace, name; NameTools.ExpandFullName(page.FullName, out nspace, out name); if(nspace == null) nspace = ""; ICommandBuilder builder = GetCommandBuilder(); QueryBuilder queryBuilder = new QueryBuilder(builder); string query = queryBuilder.SelectFrom("Message", new string[] { "Id", "Parent", "Username", "Subject", "DateTime", "Body" }); query = queryBuilder.Where(query, "Page", WhereOperator.Equals, "Page"); query = queryBuilder.AndWhere(query, "Namespace", WhereOperator.Equals, "Namespace"); query = queryBuilder.OrderBy(query, new string[] { "DateTime", "Id" }, new Ordering[] { Ordering.Asc, Ordering.Asc }); List parameters = new List(2); parameters.Add(new Parameter(ParameterType.String, "Page", name)); parameters.Add(new Parameter(ParameterType.String, "Namespace", nspace)); DbCommand command = builder.GetCommand(connection, query, parameters); DbDataReader reader = ExecuteReader(command); if(reader != null) { Dictionary allMessages = new Dictionary(50); List ids = new List(50); List parents = new List(50); while(reader.Read()) { Message msg = new Message((short)reader["Id"], reader["Username"] as string, reader["Subject"] as string, (DateTime)reader["DateTime"], reader["Body"] as string); ids.Add((short)msg.ID); // Import from V2: parent = -1, otherwise null if(!IsDBNull(reader, "Parent")) { short par = (short)reader["Parent"]; if(par >= 0) parents.Add(par); else parents.Add(null); } else parents.Add(null); allMessages.Add((short)msg.ID, msg); } CloseReader(reader); // Add messages to their parents and build the top-level messages list List result = new List(20); for(int i = 0; i < ids.Count; i++) { short? currentParent = parents[i]; short currentId = ids[i]; if(currentParent.HasValue) { List replies = new List(allMessages[currentParent.Value].Replies); replies.Add(allMessages[currentId]); allMessages[currentParent.Value].Replies = replies.ToArray(); } else result.Add(allMessages[currentId]); } return result.ToArray(); } else return null; } /// /// Gets the Page Messages. /// /// The Page. /// The list of the first-level Messages, containing the replies properly nested, sorted by date/time. /// If is null. public Message[] GetMessages(PageInfo page) { if(page == null) throw new ArgumentNullException("page"); ICommandBuilder builder = GetCommandBuilder(); DbConnection connection = builder.GetConnection(connString); Message[] messages = GetMessages(connection, page); CloseConnection(connection); return messages; } /// /// Gets the total number of Messages in a Page Discussion. /// /// The Page. /// The number of messages. /// If is null. public int GetMessageCount(PageInfo page) { if(page == null) throw new ArgumentNullException("page"); ICommandBuilder builder = GetCommandBuilder(); DbConnection connection = builder.GetConnection(connString); if(GetPage(connection, page.FullName) == null) { CloseConnection(connection); return -1; } string nspace, name; NameTools.ExpandFullName(page.FullName, out nspace, out name); if(nspace == null) nspace = ""; QueryBuilder queryBuilder = new QueryBuilder(builder); string query = queryBuilder.SelectCountFrom("Message"); query = queryBuilder.Where(query, "Page", WhereOperator.Equals, "Page"); query = queryBuilder.AndWhere(query, "Namespace", WhereOperator.Equals, "Namespace"); List parameters = new List(2); parameters.Add(new Parameter(ParameterType.String, "Page", name)); parameters.Add(new Parameter(ParameterType.String, "Namespace", nspace)); DbCommand command = builder.GetCommand(connection, query, parameters); int count = ExecuteScalar(command, 0); return count; } /// /// Removes all messages for a page and stores the new messages. /// /// The page. /// The new messages to store. /// true if the messages are stored, false otherwise. /// If or are null. public bool BulkStoreMessages(PageInfo page, Message[] messages) { if(page == null) throw new ArgumentNullException("page"); if(messages == null) throw new ArgumentNullException("messages"); ICommandBuilder builder = GetCommandBuilder(); DbConnection connection = builder.GetConnection(connString); DbTransaction transaction = BeginTransaction(connection); if(GetPage(transaction, page.FullName) == null) { RollbackTransaction(transaction); return false; } foreach(Message msg in GetMessages(transaction, page)) { UnindexMessageTree(page, msg, transaction); } string nspace, name; NameTools.ExpandFullName(page.FullName, out nspace, out name); if(nspace == null) nspace = ""; QueryBuilder queryBuilder = new QueryBuilder(builder); string query = queryBuilder.DeleteFrom("Message"); query = queryBuilder.Where(query, "Page", WhereOperator.Equals, "Page"); query = queryBuilder.AndWhere(query, "Namespace", WhereOperator.Equals, "Namespace"); List parameters = new List(2); parameters.Add(new Parameter(ParameterType.String, "Page", name)); parameters.Add(new Parameter(ParameterType.String, "Namespace", nspace)); DbCommand command = builder.GetCommand(transaction, query, parameters); ExecuteNonQuery(command, false); List allMessages; List parents; UnTreeMessages(messages, out allMessages, out parents, -1); string finalQuery = ""; int count = 1; string countString; parameters = new List(MaxStatementsInBatch * 8); int rowsDone = 0; for(int i = 0; i < allMessages.Count; i++) { // Execute the batch in smaller chunks Message msg = allMessages[i]; int parent = parents[i]; countString = count.ToString(); query = queryBuilder.InsertInto("Message", new string[] { "Page", "Namespace", "Id", "Parent", "Username", "Subject", "DateTime", "Body" }, new string[] { "Page" + countString, "Namespace" + countString, "Id" + countString, "Parent" + countString, "Username" + countString, "Subject" + countString, "DateTime" + countString, "Body" + countString }); parameters.Add(new Parameter(ParameterType.String, "Page" + countString, name)); parameters.Add(new Parameter(ParameterType.String, "Namespace" + countString, nspace)); parameters.Add(new Parameter(ParameterType.Int16, "Id" + countString, (short)msg.ID)); if(parent != -1) parameters.Add(new Parameter(ParameterType.Int16, "Parent" + countString, parent)); else parameters.Add(new Parameter(ParameterType.Int16, "Parent" + countString, DBNull.Value)); parameters.Add(new Parameter(ParameterType.String, "Username" + countString, msg.Username)); parameters.Add(new Parameter(ParameterType.String, "Subject" + countString, msg.Subject)); parameters.Add(new Parameter(ParameterType.DateTime, "DateTime" + countString, msg.DateTime)); parameters.Add(new Parameter(ParameterType.String, "Body" + countString, msg.Body)); finalQuery = queryBuilder.AppendForBatch(finalQuery, query); count++; if(count == MaxStatementsInBatch) { command = builder.GetCommand(transaction, finalQuery, parameters); rowsDone += ExecuteNonQuery(command, false); finalQuery = ""; count = 1; parameters.Clear(); } } if(finalQuery.Length > 0) { command = builder.GetCommand(transaction, finalQuery, parameters); rowsDone += ExecuteNonQuery(command, false); } if(rowsDone == allMessages.Count) { foreach(Message msg in messages) { IndexMessageTree(page, msg, transaction); } CommitTransaction(transaction); return true; } else { RollbackTransaction(transaction); return false; } } /// /// Deconstructs a tree of messages and converts it into a flat list. /// /// The input tree. /// The resulting flat message list. /// The list of parent IDs. /// The current parent ID. private static void UnTreeMessages(Message[] messages, out List flatList, out List parents, int parent) { flatList = new List(20); parents = new List(20); flatList.AddRange(messages); for(int i = 0; i < messages.Length; i++) { parents.Add(parent); } foreach(Message msg in messages) { List temp; List tempParents; UnTreeMessages(msg.Replies, out temp, out tempParents, msg.ID); flatList.AddRange(temp); parents.AddRange(tempParents); } } /// /// Adds a new Message to a Page. /// /// The Page. /// The Username. /// The Subject. /// The Date/Time. /// The Body. /// The Parent Message ID, or -1. /// True if the Message is added successfully. /// If , , or are null. /// If or are empty. /// If is less than -1. 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"); string nspace, name; NameTools.ExpandFullName(page.FullName, out nspace, out name); if(nspace == null) nspace = ""; ICommandBuilder builder = GetCommandBuilder(); DbConnection connection = builder.GetConnection(connString); DbTransaction transaction = BeginTransaction(connection); if(parent != -1 && FindMessage(GetMessages(transaction, page), parent) == null) { RollbackTransaction(transaction); return false; } QueryBuilder queryBuilder = new QueryBuilder(builder); short freeId = -1; string query = queryBuilder.SelectFrom("Message", new string[] { "Id" }); query = queryBuilder.Where(query, "Page", WhereOperator.Equals, "Page"); query = queryBuilder.AndWhere(query, "Namespace", WhereOperator.Equals, "Namespace"); query = queryBuilder.OrderBy(query, new string[] { "Id" }, new Ordering[] { Ordering.Desc }); List parameters = new List(2); parameters.Add(new Parameter(ParameterType.String, "Page", name)); parameters.Add(new Parameter(ParameterType.String, "Namespace", nspace)); DbCommand command = builder.GetCommand(transaction, query, parameters); freeId = ExecuteScalar(command, -1, false); if(freeId == -1) freeId = 0; else freeId++; query = queryBuilder.InsertInto("Message", new string[] { "Page", "Namespace", "Id", "Parent", "Username", "Subject", "DateTime", "Body" }, new string[] { "Page", "Namespace", "Id", "Parent", "Username", "Subject", "DateTime", "Body" }); parameters = new List(8); parameters.Add(new Parameter(ParameterType.String, "Page", name)); parameters.Add(new Parameter(ParameterType.String, "Namespace", nspace)); parameters.Add(new Parameter(ParameterType.Int16, "Id", freeId)); if(parent != -1) parameters.Add(new Parameter(ParameterType.Int16, "Parent", parent)); else parameters.Add(new Parameter(ParameterType.Int16, "Parent", DBNull.Value)); parameters.Add(new Parameter(ParameterType.String, "Username", username)); parameters.Add(new Parameter(ParameterType.String, "Subject", subject)); parameters.Add(new Parameter(ParameterType.DateTime, "DateTime", dateTime)); parameters.Add(new Parameter(ParameterType.String, "Body", body)); command = builder.GetCommand(transaction, query, parameters); int rows = ExecuteNonQuery(command, false); if(rows == 1) { IndexMessage(page, freeId, subject, dateTime, body, transaction); CommitTransaction(transaction); return true; } else { RollbackTransaction(transaction); return false; } } /// /// Finds a Message in a Message tree. /// /// The Message tree. /// The ID of the Message to find. /// The Message or null. /// The method is recursive. private static Message FindMessage(IEnumerable 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; } /// /// Finds the anchestor/parent of a Message. /// /// The Messages. /// The Message ID. /// The anchestor Message or null. private static Message FindAnchestor(IEnumerable 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; } /// /// Removes a Message. /// /// A database transaction. /// The Page. /// The ID of the Message to remove. /// A value specifying whether or not to remove the replies. /// True if the Message is removed successfully. private bool RemoveMessage(DbTransaction transaction, PageInfo page, int id, bool removeReplies) { string nspace, name; NameTools.ExpandFullName(page.FullName, out nspace, out name); if(nspace == null) nspace = ""; Message[] messages = GetMessages(transaction, page); if(messages == null) return false; Message message = FindMessage(messages, id); if(message == null) return false; Message parent = FindAnchestor(messages, id); int parentId = parent != null ? parent.ID : -1; UnindexMessage(page, message.ID, message.Subject, message.DateTime, message.Body, transaction); if(removeReplies) { // Recursively remove all replies BEFORE removing parent (depth-first) foreach(Message reply in message.Replies) { if(!RemoveMessage(transaction, page, reply.ID, true)) return false; } } // Remove this message ICommandBuilder builder = GetCommandBuilder(); QueryBuilder queryBuilder = new QueryBuilder(builder); string query = queryBuilder.DeleteFrom("Message"); query = queryBuilder.Where(query, "Page", WhereOperator.Equals, "Page"); query = queryBuilder.AndWhere(query, "Namespace", WhereOperator.Equals, "Namespace"); query = queryBuilder.AndWhere(query, "Id", WhereOperator.Equals, "Id"); List parameters = new List(3); parameters.Add(new Parameter(ParameterType.String, "Page", name)); parameters.Add(new Parameter(ParameterType.String, "Namespace", nspace)); parameters.Add(new Parameter(ParameterType.Int16, "Id", (short)id)); DbCommand command = builder.GetCommand(transaction, query, parameters); int rows = ExecuteNonQuery(command, false); if(!removeReplies && rows == 1) { // Update replies' parent id query = queryBuilder.Update("Message", new string[] { "Parent" }, new string[] { "NewParent" }); query = queryBuilder.Where(query, "Page", WhereOperator.Equals, "Page"); query = queryBuilder.AndWhere(query, "Namespace", WhereOperator.Equals, "Namespace"); query = queryBuilder.AndWhere(query, "Parent", WhereOperator.Equals, "OldParent"); parameters = new List(4); if(parentId != -1) parameters.Add(new Parameter(ParameterType.Int16, "NewParent", parentId)); else parameters.Add(new Parameter(ParameterType.Int16, "NewParent", DBNull.Value)); parameters.Add(new Parameter(ParameterType.String, "Page", name)); parameters.Add(new Parameter(ParameterType.String, "Namespace", nspace)); parameters.Add(new Parameter(ParameterType.Int16, "OldParent", (short)id)); command = builder.GetCommand(transaction, query, parameters); rows = ExecuteNonQuery(command, false); } return rows > 0; } /// /// Removes a Message. /// /// The Page. /// The ID of the Message to remove. /// A value specifying whether or not to remove the replies. /// True if the Message is removed successfully. /// If is null. /// If is less than zero. public bool RemoveMessage(PageInfo page, int id, bool removeReplies) { if(page == null) throw new ArgumentNullException("page"); if(id < 0) throw new ArgumentOutOfRangeException("id", "Invalid ID"); // 1. If removeReplies, recursively delete all messages with parent == id // Else remove current message, updating all replies' parent id (set to this message's parent or to NULL) // 2. If removeReplies, unindex the whole message tree // Else unindex only this message ICommandBuilder builder = GetCommandBuilder(); DbConnection connection = builder.GetConnection(connString); DbTransaction transaction = BeginTransaction(connection); bool done = RemoveMessage(transaction, page, id, removeReplies); if(done) CommitTransaction(transaction); else RollbackTransaction(transaction); return done; } /// /// Modifies a Message. /// /// The Page. /// The ID of the Message to modify. /// The Username. /// The Subject. /// The Date/Time. /// The Body. /// True if the Message is modified successfully. /// If , , or are null. /// If is less than zero. /// If or are empty. 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("id", "Invalid Message 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 string nspace, name; NameTools.ExpandFullName(page.FullName, out nspace, out name); if(nspace == null) nspace = ""; ICommandBuilder builder = GetCommandBuilder(); DbConnection connection = builder.GetConnection(connString); DbTransaction transaction = BeginTransaction(connection); Message[] messages = GetMessages(transaction, page); if(messages == null) { RollbackTransaction(transaction); return false; } Message oldMessage = FindMessage(messages, id); if(oldMessage == null) { RollbackTransaction(transaction); return false; } UnindexMessage(page, oldMessage.ID, oldMessage.Subject, oldMessage.DateTime, oldMessage.Body, transaction); QueryBuilder queryBuilder = new QueryBuilder(builder); string query = queryBuilder.Update("Message", new string[] { "Username", "Subject", "DateTime", "Body" }, new string[] { "Username", "Subject", "DateTime", "Body" }); query = queryBuilder.Where(query, "Page", WhereOperator.Equals, "Page"); query = queryBuilder.AndWhere(query, "Namespace", WhereOperator.Equals, "Namespace"); query = queryBuilder.AndWhere(query, "Id", WhereOperator.Equals, "Id"); List parameters = new List(7); parameters.Add(new Parameter(ParameterType.String, "Username", username)); parameters.Add(new Parameter(ParameterType.String, "Subject", subject)); parameters.Add(new Parameter(ParameterType.DateTime, "DateTime", dateTime)); parameters.Add(new Parameter(ParameterType.String, "Body", body)); parameters.Add(new Parameter(ParameterType.String, "Page", name)); parameters.Add(new Parameter(ParameterType.String, "Namespace", nspace)); parameters.Add(new Parameter(ParameterType.Int16, "Id", id)); DbCommand command = builder.GetCommand(transaction, query, parameters); int rows = ExecuteNonQuery(command, false); if(rows == 1) { IndexMessage(page, id, subject, dateTime, body, transaction); CommitTransaction(transaction); return true; } else { RollbackTransaction(transaction); return false; } } /// /// Gets all the Navigation Paths in a Namespace. /// /// The Namespace. /// All the Navigation Paths, sorted by name. public NavigationPath[] GetNavigationPaths(NamespaceInfo nspace) { string nspaceName = nspace != null ? nspace.Name : ""; ICommandBuilder builder = GetCommandBuilder(); QueryBuilder queryBuilder = new QueryBuilder(builder); string query = queryBuilder.SelectFrom("NavigationPath", new string[] { "Name", "Namespace", "Page" }); query = queryBuilder.Where(query, "Namespace", WhereOperator.Equals, "Namespace"); query = queryBuilder.OrderBy(query, new string[] { "Namespace", "Name", "Number" }, new Ordering[] { Ordering.Asc, Ordering.Asc, Ordering.Asc }); List parameters = new List(1); parameters.Add(new Parameter(ParameterType.String, "Namespace", nspaceName)); DbCommand command = builder.GetCommand(connString, query, parameters); DbDataReader reader = ExecuteReader(command); if(reader != null) { List result = new List(10); string prevName = "|||"; string name; string actualNamespace = ""; List pages = new List(10); while(reader.Read()) { name = reader["Name"] as string; if(name != prevName) { actualNamespace = reader["Namespace"] as string; if(prevName != "|||") { result[result.Count - 1].Pages = pages.ToArray(); pages.Clear(); } result.Add(new NavigationPath(NameTools.GetFullName(actualNamespace, name), this)); } prevName = name; pages.Add(NameTools.GetFullName(actualNamespace, reader["Page"] as string)); } if(result.Count > 0) { result[result.Count - 1].Pages = pages.ToArray(); } CloseReader(command, reader); return result.ToArray(); } else return null; } /// /// Adds a new Navigation Path. /// /// A database transaction. /// The target namespace (null for the root). /// The Name of the Path. /// The Pages array. /// The correct object. private NavigationPath AddNavigationPath(DbTransaction transaction, string nspace, string name, PageInfo[] pages) { ICommandBuilder builder = GetCommandBuilder(); QueryBuilder queryBuilder = new QueryBuilder(builder); string query, finalQuery = ""; List parameters = new List(3 * pages.Length); int count = 0; string countString; foreach(PageInfo page in pages) { countString = count.ToString(); query = queryBuilder.InsertInto("NavigationPath", new string[] { "Name", "Namespace", "Page", "Number" }, new string[] { "Name" + countString, "Namespace" + countString, "Page" + countString, "Number" + countString }); parameters.Add(new Parameter(ParameterType.String, "Name" + countString, name)); parameters.Add(new Parameter(ParameterType.String, "Namespace" + countString, nspace)); parameters.Add(new Parameter(ParameterType.String, "Page" + countString, NameTools.GetLocalName(page.FullName))); parameters.Add(new Parameter(ParameterType.Int32, "Number" + countString, (short)count)); finalQuery = queryBuilder.AppendForBatch(finalQuery, query); count++; } DbCommand command = builder.GetCommand(transaction, finalQuery, parameters); int rows = ExecuteNonQuery(command, false); if(rows == pages.Length) { NavigationPath result = new NavigationPath(NameTools.GetFullName(nspace, name), this); result.Pages = Array.ConvertAll(pages, (x) => { return x.FullName; }); return result; } else return null; } /// /// Adds a new Navigation Path. /// /// The target namespace (null for the root). /// The Name of the Path. /// The Pages array. /// The correct object. /// If or are null. /// If or are empty. 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"); if(nspace == null) nspace = ""; ICommandBuilder builder = GetCommandBuilder(); DbConnection connection = builder.GetConnection(connString); DbTransaction transaction = BeginTransaction(connection); foreach(PageInfo page in pages) { if(page == null) { RollbackTransaction(transaction); throw new ArgumentNullException("pages"); } if(GetPage(transaction, page.FullName) == null) { RollbackTransaction(transaction); throw new ArgumentException("Page not found", "pages"); } } NavigationPath path = AddNavigationPath(transaction, nspace, name, pages); if(path != null) CommitTransaction(transaction); else RollbackTransaction(transaction); return path; } /// /// Modifies an existing navigation path. /// /// The navigation path to modify. /// The new pages array. /// The correct object. /// If or are null. /// If is empty. 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"); ICommandBuilder builder = GetCommandBuilder(); DbConnection connection = builder.GetConnection(connString); DbTransaction transaction = BeginTransaction(connection); foreach(PageInfo page in pages) { if(page == null) { RollbackTransaction(transaction); throw new ArgumentNullException("pages"); } if(GetPage(transaction, page.FullName) == null) { RollbackTransaction(transaction); throw new ArgumentException("Page not found", "pages"); } } if(RemoveNavigationPath(transaction, path)) { string nspace, name; NameTools.ExpandFullName(path.FullName, out nspace, out name); if(nspace == null) nspace = ""; NavigationPath result = AddNavigationPath(transaction, nspace, name, pages); if(result != null) { CommitTransaction(transaction); return result; } else { RollbackTransaction(transaction); return null; } } else { RollbackTransaction(transaction); return null; } } /// /// Removes a Navigation Path. /// /// A database transaction. /// The navigation path to remove. /// true if the path is removed, false otherwise. private bool RemoveNavigationPath(DbTransaction transaction, NavigationPath path) { string nspace, name; NameTools.ExpandFullName(path.FullName, out nspace, out name); if(nspace == null) nspace = ""; ICommandBuilder builder = GetCommandBuilder(); QueryBuilder queryBuilder = new QueryBuilder(builder); string query = queryBuilder.DeleteFrom("NavigationPath"); query = queryBuilder.Where(query, "Name", WhereOperator.Equals, "Name"); query = queryBuilder.AndWhere(query, "Namespace", WhereOperator.Equals, "Namespace"); List parameters = new List(2); parameters.Add(new Parameter(ParameterType.String, "Name", name)); parameters.Add(new Parameter(ParameterType.String, "Namespace", nspace)); DbCommand command = builder.GetCommand(transaction, query, parameters); int rows = ExecuteNonQuery(command, false); return rows > 0; } /// /// Removes a Navigation Path. /// /// A database connection. /// The navigation path to remove. /// true if the path is removed, false otherwise. private bool RemoveNavigationPath(DbConnection connection, NavigationPath path) { string nspace, name; NameTools.ExpandFullName(path.FullName, out nspace, out name); if(nspace == null) nspace = ""; ICommandBuilder builder = GetCommandBuilder(); QueryBuilder queryBuilder = new QueryBuilder(builder); string query = queryBuilder.DeleteFrom("NavigationPath"); query = queryBuilder.Where(query, "Name", WhereOperator.Equals, "Name"); query = queryBuilder.AndWhere(query, "Namespace", WhereOperator.Equals, "Namespace"); List parameters = new List(2); parameters.Add(new Parameter(ParameterType.String, "Name", name)); parameters.Add(new Parameter(ParameterType.String, "Namespace", nspace)); DbCommand command = builder.GetCommand(connection, query, parameters); int rows = ExecuteNonQuery(command, false); return rows > 0; } /// /// Removes a Navigation Path. /// /// The navigation path to remove. /// true if the path is removed, false otherwise. /// If is null. public bool RemoveNavigationPath(NavigationPath path) { if(path == null) throw new ArgumentNullException("path"); string nspace, name; NameTools.ExpandFullName(path.FullName, out nspace, out name); if(nspace == null) nspace = ""; ICommandBuilder builder = GetCommandBuilder(); DbConnection connection = builder.GetConnection(connString); bool removed = RemoveNavigationPath(connection, path); CloseConnection(connection); return removed; } /// /// Gets all the snippets. /// /// All the snippets, sorted by name. public Snippet[] GetSnippets() { ICommandBuilder builder = GetCommandBuilder(); QueryBuilder queryBuilder = QueryBuilder.NewQuery(builder); string query = queryBuilder.SelectFrom("Snippet"); query = queryBuilder.OrderBy(query, new[] { "Name" }, new[] { Ordering.Asc }); DbCommand command = builder.GetCommand(connString, query, new List()); DbDataReader reader = ExecuteReader(command); if(reader != null) { List result = new List(10); while(reader.Read()) { result.Add(new Snippet(reader["Name"] as string, reader["Content"] as string, this)); } CloseReader(command, reader); return result.ToArray(); } else return null; } /// /// Adds a new snippet. /// /// A database transaction. /// The name of the snippet. /// The content of the snippet. /// The correct object. private Snippet AddSnippet(DbTransaction transaction, string name, string content) { ICommandBuilder builder = GetCommandBuilder(); string query = QueryBuilder.NewQuery(builder).InsertInto("Snippet", new string[] { "Name", "Content" }, new string[] { "Name", "Content" }); List parameters = new List(2); parameters.Add(new Parameter(ParameterType.String, "Name", name)); parameters.Add(new Parameter(ParameterType.String, "Content", content)); DbCommand command = builder.GetCommand(transaction, query, parameters); int rows = ExecuteNonQuery(command, false); if(rows == 1) { return new Snippet(name, content, this); } else return null; } /// /// Adds a new snippet. /// /// A database connection. /// The name of the snippet. /// The content of the snippet. /// The correct object. private Snippet AddSnippet(DbConnection connection, string name, string content) { ICommandBuilder builder = GetCommandBuilder(); string query = QueryBuilder.NewQuery(builder).InsertInto("Snippet", new string[] { "Name", "Content" }, new string[] { "Name", "Content" }); List parameters = new List(2); parameters.Add(new Parameter(ParameterType.String, "Name", name)); parameters.Add(new Parameter(ParameterType.String, "Content", content)); DbCommand command = builder.GetCommand(connection, query, parameters); int rows = ExecuteNonQuery(command, false); if(rows == 1) { return new Snippet(name, content, this); } else return null; } /// /// Adds a new snippet. /// /// The name of the snippet. /// The content of the snippet. /// The correct object. /// If or are null. /// If is empty. 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 ICommandBuilder builder = GetCommandBuilder(); DbConnection connection = builder.GetConnection(connString); Snippet snippet = AddSnippet(connection, name, content); CloseConnection(connection); return snippet; } /// /// Modifies an existing snippet. /// /// The name of the snippet to modify. /// The content of the snippet. /// The correct object. /// If or are null. /// If is empty. 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 ICommandBuilder builder = GetCommandBuilder(); DbConnection connection = builder.GetConnection(connString); DbTransaction transaction = BeginTransaction(connection); if(RemoveSnippet(transaction, name)) { Snippet result = AddSnippet(transaction, name, content); if(result != null) CommitTransaction(transaction); else RollbackTransaction(transaction); return result; } else { RollbackTransaction(transaction); return null; } } /// /// Removes a new Snippet. /// /// A database transaction. /// The Name of the Snippet to remove. /// true if the snippet is removed, false otherwise. private bool RemoveSnippet(DbTransaction transaction, string name) { ICommandBuilder builder = GetCommandBuilder(); QueryBuilder queryBuilder = new QueryBuilder(builder); string query = queryBuilder.DeleteFrom("Snippet"); query = queryBuilder.Where(query, "Name", WhereOperator.Equals, "Name"); List parameters = new List(1); parameters.Add(new Parameter(ParameterType.String, "Name", name)); DbCommand command = builder.GetCommand(transaction, query, parameters); int rows = ExecuteNonQuery(command, false); return rows == 1; } /// /// Removes a new Snippet. /// /// A database connection. /// The Name of the Snippet to remove. /// true if the snippet is removed, false otherwise. private bool RemoveSnippet(DbConnection connection, string name) { ICommandBuilder builder = GetCommandBuilder(); QueryBuilder queryBuilder = new QueryBuilder(builder); string query = queryBuilder.DeleteFrom("Snippet"); query = queryBuilder.Where(query, "Name", WhereOperator.Equals, "Name"); List parameters = new List(1); parameters.Add(new Parameter(ParameterType.String, "Name", name)); DbCommand command = builder.GetCommand(connection, query, parameters); int rows = ExecuteNonQuery(command, false); return rows == 1; } /// /// Removes a new Snippet. /// /// The Name of the Snippet to remove. /// true if the snippet is removed, false otherwise. /// If is null. /// If is empty. public bool RemoveSnippet(string name) { if(name == null) throw new ArgumentNullException("name"); if(name.Length == 0) throw new ArgumentException("Name cannot be empty", "name"); ICommandBuilder builder = GetCommandBuilder(); DbConnection connection = builder.GetConnection(connString); bool removed = RemoveSnippet(connection, name); CloseConnection(connection); return removed; } /// /// Gets all the content templates. /// /// All the content templates, sorted by name. public ContentTemplate[] GetContentTemplates() { ICommandBuilder builder = GetCommandBuilder(); QueryBuilder queryBuilder = QueryBuilder.NewQuery(builder); string query = queryBuilder.SelectFrom("ContentTemplate"); query = queryBuilder.OrderBy(query, new[] { "Name" }, new[] { Ordering.Asc }); DbCommand command = builder.GetCommand(connString, query, new List()); DbDataReader reader = ExecuteReader(command); if(reader != null) { List result = new List(10); while(reader.Read()) { result.Add(new ContentTemplate(reader["Name"] as string, reader["Content"] as string, this)); } CloseReader(command, reader); return result.ToArray(); } else return null; } /// /// Adds a new content template. /// /// A database transaction. /// The name of template. /// The content of the template. /// The correct object. private ContentTemplate AddContentTemplate(DbTransaction transaction, string name, string content) { ICommandBuilder builder = GetCommandBuilder(); string query = QueryBuilder.NewQuery(builder).InsertInto("ContentTemplate", new string[] { "Name", "Content" }, new string[] { "Name", "Content" }); List parameters = new List(2); parameters.Add(new Parameter(ParameterType.String, "Name", name)); parameters.Add(new Parameter(ParameterType.String, "Content", content)); DbCommand command = builder.GetCommand(transaction, query, parameters); int rows = ExecuteNonQuery(command, false); if(rows == 1) { return new ContentTemplate(name, content, this); } else return null; } /// /// Adds a new content template. /// /// A database connection. /// The name of template. /// The content of the template. /// The correct object. private ContentTemplate AddContentTemplate(DbConnection connection, string name, string content) { ICommandBuilder builder = GetCommandBuilder(); string query = QueryBuilder.NewQuery(builder).InsertInto("ContentTemplate", new string[] { "Name", "Content" }, new string[] { "Name", "Content" }); List parameters = new List(2); parameters.Add(new Parameter(ParameterType.String, "Name", name)); parameters.Add(new Parameter(ParameterType.String, "Content", content)); DbCommand command = builder.GetCommand(connection, query, parameters); int rows = ExecuteNonQuery(command, false); if(rows == 1) { return new ContentTemplate(name, content, this); } else return null; } /// /// Adds a new content template. /// /// The name of template. /// The content of the template. /// The correct object. /// If or are null. /// If is empty. 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"); // content can be empty ICommandBuilder builder = GetCommandBuilder(); DbConnection connection = builder.GetConnection(connString); ContentTemplate template = AddContentTemplate(connection, name, content); CloseConnection(connection); return template; } /// /// Modifies an existing content template. /// /// The name of the template to modify. /// The content of the template. /// The correct object. /// If or are null. /// If is empty. 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"); // content can be empty ICommandBuilder builder = GetCommandBuilder(); DbConnection connection = builder.GetConnection(connString); DbTransaction transaction = BeginTransaction(connection); if(RemoveContentTemplate(transaction, name)) { ContentTemplate template = AddContentTemplate(transaction, name, content); if(template != null) CommitTransaction(transaction); else RollbackTransaction(transaction); return template; } else { RollbackTransaction(transaction); return null; } } /// /// Removes a content template. /// /// A database transaction. /// The name of the template to remove. /// true if the template is removed, false otherwise. private bool RemoveContentTemplate(DbTransaction transaction, string name) { ICommandBuilder builder = GetCommandBuilder(); QueryBuilder queryBuilder = new QueryBuilder(builder); string query = queryBuilder.DeleteFrom("ContentTemplate"); query = queryBuilder.Where(query, "Name", WhereOperator.Equals, "Name"); List parameters = new List(1); parameters.Add(new Parameter(ParameterType.String, "Name", name)); DbCommand command = builder.GetCommand(transaction, query, parameters); int rows = ExecuteNonQuery(command, false); return rows == 1; } /// /// Removes a content template. /// /// A database connection. /// The name of the template to remove. /// true if the template is removed, false otherwise. private bool RemoveContentTemplate(DbConnection connection, string name) { ICommandBuilder builder = GetCommandBuilder(); QueryBuilder queryBuilder = new QueryBuilder(builder); string query = queryBuilder.DeleteFrom("ContentTemplate"); query = queryBuilder.Where(query, "Name", WhereOperator.Equals, "Name"); List parameters = new List(1); parameters.Add(new Parameter(ParameterType.String, "Name", name)); DbCommand command = builder.GetCommand(connection, query, parameters); int rows = ExecuteNonQuery(command, false); return rows == 1; } /// /// Removes a content template. /// /// The name of the template to remove. /// true if the template is removed, false otherwise. /// If is null. /// If is empty. public bool RemoveContentTemplate(string name) { if(name == null) throw new ArgumentNullException("name"); if(name.Length == 0) throw new ArgumentException("Name cannot be empty", "name"); ICommandBuilder builder = GetCommandBuilder(); DbConnection connection = builder.GetConnection(connString); bool removed = RemoveContentTemplate(connection, name); CloseConnection(connection); return removed; } #endregion #region IStorageProvider Members /// /// Gets a value specifying whether the provider is read-only, i.e. it can only provide data and not store it. /// public bool ReadOnly { get { return false; } } #endregion } }