using System; using System.Collections.Generic; using System.Linq; using System.Linq.Expressions; using System.Reflection; using Artemis.Core.DataModelExpansions; 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 LinkedList _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 LinkedList(); 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 LinkedList(); 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 LinkedList(); Save(); Initialize(); SubscribeToDataModelStore(); } internal DataModelPath(DataModel? target, DataModelPathEntity entity) { Target = target; Path = entity.Path; Entity = entity; _segments = new LinkedList(); 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?.Feature.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.Any() && Segments.All(p => p.Type != DataModelPathSegmentType.Invalid); /// /// Gets a read-only list of all segments of this path /// public IReadOnlyCollection Segments => _segments.ToList().AsReadOnly(); internal 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"); return Segments.LastOrDefault()?.GetPropertyType(); } /// /// 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; } internal void Invalidate() { foreach (DataModelPathSegment dataModelPathSegment in _segments) dataModelPathSegment.Dispose(); _segments.Clear(); _accessorLambda = null; Accessor = null; OnPathInvalidated(); } internal void Initialize() { Invalidate(); if (Target == null) return; DataModelPathSegment startSegment = new DataModelPathSegment(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 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; foreach (DataModelPathSegment segment in _segments) { BinaryExpression notNull = Expression.NotEqual(expression, Expression.Default(expression.Type)); 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; } #region IDisposable /// public void Dispose() { _disposed = true; DataModelStore.DataModelAdded -= DataModelStoreOnDataModelAdded; DataModelStore.DataModelRemoved -= DataModelStoreOnDataModelRemoved; Invalidate(); } #endregion #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; Entity.WrapperType = Target switch { ListPredicateWrapperDataModel _ => PathWrapperType.List, EventPredicateWrapperDataModel _ => PathWrapperType.Event, _ => PathWrapperType.None }; } #endregion #region Event handlers private void DataModelStoreOnDataModelAdded(object? sender, DataModelStoreEvent e) { if (e.Registration.DataModel.Feature.Id != Entity.DataModelId) return; Target = e.Registration.DataModel; Initialize(); } private void DataModelStoreOnDataModelRemoved(object? sender, DataModelStoreEvent e) { if (e.Registration.DataModel.Feature.Id != Entity.DataModelId) return; Target = null; Invalidate(); } #endregion #region Events /// /// Occurs whenever the path becomes invalid /// public event EventHandler PathInvalidated; /// /// Occurs whenever the path becomes valid /// public event EventHandler PathValidated; protected virtual void OnPathValidated() { PathValidated?.Invoke(this, EventArgs.Empty); } protected virtual void OnPathInvalidated() { PathInvalidated?.Invoke(this, EventArgs.Empty); } #endregion } }