commit 1dbc0576202427237578d5eb930fc7d746147706 Author: Harald Wolff-Thobaben Date: Fri Jun 3 13:07:13 2022 +0200 BsonDocumentStorage et al diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..add57be --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +bin/ +obj/ +/packages/ +riderModule.iml +/_ReSharper.Caches/ \ No newline at end of file diff --git a/.idea/.idea.ln.bson/.idea/.gitignore b/.idea/.idea.ln.bson/.idea/.gitignore new file mode 100644 index 0000000..65e96b6 --- /dev/null +++ b/.idea/.idea.ln.bson/.idea/.gitignore @@ -0,0 +1,13 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Rider ignored files +/contentModel.xml +/projectSettingsUpdater.xml +/modules.xml +/.idea.ln.bson.iml +# Editor-based HTTP Client requests +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/.idea/.idea.ln.bson/.idea/encodings.xml b/.idea/.idea.ln.bson/.idea/encodings.xml new file mode 100644 index 0000000..df87cf9 --- /dev/null +++ b/.idea/.idea.ln.bson/.idea/encodings.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/.idea/.idea.ln.bson/.idea/indexLayout.xml b/.idea/.idea.ln.bson/.idea/indexLayout.xml new file mode 100644 index 0000000..7b08163 --- /dev/null +++ b/.idea/.idea.ln.bson/.idea/indexLayout.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/.idea.ln.bson/.idea/vcs.xml b/.idea/.idea.ln.bson/.idea/vcs.xml new file mode 100644 index 0000000..94a25f7 --- /dev/null +++ b/.idea/.idea.ln.bson/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/ln.bson.sln b/ln.bson.sln new file mode 100644 index 0000000..489120c --- /dev/null +++ b/ln.bson.sln @@ -0,0 +1,34 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ln.bson", "ln.bson\ln.bson.csproj", "{F01BEC8F-5554-4738-B200-C86547FE3464}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ln.bson.tests", "ln.bson.tests\ln.bson.tests.csproj", "{D8C856D0-BA32-47A4-A334-71C9A1C66B07}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ln.bson.storage", "ln.bson.storage\ln.bson.storage.csproj", "{FEB2C12B-7A0F-468C-9063-980FC4A4B8BE}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ln.collections", "..\ln.collections\ln.collections\ln.collections.csproj", "{168FA1B8-0C0D-4AAE-B026-F1A736027C03}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {F01BEC8F-5554-4738-B200-C86547FE3464}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F01BEC8F-5554-4738-B200-C86547FE3464}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F01BEC8F-5554-4738-B200-C86547FE3464}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F01BEC8F-5554-4738-B200-C86547FE3464}.Release|Any CPU.Build.0 = Release|Any CPU + {D8C856D0-BA32-47A4-A334-71C9A1C66B07}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D8C856D0-BA32-47A4-A334-71C9A1C66B07}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D8C856D0-BA32-47A4-A334-71C9A1C66B07}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D8C856D0-BA32-47A4-A334-71C9A1C66B07}.Release|Any CPU.Build.0 = Release|Any CPU + {FEB2C12B-7A0F-468C-9063-980FC4A4B8BE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {FEB2C12B-7A0F-468C-9063-980FC4A4B8BE}.Debug|Any CPU.Build.0 = Debug|Any CPU + {FEB2C12B-7A0F-468C-9063-980FC4A4B8BE}.Release|Any CPU.ActiveCfg = Release|Any CPU + {FEB2C12B-7A0F-468C-9063-980FC4A4B8BE}.Release|Any CPU.Build.0 = Release|Any CPU + {168FA1B8-0C0D-4AAE-B026-F1A736027C03}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {168FA1B8-0C0D-4AAE-B026-F1A736027C03}.Debug|Any CPU.Build.0 = Debug|Any CPU + {168FA1B8-0C0D-4AAE-B026-F1A736027C03}.Release|Any CPU.ActiveCfg = Release|Any CPU + {168FA1B8-0C0D-4AAE-B026-F1A736027C03}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal diff --git a/ln.bson.sln.DotSettings.user b/ln.bson.sln.DotSettings.user new file mode 100644 index 0000000..7f9cf57 --- /dev/null +++ b/ln.bson.sln.DotSettings.user @@ -0,0 +1,6 @@ + + <SessionState ContinuousTestingMode="0" IsActive="True" Name="Test_Indeces" xmlns="urn:schemas-jetbrains-com:jetbrains-ut-session"> + <TestAncestor> + <TestId>NUnit3x::D8C856D0-BA32-47A4-A334-71C9A1C66B07::net5.0::ln.bson.tests.Tests.Test_Indeces</TestId> + </TestAncestor> +</SessionState> \ No newline at end of file diff --git a/ln.bson.storage/BsonDocumentStorage.cs b/ln.bson.storage/BsonDocumentStorage.cs new file mode 100644 index 0000000..dfa77fc --- /dev/null +++ b/ln.bson.storage/BsonDocumentStorage.cs @@ -0,0 +1,361 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.IO; +using ln.bson.storage.index; +using ln.collections; +using ln.type; + +namespace ln.bson.storage +{ + public class BsonDocumentStorageConfiguration + { + private bool _sealed; + public string BaseFileName { get; } + + private Dictionary> indeces = new Dictionary>(); + public IEnumerable>> Indeces => indeces; + + private Action _logTarget; + public Action LogTarget => _logTarget; + + public BsonDocumentStorageConfiguration(string baseFileName) + { + BaseFileName = baseFileName; + _logTarget = (s) => { }; + } + + public BsonDocumentStorageConfiguration Index(string path) => Index(path, BsonIndex.CompareBytesLittleEndian); + public BsonDocumentStorageConfiguration Index(string path, Comparison comparison) + { + if (_sealed) + throw new ApplicationException(); + + indeces.Add(path, comparison); + return this; + } + + public BsonDocumentStorageConfiguration SetLogTarget(Action logTarget) + { + if (_sealed) + throw new ApplicationException(); + _logTarget = logTarget; + return this; + } + + internal BsonDocumentStorageConfiguration Seal() + { + _sealed = true; + return this; + } + } + + public class BsonDocumentStorage : IEnumerable, IDisposable + { + private BsonDocumentStorageConfiguration _configuration; + private ByteArrayFileStorage _fileStorage; + + private WeakValueDictionary _documentCache = new WeakValueDictionary(); + private WeakKeyDictionary _reverseCache = new WeakKeyDictionary(); + + private IndexTree _indexTree = new IndexTree(); + + public BsonDocumentStorageConfiguration Configuration => _configuration; + + public BsonDocumentStorage(string baseFileName) : this(new BsonDocumentStorageConfiguration(baseFileName)) + { + } + + public BsonDocumentStorage(BsonDocumentStorageConfiguration configuration) + { + _configuration = configuration.Seal(); + _fileStorage = new ByteArrayFileStorage(_configuration.BaseFileName); + + SetupIndeces(); + } + + public void Close() + { + lock (this) + { + SaveIndeces(); + _fileStorage.Close(); + } + } + + public BsonDocument LoadDocument(long documentOffset) + { + if (TryLoadDocument(documentOffset, out BsonDocument bsonDocument)) + return bsonDocument; + throw new KeyNotFoundException(); + } + public bool TryLoadDocument(long documentOffset, out BsonDocument bsonDocument) + { + lock (this) + { + if (TryGetCachedDocument(documentOffset, out bsonDocument)) + return true; + + if (_fileStorage.TryLoadDocumentBytes(documentOffset, out byte[] documentBytes)) + { + bsonDocument = BsonReader.ReadDocument(documentBytes); + TryAddOrUpdateCachedDocument(documentOffset, bsonDocument); + return true; + } + } + + return false; + } + + /** + * Save Document to Storage + * If bsonDocument originated from this store, replaces this document + * If bsonDocument is unknown to this store, the document is added + */ + public bool SaveDocument(BsonDocument bsonDocument) + { + lock (this) + { + TryGetCachedDocumentOffset(bsonDocument, out long documentOffset); + byte[] documentBytes = bsonDocument.GetBytes(); + if (_fileStorage.TrySaveDocumentBytes(documentBytes, out long newDocumentOffset)) + { + if (documentOffset != 0) + _documentCache.Remove(documentOffset); + + _reverseCache[bsonDocument] = newDocumentOffset; + _documentCache.Add(newDocumentOffset, bsonDocument); + + if (documentOffset != 0) + _indexTree.ReplaceDocument(documentOffset, newDocumentOffset, bsonDocument); + else + _indexTree.AddDocument(newDocumentOffset, bsonDocument); + + return true; + } + } + + return false; + } + + /** + * Remove bsonDocument from the storage + * Returns true if document has been successfully removed + * Returns false if document is not known to storage or can't be removed successfully + */ + public bool DeleteDocument(BsonDocument bsonDocument) + { + lock (this) + { + if (TryGetCachedDocumentOffset(bsonDocument, out long documentOffset) && + _fileStorage.Remove(documentOffset)) + { + _documentCache.Remove(documentOffset); + _reverseCache.Remove(bsonDocument); + _indexTree.RemoveDocument(documentOffset); + return true; + } + } + + return false; + } + + + /* Document Cache */ + + public bool TryGetCachedDocument(long documentOffset, out BsonDocument cachedDocument) => + _documentCache.TryGetValue(documentOffset, out cachedDocument); + + public bool TryGetCachedDocumentOffset(BsonDocument bsonDocument, out long documentOffset) => + _reverseCache.TryGetValue(bsonDocument, out documentOffset); + + public bool TryAddOrUpdateCachedDocument(long documentOffset, BsonDocument bsonDocument) + { + if (_documentCache.TryGetValue(documentOffset, out BsonDocument cachedDocument)) + { + _reverseCache.Remove(cachedDocument); + _documentCache.Remove(documentOffset); + } + _documentCache.Add(documentOffset, bsonDocument); + _reverseCache.Add(bsonDocument, documentOffset); + + return true; + } + + /* End Document Cache */ + + /* Index methods */ + + private void SetupIndeces() + { + bool rebuildNeeded = false; + + foreach (var indexConfiguration in _configuration.Indeces) + { + if (!_indexTree.EnsureIndex(indexConfiguration.Key, indexConfiguration.Value, out IBsonIndex bsonIndex)) + throw new ArgumentOutOfRangeException("duplicate index path"); + + string indexFileName = + String.Format("{0}.{1}.idx", _configuration.BaseFileName, indexConfiguration.Key); + + if (rebuildNeeded || !File.Exists(indexFileName) || !LoadIndexFile(indexFileName, bsonIndex)) + rebuildNeeded = true; + } + + if (rebuildNeeded) + RebuildIndeces(); + } + + void RebuildIndeces() + { + _configuration.LogTarget("rebuilding indeces"); + _indexTree.Clear(); + + foreach (BsonDocument bsonDocument in this) + { + if (!_reverseCache.TryGetValue(bsonDocument, out long documentOffset)) + throw new ApplicationException("serious bug! no document offset"); + _indexTree.AddDocument(documentOffset, bsonDocument); + } + } + + private void UpdateIndex(BsonDocument bsonDocument) + { + if (TryGetCachedDocumentOffset(bsonDocument, out long documentOffset)) + UpdateIndex(bsonDocument, documentOffset); + else + throw new KeyNotFoundException(); + } + + private void UpdateIndex(BsonDocument bsonDocument, long documentOffset) + { + _indexTree.AddDocument(documentOffset, bsonDocument); + } + + public IEnumerable Query(String path, QueryOperator op, BsonValue value) + { + HashSet resultsSet = new HashSet(); + _indexTree.Query(path, op, value.GetBytes(), resultsSet); + return new BsonDocumentEnumerable(() => new BsonDocumentEnumerator(this, resultsSet.GetEnumerator())); + } + + private void SaveIndeces() + { + _configuration.LogTarget("saving indeces"); + + foreach (var indexConfiguration in _configuration.Indeces) + { + string indexFileName = + String.Format("{0}.{1}.idx", _configuration.BaseFileName, indexConfiguration.Key); + + if (_indexTree.EnsureIndex(indexConfiguration.Key, indexConfiguration.Value, out IBsonIndex bsonIndex)) + throw new ArgumentOutOfRangeException("unknown index path"); + + if (!SaveIndexFile(indexFileName, bsonIndex)) + throw new Exception(String.Format("Failed to save index {0} to file {1}", indexConfiguration.Key, indexFileName)); + } + } + + bool LoadIndexFile(string indexFileName, IBsonIndex index) + { + try + { + using (FileStream indexFile = new FileStream(indexFileName, FileMode.Open, FileAccess.Read)) + { + Guid indexGuid = new Guid(indexFile.ReadBytes(16)); + if (!indexGuid.Equals(_fileStorage.CurrentGuid)) + return false; + + while (indexFile.Position < indexFile.Length) + { + long o = indexFile.ReadLong(); + int l = indexFile.ReadInteger(); + byte[] v = indexFile.ReadBytes(l); + index.AddValue(o, v); + } + } + return true; + } + catch (Exception) + { + return false; + } + } + + bool SaveIndexFile(string indexFileName, IBsonIndex index) + { + using (FileStream indexFile = new FileStream(indexFileName, FileMode.Create, FileAccess.Write)) + { + indexFile.WriteBytes(_fileStorage.CurrentGuid.ToByteArray()); + + foreach (var ie in index) + { + indexFile.WriteLong(ie.Key); + indexFile.WriteInteger(ie.Value.Length); + indexFile.WriteBytes(ie.Value); + } + indexFile.Flush(); + indexFile.Close(); + } + return true; + } + + + /* End Index methods */ + + public void Dispose() + { + Close(); + + _fileStorage?.Dispose(); + _fileStorage = null; + } + + public class BsonDocumentEnumerable : IEnumerable + { + private Func> _getenumerator; + + public BsonDocumentEnumerable(Func> getenumerator) + { + _getenumerator = getenumerator; + } + + public IEnumerator GetEnumerator() => _getenumerator(); + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + } + + public IEnumerator GetEnumerator() => new BsonDocumentEnumerator(this); + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + class BsonDocumentEnumerator : IEnumerator + { + private BsonDocumentStorage _documentStorage; + private IEnumerator OffsetEnumerator; + + public BsonDocumentEnumerator(BsonDocumentStorage documentStorage) + { + _documentStorage = documentStorage; + OffsetEnumerator = _documentStorage._fileStorage.Offsets.GetEnumerator(); + } + + public BsonDocumentEnumerator(BsonDocumentStorage documentStorage, IEnumerator offsetEnumerator) + { + _documentStorage = documentStorage; + OffsetEnumerator = offsetEnumerator; + } + + public bool MoveNext() => OffsetEnumerator.MoveNext(); + public void Reset() => OffsetEnumerator.Reset(); + + public BsonDocument Current => _documentStorage.LoadDocument(OffsetEnumerator.Current); + object IEnumerator.Current => Current; + + public void Dispose() + { + OffsetEnumerator?.Dispose(); + OffsetEnumerator = null; + } + } + + + } +} \ No newline at end of file diff --git a/ln.bson.storage/BsonFileMapper.cs b/ln.bson.storage/BsonFileMapper.cs new file mode 100644 index 0000000..1b515ff --- /dev/null +++ b/ln.bson.storage/BsonFileMapper.cs @@ -0,0 +1,86 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using ln.bson.mapper; + +namespace ln.bson.storage +{ + /* + public class BsonFileMapper : IEnumerable,IDisposable + { + public ByteArrayFileStorage FileStorage { get; } + public BsonMapper BsonMapper { get; } + + private bool DisposeFileStorage; + + public BsonFileMapper(string filename) : this(new ByteArrayFileStorage(filename), BsonMapper.DefaultInstance, true) { } + public BsonFileMapper(string filename, BsonMapper bsonMapper) : this(new ByteArrayFileStorage(filename), bsonMapper, true) { } + + public BsonFileMapper(ByteArrayFileStorage byteArrayFileStorage) :this(byteArrayFileStorage, BsonMapper.DefaultInstance, true) { } + public BsonFileMapper(ByteArrayFileStorage byteArrayFileStorage, BsonMapper bsonMapper, bool disposeFileStorage) + { + FileStorage = byteArrayFileStorage; + BsonMapper = bsonMapper; + DisposeFileStorage = disposeFileStorage; + } + + public void Save(T element) + { + BsonDocument bsonDocument = BsonMapper.Map(element); + FileStorage.TrySaveDocumentBytes(bsonDocument.GetBytes(), out long documentOffset); + } + + class SimpleEnumerator : IEnumerator + { + public BsonFileMapper _fileMapper; + private IEnumerator _documentEnumerator; + + private TT currentInstance; + + public SimpleEnumerator(BsonFileMapper bsonFileMapper) + { + _fileMapper = bsonFileMapper; + _documentEnumerator = _fileMapper.FileStorage.GetEnumerator(); + } + + public bool MoveNext() + { + if (_documentEnumerator.MoveNext()) + { + currentInstance = BsonMapper.DefaultInstance.Unmap(_documentEnumerator.Current); + return true; + } + + return false; + } + + public void Reset() + { + _documentEnumerator.Reset(); + currentInstance = default(TT); + } + + public TT Current => currentInstance; + + object IEnumerator.Current => Current; + + public void Dispose() + { + _documentEnumerator?.Dispose(); + _documentEnumerator = null; + } + } + + public IEnumerator GetEnumerator() => new SimpleEnumerator(this); + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + public void Dispose() + { + FileStorage?.Dispose(); + } + } + */ +} \ No newline at end of file diff --git a/ln.bson.storage/ByteArrayFileStorage.cs b/ln.bson.storage/ByteArrayFileStorage.cs new file mode 100644 index 0000000..0ba3d50 --- /dev/null +++ b/ln.bson.storage/ByteArrayFileStorage.cs @@ -0,0 +1,348 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.IO; +using System.Xml; +using ln.type; + +namespace ln.bson.storage +{ + public class ByteArrayFileStorage : IDisposable, IEnumerable> + { + private string _baseFileName; + private string _offsetFileName; + private FileStream _fileStream; + private SortedSet _storageOffsets = new SortedSet(); + + public Guid CurrentGuid { get; private set; } + + public ByteArrayFileStorage(string baseFilename) : this(baseFilename, baseFilename + ".offset") + { + } + + public ByteArrayFileStorage(string baseFilename, string offsetFileName) + { + _baseFileName = baseFilename; + _offsetFileName = offsetFileName; + + _fileStream = new FileStream(baseFilename, FileMode.OpenOrCreate, FileAccess.ReadWrite); + _fileStream.Position = 0; + if (_fileStream.Length == 0) + { + CurrentGuid = Guid.NewGuid(); + _fileStream.Write(CurrentGuid.ToByteArray()); + } + else + { + CurrentGuid = new Guid(_fileStream.ReadBytes(16)); + } + + if (File.Exists(_offsetFileName)) + { + using (FileStream _offsetStream = new FileStream(offsetFileName, FileMode.Open, FileAccess.Read)) + { + Guid offsetGuid = new Guid(_offsetStream.ReadBytes(16)); + if (offsetGuid.Equals(CurrentGuid)) + { + byte[] offsets = _offsetStream.ReadToEnd(); + if ((offsets.Length % 8) != 0) + throw new InvalidDataException("offsetfile has invalid length"); + + int n = 0; + while (n < offsets.Length) + { + _storageOffsets.Add(BitConverter.ToInt64(offsets, n)); + n += 8; + } + } + } + } + + if (_storageOffsets.Count == 0) + ScanOffsets(); + + _fileStream.Position = 0; + _fileStream.WriteBytes(Guid.Empty.ToByteArray()); + _fileStream.Flush(); + } + + void ScanOffsets() + { + long currentStorageOffset = 16; + int currentStorageSize = 0; + + while (currentStorageOffset < _fileStream.Length) + { + _fileStream.Position = currentStorageOffset; + currentStorageSize = _fileStream.ReadInteger(); + + if (currentStorageSize < 0) + { + _storageOffsets.Add(-currentStorageOffset); + currentStorageOffset -= currentStorageSize; + } + else + { + _storageOffsets.Add(currentStorageOffset); + currentStorageOffset += currentStorageSize; + } + + currentStorageOffset += 4; + } + } + + void SaveOffsets() + { + byte[] offsets = new byte[_storageOffsets.Count * 8]; + int n = 0; + foreach (long storageOffset in _storageOffsets) + { + Array.Copy(BitConverter.GetBytes(storageOffset), 0, offsets, n * 8, 8); + n++; + } + + using (FileStream _offsetStream = new FileStream(_offsetFileName, FileMode.Create, FileAccess.Write)) + { + _offsetStream.WriteBytes(CurrentGuid.ToByteArray()); + _offsetStream.WriteBytes(offsets); + } + } + + public bool TryLoadDocumentBytes(long documentOffset, out byte[] documentBytes) + { + lock (this) + { + if (!_storageOffsets.Contains(documentOffset)) + throw new IOException(); + + _fileStream.Position = documentOffset; + int storageSize = _fileStream.ReadInteger(); + if (storageSize < 0) + { + documentBytes = null; + return false; + } + else + { + int documentSize = _fileStream.ReadInteger(); + documentBytes = new byte[documentSize]; + int nRead = _fileStream.Read(documentBytes, 0, documentSize); + if (nRead != documentSize) + throw new IOException(); + return true; + } + } + } + + public bool TrySaveDocumentBytes(byte[] documentBytes, out long documentOffset) + { + lock (this) + { + foreach (long currentDocumentOffset in _storageOffsets.GetViewBetween(long.MinValue, 0).Reverse()) + { + _fileStream.Position = -currentDocumentOffset; + int currentStorageSize = -_fileStream.ReadInteger(); + if (currentStorageSize >= documentBytes.Length + 4) + { + _fileStream.Position = -currentDocumentOffset; + _fileStream.WriteInteger( + currentStorageSize > (documentBytes.Length + 16) + ? documentBytes.Length + 4 + : currentStorageSize + ); + documentOffset = -currentDocumentOffset; + _fileStream.WriteInteger(documentBytes.Length); + _fileStream.Write(documentBytes, 0, documentBytes.Length); + CurrentGuid = Guid.NewGuid(); + _storageOffsets.Remove(currentDocumentOffset); + _storageOffsets.Add(documentOffset); + + if (currentStorageSize > (documentBytes.Length + 16)) + { + long nextOffset = documentOffset + documentBytes.Length + 4; + int nextSize = currentStorageSize - documentBytes.Length - 8; + _fileStream.Position = nextOffset; + _fileStream.WriteInteger(-nextSize); + long testOffset = nextOffset + nextSize + 4; + if ((testOffset != _fileStream.Length) && + !_storageOffsets.Contains(-testOffset) && + !_storageOffsets.Contains(testOffset)) + throw new IOException("testOffset failed"); + _storageOffsets.Add(-nextOffset); + } + + _fileStream.Flush(); + return true; + } + } + + documentOffset = _fileStream.Length; + _fileStream.Position = _fileStream.Length; + _fileStream.WriteInteger(documentBytes.Length + 4); + _fileStream.WriteInteger(documentBytes.Length); + _fileStream.Write(documentBytes, 0, documentBytes.Length); + CurrentGuid = Guid.NewGuid(); + _fileStream.Flush(); + _storageOffsets.Add(documentOffset); + return true; + } + } + + public bool Remove(long documentOffset) + { + lock (this) + { + if (!_storageOffsets.Contains(documentOffset)) + throw new IOException(); + + _fileStream.Position = documentOffset; + int currentStorageSize = _fileStream.ReadInteger(); + if (currentStorageSize > 0) + { + currentStorageSize = -currentStorageSize; + _fileStream.Position = documentOffset; + _fileStream.WriteInteger(currentStorageSize); + CurrentGuid = Guid.NewGuid(); + _storageOffsets.Remove(documentOffset); + _storageOffsets.Add(-documentOffset); + return true; + } + } + + return false; + } + + public IEnumerable Offsets => _storageOffsets.GetViewBetween(0,long.MaxValue); + + + public void Close() + { + if (_fileStream == null) + throw new ObjectDisposedException(nameof(ByteArrayFileStorage)); + + SaveOffsets(); + _fileStream.Position = 0; + _fileStream.WriteBytes(CurrentGuid.ToByteArray()); + _fileStream.Flush(); + _fileStream.Close(); + _fileStream = null; + } + + + class SimpleDocumentOffsetEnumerator : IEnumerator> + { + private ByteArrayFileStorage _fileStorage; + private IEnumerator _offsetEnumerator; + private byte[] currentDocument; + + public SimpleDocumentOffsetEnumerator(ByteArrayFileStorage fileStorage) + { + this._fileStorage = fileStorage; + } + + public bool MoveNext() + { + if (_offsetEnumerator is null) + _offsetEnumerator = _fileStorage.Offsets.GetEnumerator(); + + if (_offsetEnumerator.MoveNext()) + { + if (_fileStorage.TryLoadDocumentBytes(_offsetEnumerator.Current, out currentDocument)) + return true; + } + return false; + } + + public void Reset() + { + _offsetEnumerator?.Dispose(); + _offsetEnumerator = null; + currentDocument = null; + } + + public KeyValuePair Current => + new KeyValuePair(_offsetEnumerator.Current, currentDocument); + + object IEnumerator.Current => Current; + + public void Dispose() + { + currentDocument = null; + } + } + +/* + class SimpleDocumentEnumerator : IEnumerator + { + private SimpleDocumentOffsetEnumerator _offsetEnumerator; + + public SimpleDocumentEnumerator(ByteArrayFileStorage fileStorage) + { + _offsetEnumerator = new SimpleDocumentOffsetEnumerator(fileStorage); + } + + public bool MoveNext() => _offsetEnumerator.MoveNext(); + public void Reset() => _offsetEnumerator.Reset(); + + public BsonDocument Current => _offsetEnumerator.Current.Value; + object IEnumerator.Current => Current; + + public void Dispose() + { + _offsetEnumerator?.Dispose(); + } + } + */ + + public void Dispose() + { + if (_fileStream != null) + Close(); + + _fileStream?.Dispose(); + _fileStream = null; + } + + public IEnumerator> GetEnumerator() => new SimpleDocumentOffsetEnumerator(this); + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + } + + /* +bool TryGetCachedDocument(long documentOffset, out BsonDocument bsonDocument) +{ + if (_documentsCache.TryGetValue(documentOffset, out WeakReference weakDocument)) + { + if (weakDocument.TryGetTarget(out bsonDocument)) + return true; + + _documentsCache.Remove(documentOffset); + } + + bsonDocument = null; + return false; +} + +public bool TryLoadDocument(long documentOffset, out BsonDocument bsonDocument) +{ + lock (this) + { + if (!_documentOffsets.Contains(documentOffset)) + throw new IOException(); + + if (TryGetCachedDocument(documentOffset, out bsonDocument)) + return true; + + if (TryLoadDocumentBytes(documentOffset, out int documentSize, out byte[] documentBytes)) + { + bsonDocument = BsonReader.ReadDocument(documentBytes, 0, documentSize); + bsonDocument.StorageTag = documentOffset; + _documentsCache.Add(documentOffset, new WeakReference(bsonDocument)); + return true; + } + } + + return false; +} +*/ + +} \ No newline at end of file diff --git a/ln.bson.storage/CompareEndianess.cs b/ln.bson.storage/CompareEndianess.cs new file mode 100644 index 0000000..6f60a37 --- /dev/null +++ b/ln.bson.storage/CompareEndianess.cs @@ -0,0 +1,9 @@ +namespace ln.bson.storage +{ + public enum CompareEndianess + { + LINEAR, + LITTLE, + BIG + } +} \ No newline at end of file diff --git a/ln.bson.storage/MapperSession.cs b/ln.bson.storage/MapperSession.cs new file mode 100644 index 0000000..6b7ede9 --- /dev/null +++ b/ln.bson.storage/MapperSession.cs @@ -0,0 +1,135 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using ln.bson.mapper; + +namespace ln.bson.storage +{ + /* + public class MapperSession : IEnumerable, IDisposable + { + private ByteArrayFileStorage _byteArrayFileStorage; + public BsonMapper BsonMapper { get; } + + private Dictionary instanceCache = new Dictionary(); + private Dictionary lookupCache = new Dictionary(); + + public MapperSession(ByteArrayFileStorage byteArrayFileStorage) + { + _byteArrayFileStorage = byteArrayFileStorage; + BsonMapper = BsonMapper.DefaultInstance; + } + + bool GetInstance(BsonDocument bsonDocument, out T instance) + { + if (!instanceCache.TryGetValue(bsonDocument.StorageTag, out instance)) + { + instance = BsonMapper.Unmap(bsonDocument); + instanceCache.Add(bsonDocument.StorageTag, instance); + lookupCache.Add(instance, bsonDocument); + } + return true; + } + + public bool Save(T instance) + { + BsonDocument newBsonDocument = BsonMapper.Map(instance); + BsonDocument oldBsonDocument = null; + long documentOffset; + + if (lookupCache.TryGetValue(instance, out oldBsonDocument)) + { + if (oldBsonDocument.Equals(newBsonDocument)) + return true; + + if (_byteArrayFileStorage.TrySaveDocumentBytes(newBsonDocument.GetBytes(), out documentOffset)) + { + newBsonDocument.StorageTag = documentOffset; + instanceCache.Remove(oldBsonDocument.StorageTag); + instanceCache.Add(documentOffset, instance); + lookupCache.Remove(instance); + lookupCache.Add(instance, newBsonDocument); + return _byteArrayFileStorage.Remove(oldBsonDocument.StorageTag); + } + + return false; + } + + if (_byteArrayFileStorage.TrySaveDocumentBytes(newBsonDocument.GetBytes(), out documentOffset)) + { + instanceCache.Add(documentOffset, instance); + lookupCache.Add(instance, newBsonDocument); + return true; + } + + return false; + } + + public bool Delete(T instance) + { + if (!lookupCache.TryGetValue(instance, out BsonDocument bsonDocument)) + return true; + + if (_byteArrayFileStorage.Remove(bsonDocument.StorageTag)) + { + lookupCache.Remove(instance); + instanceCache.Remove(bsonDocument.StorageTag); + return true; + } + + return false; + } + + public void Dispose() + { + instanceCache?.Clear(); + instanceCache = null; + lookupCache?.Clear(); + lookupCache = null; + _byteArrayFileStorage = null; + } + + public class Enumerator : IEnumerator + { + private MapperSession MapperSession; + private IEnumerator StorageIterator; + private T currentInstance; + public Enumerator(MapperSession mappingSession) + { + MapperSession = mappingSession; + StorageIterator = MapperSession._byteArrayFileStorage.GetEnumerator(); + } + + public bool MoveNext() + { + if (StorageIterator.MoveNext()) + { + if (!MapperSession.GetInstance(StorageIterator.Current, out currentInstance)) + throw new Exception(); + + return true; + } + return false; + } + + public void Reset() + { + currentInstance = default(T); + StorageIterator.Reset(); + } + + public T Current => currentInstance; + object IEnumerator.Current => Current; + + public void Dispose() + { + Reset(); + MapperSession = null; + } + } + + public IEnumerator GetEnumerator() => new Enumerator(this); + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + } + */ +} \ No newline at end of file diff --git a/ln.bson.storage/QueryOperator.cs b/ln.bson.storage/QueryOperator.cs new file mode 100644 index 0000000..6aa7370 --- /dev/null +++ b/ln.bson.storage/QueryOperator.cs @@ -0,0 +1,14 @@ +using System; + +namespace ln.bson.storage +{ + [Flags] + public enum QueryOperator + { + NOT = (1<<0), + EQUAL = (1<<1), + LESS = (1<<2), + GREATER = (1<<3), + IN = (1 << 4) + } +} \ No newline at end of file diff --git a/ln.bson.storage/index/BsonIndex.cs b/ln.bson.storage/index/BsonIndex.cs new file mode 100644 index 0000000..d141bc2 --- /dev/null +++ b/ln.bson.storage/index/BsonIndex.cs @@ -0,0 +1,155 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using ln.collections; + +namespace ln.bson.storage.index +{ + public delegate int BsonCompareDelegate(byte[] a, byte[] b); + public class BsonIndex : IBsonIndex + { + + private BTreeValueSet _lookup; + private BTreeValueList _reverse = new BTreeValueList(); + + public BsonIndex() : this(CompareBytesLittleEndian) { } + public BsonIndex(Comparison compareDelegate) + { + _lookup = new BTreeValueSet(compareDelegate); + } + + public void AddValue(long key, BsonValue bsonValue) => AddValue(key, bsonValue.GetBytes()); + public void AddValue(long key, byte[] value) + { + if (_reverse.TryAdd(key, value)) + { + if (_lookup.TryAdd(value, key)) + return; + _reverse.TryRemove(key, value); + } + + throw new ArgumentOutOfRangeException(); + } + + public void RemoveValue(long key) + { + if (_reverse.TryGet(key, out IEnumerable values)) + { + foreach (var value in values) + { + if (!_lookup.TryRemove(value, key)) + throw new InvalidOperationException(); + } + + _reverse.TryRemove(key); + return; + } + throw new ArgumentOutOfRangeException(); + } + + public void ReplaceValue(long oldKey, long newKey, BsonValue newValue) + { + AddValue(newKey, newValue); + RemoveValue(oldKey); + } + + public void Clear() + { + _lookup.Clear(); + _reverse.Clear(); + } + + public void Query(QueryOperator queryOperator, byte[] value, ISet resultSet) + { + IEnumerable> enumeration; + switch (queryOperator) + { + case QueryOperator.EQUAL: + enumeration = _lookup.GetInterval(value, value); + break; + case QueryOperator.EQUAL | QueryOperator.LESS: + enumeration = _lookup.GetInterval(null, value); + break; + case QueryOperator.EQUAL | QueryOperator.GREATER: + enumeration = _lookup.GetInterval(value, null); + break; + case QueryOperator.LESS: + enumeration = _lookup.GetInterval(null, value, (k,v)=>_lookup.Comparison(k,value) < 0); + break; + case QueryOperator.GREATER: + enumeration = _lookup.GetInterval(value, null, (k,v)=>_lookup.Comparison(k,value) > 0); + break; + case QueryOperator.IN: + // ToDo: Implementation + throw new NotSupportedException(); + default: + throw new NotSupportedException(); + } + + resultSet.UnionWith(enumeration.Select(x => x.Value)); + } + + public static int CompareBytesBigEndian(byte[] a, byte[] b) + { + int cntBytes = a.Length > b.Length ? a.Length : b.Length; + int pa = a.Length - cntBytes; + int pb = b.Length - cntBytes; + while ((pa < a.Length) || (pb < b.Length)) + { + pa++; + pb++; + + int va = (pa < 0) ? 0 : a[pa]; + int vb = (pb < 0) ? 0 : b[pb]; + + int d = va - vb; + if (d != 0) + return d; + } + + return 0; + } + public static int CompareBytesLittleEndian(byte[] a, byte[] b) + { + int cntBytes = a.Length > b.Length ? a.Length : b.Length; + int pa = a.Length; + int pb = b.Length; + while ((pa > 0) || (pb > 0)) + { + pa--; + pb--; + + int va = (pa >= a.Length) ? 0 : a[pa]; + int vb = (pb >= b.Length) ? 0 : b[pb]; + + int d = va - vb; + if (d != 0) + return d; + } + + return 0; + } + public static int CompareBytesLinear(byte[] a, byte[] b) + { + int cntBytes = a.Length > b.Length ? a.Length : b.Length; + int p = 0; + while (p < cntBytes) + { + int va = (p >= a.Length) ? -1 : a[p]; + int vb = (p >= b.Length) ? -1 : b[p]; + + int d = va - vb; + if (d != 0) + return d; + + p++; + } + + return 0; + } + + public IEnumerator> GetEnumerator() => _reverse.GetEnumerator(); + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + } +} \ No newline at end of file diff --git a/ln.bson.storage/index/IBsonIndex.cs b/ln.bson.storage/index/IBsonIndex.cs new file mode 100644 index 0000000..9814df6 --- /dev/null +++ b/ln.bson.storage/index/IBsonIndex.cs @@ -0,0 +1,17 @@ +using System.Collections; +using System.Collections.Generic; + +namespace ln.bson.storage.index +{ + public interface IBsonIndex : IEnumerable> + { + void Query(QueryOperator queryOperator, byte[] value, ISet resultSet); + + public void AddValue(long key, BsonValue bsonValue); + public void AddValue(long key, byte[] value); + public void RemoveValue(long key); + public void ReplaceValue(long oldKey, long newKey, BsonValue newValue); + + public void Clear(); + } +} \ No newline at end of file diff --git a/ln.bson.storage/index/IndexTree.cs b/ln.bson.storage/index/IndexTree.cs new file mode 100644 index 0000000..c42a0f9 --- /dev/null +++ b/ln.bson.storage/index/IndexTree.cs @@ -0,0 +1,146 @@ +using System; +using System.Collections.Generic; +using System.Threading; + +namespace ln.bson.storage.index +{ + public class IndexTree + { + private IndexLeaf _root = new IndexLeaf(); + + public void Query(string path, QueryOperator queryOperator, byte[] value, ISet resultSet) => Query(path.Split('.'), queryOperator, value, resultSet); + public void Query(Span path, QueryOperator queryOperator, byte[] value, ISet resultSet) + { + if (!_root.TryGetLeaf(path, out IndexLeaf indexLeaf)) + throw new KeyNotFoundException(); + + indexLeaf.Index.Query(queryOperator, value, resultSet); + } + + public void AddDocument(long key, BsonDocument bsonDocument) => _root.AddValue(key, bsonDocument); + public void RemoveDocument(long key) => _root.RemoveValue(key); + public void ReplaceDocument(long oldKey, long newKey, BsonDocument newDocument) => + _root.ReplaceValue(oldKey, newKey, newDocument); + + public bool EnsureIndex(string path, Comparison comparison) => + EnsureIndex(path, comparison, out IBsonIndex bsonIndex); + public bool EnsureIndex(string path, Comparison comparison, out IBsonIndex bsonIndex) + { + _root.EnsureLeaf(path.Split('.',StringSplitOptions.None), out IndexLeaf indexLeaf); + return indexLeaf.EnsureIndex(comparison, out bsonIndex); + } + + public void Clear() => _root.Clear(); + + class IndexLeaf + { + private Dictionary _children = new Dictionary(); + private IBsonIndex _index; + + public IBsonIndex Index => _index; + + public void EnsureLeaf(Span path, out IndexLeaf indexLeaf) + { + if (path.IsEmpty) + indexLeaf = this; + else + { + if (!_children.TryGetValue(path[0], out IndexLeaf childLeaf)) + { + childLeaf = new IndexLeaf(); + _children.Add(path[0], childLeaf); + } + childLeaf.EnsureLeaf(path.Slice(1), out indexLeaf); + } + } + public bool TryGetLeaf(Span path, out IndexLeaf indexLeaf) + { + if (path.IsEmpty) + { + indexLeaf = this; + return true; + } + else + { + if (!_children.TryGetValue(path[0], out IndexLeaf childLeaf)) + { + indexLeaf = null; + return false; + } + + return childLeaf.TryGetLeaf(path.Slice(1), out indexLeaf); + } + } + + public bool EnsureIndex() => EnsureIndex(BsonIndex.CompareBytesLittleEndian, out IBsonIndex bsonIndex); + public bool EnsureIndex(Comparison comparison, out IBsonIndex bsonIndex) + { + if (_index is null) + { + bsonIndex = _index = new BsonIndex(comparison); + return true; + } + + bsonIndex = _index; + return false; + } + + public void AddValue(long key, BsonValue bsonValue) + { + if (_index is not null) + _index.AddValue(key, bsonValue); + + if (bsonValue is BsonDocument bsonDocument) + { + foreach (KeyValuePair child in _children) + { + if (bsonDocument.Contains(child.Key)) + child.Value.AddValue(key, bsonDocument[child.Key]); + } + } + } + + public void RemoveValue(long key) + { + if (_index is not null) + _index.RemoveValue(key); + + foreach (KeyValuePair child in _children) + child.Value.RemoveValue(key); + } + + public void ReplaceValue(long oldKey, long newKey, BsonValue newValue) + { + if (_index is not null) + _index.ReplaceValue(oldKey, newKey, newValue); + + if (newValue is BsonDocument newDocument) + { + foreach (KeyValuePair child in _children) + { + if (newDocument.Contains(child.Key)) + child.Value.ReplaceValue(oldKey, newKey, newDocument[child.Key]); + else + child.Value.RemoveValue(oldKey); + } + } + else + { + foreach (KeyValuePair child in _children) + child.Value.RemoveValue(oldKey); + } + } + + public void Clear() + { + if (_index is not null) + _index.Clear(); + + foreach (KeyValuePair child in _children) + child.Value.Clear(); + } + + } + + } +} \ No newline at end of file diff --git a/ln.bson.storage/ln.bson.storage.csproj b/ln.bson.storage/ln.bson.storage.csproj new file mode 100644 index 0000000..b771b26 --- /dev/null +++ b/ln.bson.storage/ln.bson.storage.csproj @@ -0,0 +1,17 @@ + + + + net5.0 + true + + + + + + + + + + + + diff --git a/ln.bson.tests/Document.cs b/ln.bson.tests/Document.cs new file mode 100644 index 0000000..35a6c40 --- /dev/null +++ b/ln.bson.tests/Document.cs @@ -0,0 +1,22 @@ +using System; +using System.Collections.Generic; + +namespace ln.bson.tests +{ + public class Document + { + public Guid UniqueId { get; set; } + public string Title { get; set; } + public string FileName { get; set; } + public string Description { get; set; } + public Guid FolderId { get; set; } + public DateTime Created { get; set; } + public string ContentType { get; set; } + + public Dictionary DocumentContents { get; set; } = new Dictionary(); + public Dictionary Keywords { get; set; } = new Dictionary(); + + + + } +} \ No newline at end of file diff --git a/ln.bson.tests/DocumentContent.cs b/ln.bson.tests/DocumentContent.cs new file mode 100644 index 0000000..aed40d3 --- /dev/null +++ b/ln.bson.tests/DocumentContent.cs @@ -0,0 +1,11 @@ +using System; + +namespace ln.bson.tests +{ + public class DocumentContent + { + public bool Authentic { get; set; } + public DateTime Created { get; set; } + public byte[] ContentHash { get; set; } + } +} \ No newline at end of file diff --git a/ln.bson.tests/DocumentFolder.cs b/ln.bson.tests/DocumentFolder.cs new file mode 100644 index 0000000..83699ac --- /dev/null +++ b/ln.bson.tests/DocumentFolder.cs @@ -0,0 +1,17 @@ +using System; +using System.Collections.Generic; + +namespace ln.bson.tests +{ + public class DocumentFolder + { + public Guid UniqueId { get; set; } + public string Name { get; set; } + public string Label { get; set; } + + public Guid ParentId { get; set; } + + internal List _children { get; } = new List(); + public DocumentFolder[] Children => _children.ToArray(); + } +} \ No newline at end of file diff --git a/ln.bson.tests/UnitTest1.cs b/ln.bson.tests/UnitTest1.cs new file mode 100644 index 0000000..58628f7 --- /dev/null +++ b/ln.bson.tests/UnitTest1.cs @@ -0,0 +1,272 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection; +using ln.bson.mapper; +using ln.bson.storage; +using Newtonsoft.Json; +using NUnit.Framework; + +namespace ln.bson.tests +{ + public class Tests + { + [SetUp] + public void Setup() + { + } + + [Test] + public void Test_GetBytes() + { + BsonDocument bsonDocument = new BsonDocument(); + bsonDocument["alpha"] = new BsonString("Hallo Welt"); + bsonDocument["beta"] = new BsonString(""); + bsonDocument["gamma"] = new BsonInt32(int.MaxValue); + bsonDocument["delta"] = new BsonDateTime(DateTime.Now); + BsonDocument embeddedDocument = new BsonDocument(); + bsonDocument["zeta"] = embeddedDocument; + BsonArray bsonArray = new BsonArray(); + bsonArray.Add(bsonDocument["alpha"]); + bsonArray.Add(bsonDocument["beta"]); + bsonArray.Add(bsonDocument["gamma"]); + bsonDocument["eta"] = bsonArray; + embeddedDocument["alpha"] = bsonArray; + + bsonDocument["phi"] = new BsonGuid(Guid.NewGuid()); + + byte[] documentBytes = bsonDocument.GetBytes(); + BsonDocument compareDocument = BsonReader.ReadDocument(documentBytes); + byte[] compareBytes = compareDocument.GetBytes(); + + Assert.AreEqual(documentBytes, compareBytes); + Assert.Pass(); + } + + + [Test] + public void Test_Mapper() + { + BsonMapping bsonMapping = BsonMapper.DefaultInstance.CreateMapping(); + Assert.NotNull(bsonMapping); + + MapperTestA testA = new MapperTestA(true) { text = "jetzt bin ich ein anderer text" }; + + BsonDocument mappedDocument = BsonMapper.DefaultInstance.Map(testA); + + MapperTestA testA2 = BsonMapper.DefaultInstance.Unmap(mappedDocument); + + FieldsAreEqual(testA, testA2); + + Document document = new Document(); + document.Keywords.Add("supplier", "123-4567-890-AAA"); + document.Keywords.Add("DocType", "supplier-invoice"); + //document.DocumentContents.Add("application/pdf", new DocumentContent(){ Authentic = true, Created = DateTime.Now }); + + BsonDocument bsonDocument = BsonMapper.DefaultInstance.Map(document); + Document unmappedDocument = BsonMapper.DefaultInstance.Unmap(bsonDocument); + + FieldsAreEqual(document, unmappedDocument); + + Assert.Pass(); + } + + [Test] + public void Test_FileStorage() + { + BsonDocument[] bsonDocuments = PrepareDocuments(256); + int n = 0; + + File.Delete("test1.bson"); + using (ByteArrayFileStorage byteArrayFileStorage = new ByteArrayFileStorage("test1.bson")) + { + foreach (BsonDocument bsonDocument in bsonDocuments) + Assert.IsTrue(byteArrayFileStorage.TrySaveDocumentBytes(bsonDocument.GetBytes(), out long documentOffset)); + + foreach (KeyValuePair storageData in byteArrayFileStorage) + { + Assert.AreEqual(bsonDocuments[n].GetBytes(), storageData.Value); + n++; + } + + n = 0; + foreach (KeyValuePair storageData in byteArrayFileStorage.ToArray()) + { + Assert.IsTrue(byteArrayFileStorage.Remove(storageData.Key)); + + n++; + if (n >= 64) + break; + } + + n = 64; + foreach (KeyValuePair storageData in byteArrayFileStorage) + { + Assert.AreEqual(bsonDocuments[n].GetBytes(), storageData.Value); + n++; + } + + } + + } + [Test] + public void Test_DocumentStorage() + { + BsonDocument[] bsonDocuments = PrepareDocuments(256); + int n = 0; + + File.Delete("documents.bson"); + using (BsonDocumentStorage storage = new BsonDocumentStorage(new BsonDocumentStorageConfiguration("documents.bson"))) + { + foreach (BsonDocument bsonDocument in bsonDocuments) + Assert.IsTrue(storage.SaveDocument(BsonReader.ReadDocument(bsonDocument.GetBytes()))); + + foreach (BsonDocument bsonDocument in storage) + { + Assert.AreEqual(bsonDocuments[n], bsonDocument); + Assert.AreNotSame(bsonDocuments[n], bsonDocument); + n++; + } + + n = 0; + foreach (BsonDocument bsonDocument in storage.ToArray()) + { + Assert.IsTrue(storage.DeleteDocument(bsonDocument)); + + n++; + if (n >= 64) + break; + } + + n = 64; + foreach (BsonDocument bsonDocument in storage) + { + Assert.AreEqual(bsonDocuments[n], bsonDocument); + Assert.AreNotSame(bsonDocuments[n], bsonDocument); + n++; + } + } + + } + + /* + [Test] + public void Test_FileMapper() + { + Document[] documents = new Document[64]; + for (int n = 0; n < documents.Length; n++) + documents[n] = new Document() + { UniqueId = Guid.NewGuid(), FolderId = Guid.NewGuid(), FileName = string.Format("{0}.txt", n) }; + + File.Delete("test.documents.bson"); + using (BsonFileMapper fileMapper = new BsonFileMapper("test.documents.bson")) + { + foreach (var document in documents) + fileMapper.Save(document); + + int n = 0; + foreach (Document document in fileMapper) + { + Assert.AreEqual(documents[n].UniqueId, document.UniqueId); + Assert.AreEqual(documents[n].FolderId, document.FolderId); + Assert.AreEqual(documents[n].FileName, document.FileName); + n++; + } + + + } + + + } + */ + + [Test] + public void Test_Indeces() + { + BsonDocument[] bsonDocuments = PrepareDocuments(256); + int n = 0; + + File.Delete("documents.bson"); + using (BsonDocumentStorage storage = new BsonDocumentStorage( + new BsonDocumentStorageConfiguration("documents.bson") + .Index("__NO__") + .SetLogTarget(Console.WriteLine) + )) + { + foreach (BsonDocument bsonDocument in bsonDocuments) + Assert.IsTrue(storage.SaveDocument(BsonReader.ReadDocument(bsonDocument.GetBytes()))); + } + + using (BsonDocumentStorage storage = new BsonDocumentStorage( + new BsonDocumentStorageConfiguration("documents.bson") + .Index("__NO__") + .SetLogTarget(Console.WriteLine) + )) + { + var e = storage.Query("__NO__", QueryOperator.LESS, 10); + + Assert.AreEqual( + 10, + e.Count() + ); + } + } + + + + public BsonDocument[] PrepareDocuments(int count) + { + BsonDocument[] documents = new BsonDocument[count]; + for (int n = 0; n < documents.Length; n++) + { + documents[n] = new BsonDocument(); + documents[n]["__NO__"] = new BsonInt32(n); + documents[n]["__UUID__"] = Guid.NewGuid(); + } + + return documents; + } + + + + + + public static void FieldsAreEqual(T a, T b) + { + foreach (FieldInfo fieldInfo in typeof(T).GetFields(BindingFlags.Instance | BindingFlags.Public | + BindingFlags.NonPublic)) + { + if (fieldInfo.FieldType.IsAssignableTo(typeof(ICollection))) + CollectionAssert.AreEquivalent((ICollection)fieldInfo.GetValue(a), (ICollection)fieldInfo.GetValue(b)); + else + Assert.AreEqual(fieldInfo.GetValue(a), fieldInfo.GetValue(b)); + } + } + + class MapperTestA + { + public string text = "ich bin ein text"; + public int i32 = Int32.MinValue; + public long i64 = Int64.MaxValue; + public double d64 = Double.MaxValue; + + public Dictionary keywords = new Dictionary(); + + public MapperTestA(){} + public MapperTestA(bool populateKeywords) + { + if (populateKeywords) + { + keywords.Add("keyword1", "some-value"); + keywords.Add("keyword2", "another-value"); + keywords.Add("keyword3", "some text that you would expect to be part of good test scenario"); + } + } + } + + + + } +} \ No newline at end of file diff --git a/ln.bson.tests/ln.bson.tests.csproj b/ln.bson.tests/ln.bson.tests.csproj new file mode 100644 index 0000000..5209d31 --- /dev/null +++ b/ln.bson.tests/ln.bson.tests.csproj @@ -0,0 +1,21 @@ + + + + net5.0 + + false + + + + + + + + + + + + + + + diff --git a/ln.bson/BsonDocument.cs b/ln.bson/BsonDocument.cs new file mode 100644 index 0000000..e078da3 --- /dev/null +++ b/ln.bson/BsonDocument.cs @@ -0,0 +1,184 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Dynamic; +using System.IO; +using System.Text; +using System.Text.Json; + +namespace ln.bson +{ + public class BsonDocument : BsonValue, IEnumerable> + { + private Dictionary _values = new Dictionary(); + + public long StorageTag { get; set; } + public object UserTag { get; set; } + + public BsonDocument():base(BsonType.Document){} + + public override object ToNative() + { + Dictionary native = new Dictionary(); + foreach (var bsonValue in _values) + native.Add(bsonValue.Key, bsonValue.Value.ToNative()); + return native; + } + + public override byte[] GetBytes() + { + byte[][] names = new byte[_values.Count][]; + byte[][] bvalues = new byte[_values.Count][]; + BsonType[] bsonTypes = new BsonType[_values.Count]; + int n = 0; + int totalBytes = 0; + foreach (var bsonValue in _values) + { + bsonTypes[n] = bsonValue.Value.BsonType; + names[n] = Encoding.UTF8.GetBytes(bsonValue.Key); + bvalues[n] = bsonValue.Value.GetBytes(); + totalBytes += names[n].Length + bvalues[n].Length; + n++; + } + + byte[] containerBytes = new byte[totalBytes + 5 + (_values.Count << 1 )]; + BitConverter.TryWriteBytes(containerBytes, containerBytes.Length); + + int offset = 4; + for (n = 0; n < names.Length; n++) + { + containerBytes[offset] = (byte)bsonTypes[n]; + Array.Copy(names[n], 0, containerBytes, offset + 1, names[n].Length); + Array.Copy(bvalues[n], 0, containerBytes, offset + 2 + names[n].Length, bvalues[n].Length); + offset += 2 + names[n].Length + bvalues[n].Length; + } + + return containerBytes; + } + + + public BsonValue this[string name] + { + get => _values[name]; + set => _values[name] = value; + } + + public void Add(string name, BsonValue bsonValue) => _values.Add(name, bsonValue); + public void Remove(string name) => _values.Remove(name); + public bool Contains(string name) => _values.ContainsKey(name); + + public IEnumerable Keys => _values.Keys; + public IEnumerable Values => _values.Values; + public IEnumerator> GetEnumerator() + { + return _values.GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return ((IEnumerable)_values).GetEnumerator(); + } + + public override bool Equals(object? obj) + { + if (obj is BsonDocument other) + { + if (_values.Count == other._values.Count) + { + foreach (String key in _values.Keys) + { + if (!other._values.ContainsKey(key) || !_values[key].Equals(other._values[key])) + return false; + } + + return true; + } + } + + return false; + } + + public override bool TryGetMember(GetMemberBinder binder, out object? result) + { + if (_values.TryGetValue(binder.Name, out BsonValue bsonValue)) + { + result = bsonValue; + return true; + } + return base.TryGetMember(binder, out result); + } + + public override bool TrySetMember(SetMemberBinder binder, object? value) + { + Add(binder.Name, (BsonValue)value); + return true; + } + } + + public class BsonArray : BsonValue, IEnumerable + { + private List _values = new List(); + + public BsonArray():base(BsonType.Array){} + public override object ToNative() + { + object[] native = new object[_values.Count]; + for (int n = 0; n < native.Length; n++) + native[n] = _values[n].ToNative(); + return native; + } + public override byte[] GetBytes() + { + byte[][] names = new byte[_values.Count][]; + byte[][] bvalues = new byte[_values.Count][]; + BsonType[] bsonTypes = new BsonType[_values.Count]; + int n = 0; + int totalBytes = 0; + foreach (var bsonValue in _values) + { + bsonTypes[n] = bsonValue.BsonType; + names[n] = Encoding.UTF8.GetBytes(n.ToString()); + bvalues[n] = bsonValue.GetBytes(); + totalBytes += names[n].Length + bvalues[n].Length; + n++; + } + + byte[] containerBytes = new byte[totalBytes + 5 + (_values.Count << 1)]; + BitConverter.TryWriteBytes(containerBytes, totalBytes); + + int offset = 4; + for (n = 0; n < names.Length; n++) + { + containerBytes[offset] = (byte)bsonTypes[n]; + Array.Copy(names[n], 0, containerBytes, offset + 1, names[n].Length); + Array.Copy(bvalues[n], 0, containerBytes, offset + 2 + names[n].Length, bvalues[n].Length); + offset += 2 + names[n].Length + bvalues[n].Length; + } + + return containerBytes; + } + + public BsonValue this[int i] + { + get => _values[i]; + set => _values[i] = value; + } + + public void Add(BsonValue bsonValue) => _values.Add(bsonValue); + public void Remove(BsonValue bsonValue) => _values.Remove(bsonValue); + public void RemoveAt(int i) => _values.RemoveAt(i); + public bool Contains(BsonValue bsonValue) => _values.Contains(bsonValue); + + public int Length => _values.Count; + + public IEnumerator GetEnumerator() + { + return _values.GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return ((IEnumerable)_values).GetEnumerator(); + } + } +} \ No newline at end of file diff --git a/ln.bson/BsonReader.cs b/ln.bson/BsonReader.cs new file mode 100644 index 0000000..7e72dc8 --- /dev/null +++ b/ln.bson/BsonReader.cs @@ -0,0 +1,103 @@ +using System; +using System.IO; +using System.Text; +using ln.type; + +namespace ln.bson +{ + public static class BsonReader + { + public static BsonDocument ReadDocument(byte[] buffer) => ReadDocument(buffer, 0, buffer.Length); + public static BsonDocument ReadDocument(byte[] buffer, int offset) => ReadDocument(buffer, offset, buffer.Length - offset); + + public static BsonDocument ReadDocument(byte[] buffer, int offset, int length) + { + using (var ms = new MemoryStream(buffer, offset, length)) + return ReadDocument(ms); + } + + public static BsonDocument ReadDocument(Stream stream) + { + BsonDocument bsonDocument = new BsonDocument(); + ReadDocumentItems(stream, ((s, value) => bsonDocument.Add(s,value))); + return bsonDocument; + } + private static BsonArray ReadArray(Stream stream) + { + BsonArray bsonArray = new BsonArray(); + ReadDocumentItems(stream, ((s, value) => bsonArray.Add(value))); + return bsonArray; + } + + private static void ReadDocumentItems(Stream stream, Action appender) + { + int documentLength = stream.ReadInteger(); + BsonType bsonType = BsonType.Null; + while (bsonType != BsonType.EndOfDocument) + { + bsonType = (BsonType)stream.ReadByte(); + if (bsonType == BsonType.EndOfDocument) + return; + + string name = stream.ReadCString(); + BsonValue bsonValue = null; + switch (bsonType) + { + case BsonType.Boolean: + if (stream.ReadByte() == 0) + bsonValue = BsonBoolean.False; + else + bsonValue = BsonBoolean.True; + break; + case BsonType.Double: + bsonValue = new BsonDouble(stream.ReadDouble()); + break; + case BsonType.Int32: + bsonValue = new BsonInt32(stream.ReadInteger()); + break; + case BsonType.Int64: + bsonValue = new BsonInt64(stream.ReadLong()); + break; + case BsonType.Null: + bsonValue = BsonNull.Instance; + break; + case BsonType.Binary: + int blength = stream.ReadInteger(); + BsonBinarySubType subType = (BsonBinarySubType)stream.ReadByte(); + switch (subType) + { + case BsonBinarySubType.UuidOld: + case BsonBinarySubType.Uuid: + if (blength != 16) + throw new ArgumentOutOfRangeException(nameof(blength)); + bsonValue = new BsonGuid(stream.ReadBytes(blength)); + break; + default: + bsonValue = new BsonBinary(subType, stream.ReadBytes(blength)); + break; + } + break; + case BsonType.String: + blength = stream.ReadInteger(); + bsonValue = new BsonString(Encoding.UTF8.GetString(stream.ReadBytes(blength),0, blength-1)); + break; + case BsonType.DateTime: + bsonValue = new BsonDateTime(DateTimeExtensions.FromUnixTimeMilliseconds(stream.ReadLong())); + break; + case BsonType.Document: + bsonValue = ReadDocument(stream); + break; + case BsonType.Array: + bsonValue = ReadArray(stream); + break; + case BsonType.EndOfDocument: + return; + default: + throw new NotSupportedException(); + } + + appender(name, bsonValue); + } + } + } +} \ No newline at end of file diff --git a/ln.bson/BsonType.cs b/ln.bson/BsonType.cs new file mode 100644 index 0000000..e931e61 --- /dev/null +++ b/ln.bson/BsonType.cs @@ -0,0 +1,39 @@ +namespace ln.bson +{ + public enum BsonType : byte + { + EndOfDocument = 0x00, + Double = 0x01, + String = 0x02, + Document = 0x03, + Array = 0x04, + Binary = 0x05, + ObjectId = 0x07, + Boolean = 0x08, + DateTime = 0x09, + Null = 0x0a, + Regex = 0x0b, + DBPointer = 0x0c, + JavaScript = 0x0d, + Symbol = 0x0e, + ScopedJavaScript = 0x0f, + Int32 = 0x10, + TimeStamp = 0x11, + Int64 = 0x12, + Decimal128 = 0x13, + MinKey = 0xFF, + MaxKey = 0x7F, + } + + public enum BsonBinarySubType : byte + { + Generic = 0x00, + Function = 0x01, + Binary = 0x02, + UuidOld = 0x03, + Uuid = 0x04, + MD5 = 0x05, + Encrypted = 0x06, + UserDefined = 0x80, + } +} \ No newline at end of file diff --git a/ln.bson/BsonTypes.cs b/ln.bson/BsonTypes.cs new file mode 100644 index 0000000..3e0206f --- /dev/null +++ b/ln.bson/BsonTypes.cs @@ -0,0 +1,154 @@ +using System; +using System.Text; +using ln.type; + +namespace ln.bson +{ + public class BsonString : BsonValue + { + public string Value { get; set; } + public override object ToNative() => Value; + + public BsonString() :base(BsonType.String){} + + public BsonString(string value) : this() + { + Value = value; + } + + public override byte[] GetBytes() + { + byte[] sbytes = Encoding.UTF8.GetBytes(Value); + byte[] buffer = new byte[sbytes.Length + 5]; + Array.Copy(BitConverter.GetBytes(sbytes.Length+1), 0, buffer, 0, 4); + Array.Copy(sbytes, 0, buffer, 4, sbytes.Length); + return buffer; + } + + public static implicit operator string(BsonString bsonString) => bsonString.Value; + } + + public class BsonDouble : BsonValue + { + public double Value { get; set; } + public override object ToNative() => Value; + public override byte[] GetBytes() => BitConverter.GetBytes(Value); + + public BsonDouble():base(BsonType.Double){} + + public BsonDouble(double value) : this() + { + Value = value; + } + } + + public class BsonBinary : BsonValue + { + public virtual byte[] Value { get; set; } + public BsonBinarySubType SubType { get; set; } + public override object ToNative() => Value; + + public BsonBinary() : base(BsonType.Binary) { } + + public BsonBinary(BsonBinarySubType subType) : this() + { + SubType = subType; + } + public BsonBinary(BsonBinarySubType subType, byte[] value) : this(subType) + { + Value = value; + } + + public override byte[] GetBytes() + { + byte[] b = new byte[Value.Length + 5]; + Array.Copy(BitConverter.GetBytes(Value.Length), 0, b, 0, 4); + b[4] = (byte)SubType; + Array.Copy(Value, 0, b, 5, Value.Length); + return b; + } + } + + public class BsonGuid : BsonBinary + { + public override byte[] Value { get => GuidValue.ToByteArray(); set => GuidValue = new Guid(value); } + public Guid GuidValue { get; set; } + public override object ToNative() => GuidValue; + + public BsonGuid() : this(Guid.Empty) { } + + public BsonGuid(Guid guid) : base(BsonBinarySubType.Uuid) + { + GuidValue = guid; + } + public BsonGuid(byte[] guid) : this(new Guid(guid)) { } + } + + public class BsonBoolean : BsonValue + { + public static readonly BsonBoolean True = new BsonBoolean(true); + public static readonly BsonBoolean False = new BsonBoolean(false); + + public bool Value { get; } + + private BsonBoolean(bool b) : base(BsonType.Boolean) + { + Value = b; + } + + public override byte[] GetBytes() => new[] { Value ? (byte)0x01 : (byte)0x00 }; + public override object ToNative() => Value; + } + + public class BsonDateTime : BsonValue + { + public DateTime Value { get; set; } + public override byte[] GetBytes() => BitConverter.GetBytes((long)Value.ToUnixTimeMilliseconds()); + public override object ToNative() => Value; + + public BsonDateTime(): base(BsonType.DateTime){} + + public BsonDateTime(DateTime dateTime) : base(BsonType.DateTime) + { + Value = dateTime; + } + } + + public class BsonNull : BsonValue + { + public static readonly BsonNull Instance = new BsonNull(); + protected BsonNull(): base(BsonType.Null){} + public override byte[] GetBytes() => new byte[0]; + public override object ToNative() => null; + } + + public class BsonInt32 : BsonValue + { + public Int32 Value { get; set; } + public override object ToNative() => Value; + public override byte[] GetBytes() => BitConverter.GetBytes(Value); + + public BsonInt32():base(BsonType.Int32){} + + public BsonInt32(Int32 value) : this() + { + Value = value; + } + } + + public class BsonInt64 : BsonValue + { + public Int64 Value { get; set; } + public override object ToNative() => Value; + public override byte[] GetBytes() => BitConverter.GetBytes(Value); + + public BsonInt64():base(BsonType.Int64){} + + public BsonInt64(Int64 value) : this() + { + Value = value; + } + } + + +} \ No newline at end of file diff --git a/ln.bson/BsonValue.cs b/ln.bson/BsonValue.cs new file mode 100644 index 0000000..a030969 --- /dev/null +++ b/ln.bson/BsonValue.cs @@ -0,0 +1,44 @@ +using System; +using System.Dynamic; +using System.IO; + +namespace ln.bson +{ + public abstract class BsonValue : DynamicObject + { + public BsonType BsonType { get; } + + protected BsonValue(BsonType bsonType) + { + BsonType = bsonType; + } + + public abstract object ToNative(); + public abstract Byte[] GetBytes(); + + public virtual T As() => throw new NotImplementedException(); + + public virtual void WriteTo(Stream stream) => stream.Write(GetBytes()); + + public virtual int WriteTo(byte[] buffer, int offset) + { + byte[] vBytes = GetBytes(); + Array.Copy(vBytes, 0, buffer, offset, vBytes.Length); + return vBytes.Length; + } + + public override bool Equals(object? obj) => (obj is BsonValue bsonValue) && object.Equals(this.ToNative(), bsonValue.ToNative()); + + + public static implicit operator BsonValue(bool b) => b ? BsonBoolean.True : BsonBoolean.False; + public static implicit operator BsonValue(String s) => new BsonString(s); + public static implicit operator BsonValue(double d) => new BsonDouble(d); + public static implicit operator BsonValue(DateTime dt) => new BsonDateTime(dt); + public static implicit operator BsonValue(int i) => new BsonInt32(i); + public static implicit operator BsonValue(long l) => new BsonInt64(l); + public static implicit operator BsonValue(byte[] b) => new BsonBinary(BsonBinarySubType.Generic, b); + public static implicit operator BsonValue(Guid guid) => new BsonGuid(guid); + + public static implicit operator string(BsonValue bsonString) => ((BsonString)bsonString).Value; + } +} \ No newline at end of file diff --git a/ln.bson/ln.bson.csproj b/ln.bson/ln.bson.csproj new file mode 100644 index 0000000..f8efa66 --- /dev/null +++ b/ln.bson/ln.bson.csproj @@ -0,0 +1,12 @@ + + + + net5.0 + true + + + + + + + diff --git a/ln.bson/mapper/BsonMapper.cs b/ln.bson/mapper/BsonMapper.cs new file mode 100644 index 0000000..fc2e49d --- /dev/null +++ b/ln.bson/mapper/BsonMapper.cs @@ -0,0 +1,114 @@ +using System; +using System.Collections.Generic; +using ln.bson.mapper.mappings; + +namespace ln.bson.mapper +{ + public class BsonMapper + { + private static BsonMapper _defaultInstance; + public static BsonMapper DefaultInstance + { + get + { + if (_defaultInstance is null) + _defaultInstance = new BsonMapper(); + return _defaultInstance; + } + } + + + private Dictionary _bsonMappings = new Dictionary(); + + + + public BsonDocument Map(T o) => Map(typeof(T), o); + public BsonDocument Map(object o) => Map(o.GetType(), o); + public BsonDocument Map(Type type, object o) + { + if (TryGetMapping(type, out BsonMapping bsonMapping)) + { + if (bsonMapping.TryMapValue(this, o, out BsonValue bsonValue) && + (bsonValue is BsonDocument bsonDocument)) + return bsonDocument; + throw new FormatException("could not map object to BsonDocument"); + } + throw new NotSupportedException(); + } + + public T Unmap(BsonDocument document) => (T)Unmap(typeof(T), document); + public object Unmap(Type type, BsonDocument document) + { + if (TryGetMapping(type, out BsonMapping bsonMapping) && + bsonMapping.TryMapValue(this, document, out object o) + ) + return o; + return null; + } + + + public bool TryGetMapping(Type type, out BsonMapping bsonMapping) + { + if (!_defaultMappings.TryGetValue(type, out bsonMapping) && !_bsonMappings.TryGetValue(type, out bsonMapping)) + { + if (type.IsArray) + { + Type mappingType = typeof(ArrayMapping<>).MakeGenericType(type.GetElementType()); + bsonMapping = (BsonMapping) Activator.CreateInstance(mappingType); + AddMapping(bsonMapping); + } else if (type.IsGenericType && + type.GetGenericTypeDefinition().Equals(typeof(Dictionary<,>)) && + type.GetGenericArguments()[0].Equals(typeof(string))) + { + Type mappingType = typeof(DictionaryMapping<>).MakeGenericType(type.GetGenericArguments()[1]); + bsonMapping = (BsonMapping)Activator.CreateInstance(mappingType); + AddMapping(bsonMapping); + } else if (type.IsGenericType && + type.GetGenericTypeDefinition().Equals(typeof(IList<>))) + { + Type mappingType = typeof(IListMapping<,>).MakeGenericType(type, type.GetGenericArguments()[0]); + bsonMapping = (BsonMapping)Activator.CreateInstance(mappingType); + AddMapping(bsonMapping); + } else if (!type.IsPrimitive) + { + Type mappingType = typeof(ClassStructMapping<>).MakeGenericType(type); + bsonMapping = (BsonMapping) Activator.CreateInstance(mappingType); + AddMapping(bsonMapping); + } else + throw new NotSupportedException(); + } + return true; + } + + public void AddMapping(BsonMapping bsonMapping) => AddMapping(bsonMapping, false); + public void AddMapping(BsonMapping bsonMapping, bool replace) + { + _bsonMappings.Add(bsonMapping.SourceType, bsonMapping); + } + + public BsonMapping CreateMapping() + { + if (TryGetMapping(typeof(T), out BsonMapping mapping)) + return mapping; + return null; + } + + + private static Dictionary _defaultMappings = new Dictionary(); + + public static void AddDefaultMapping(BsonMapping bsonMapping) => + _defaultMappings.Add(bsonMapping.SourceType, bsonMapping); + + static BsonMapper() + { + AddDefaultMapping(new BsonMapping.Int32()); + AddDefaultMapping(new BsonMapping.Int64()); + AddDefaultMapping(new BsonMapping.Double()); + AddDefaultMapping(new BsonMapping.String()); + AddDefaultMapping(new BsonMapping.Guid()); + AddDefaultMapping(new BsonMapping.Boolean()); + AddDefaultMapping(new BsonMapping.Binary()); + AddDefaultMapping(new BsonMapping.DateTime()); + } + } +} \ No newline at end of file diff --git a/ln.bson/mapper/BsonMapping.cs b/ln.bson/mapper/BsonMapping.cs new file mode 100644 index 0000000..c8bb515 --- /dev/null +++ b/ln.bson/mapper/BsonMapping.cs @@ -0,0 +1,219 @@ +using System; + +namespace ln.bson.mapper +{ + public abstract class BsonMapping + { + public Type SourceType { get; } + + public BsonMapping(Type sourceType) + { + SourceType = sourceType; + } + + + + public abstract bool TryMapValue(BsonMapper mapper, object o, out BsonValue bsonValue); + public abstract bool TryMapValue(BsonMapper mapper, BsonValue bsonValue, out object o); + + + public class Int32 : BsonMapping + { + public Int32() : base(typeof(int)) + { + } + + public override bool TryMapValue(BsonMapper mapper, object o, out BsonValue bsonValue) + { + bsonValue = new BsonInt32((int)o); + return true; + } + + public override bool TryMapValue(BsonMapper mapper, BsonValue bsonValue, out object o) + { + if (bsonValue is BsonInt32 bsonInt32) + { + o = bsonInt32.Value; + return true; + } + + o = null; + return false; + } + } + public class Int64 : BsonMapping + { + public Int64() : base(typeof(long)) + { + } + + public override bool TryMapValue(BsonMapper mapper, object o, out BsonValue bsonValue) + { + bsonValue = new BsonInt64((long)o); + return true; + } + + public override bool TryMapValue(BsonMapper mapper, BsonValue bsonValue, out object o) + { + if (bsonValue is BsonInt64 bsonInt64) + { + o = bsonInt64.Value; + return true; + } + + o = null; + return false; + } + } + + public class String : BsonMapping + { + public String() : base(typeof(string)) + { + } + + public override bool TryMapValue(BsonMapper mapper, object o, out BsonValue bsonValue) + { + bsonValue = new BsonString((string)o); + return true; + } + + public override bool TryMapValue(BsonMapper mapper, BsonValue bsonValue, out object o) + { + if (bsonValue is BsonString bson) + { + o = bson.Value; + return true; + } + + o = null; + return false; + } + } + + public class Double : BsonMapping + { + public Double() : base(typeof(double)) + { + } + + public override bool TryMapValue(BsonMapper mapper, object o, out BsonValue bsonValue) + { + bsonValue = new BsonDouble((double)o); + return true; + } + + public override bool TryMapValue(BsonMapper mapper, BsonValue bsonValue, out object o) + { + if (bsonValue is BsonDouble bsonDouble) + { + o = bsonDouble.Value; + return true; + } + + o = null; + return false; + } + } + + public class DateTime : BsonMapping + { + public DateTime() : base(typeof(System.DateTime)) + { + } + + public override bool TryMapValue(BsonMapper mapper, object o, out BsonValue bsonValue) + { + bsonValue = new BsonDateTime((System.DateTime)o); + return true; + } + + public override bool TryMapValue(BsonMapper mapper, BsonValue bsonValue, out object o) + { + if (bsonValue is BsonDateTime bsonDateTime) + { + o = bsonDateTime.Value; + return true; + } + + o = null; + return false; + } + } + + public class Boolean : BsonMapping + { + public Boolean() : base(typeof(bool)) + { + } + + public override bool TryMapValue(BsonMapper mapper, object o, out BsonValue bsonValue) + { + bsonValue = (bool)o ? BsonBoolean.True : BsonBoolean.False; + return true; + } + + public override bool TryMapValue(BsonMapper mapper, BsonValue bsonValue, out object o) + { + if (bsonValue is BsonBoolean bson) + { + o = bson.Value; + return true; + } + + o = null; + return false; + } + } + + public class Guid : BsonMapping + { + public Guid() : base(typeof(System.Guid)) + { + } + + public override bool TryMapValue(BsonMapper mapper, object o, out BsonValue bsonValue) + { + bsonValue = new BsonGuid((System.Guid)o); + return true; + } + + public override bool TryMapValue(BsonMapper mapper, BsonValue bsonValue, out object o) + { + if (bsonValue is BsonGuid bson) + { + o = bson.GuidValue; + return true; + } + + o = null; + return false; + } + } + + public class Binary : BsonMapping + { + public Binary() : base(typeof(byte[])) + { + } + + public override bool TryMapValue(BsonMapper mapper, object o, out BsonValue bsonValue) + { + bsonValue = new BsonBinary(BsonBinarySubType.Generic, (byte[])o); + return true; + } + + public override bool TryMapValue(BsonMapper mapper, BsonValue bsonValue, out object o) + { + if (bsonValue is BsonBinary bson) + { + o = bson.Value; + return true; + } + + o = null; + return false; + } + } + } +} \ No newline at end of file diff --git a/ln.bson/mapper/mappings/ArrayMapping.cs b/ln.bson/mapper/mappings/ArrayMapping.cs new file mode 100644 index 0000000..046467a --- /dev/null +++ b/ln.bson/mapper/mappings/ArrayMapping.cs @@ -0,0 +1,61 @@ +using System; + +namespace ln.bson.mapper.mappings +{ + public class ArrayMapping : BsonMapping + { + public ArrayMapping() : base(typeof(T[])) + { + } + + public override bool TryMapValue(BsonMapper mapper, object o, out BsonValue bsonValue) + { + if (mapper.TryGetMapping(typeof(T), out BsonMapping elementMapping)) + { + BsonArray bsonArray = new BsonArray(); + T[] sourceArray = (T[])o; + + foreach (T element in sourceArray) + { + if (!elementMapping.TryMapValue(mapper, element, out BsonValue elementValue)) + { + bsonValue = null; + return false; + } + bsonArray.Add(elementValue); + } + + bsonValue = bsonArray; + return true; + } + + bsonValue = null; + return false; + } + + public override bool TryMapValue(BsonMapper mapper, BsonValue bsonValue, out object o) + { + if (bsonValue is BsonArray bsonArray && mapper.TryGetMapping(typeof(T), out BsonMapping elementMapping)) + { + T[] destinationArray = new T[bsonArray.Length]; + + for (int i = 0; i < destinationArray.Length; i++) + { + if (!elementMapping.TryMapValue(mapper, bsonArray[i], out object element)) + { + o = null; + return false; + } + + destinationArray[i] = (T)element; + } + + o = destinationArray; + return true; + } + + o = null; + return false; + } + } +} \ No newline at end of file diff --git a/ln.bson/mapper/mappings/ClassStructMapping.cs b/ln.bson/mapper/mappings/ClassStructMapping.cs new file mode 100644 index 0000000..e0c355b --- /dev/null +++ b/ln.bson/mapper/mappings/ClassStructMapping.cs @@ -0,0 +1,65 @@ +using System; +using System.Collections.Generic; +using System.Reflection; + +namespace ln.bson.mapper.mappings +{ + public class ClassStructMapping : BsonMapping + { + private FieldInfo[] mappedFields; + //private PropertyInfo[] mappedProperties; + + public ClassStructMapping() : base(typeof(T)) + { + mappedFields = typeof(T).GetFields(BindingFlags.Instance | BindingFlags.Public | + BindingFlags.NonPublic); + } + + public override bool TryMapValue(BsonMapper mapper, object o, out BsonValue bsonValue) + { + BsonDocument bsonDocument = new BsonDocument(); + foreach (FieldInfo fieldInfo in mappedFields) + { + object fieldValue = fieldInfo.GetValue(o); + if (fieldValue is null) + bsonDocument.Add(fieldInfo.Name, BsonNull.Instance); + else if (mapper.TryGetMapping(fieldInfo.FieldType, out BsonMapping fieldMapping) && + fieldMapping.TryMapValue(mapper, fieldValue, out BsonValue bsonElementValue)) + bsonDocument.Add(fieldInfo.Name, bsonElementValue); + } + + bsonValue = bsonDocument; + return true; + } + + public override bool TryMapValue(BsonMapper mapper, BsonValue bsonValue, out object o) + { + o = Activator.CreateInstance(SourceType, true); + + if (bsonValue is BsonDocument bsonDocument) + { + foreach (FieldInfo fieldInfo in mappedFields) + { + if (bsonDocument.Contains(fieldInfo.Name)) + { + object fieldValue; + BsonValue bsonFieldValue = bsonDocument[fieldInfo.Name]; + + if (bsonFieldValue is BsonNull) + fieldValue = null; + else if (mapper.TryGetMapping(fieldInfo.FieldType, out BsonMapping fieldMapping) && + fieldMapping.TryMapValue(mapper, bsonDocument[fieldInfo.Name], out fieldValue)) + { } + else + { + o = null; + return false; + } + fieldInfo.SetValue(o, fieldValue); + } + } + } + return true; + } + } +} \ No newline at end of file diff --git a/ln.bson/mapper/mappings/DictionaryMapping.cs b/ln.bson/mapper/mappings/DictionaryMapping.cs new file mode 100644 index 0000000..238d4da --- /dev/null +++ b/ln.bson/mapper/mappings/DictionaryMapping.cs @@ -0,0 +1,62 @@ +using System; +using System.Collections.Generic; +using System.Text.Json; + +namespace ln.bson.mapper.mappings +{ + public class DictionaryMapping : BsonMapping + { + public DictionaryMapping() : base(typeof(Dictionary)) + { + } + + public override bool TryMapValue(BsonMapper mapper, object o, out BsonValue bsonValue) + { + if (mapper.TryGetMapping(typeof(T), out BsonMapping elementMapping)) + { + Dictionary d = (Dictionary)o; + BsonDocument bsonDocument = new BsonDocument(); + + foreach (KeyValuePair keyValuePair in d) + { + if (!elementMapping.TryMapValue(mapper, keyValuePair.Value, out BsonValue elementValue)) + { + bsonValue = null; + return false; + } + bsonDocument.Add(keyValuePair.Key, elementValue); + } + + bsonValue = bsonDocument; + return true; + } + bsonValue = null; + return false; + + } + + public override bool TryMapValue(BsonMapper mapper, BsonValue bsonValue, out object o) + { + if (bsonValue is BsonDocument bsonDocument && mapper.TryGetMapping(typeof(T), out BsonMapping elementMapping)) + { + Dictionary d = new Dictionary(); + + foreach (KeyValuePair keyValuePair in bsonDocument) + { + if (!elementMapping.TryMapValue(mapper, keyValuePair.Value, out object element)) + { + o = null; + return false; + } + d.Add(keyValuePair.Key, (T)element); + } + + o = d; + return true; + } + + o = null; + return false; + } + } +} \ No newline at end of file diff --git a/ln.bson/mapper/mappings/ListMapping.cs b/ln.bson/mapper/mappings/ListMapping.cs new file mode 100644 index 0000000..ee6c6ee --- /dev/null +++ b/ln.bson/mapper/mappings/ListMapping.cs @@ -0,0 +1,63 @@ +using System; +using System.Collections.Generic; + +namespace ln.bson.mapper.mappings +{ + public class IListMapping : BsonMapping where T : IList + { + public IListMapping() : base(typeof(T)) + { + } + + public override bool TryMapValue(BsonMapper mapper, object o, out BsonValue bsonValue) + { + if (mapper.TryGetMapping(typeof(E), out BsonMapping elementMapping)) + { + BsonArray bsonArray = new BsonArray(); + T source = (T)o; + + foreach (E element in source) + { + if (!elementMapping.TryMapValue(mapper, element, out BsonValue elementValue)) + { + bsonValue = null; + return false; + } + + bsonArray.Add(elementValue); + } + + bsonValue = bsonArray; + return true; + } + + bsonValue = null; + return false; + } + + public override bool TryMapValue(BsonMapper mapper, BsonValue bsonValue, out object o) + { + if (bsonValue is BsonArray bsonArray && mapper.TryGetMapping(typeof(T), out BsonMapping elementMapping)) + { + T destinationList = Activator.CreateInstance(); + + for (int i = 0; i < bsonArray.Length; i++) + { + if (!elementMapping.TryMapValue(mapper, bsonArray[i], out object element)) + { + o = null; + return false; + } + + destinationList.Add((E)element); + } + + o = destinationList; + return true; + } + + o = null; + return false; + } + } +} \ No newline at end of file