using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Linq.Expressions; using System.Reflection; using Artemis.Core.Modules; using Artemis.Storage.Entities.Profile; namespace Artemis.Core; /// /// Represents a path that points to a property in data model /// public class DataModelPath : IStorageModel, IDisposable { private readonly List _segments; private Expression>? _accessorLambda; private bool _disposed; /// /// Creates a new instance of the class pointing directly to the target /// /// The target at which this path starts public DataModelPath(DataModel target) { Target = target ?? throw new ArgumentNullException(nameof(target)); Path = ""; Entity = new DataModelPathEntity(); _segments = new List(); Save(); Initialize(); SubscribeToDataModelStore(); } /// /// Creates a new instance of the class pointing to the provided path /// /// The target at which this path starts /// A point-separated path 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 List(); Save(); Initialize(); SubscribeToDataModelStore(); } /// /// Creates a new instance of the class based on an existing path /// /// The path to base the new instance on public DataModelPath(DataModelPath dataModelPath) { if (dataModelPath == null) throw new ArgumentNullException(nameof(dataModelPath)); Target = dataModelPath.Target; Path = dataModelPath.Path; Entity = new DataModelPathEntity(); _segments = new List(); Save(); Initialize(); SubscribeToDataModelStore(); } /// /// Creates a new instance of the class based on a /// /// public DataModelPath(DataModelPathEntity entity) { Path = entity.Path; Entity = entity; _segments = new List(); Load(); Initialize(); SubscribeToDataModelStore(); } /// /// Gets the data model at which this path starts /// public DataModel? Target { get; private set; } /// /// Gets the data model ID of the if it is a /// public string? DataModelId => Target?.Module.Id; /// /// Gets the point-separated path associated with this /// public string Path { get; private set; } /// /// Gets a boolean indicating whether all are valid /// public bool IsValid => _segments.Count != 0 && _segments.All(p => p.Type != DataModelPathSegmentType.Invalid); /// /// Gets a read-only list of all segments of this path /// public IReadOnlyCollection Segments => _segments; /// /// Gets the entity used for persistent storage /// public DataModelPathEntity Entity { get; } internal Func? Accessor { get; private set; } /// /// Gets the current value of the path /// 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); } /// /// Gets the property info of the property this path points to /// /// If static, the property info. If dynamic, null public PropertyInfo? GetPropertyInfo() { if (_disposed) throw new ObjectDisposedException("DataModelPath"); return Segments.LastOrDefault()?.GetPropertyInfo(); } /// /// Gets the type of the property this path points to /// /// If possible, the property type 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; } /// /// Gets the property description of the property this path points to /// /// If found, the data model property description public DataModelPropertyAttribute? GetPropertyDescription() { if (_disposed) throw new ObjectDisposedException("DataModelPath"); return Segments.LastOrDefault()?.GetPropertyDescription(); } /// public override string ToString() { return string.IsNullOrWhiteSpace(Path) ? "this" : Path; } /// /// Occurs whenever the path becomes invalid /// public event EventHandler? PathInvalidated; /// /// Occurs whenever the path becomes valid /// public event EventHandler? PathValidated; /// /// Releases the unmanaged resources used by the object and optionally releases the managed resources. /// /// /// to release both managed and unmanaged resources; /// to release only unmanaged resources. /// protected virtual void Dispose(bool disposing) { if (disposing) { _disposed = true; DataModelStore.DataModelAdded -= DataModelStoreOnDataModelAdded; DataModelStore.DataModelRemoved -= DataModelStoreOnDataModelRemoved; Invalidate(); } } /// /// Invokes the event /// protected virtual void OnPathValidated() { PathValidated?.Invoke(this, EventArgs.Empty); } /// /// Invokes the event /// 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); Debug.Assert(_segments.Count == 0, "Segments should be cleared before initializing"); DataModelPathSegment startSegment = new(this, "target", "target"); _segments.Add(startSegment); //store the previous segment to link them together DataModelPathSegment previous = 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]; DataModelPathSegment segment = new(this, identifier, string.Join('.', segments.Take(index + 1))); // Set the 'next' pointer on the previous segment to the current segment previous.Next = segment; // Set the 'previous' pointer on the current segment to the previous segment segment.Previous = previous; _segments.Add(segment); // The current segment becomes the previous segment for the next iteration previous = segment; } } 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>( // 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; } /// public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } #region Storage /// public void Load() { Path = Entity.Path; if (Target == null && Entity.DataModelId != null) Target = DataModelStore.Get(Entity.DataModelId)?.DataModel; } /// 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 /// /// > protected bool Equals(DataModelPath other) { return ReferenceEquals(Target, other.Target) && Path == other.Path; } /// 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); } /// public override int GetHashCode() { return HashCode.Combine(Target, Path); } #endregion #endregion }