1
0
mirror of https://github.com/Artemis-RGB/Artemis synced 2025-12-13 05:48:35 +00:00
Robert 919199a8d0 Nodes - Avoid overriding paths pointing to unloaded plugins with empty values
Nodes  - Store the type of paths so that node connections can be restored even if the path their types are based on are unavailable
2023-09-25 20:21:00 +02:00

407 lines
12 KiB
C#

using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using Artemis.Core.Modules;
using Artemis.Storage.Entities.Profile;
namespace Artemis.Core;
/// <summary>
/// Represents a path that points to a property in data model
/// </summary>
public class DataModelPath : IStorageModel, IDisposable
{
private readonly LinkedList<DataModelPathSegment> _segments;
private Expression<Func<object, object>>? _accessorLambda;
private bool _disposed;
/// <summary>
/// Creates a new instance of the <see cref="DataModelPath" /> class pointing directly to the target
/// </summary>
/// <param name="target">The target at which this path starts</param>
public DataModelPath(DataModel target)
{
Target = target ?? throw new ArgumentNullException(nameof(target));
Path = "";
Entity = new DataModelPathEntity();
_segments = new LinkedList<DataModelPathSegment>();
Save();
Initialize();
SubscribeToDataModelStore();
}
/// <summary>
/// Creates a new instance of the <see cref="DataModelPath" /> class pointing to the provided path
/// </summary>
/// <param name="target">The target at which this path starts</param>
/// <param name="path">A point-separated path</param>
public DataModelPath(DataModel target, string path)
{
Target = target ?? throw new ArgumentNullException(nameof(target));
Path = path ?? throw new ArgumentNullException(nameof(path));
Entity = new DataModelPathEntity();
_segments = new LinkedList<DataModelPathSegment>();
Save();
Initialize();
SubscribeToDataModelStore();
}
/// <summary>
/// Creates a new instance of the <see cref="DataModelPath" /> class based on an existing path
/// </summary>
/// <param name="dataModelPath">The path to base the new instance on</param>
public DataModelPath(DataModelPath dataModelPath)
{
if (dataModelPath == null)
throw new ArgumentNullException(nameof(dataModelPath));
Target = dataModelPath.Target;
Path = dataModelPath.Path;
Entity = new DataModelPathEntity();
_segments = new LinkedList<DataModelPathSegment>();
Save();
Initialize();
SubscribeToDataModelStore();
}
/// <summary>
/// Creates a new instance of the <see cref="DataModelPath" /> class based on a <see cref="DataModelPathEntity" />
/// </summary>
/// <param name="entity"></param>
public DataModelPath(DataModelPathEntity entity)
{
Path = entity.Path;
Entity = entity;
_segments = new LinkedList<DataModelPathSegment>();
Load();
Initialize();
SubscribeToDataModelStore();
}
/// <summary>
/// Gets the data model at which this path starts
/// </summary>
public DataModel? Target { get; private set; }
/// <summary>
/// Gets the data model ID of the <see cref="Target" /> if it is a <see cref="DataModel" />
/// </summary>
public string? DataModelId => Target?.Module.Id;
/// <summary>
/// Gets the point-separated path associated with this <see cref="DataModelPath" />
/// </summary>
public string Path { get; private set; }
/// <summary>
/// Gets a boolean indicating whether all <see cref="Segments" /> are valid
/// </summary>
public bool IsValid => Segments.Any() && Segments.All(p => p.Type != DataModelPathSegmentType.Invalid);
/// <summary>
/// Gets a read-only list of all segments of this path
/// </summary>
public IReadOnlyCollection<DataModelPathSegment> Segments => _segments.ToList().AsReadOnly();
/// <summary>
/// Gets the entity used for persistent storage
/// </summary>
public DataModelPathEntity Entity { get; }
internal Func<object, object>? Accessor { get; private set; }
/// <summary>
/// Gets the current value of the path
/// </summary>
public object? GetValue()
{
if (_disposed)
throw new ObjectDisposedException("DataModelPath");
if (_accessorLambda == null || Target == null)
return null;
// If the accessor has not yet been compiled do it now that it's first required
if (Accessor == null)
Accessor = _accessorLambda.Compile();
return Accessor(Target);
}
/// <summary>
/// Gets the property info of the property this path points to
/// </summary>
/// <returns>If static, the property info. If dynamic, <c>null</c></returns>
public PropertyInfo? GetPropertyInfo()
{
if (_disposed)
throw new ObjectDisposedException("DataModelPath");
return Segments.LastOrDefault()?.GetPropertyInfo();
}
/// <summary>
/// Gets the type of the property this path points to
/// </summary>
/// <returns>If possible, the property type</returns>
public Type? GetPropertyType()
{
if (_disposed)
throw new ObjectDisposedException("DataModelPath");
// Prefer the actual type from the segments
Type? segmentType = Segments.LastOrDefault()?.GetPropertyType();
if (segmentType != null)
return segmentType;
// Fall back to stored type
if (!string.IsNullOrWhiteSpace(Entity.Type))
return Type.GetType(Entity.Type);
return null;
}
/// <summary>
/// Gets the property description of the property this path points to
/// </summary>
/// <returns>If found, the data model property description</returns>
public DataModelPropertyAttribute? GetPropertyDescription()
{
if (_disposed)
throw new ObjectDisposedException("DataModelPath");
return Segments.LastOrDefault()?.GetPropertyDescription();
}
/// <inheritdoc />
public override string ToString()
{
return string.IsNullOrWhiteSpace(Path) ? "this" : Path;
}
/// <summary>
/// Occurs whenever the path becomes invalid
/// </summary>
public event EventHandler? PathInvalidated;
/// <summary>
/// Occurs whenever the path becomes valid
/// </summary>
public event EventHandler? PathValidated;
/// <summary>
/// Releases the unmanaged resources used by the object and optionally releases the managed resources.
/// </summary>
/// <param name="disposing">
/// <see langword="true" /> to release both managed and unmanaged resources;
/// <see langword="false" /> to release only unmanaged resources.
/// </param>
protected virtual void Dispose(bool disposing)
{
if (disposing)
{
_disposed = true;
DataModelStore.DataModelAdded -= DataModelStoreOnDataModelAdded;
DataModelStore.DataModelRemoved -= DataModelStoreOnDataModelRemoved;
Invalidate();
}
}
/// <summary>
/// Invokes the <see cref="PathInvalidated" /> event
/// </summary>
protected virtual void OnPathValidated()
{
PathValidated?.Invoke(this, EventArgs.Empty);
}
/// <summary>
/// Invokes the <see cref="PathValidated" /> event
/// </summary>
protected virtual void OnPathInvalidated()
{
PathInvalidated?.Invoke(this, EventArgs.Empty);
}
internal void Invalidate()
{
Target?.RemoveDataModelPath(this);
foreach (DataModelPathSegment dataModelPathSegment in _segments)
dataModelPathSegment.Dispose();
_segments.Clear();
_accessorLambda = null;
Accessor = null;
OnPathInvalidated();
}
internal void Initialize()
{
if (Target == null)
return;
Target.AddDataModelPath(this);
DataModelPathSegment startSegment = new(this, "target", "target");
startSegment.Node = _segments.AddFirst(startSegment);
// On an empty path don't bother processing segments
if (!string.IsNullOrWhiteSpace(Path))
{
string[] segments = Path.Split(".");
for (int index = 0; index < segments.Length; index++)
{
string identifier = segments[index];
LinkedListNode<DataModelPathSegment> node = _segments.AddLast(
new DataModelPathSegment(this, identifier, string.Join('.', segments.Take(index + 1)))
);
node.Value.Node = node;
}
}
ParameterExpression parameter = Expression.Parameter(typeof(object), "t");
Expression? expression = Expression.Convert(parameter, Target.GetType());
Expression? nullCondition = null;
MethodInfo equals = typeof(object).GetMethod("Equals", BindingFlags.Static | BindingFlags.Public)!;
foreach (DataModelPathSegment segment in _segments)
{
BinaryExpression notNull;
try
{
notNull = Expression.NotEqual(expression, Expression.Default(expression.Type));
}
catch (InvalidOperationException)
{
notNull = Expression.NotEqual(
Expression.Call(
null,
equals,
Expression.Convert(expression, typeof(object)),
Expression.Convert(Expression.Default(expression.Type), typeof(object))),
Expression.Constant(true));
}
nullCondition = nullCondition != null ? Expression.AndAlso(nullCondition, notNull) : notNull;
expression = segment.Initialize(parameter, expression, nullCondition);
if (expression == null)
return;
}
if (nullCondition == null)
return;
_accessorLambda = Expression.Lambda<Func<object, object>>(
// Wrap with a null check
Expression.Condition(
nullCondition,
Expression.Convert(expression, typeof(object)),
Expression.Convert(Expression.Default(expression.Type), typeof(object))
),
parameter
);
if (IsValid)
OnPathValidated();
}
private void SubscribeToDataModelStore()
{
DataModelStore.DataModelAdded += DataModelStoreOnDataModelAdded;
DataModelStore.DataModelRemoved += DataModelStoreOnDataModelRemoved;
}
private void DataModelStoreOnDataModelAdded(object? sender, DataModelStoreEvent e)
{
if (e.Registration.DataModel.Module.Id != Entity.DataModelId)
return;
Invalidate();
Target = e.Registration.DataModel;
Initialize();
}
private void DataModelStoreOnDataModelRemoved(object? sender, DataModelStoreEvent e)
{
if (e.Registration.DataModel.Module.Id != Entity.DataModelId)
return;
Invalidate();
Target = null;
}
/// <inheritdoc />
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
#region Storage
/// <inheritdoc />
public void Load()
{
Path = Entity.Path;
if (Target == null && Entity.DataModelId != null)
Target = DataModelStore.Get(Entity.DataModelId)?.DataModel;
}
/// <inheritdoc />
public void Save()
{
// Do not save an invalid state
if (!IsValid)
return;
Entity.Path = Path;
Entity.DataModelId = DataModelId;
// Store the type name but only if available
Type? pathType = Segments.LastOrDefault()?.GetPropertyType();
if (pathType != null)
Entity.Type = pathType.FullName;
}
#region Equality members
/// <inheritdoc cref="Equals(object)" />
/// >
protected bool Equals(DataModelPath other)
{
return ReferenceEquals(Target, other.Target) && Path == other.Path;
}
/// <inheritdoc />
public override bool Equals(object? obj)
{
if (ReferenceEquals(null, obj)) return false;
if (ReferenceEquals(this, obj)) return true;
if (obj.GetType() != GetType()) return false;
return Equals((DataModelPath) obj);
}
/// <inheritdoc />
public override int GetHashCode()
{
return HashCode.Combine(Target, Path);
}
#endregion
#endregion
}