/* * Copyright (c) 2022 Vaughn Nugent * * Library: VNLib * Package: VNLib.Plugins.Extensions.Data * File: LWStorageDescriptor.cs * * LWStorageDescriptor.cs is part of VNLib.Plugins.Extensions.Data which is part of the larger * VNLib collection of libraries and utilities. * * VNLib.Plugins.Extensions.Data 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. * * VNLib.Plugins.Extensions.Data 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 VNLib.Plugins.Extensions.Data. If not, see http://www.gnu.org/licenses/. */ using System; using System.IO; using System.Text.Json; using System.Collections; using System.IO.Compression; using System.Threading.Tasks; using System.Collections.Generic; using System.Text.Json.Serialization; using VNLib.Utils; using VNLib.Utils.Async; using VNLib.Utils.Extensions; using VNLib.Utils.Memory; namespace VNLib.Plugins.Extensions.Data.Storage { /// /// Represents an open storage object, that when released or disposed, will flush its changes to the underlying table /// for which this descriptor represents /// public sealed class LWStorageDescriptor : AsyncUpdatableResource, IObjectStorage, IEnumerable>, IIndexable { private static readonly JsonSerializerOptions SerializerOptions = new() { DictionaryKeyPolicy = JsonNamingPolicy.CamelCase, NumberHandling = JsonNumberHandling.Strict, ReadCommentHandling = JsonCommentHandling.Disallow, WriteIndented = false, DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, IgnoreReadOnlyFields = true, DefaultBufferSize = Environment.SystemPageSize, }; internal LWStorageEntry Entry { get; } private readonly Lazy> StringStorage; /// /// The currnt descriptor's identifier string within its backing table. Usually the primary key. /// public string DescriptorID => Entry.Id; /// /// The identifier of the user for which this descriptor belongs to /// public string UserID => Entry.UserId!; /// /// The when the descriptor was created /// public DateTimeOffset Created => Entry.Created; /// /// The last time this descriptor was updated /// public DateTimeOffset LastModified => Entry.LastModified; /// protected override AsyncUpdateCallback UpdateCb { get; } /// protected override AsyncDeleteCallback DeleteCb { get; } /// protected override JsonSerializerOptions JSO => SerializerOptions; internal LWStorageDescriptor(LWStorageManager manager, LWStorageEntry entry) { Entry = entry; UpdateCb = manager.UpdateDescriptorAsync; DeleteCb = manager.RemoveDescriptorAsync; StringStorage = new(OnStringStoreLoad); } internal Dictionary OnStringStoreLoad() { if(Entry.Data == null || Entry.Data.Length == 0) { return new(StringComparer.OrdinalIgnoreCase); } else { //Calc and alloc decode buffer int bufferSize = (int)(Entry.Data.Length * 1.75); using UnsafeMemoryHandle decodeBuffer = Memory.UnsafeAlloc(bufferSize); //Decode and deserialize the data return BrotliDecoder.TryDecompress(Entry.Data, decodeBuffer, out int written) ? JsonSerializer.Deserialize>(Entry.Data, SerializerOptions) ?? new(StringComparer.OrdinalIgnoreCase) : throw new InvalidDataException("Failed to decompress data"); } } /// /// /// /// /// public T? GetObject(string key) { Check(); //De-serialize and return object return StringStorage.Value.TryGetValue(key, out string? val) ? val.AsJsonObject(SerializerOptions) : default; } /// /// /// public void SetObject(string key, T obj) { //Remove the object from storage if its null if (obj == null) { SetStringValue(key, null); } else { //Serialize the object to a string string value = obj.ToJsonString(SerializerOptions)!; //Attempt to store string in storage SetStringValue(key, value); } } /// /// Gets a string value from string storage matching a given key /// /// Key for storage /// Value associaetd with key if exists, otherwise /// If key is null /// public string GetStringValue(string key) { Check(); return StringStorage.Value.TryGetValue(key, out string? val) ? val : string.Empty; } /// /// Creates, overwrites, or removes a string value identified by key. /// /// Entry key /// String to store or overwrite, set to null or string.Empty to remove a property /// /// If key is null public void SetStringValue(string key, string? value) { if (string.IsNullOrEmpty(key)) { throw new ArgumentNullException(nameof(key)); } Check(); //If the value is null, see if the the properties are null if (string.IsNullOrWhiteSpace(value)) { //If the value is null and properies exist, remove the entry StringStorage.Value.Remove(key); Modified |= true; } else { //Set the value StringStorage.Value[key] = value; //Set modified flag Modified |= true; } } /// /// Gets or sets a string value from string storage matching a given key /// /// Key for storage /// Value associaetd with key if exists, otherwise /// /// If key is null public string this[string key] { get => GetStringValue(key); set => SetStringValue(key, value); } /// /// Flushes all pending changes to the backing store asynchronously /// /// public ValueTask WritePendingChangesAsync() { Check(); return Modified ? (new(FlushPendingChangesAsync())) : ValueTask.CompletedTask; } /// public override async ValueTask ReleaseAsync() { await base.ReleaseAsync(); //Cleanup dict on exit if (StringStorage.IsValueCreated) { StringStorage.Value.Clear(); } } /// public IEnumerator> GetEnumerator() => StringStorage.Value.GetEnumerator(); IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); /// protected override object GetResource() => StringStorage.Value; } }