using System; using System.Collections; 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; /// /// Creates a new instance of the class pointing directly to the target /// /// The target at which this path starts public DataModelPath(object target) { Target = target ?? throw new ArgumentNullException(nameof(target)); Path = ""; Entity = new DataModelPathEntity(); if (Target is DataModel dataModel) DataModelGuid = dataModel.PluginInfo.Guid; _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(object target, string path) { Target = target ?? throw new ArgumentNullException(nameof(target)); Path = path ?? throw new ArgumentNullException(nameof(path)); Entity = new DataModelPathEntity(); if (Target is DataModel dataModel) DataModelGuid = dataModel.PluginInfo.Guid; _segments = new LinkedList(); Save(); Initialize(); SubscribeToDataModelStore(); } internal DataModelPath(object? 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 object? Target { get; private set; } internal DataModelPathEntity Entity { get; } /// /// Gets the data model GUID of the if it is a /// public Guid? DataModelGuid { get; private set; } /// /// 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(); /// /// Gets a boolean indicating whether this data model path points to a list /// public bool PointsToList => Segments.LastOrDefault()?.GetPropertyType() != null && typeof(IList).IsAssignableFrom(Segments.LastOrDefault()?.GetPropertyType()); internal Func? Accessor { get; private set; } /// /// Gets the current value of the path /// public object? GetValue() { 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() { return Segments.LastOrDefault()?.GetPropertyInfo(); } /// /// Gets the type of the property this path points to /// /// If possible, the property type public Type? GetPropertyType() { 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() { 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; } 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 ); } private void SubscribeToDataModelStore() { DataModelStore.DataModelAdded += DataModelStoreOnDataModelAdded; DataModelStore.DataModelRemoved += DataModelStoreOnDataModelRemoved; } #region Storage /// public void Load() { Path = Entity.Path; DataModelGuid = Entity.DataModelGuid; if (Target == null && Entity.DataModelGuid != null) Target = DataModelStore.Get(Entity.DataModelGuid.Value); } /// public void Save() { Entity.Path = Path; Entity.DataModelGuid = DataModelGuid; } #endregion #region IDisposable /// public void Dispose() { DataModelStore.DataModelAdded -= DataModelStoreOnDataModelAdded; DataModelStore.DataModelRemoved -= DataModelStoreOnDataModelRemoved; Invalidate(); } #endregion #region Event handlers private void DataModelStoreOnDataModelAdded(object? sender, DataModelStoreEvent e) { if (e.Registration.DataModel.PluginInfo.Guid != DataModelGuid) return; Target = e.Registration.DataModel; Initialize(); } private void DataModelStoreOnDataModelRemoved(object? sender, DataModelStoreEvent e) { if (e.Registration.DataModel.PluginInfo.Guid != DataModelGuid) return; Target = null; Invalidate(); } #endregion } }