/*
* 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;
}
}