[smuxi/experiments/sqlite: 1/27] [Engine] New partial message buffer implementation based on JSON and Git



commit 6894d2816dc8c30fb838cc86b5cd78100613eac5
Author: Mirco Bauer <meebey meebey net>
Date:   Sun Feb 19 15:50:31 2012 +0100

    [Engine] New partial message buffer implementation based on JSON and Git
    
    - Include BSD 3-clause licensed ServiceStack.Text library for
      JSON serialization / deserialization
    - Include GPLv2-only + linking exception licensed libgit2sharp library for
      storing messages in a Git repository
    - Added DataContract attributes to MessageModel and related to hint
      the JSON serializer
    - Initial partial GitMessageBuffer implementation based on Index
      - Implemented Add() and get_Count()
      - Stage all new messages to Index
      - Commit non-empty Index every 60 seconds
      - Commit on Dispose

 .gitmodules                                    |    3 +
 lib/libgit2sharp                               |    1 +
 src/Engine-Tests/Db4oMessageBufferTests.cs     |    7 +-
 src/Engine-Tests/GitMessageBufferTests.cs      |   53 +++++
 src/Engine-Tests/ListMessageBufferTests.cs     |    5 +
 src/Engine-Tests/MessageBufferTestsBase.cs     |   73 +++++++
 src/Engine-Tests/MessageModelTests.cs          |  193 +++++++++++++++++++
 src/Engine/MessageBuffers/Db4oMessageBuffer.cs |   47 +----
 src/Engine/MessageBuffers/GitMessageBuffer.cs  |  245 ++++++++++++++++++++++++
 src/Engine/MessageBuffers/MessageBufferBase.cs |   41 ++++
 src/Engine/Messages/ImageMessagePartModel.cs   |   12 +-
 src/Engine/Messages/MessageModel.cs            |    8 +-
 src/Engine/Messages/MessagePartModel.cs        |    9 +-
 src/Engine/Messages/MessageType.cs             |    2 +
 src/Engine/Messages/TextMessagePartModel.cs    |   17 ++-
 src/Engine/Messages/UrlMessagePartModel.cs     |   12 +-
 src/Engine/TextColor.cs                        |    8 +-
 17 files changed, 688 insertions(+), 48 deletions(-)
---
diff --git a/.gitmodules b/.gitmodules
index 8790c08..9dbc624 100644
--- a/.gitmodules
+++ b/.gitmodules
@@ -31,3 +31,6 @@
 [submodule "lib/StarkSoftProxy"]
        path = lib/StarkSoftProxy
        url = https://github.com/meebey/starksoftproxy.git
+[submodule "lib/libgit2sharp"]
+       path = lib/libgit2sharp
+       url = git://git.qnetp.net/libgit2sharp.git
diff --git a/lib/libgit2sharp b/lib/libgit2sharp
new file mode 160000
index 0000000..1cb40da
--- /dev/null
+++ b/lib/libgit2sharp
@@ -0,0 +1 @@
+Subproject commit 1cb40daeab62551b44a964e2dce60bad098a0477
diff --git a/src/Engine-Tests/Db4oMessageBufferTests.cs b/src/Engine-Tests/Db4oMessageBufferTests.cs
index 59b3133..4ee199a 100644
--- a/src/Engine-Tests/Db4oMessageBufferTests.cs
+++ b/src/Engine-Tests/Db4oMessageBufferTests.cs
@@ -39,6 +39,11 @@ namespace Smuxi.Engine
                 File.Delete(dbFile);
             }
 
+            return OpenBuffer();
+        }
+
+        protected override IMessageBuffer OpenBuffer()
+        {
             return new Db4oMessageBuffer("testuser", "testprot", "testnet", "testchat");
         }
 
@@ -46,7 +51,7 @@ namespace Smuxi.Engine
         public void Reopen()
         {
             Buffer.Dispose();
-            Buffer = new Db4oMessageBuffer("testuser", "testprot", "testnet", "testchat");
+            OpenBuffer();
             Enumerator();
         }
 
diff --git a/src/Engine-Tests/GitMessageBufferTests.cs b/src/Engine-Tests/GitMessageBufferTests.cs
new file mode 100644
index 0000000..0e2a2e4
--- /dev/null
+++ b/src/Engine-Tests/GitMessageBufferTests.cs
@@ -0,0 +1,53 @@
+// Smuxi - Smart MUltipleXed Irc
+// 
+// Copyright (c) 2012 Mirco Bauer <meebey meebey net>
+// 
+// Full GPL License: <http://www.gnu.org/licenses/gpl.txt>
+// 
+// This program is free software; you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation; either version 2 of the License, or
+// (at your option) any later version.
+// 
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+// 
+// You should have received a copy of the GNU General Public License
+// along with this program; if not, write to the Free Software
+// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307 USA
+using System;
+using System.IO;
+using NUnit.Framework;
+using Smuxi.Common;
+
+namespace Smuxi.Engine
+{
+    [TestFixture]
+    public class GitMessageBufferTests : MessageBufferTestsBase
+    {
+        static GitMessageBufferTests() {
+            log4net.Config.BasicConfigurator.Configure();
+        }
+
+        protected override IMessageBuffer CreateBuffer()
+        {
+            var repoPath = Path.Combine(Platform.GetBuffersPath("testuser"),
+                                        "testprot");
+            repoPath = Path.Combine(repoPath, "testnet");
+            repoPath = Path.Combine(repoPath, "testchat.git");
+            if (Directory.Exists(repoPath)) {
+                Directory.Delete(repoPath, true);
+            }
+
+            return OpenBuffer();
+        }
+
+        protected override IMessageBuffer OpenBuffer()
+        {
+            return new GitMessageBuffer("testuser", "testprot", "testnet", "testchat");
+        }
+
+    }
+}
diff --git a/src/Engine-Tests/ListMessageBufferTests.cs b/src/Engine-Tests/ListMessageBufferTests.cs
index 1605701..70b1c96 100644
--- a/src/Engine-Tests/ListMessageBufferTests.cs
+++ b/src/Engine-Tests/ListMessageBufferTests.cs
@@ -32,5 +32,10 @@ namespace Smuxi.Engine
         {
             return new ListMessageBuffer();
         }
+
+        protected override IMessageBuffer OpenBuffer()
+        {
+            return CreateBuffer();
+        }
     }
 }
diff --git a/src/Engine-Tests/MessageBufferTestsBase.cs b/src/Engine-Tests/MessageBufferTestsBase.cs
index 9537573..408c30c 100644
--- a/src/Engine-Tests/MessageBufferTestsBase.cs
+++ b/src/Engine-Tests/MessageBufferTestsBase.cs
@@ -30,6 +30,7 @@ namespace Smuxi.Engine
         protected List<MessageModel> TestMessages { get; set; }
 
         protected abstract IMessageBuffer CreateBuffer();
+        protected abstract IMessageBuffer OpenBuffer();
 
         [SetUp]
         public void SetUp()
@@ -119,6 +120,49 @@ namespace Smuxi.Engine
         }
 
         [Test]
+        public void GetRangeBenchmarkCold()
+        {
+            var builder = new MessageBuilder();
+            builder.AppendIdendityName(
+                new ContactModel("meeebey", "meebey", "netid", "netprot")
+            );
+            builder.AppendText("solange eine message aber keine url hat ist der vorteil nur gering (wenn 
ueberhaupt)");
+            var msg = builder.ToMessage();
+
+            var itemCount = 50000;
+            // generate items
+            for (int i = 0; i < itemCount; i++) {
+                Buffer.Add(new MessageModel(msg));
+            }
+            // close buffer
+            Buffer.Dispose();
+
+            int runs = 10;
+            var messageCount = 0;
+            DateTime start, stop;
+            start = DateTime.UtcNow;
+            for (int i = 0; i < runs; i++) {
+                Buffer = OpenBuffer();
+                // retrieve the newest 200 messages
+                messageCount += Buffer.GetRange(itemCount - 200, 200).Count;
+                Buffer.Dispose();
+            }
+            stop = DateTime.UtcNow;
+            Assert.AreEqual(runs * 200, messageCount);
+
+            var total = (stop - start).TotalMilliseconds;
+            Console.WriteLine(
+                "Buffer.GetRange({0}, 200): avg: {1:0.00} ms ({2:0.00} ms per item) items: {3} runs: {4} 
took: {5:0.00} ms",
+                itemCount - 200,
+                total / runs,
+                total / runs / messageCount,
+                itemCount,
+                runs,
+                total
+            );
+        }
+
+        [Test]
         public void Add()
         {
             MessageBuilder msg = new MessageBuilder();
@@ -130,6 +174,35 @@ namespace Smuxi.Engine
         }
 
         [Test]
+        public void AddBenchmark()
+        {
+            var itemCount = 50000;
+            //var itemCount = 15000;
+            DateTime start, stop;
+            start = DateTime.UtcNow;
+            for (int i = 0; i < itemCount; i++) {
+                var builder = new MessageBuilder();
+                builder.AppendIdendityName(
+                    new ContactModel("meeebey", "meebey", "netid", "netprot")
+                );
+                builder.AppendText("solange eine message aber keine url hat ist der vorteil nur gering (wenn 
ueberhaupt)");
+                var msg = builder.ToMessage();
+                Buffer.Add(msg);
+            }
+            // force flush / close
+            Buffer.Dispose();
+            stop = DateTime.UtcNow;
+
+            var total = (stop - start).TotalMilliseconds;
+            Console.WriteLine(
+                "Buffer.Add(msg): avg: {0:0.00} ms/msg items: {1} took: {2:0.00} ms",
+                total / itemCount,
+                itemCount,
+                total
+            );
+        }
+
+        [Test]
         public void Clear()
         {
             Buffer.Clear();
diff --git a/src/Engine-Tests/MessageModelTests.cs b/src/Engine-Tests/MessageModelTests.cs
index 031879b..a26d536 100644
--- a/src/Engine-Tests/MessageModelTests.cs
+++ b/src/Engine-Tests/MessageModelTests.cs
@@ -28,6 +28,27 @@ namespace Smuxi.Engine
     [TestFixture]
     public class MessageModelTests
     {
+        MessageModel SimpleMessage { get; set; }
+        MessageModel ComplexMessage { get; set; }
+
+        [TestFixtureSetUp]
+        public void SetUp()
+        {
+            var builder = new MessageBuilder();
+            builder.AppendIdendityName(
+                new ContactModel("meeebey", "meebey", "netid", "netprot")
+            );
+            builder.AppendSpace();
+            builder.AppendText("solange eine message aber keine url hat ist der vorteil nur gering (wenn 
ueberhaupt)");
+            SimpleMessage = builder.ToMessage();
+
+            var topic = "Smuxi the IRC client for sophisticated users: http://smuxi.org/ | Smuxi 0.7.2.2 
'Lovegood' released (2010-07-27) http://bit.ly/9nvsZF | FAQ: http://smuxi.org/faq/ | Deutsch? -> #smuxi.de | 
Español? -> #smuxi.es | Smuxi @ FOSDEM 2010 talk: http://bit.ly/anHJfm";;
+            var msg = new MessageModel(topic);
+            MessageParser.ParseUrls(msg);
+            msg.Compact();
+            ComplexMessage = msg;
+        }
+
         [Test]
         public void Equals()
         {
@@ -55,6 +76,37 @@ namespace Smuxi.Engine
         }
 
         [Test]
+        public void LameCopyConstructor()
+        {
+            var copiedMsg = new MessageModel(SimpleMessage);
+
+            Assert.AreNotSame(SimpleMessage, copiedMsg);
+            Assert.IsNotNull(copiedMsg.MessageParts);
+            Assert.AreNotSame(SimpleMessage.MessageParts, copiedMsg.MessageParts);
+            Assert.AreEqual(SimpleMessage, copiedMsg);
+        }
+
+        [Test]
+        public void LameCopyConstructorBenchmark()
+        {
+            int runs = 50000;
+            DateTime start, stop;
+
+            start = DateTime.UtcNow;
+            for (int i = 0; i < runs; i++) {
+                var copiedMsg = new MessageModel(SimpleMessage);
+            }
+            stop = DateTime.UtcNow;
+            var total = (stop - start).TotalMilliseconds;
+            Console.WriteLine(
+                "Ctor(): avg: {0:0.00} ms runs: {1} took: {2:0.00} ms",
+                total / runs,
+                runs,
+                total
+            );
+        }
+
+        [Test]
         public void Compact()
         {
             var msg = new MessageModel("foo bar");
@@ -210,5 +262,146 @@ namespace Smuxi.Engine
             Console.WriteLine("Compacted Parts: " + msg.MessageParts.Count);
             Console.WriteLine("Compacted Size: " + stream.Length);
         }
+
+        [Test]
+        public void BinarySerializeDeserializeBenchmark()
+        {
+        }
+
+        [Test]
+        public void ServiceStackJsonSerialize()
+        {
+            //ServiceStack.Text.JsConfig<TextColor>.SerializeFn = color => color.ToString();
+            ServiceStack.Text.JsConfig<MessagePartModel>.ExcludeTypeInfo = true;
+
+            ComplexMessage.TimeStamp = DateTime.Parse("2012-01-01T00:00:00Z").ToUniversalTime();
+            var json = ServiceStack.Text.JsonSerializer.SerializeToString(ComplexMessage);
+            //var json = ServiceStack.Text.TypeSerializer.SerializeAndFormat(TestMessage);
+            //Console.WriteLine(json);
+            Console.WriteLine(ServiceStack.Text.JsvFormatter.Format(json));
+            Assert.IsNotNull(json);
+            Assert.IsNotEmpty(json);
+            
Assert.AreEqual(@"{""TimeStamp"":""\/Date(1325376000000)\/"",""MessageParts"":[{""Type"":""Text"",""ForegroundColor"":{""Value"":-1},""BackgroundColor"":{""Value"":-1},""Underline"":false,""Bold"":false,""Italic"":false,""Text"":""Smuxi
 the IRC client for sophisticated users: 
"",""IsHighlight"":false},{""Type"":""URL"",""Url"":""http://smuxi.org/"",""Protocol"":""Http"",""ForegroundColor"":{""Value"":-1},""BackgroundColor"":{""Value"":-1},""Underline"":false,""Bold"":false,""Italic"":false,""IsHighlight"":false},{""Type"":""Text"",""ForegroundColor"":{""Value"":-1},""BackgroundColor"":{""Value"":-1},""Underline"":false,""Bold"":false,""Italic"":false,""Text"":"";
 | Smuxi 0.7.2.2 'Lovegood' released (2010-07-27) 
"",""IsHighlight"":false},{""Type"":""URL"",""Url"":""http://bit.ly/9nvsZF"",""Protocol"":""Http"",""ForegroundColor"":{""Value"":-1},""BackgroundColor"":{""Value"":-1},""Underline"":false,""Bold"":false,""Italic"":false,""IsHighlight"":false},{""Type"":""T
 
ext"",""ForegroundColor"":{""Value"":-1},""BackgroundColor"":{""Value"":-1},""Underline"":false,""Bold"":false,""Italic"":false,""Text"":""
 | FAQ: 
"",""IsHighlight"":false},{""Type"":""URL"",""Url"":""http://smuxi.org/faq/"",""Protocol"":""Http"",""ForegroundColor"":{""Value"":-1},""BackgroundColor"":{""Value"":-1},""Underline"":false,""Bold"":false,""Italic"":false,""IsHighlight"":false},{""Type"":""Text"",""ForegroundColor"":{""Value"":-1},""BackgroundColor"":{""Value"":-1},""Underline"":false,""Bold"":false,""Italic"":false,""Text"":"";
 | Deutsch? -> #smuxi.de | Español? -> #smuxi.es | Smuxi @ FOSDEM 2010 talk: 
"",""IsHighlight"":false},{""Type"":""URL"",""Url"":""http://bit.ly/anHJfm"",""Protocol"":""Http"",""ForegroundColor"":{""Value"":-1},""BackgroundColor"":{""Value"":-1},""Underline"":false,""Bold"":false,""Italic"":false,""IsHighlight"":false},{""Type"":""Text"",""ForegroundColor"":{""Value"":-1},""BackgroundColor"":{""Value"":-1},""Underline"":false,""Bold"":false
 ,""Italic"":false,""Text"":"" "",""IsHighlight"":false}],""MessageType"":""Normal""}",
+                            json);
+        }
+
+        [Test]
+        public void ServiceStackJsonSerializeBenchmark()
+        {
+            ServiceStack.Text.JsConfig<MessagePartModel>.ExcludeTypeInfo = true;
+
+            int runs = 50000;
+            DateTime start, stop;
+
+            MessageModel msg = null;
+            start = DateTime.UtcNow;
+            for (int i = 0; i < runs; i++) {
+                var json = ServiceStack.Text.JsonSerializer.SerializeToString(SimpleMessage);
+            }
+            stop = DateTime.UtcNow;
+            //Assert.AreEqual(ComplexMessage, msg);
+            var total = (stop - start).TotalMilliseconds;
+            Console.WriteLine(
+                "Serialize(Simple): avg: {0:0.00} ms runs: {1} took: {2:0.00} ms",
+                total / runs,
+                runs,
+                total
+            );
+
+            start = DateTime.UtcNow;
+            for (int i = 0; i < runs; i++) {
+                var json = ServiceStack.Text.JsonSerializer.SerializeToString(ComplexMessage);
+            }
+            stop = DateTime.UtcNow;
+            //Assert.AreEqual(ComplexMessage, msg);
+            total = (stop - start).TotalMilliseconds;
+            Console.WriteLine(
+                "Serialize(Complex): avg: {0:0.00} ms runs: {1} took: {2:0.00} ms",
+                total / runs,
+                runs,
+                total
+            );
+        }
+
+        [Test]
+        public void ServiceStackJsonSerializeDeserializeBenchmark()
+        {
+            ServiceStack.Text.JsConfig<MessagePartModel>.ExcludeTypeInfo = true;
+
+            int runs = 50000;
+            DateTime start, stop;
+
+            MessageModel msg = null;
+            start = DateTime.UtcNow;
+            for (int i = 0; i < runs; i++) {
+                var json = ServiceStack.Text.JsonSerializer.SerializeToString(ComplexMessage);
+                //msg = ServiceStack.Text.JsonSerializer.DeserializeFromString<MessageModel>(json);
+            }
+            stop = DateTime.UtcNow;
+            //Assert.AreEqual(ComplexMessage, msg);
+            var total = (stop - start).TotalMilliseconds;
+            Console.WriteLine(
+                "Serialize(): avg: {0:0.00} ms runs: {1} took: {2:0.00} ms",
+                total / runs,
+                runs,
+                total
+            );
+        }
+
+        [Test]
+        public void NewtonsoftJsonSerialize()
+        {
+            var serializer = new Newtonsoft.Json.JsonSerializer() {
+                DefaultValueHandling = Newtonsoft.Json.DefaultValueHandling.Ignore,
+                NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore,
+            };
+            var writer = new StringWriter();
+            serializer.Serialize(writer, ComplexMessage);
+            Console.WriteLine(writer.ToString());
+        }
+
+        [Test]
+        public void NewtonsoftJsonSerializeBenchmark()
+        {
+            var serializer = new Newtonsoft.Json.JsonSerializer() {
+                DefaultValueHandling = Newtonsoft.Json.DefaultValueHandling.Ignore,
+                NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore,
+            };
+
+            int runs = 50000;
+            DateTime start, stop;
+
+            MessageModel msg = null;
+            start = DateTime.UtcNow;
+            for (int i = 0; i < runs; i++) {
+                var writer = new StringWriter();
+                serializer.Serialize(writer, SimpleMessage);
+                var json = writer.ToString();
+            }
+            stop = DateTime.UtcNow;
+            //Assert.AreEqual(ComplexMessage, msg);
+            var total = (stop - start).TotalMilliseconds;
+            Console.WriteLine(
+                "Serialize(SimpleMessage): avg: {0:0.00} ms runs: {1} took: {2:0.00} ms",
+                total / runs,
+                runs,
+                total
+            );
+
+            start = DateTime.UtcNow;
+            for (int i = 0; i < runs; i++) {
+                var writer = new StringWriter();
+                serializer.Serialize(writer, ComplexMessage);
+            }
+            stop = DateTime.UtcNow;
+            //Assert.AreEqual(ComplexMessage, msg);
+            total = (stop - start).TotalMilliseconds;
+            Console.WriteLine(
+                "Serialize(ComplexMessage): avg: {0:0.00} ms runs: {1} took: {2:0.00} ms",
+                total / runs,
+                runs,
+                total
+            );
+        }
     }
 }
diff --git a/src/Engine/MessageBuffers/Db4oMessageBuffer.cs b/src/Engine/MessageBuffers/Db4oMessageBuffer.cs
index 138621c..5ad3cef 100644
--- a/src/Engine/MessageBuffers/Db4oMessageBuffer.cs
+++ b/src/Engine/MessageBuffers/Db4oMessageBuffer.cs
@@ -42,7 +42,6 @@ namespace Smuxi.Engine
         IObjectContainer Database { get; set; }
         string           DatabaseFile { get; set; }
         bool             IsEmptyDatabase { get; set; }
-        string           SessionUsername { get; set; }
         bool             AggressiveGC { get; set; }
 #if DB4O_8_0
         IEmbeddedConfiguration DatabaseConfiguration { get; set; }
@@ -73,43 +72,23 @@ namespace Smuxi.Engine
             }
         }
 
-        private Db4oMessageBuffer()
-        {
-            FlushInterval = DefaultFlushInterval;
-        }
-
         public Db4oMessageBuffer(string sessionUsername, string protocol,
-                                 string networkId, string chatId) : this()
+                                 string networkId, string chatId) :
+                            base(sessionUsername, protocol, networkId, chatId)
         {
-            if (sessionUsername == null) {
-                throw new ArgumentNullException("sessionUsername");
-            }
-            if (protocol == null) {
-                throw new ArgumentNullException("protocol");
-            }
-            if (networkId == null) {
-                throw new ArgumentNullException("networkId");
-            }
-            if (chatId == null) {
-                throw new ArgumentNullException("chatId");
-            }
-
-            SessionUsername = sessionUsername;
-            Protocol = protocol;
-            NetworkID = networkId;
-            ChatID = chatId;
-
+            FlushInterval = DefaultFlushInterval;
             AggressiveGC = true;
             DatabaseFile = GetDatabaseFile();
             InitDatabase();
         }
 
-        public Db4oMessageBuffer(string dbPath) : this()
+        public Db4oMessageBuffer(string dbPath)
         {
             if (dbPath == null) {
                 throw new ArgumentNullException("dbPath");
             }
 
+            FlushInterval = DefaultFlushInterval;
             DatabaseFile = dbPath;
             InitDatabase();
         }
@@ -375,21 +354,7 @@ namespace Smuxi.Engine
 
         string GetDatabaseFile()
         {
-            var dbPath = Platform.GetBuffersPath(SessionUsername);
-            var protocol = Protocol.ToLower();
-            var network = NetworkID.ToLower();
-            dbPath = Path.Combine(dbPath, protocol);
-            if (network != protocol) {
-                dbPath = Path.Combine(dbPath, network);
-            }
-            dbPath = IOSecurity.GetFilteredPath(dbPath);
-            if (!Directory.Exists(dbPath)) {
-                Directory.CreateDirectory(dbPath);
-            }
-
-            var chatId = IOSecurity.GetFilteredFileName(ChatID.ToLower());
-            dbPath = Path.Combine(dbPath, String.Format("{0}.db4o", chatId));
-            return dbPath;
+            return String.Format("{0}.db4o", GetBufferPath());
         }
 
         void OpenDatabase()
diff --git a/src/Engine/MessageBuffers/GitMessageBuffer.cs b/src/Engine/MessageBuffers/GitMessageBuffer.cs
new file mode 100644
index 0000000..7debc76
--- /dev/null
+++ b/src/Engine/MessageBuffers/GitMessageBuffer.cs
@@ -0,0 +1,245 @@
+// Smuxi - Smart MUltipleXed Irc
+// 
+// Copyright (c) 2012 Mirco Bauer <meebey meebey net>
+// 
+// Full GPL License: <http://www.gnu.org/licenses/gpl.txt>
+// 
+// This program is free software; you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation; either version 2 of the License, or
+// (at your option) any later version.
+// 
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+// 
+// You should have received a copy of the GNU General Public License
+// along with this program; if not, write to the Free Software
+// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307 USA
+using System;
+using System.IO;
+using System.Text;
+using System.Threading;
+using ServiceStack.Text;
+using LibGit2Sharp;
+using Smuxi.Common;
+
+namespace Smuxi.Engine
+{
+    public class GitMessageBuffer : MessageBufferBase
+    {
+#if LOG4NET
+        static readonly log4net.ILog f_Logger = 
log4net.LogManager.GetLogger(System.Reflection.MethodBase.GetCurrentMethod().DeclaringType);
+#endif
+        Int64 f_MessageNumber = -1;
+        Repository Repository { get; set; }
+        string RepositoryPath { get; set; }
+        TimeSpan CommitInterval { get; set; }
+        Timer CommitTimer { get; set; }
+        StringBuilder CommitMessage { get; set; }
+
+        Int64 MessageNumber {
+            get {
+                if (f_MessageNumber == -1) {
+                    var msgNumber = 0L;
+                    var reference = Repository.Refs["refs/heads/master"] as DirectReference;
+                    if (reference != null) {
+                        var commit = reference.Target as Commit;
+                        foreach (var treeEntry in commit.Tree) {
+                            var filename = treeEntry.Name;
+                            var strNumber = filename.Substring(0, filename.IndexOf("."));
+                            var intNumber = 0L;
+                            Int64.TryParse(strNumber, out intNumber);
+                            if (intNumber > msgNumber) {
+                                msgNumber = intNumber;
+                            }
+                        }
+                    }
+                    f_MessageNumber = msgNumber;
+                }
+                return f_MessageNumber;
+            }
+            set {
+                f_MessageNumber = value;
+            }
+        }
+
+        static GitMessageBuffer() {
+            JsConfig<MessagePartModel>.ExcludeTypeInfo = true;
+        }
+
+        public GitMessageBuffer(string sessionUsername, string protocol,
+                                string networkId, string chatId) :
+                           base(sessionUsername, protocol, networkId, chatId)
+        {
+            var bufferPath = GetBufferPath();
+            RepositoryPath = bufferPath + ".git";
+            if (!Directory.Exists(RepositoryPath)) {
+                Repository.Init(RepositoryPath, false);
+            }
+            Repository = new Repository(RepositoryPath);
+
+            CommitMessage = new StringBuilder(1024);
+            CommitInterval = TimeSpan.FromMinutes(1);
+            CommitTimer = new Timer(delegate { Flush(); }, null,
+                                    CommitInterval, CommitInterval);
+        }
+
+        public override void Add(MessageModel msg)
+        {
+            if (msg == null) {
+                throw new ArgumentNullException("msg");
+            }
+
+            /*
+            if (MaxCapacity > 0 && Index.Count >= MaxCapacity) {
+                RemoveAt(0);
+            }
+            */
+
+            var msgFileName = String.Format("{0}.v1.json", ++MessageNumber);
+            var msgFilePath = Path.Combine(RepositoryPath, msgFileName);
+            using (var writer = File.OpenWrite(msgFilePath))
+            using (var textWriter = new StreamWriter(writer, Encoding.UTF8)) {
+                //JsonSerializer.SerializeToStream(msg, writer);
+                JsonSerializer.SerializeToWriter(msg, textWriter);
+            }
+
+            DateTime start, stop;
+            lock (Repository) {
+                start = DateTime.UtcNow;
+                var index = Repository.Index;
+                //index.Stage(msgFilePath);
+                index.AddToIndex(msgFileName);
+                stop = DateTime.UtcNow;
+            }
+            f_Logger.DebugFormat("Add(): Index.AddToIndex() took: {0:0.00} ms",
+                                 (stop - start).TotalMilliseconds);
+
+            File.Delete(msgFilePath);
+            CommitMessage.Append(
+                String.Format("{0}: {1}\n", msgFileName, msg.ToString())
+            );
+
+            // TODO: create tree, commit tree, repack repo reguraly (see rugged docs)
+        }
+
+        #region implemented abstract members of Smuxi.Engine.MessageBufferBase
+        public override int Count {
+            get {
+                var repo = Repository;
+                var count = repo.Index.Count;
+                var reference = repo.Refs["refs/heads/master"] as DirectReference;
+                if (reference == null) {
+                    return count;
+                }
+                var commit = reference.Target as Commit;
+                if (commit == null) {
+                    return count;
+                }
+                count += commit.Tree.Count;
+                return count;
+            }
+        }
+
+        public override MessageModel this[int index] {
+            get {
+                throw new NotImplementedException ();
+            }
+            set {
+                throw new NotImplementedException ();
+            }
+        }
+
+        public override void Clear ()
+        {
+            throw new NotImplementedException ();
+        }
+
+        public override bool Contains (MessageModel item)
+        {
+            throw new NotImplementedException ();
+        }
+
+        public override void CopyTo (MessageModel[] array, int arrayIndex)
+        {
+            throw new NotImplementedException ();
+        }
+
+        public override bool Remove (MessageModel item)
+        {
+            throw new NotImplementedException ();
+        }
+
+        public override System.Collections.Generic.IEnumerator<MessageModel> GetEnumerator ()
+        {
+            throw new NotImplementedException ();
+        }
+
+        public override int IndexOf (MessageModel item)
+        {
+            throw new NotImplementedException ();
+        }
+
+        public override void Insert (int index, MessageModel item)
+        {
+            throw new NotImplementedException ();
+        }
+
+        public override void RemoveAt (int index)
+        {
+            throw new NotImplementedException ();
+        }
+
+        public override System.Collections.Generic.IList<MessageModel> GetRange (int offset, int limit)
+        {
+            throw new NotImplementedException ();
+        }
+
+        public override void Dispose()
+        {
+            Flush();
+
+            var repo = Repository;
+            if (repo != null) {
+                Repository = null;
+                repo.Dispose();
+            }
+        }
+        #endregion
+
+        void Flush()
+        {
+            Trace.Call();
+
+            var repo = Repository;
+            if (repo == null) {
+                return;
+            }
+            lock (repo) {
+                if (repo.Index.Count == 0 || CommitMessage.Length == 0) {
+                    // nothing to commit
+                    return;
+                }
+
+                DateTime start, stop;
+
+                start = DateTime.UtcNow;
+                repo.Index.UpdatePhysicalIndex();
+                stop = DateTime.UtcNow;
+                f_Logger.DebugFormat("Commit(): Repository.Index.UpdatePhysicalIndex() took: {0:0.00} ms",
+                                     (stop - start).TotalMilliseconds);
+
+                start = DateTime.UtcNow;
+                // FIXME: CommitMessage is not thread-safe!
+                repo.Commit(CommitMessage.ToString(), false);
+                stop = DateTime.UtcNow;
+                f_Logger.DebugFormat("Commit(): Repository.Commit() took: {0:0.00} ms",
+                                     (stop - start).TotalMilliseconds);
+
+                CommitMessage.Clear();
+            }
+        }
+    }
+}
diff --git a/src/Engine/MessageBuffers/MessageBufferBase.cs b/src/Engine/MessageBuffers/MessageBufferBase.cs
index 4ca9793..144effb 100644
--- a/src/Engine/MessageBuffers/MessageBufferBase.cs
+++ b/src/Engine/MessageBuffers/MessageBufferBase.cs
@@ -19,6 +19,7 @@
 // Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307 USA
 
 using System;
+using System.IO;
 using System.Collections;
 using System.Collections.Generic;
 using Smuxi.Common;
@@ -30,6 +31,7 @@ namespace Smuxi.Engine
         protected string Protocol { get; set; }
         protected string NetworkID { get; set; }
         protected string ChatID { get; set; }
+        protected string SessionUsername { get; set; }
         public    int    MaxCapacity { get; set; }
 
         public bool IsReadOnly {
@@ -42,11 +44,50 @@ namespace Smuxi.Engine
         {
         }
 
+        protected MessageBufferBase(string sessionUsername, string protocol,
+                                    string networkId, string chatId)
+        {
+            if (sessionUsername == null) {
+                throw new ArgumentNullException("sessionUsername");
+            }
+            if (protocol == null) {
+                throw new ArgumentNullException("protocol");
+            }
+            if (networkId == null) {
+                throw new ArgumentNullException("networkId");
+            }
+            if (chatId == null) {
+                throw new ArgumentNullException("chatId");
+            }
+
+            SessionUsername = sessionUsername;
+            Protocol = protocol;
+            NetworkID = networkId;
+            ChatID = chatId;
+        }
+
         IEnumerator IEnumerable.GetEnumerator()
         {
             return GetEnumerator();
         }
 
+        protected string GetBufferPath()
+        {
+            var path = Platform.GetBuffersPath(SessionUsername);
+            var protocol = Protocol.ToLower();
+            var network = NetworkID.ToLower();
+            path = Path.Combine(path, protocol);
+            if (network != protocol) {
+                path = Path.Combine(path, network);
+            }
+            path = IOSecurity.GetFilteredPath(path);
+            if (!Directory.Exists(path)) {
+                Directory.CreateDirectory(path);
+            }
+            var chatId = IOSecurity.GetFilteredFileName(ChatID.ToLower());
+            return Path.Combine(path, chatId);
+        }
+
         public abstract int Count { get; }
         public abstract MessageModel this[int index] { get; set; }
 
diff --git a/src/Engine/Messages/ImageMessagePartModel.cs b/src/Engine/Messages/ImageMessagePartModel.cs
index 3e9df19..d76796a 100644
--- a/src/Engine/Messages/ImageMessagePartModel.cs
+++ b/src/Engine/Messages/ImageMessagePartModel.cs
@@ -34,6 +34,7 @@ using Smuxi.Common;
 namespace Smuxi.Engine
 {
     [Serializable]
+    [DataContract]
     public class ImageMessagePartModel : MessagePartModel
     {
 #if LOG4NET
@@ -41,7 +42,15 @@ namespace Smuxi.Engine
 #endif
         private string f_ImageFileName;
         private string f_AlternativeText;
-        
+
+        [DataMember]
+        public override string Type {
+            get {
+                return "Image";
+            }
+        }
+
+        [DataMember]
         public string ImageFileName {
             get {
                 return f_ImageFileName;
@@ -51,6 +60,7 @@ namespace Smuxi.Engine
             }
         }
         
+        [DataMember]
         public string AlternativeText {
             get {
                 return f_AlternativeText;
diff --git a/src/Engine/Messages/MessageModel.cs b/src/Engine/Messages/MessageModel.cs
index 4fb360a..679624c 100644
--- a/src/Engine/Messages/MessageModel.cs
+++ b/src/Engine/Messages/MessageModel.cs
@@ -31,6 +31,7 @@ using Smuxi.Common;
 namespace Smuxi.Engine
 {
     [Serializable]
+    [DataContract]
     public class MessageModel : ISerializable
     {
         static readonly Regex NickRegex = new Regex("^<([^ ]+)> ");
@@ -40,6 +41,7 @@ namespace Smuxi.Engine
         [NonSerialized]
         private bool                    f_IsCompactable;
 
+        [DataMember]
         public DateTime TimeStamp {
             get {
                 return f_TimeStamp;
@@ -48,19 +50,22 @@ namespace Smuxi.Engine
                 f_TimeStamp = value;
             }
         }
-        
+
+        [DataMember]
         public IList<MessagePartModel> MessageParts {
             get {
                 return f_MessageParts;
             }
         }
 
+        [IgnoreDataMember]
         public bool IsEmpty {
             get {
                 return f_MessageParts.Count == 0;
             }
         }
         
+        [DataMember]
         public MessageType MessageType {
             get {
                 return f_MessageType;
@@ -70,6 +75,7 @@ namespace Smuxi.Engine
             }
         }
         
+        [IgnoreDataMember]
         public bool IsCompactable {
             get {
                 return f_IsCompactable;
diff --git a/src/Engine/Messages/MessagePartModel.cs b/src/Engine/Messages/MessagePartModel.cs
index 85861e5..7f91f8d 100644
--- a/src/Engine/Messages/MessagePartModel.cs
+++ b/src/Engine/Messages/MessagePartModel.cs
@@ -33,10 +33,17 @@ using Smuxi.Common;
 namespace Smuxi.Engine
 {
     [Serializable]
+    [DataContract]
     public abstract class MessagePartModel : ISerializable
     {
         private bool                     f_IsHighlight;
         
+        [DataMember]
+        public abstract string Type {
+             get;
+        }
+
+        [DataMember]
         public bool IsHighlight {
             get {
                 return f_IsHighlight;
@@ -45,7 +52,7 @@ namespace Smuxi.Engine
                 f_IsHighlight = value;
             }
         }
-        
+
         protected MessagePartModel()
         {
         }
diff --git a/src/Engine/Messages/MessageType.cs b/src/Engine/Messages/MessageType.cs
index 41ad96e..14e09dc 100644
--- a/src/Engine/Messages/MessageType.cs
+++ b/src/Engine/Messages/MessageType.cs
@@ -27,9 +27,11 @@
  */
 
 using System;
+using System.Runtime.Serialization;
 
 namespace Smuxi.Engine
 {
+    [DataContract]
     public enum MessageType
     {
         Normal,
diff --git a/src/Engine/Messages/TextMessagePartModel.cs b/src/Engine/Messages/TextMessagePartModel.cs
index 85c9329..131b50f 100644
--- a/src/Engine/Messages/TextMessagePartModel.cs
+++ b/src/Engine/Messages/TextMessagePartModel.cs
@@ -33,6 +33,7 @@ using Smuxi.Common;
 namespace Smuxi.Engine
 {
     [Serializable]
+    [DataContract]
     public class TextMessagePartModel : MessagePartModel
     {
         private TextColor f_ForegroundColor;
@@ -41,7 +42,15 @@ namespace Smuxi.Engine
         private bool      f_Bold;
         private bool      f_Italic;
         private string    f_Text;
-        
+
+        [DataMember]
+        public override string Type {
+            get {
+                return "Text";
+            }
+        }
+
+        [DataMember]
         public TextColor ForegroundColor {
             get {
                 return f_ForegroundColor;
@@ -54,6 +63,7 @@ namespace Smuxi.Engine
             }
         }
         
+        [DataMember]
         public TextColor BackgroundColor {
             get {
                 return f_BackgroundColor;
@@ -66,6 +76,7 @@ namespace Smuxi.Engine
             }
         }
         
+        [DataMember]
         public bool Underline {
             get {
                 return f_Underline;
@@ -75,6 +86,7 @@ namespace Smuxi.Engine
             }
         }
         
+        [DataMember]
         public bool Bold {
             get {
                 return f_Bold;
@@ -84,6 +96,7 @@ namespace Smuxi.Engine
             }
         }
         
+        [DataMember]
         public bool Italic {
             get {
                 return f_Italic;
@@ -93,6 +106,7 @@ namespace Smuxi.Engine
             }
         }
         
+        [DataMember]
         public string Text {
             get {
                 return f_Text;
@@ -102,6 +116,7 @@ namespace Smuxi.Engine
             }
         }
 
+        [IgnoreDataMember]
         public int Length {
             get {
                 if (f_Text == null) {
diff --git a/src/Engine/Messages/UrlMessagePartModel.cs b/src/Engine/Messages/UrlMessagePartModel.cs
index e738e2d..0a787ee 100644
--- a/src/Engine/Messages/UrlMessagePartModel.cs
+++ b/src/Engine/Messages/UrlMessagePartModel.cs
@@ -33,6 +33,7 @@ using Smuxi.Common;
 
 namespace Smuxi.Engine
 {
+    [DataContract]
     public enum UrlProtocol {
         None,
         Unknown,
@@ -46,13 +47,22 @@ namespace Smuxi.Engine
     }
     
     [Serializable]
+    [DataContract]
     public class UrlMessagePartModel : TextMessagePartModel
     {
 #if LOG4NET
         private static readonly log4net.ILog _Logger = 
log4net.LogManager.GetLogger(System.Reflection.MethodBase.GetCurrentMethod().DeclaringType);
 #endif
         private string      _Url;
-        
+
+        [DataMember]
+        public override string Type {
+            get {
+                return "URL";
+            }
+        }
+
+        [DataMember]
         public string Url {
             get {
                 if (_Url == null) {
diff --git a/src/Engine/TextColor.cs b/src/Engine/TextColor.cs
index 2f89a27..600791e 100644
--- a/src/Engine/TextColor.cs
+++ b/src/Engine/TextColor.cs
@@ -34,6 +34,7 @@ using Smuxi.Common;
 namespace Smuxi.Engine
 {
     [Serializable]
+    [DataContract]
     public class TextColor : ISerializable
     {
         public static readonly TextColor None  = new TextColor();
@@ -42,7 +43,8 @@ namespace Smuxi.Engine
         public static readonly TextColor Grey  = new TextColor(128, 128, 128);
         
         private int f_Value;
-        
+
+        [DataMember]
         public int Value {
             get {
                 return f_Value;
@@ -52,24 +54,28 @@ namespace Smuxi.Engine
             }
         }
         
+        [IgnoreDataMember]
         public string HexCode {
             get {
                 return f_Value.ToString("X6");
             }
         }
         
+        [IgnoreDataMember]
         public byte Red {
             get {
                 return (byte) ((f_Value & 0xFF0000) >> 16);
             }
         }
         
+        [IgnoreDataMember]
         public byte Green {
             get {
                 return (byte) ((f_Value & 0xFF00) >> 8);
             }
         }
         
+        [IgnoreDataMember]
         public byte Blue {
             get {
                 return (byte) (f_Value & 0xFF);



[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]