using System; using System.Linq; using System.Linq.Expressions; using Artemis.Core.DataModelExpansions; using Artemis.Storage.Entities.Profile.Abstract; using Artemis.Storage.Entities.Profile.Conditions; using Newtonsoft.Json; namespace Artemis.Core { /// /// A predicate in a data model condition using either two data model values or one data model value and a /// static value /// public class DataModelConditionPredicate : DataModelConditionPart { /// /// Creates a new instance of the class /// /// /// public DataModelConditionPredicate(DataModelConditionPart parent, ProfileRightSideType predicateType) { Parent = parent; PredicateType = predicateType; Entity = new DataModelConditionPredicateEntity(); Initialize(); } internal DataModelConditionPredicate(DataModelConditionPart parent, DataModelConditionPredicateEntity entity) { Parent = parent; Entity = entity; PredicateType = (ProfileRightSideType) entity.PredicateType; Initialize(); } /// /// Gets or sets the predicate type /// public ProfileRightSideType PredicateType { get; set; } /// /// Gets the operator /// public ConditionOperator Operator { get; private set; } /// /// Gets the currently used instance of the left data model /// public DataModel LeftDataModel { get; private set; } /// /// Gets the path of the left property in the /// public string LeftPropertyPath { get; private set; } /// /// Gets the currently used instance of the right data model /// public DataModel RightDataModel { get; private set; } /// /// Gets the path of the right property in the /// public string RightPropertyPath { get; private set; } /// /// Gets the right static value, only used it is /// /// public object RightStaticValue { get; private set; } public Func LeftSideAccessor { get; set; } public Func RightSideAccessor { get; set; } internal DataModelConditionPredicateEntity Entity { get; set; } /// /// Updates the left side of the predicate /// /// The data model of the left side value /// The path pointing to the left side value inside the data model public void UpdateLeftSide(DataModel dataModel, string path) { if (dataModel != null && path == null) throw new ArtemisCoreException("If a data model is provided, a path is also required"); if (dataModel == null && path != null) throw new ArtemisCoreException("If path is provided, a data model is also required"); if (dataModel != null) { if (!dataModel.ContainsPath(path)) throw new ArtemisCoreException($"Data model of type {dataModel.GetType().Name} does not contain a property at path '{path}'"); } LeftDataModel = dataModel; LeftPropertyPath = path; ValidateOperator(); ValidateRightSide(); CreateAccessors(); } /// /// Updates the right side of the predicate, makes the predicate dynamic and re-compiles the expression /// /// The data model of the right side value /// The path pointing to the right side value inside the data model public void UpdateRightSide(DataModel dataModel, string path) { if (dataModel != null && path == null) throw new ArtemisCoreException("If a data model is provided, a path is also required"); if (dataModel == null && path != null) throw new ArtemisCoreException("If path is provided, a data model is also required"); if (dataModel != null) { if (!dataModel.ContainsPath(path)) throw new ArtemisCoreException($"Data model of type {dataModel.GetType().Name} does not contain a property at path '{path}'"); } PredicateType = ProfileRightSideType.Dynamic; RightDataModel = dataModel; RightPropertyPath = path; CreateAccessors(); } /// /// Updates the right side of the predicate, makes the predicate static and re-compiles the expression /// /// The right side value to use public void UpdateRightSide(object staticValue) { PredicateType = ProfileRightSideType.Static; RightDataModel = null; RightPropertyPath = null; // If the left side is empty simply apply the value, any validation will wait if (LeftDataModel == null) { RightStaticValue = staticValue; return; } Type leftSideType = LeftDataModel.GetTypeAtPath(LeftPropertyPath); // If not null ensure the types match and if not, convert it if (staticValue != null && staticValue.GetType() == leftSideType) RightStaticValue = staticValue; else if (staticValue != null) RightStaticValue = Convert.ChangeType(staticValue, leftSideType); // If null create a default instance for value types or simply make it null for reference types else if (leftSideType.IsValueType) RightStaticValue = Activator.CreateInstance(leftSideType); else RightStaticValue = null; CreateAccessors(); } /// /// Updates the operator of the predicate and re-compiles the expression /// /// public void UpdateOperator(ConditionOperator conditionOperator) { // Calling CreateExpression will clear compiled expressions if (conditionOperator == null) { Operator = null; CreateAccessors(); return; } // No need to clear compiled expressions, without a left data model they are already null if (LeftDataModel == null) { Operator = conditionOperator; return; } Type leftType = LeftDataModel.GetTypeAtPath(LeftPropertyPath); if (!conditionOperator.SupportsType(leftType)) { throw new ArtemisCoreException($"Cannot apply operator {conditionOperator.GetType().Name} to this predicate because " + $"it does not support left side type {leftType.Name}"); } Operator = conditionOperator; CreateAccessors(); } /// public override bool Evaluate() { if (Operator == null || LeftSideAccessor == null || PredicateType != ProfileRightSideType.Static && RightSideAccessor == null) return false; // Compare with a static value if (PredicateType == ProfileRightSideType.Static) { object leftSideValue = LeftSideAccessor(LeftDataModel); if (leftSideValue.GetType().IsValueType && RightStaticValue == null) return false; return Operator.Evaluate(leftSideValue, RightStaticValue); } // Compare with dynamic values if (PredicateType == ProfileRightSideType.Dynamic) return Operator.Evaluate(LeftSideAccessor(LeftDataModel), RightSideAccessor(RightDataModel)); return false; } /// internal override bool EvaluateObject(object target) { return false; } /// public override string ToString() { if (PredicateType == ProfileRightSideType.Dynamic) return $"[Dynamic] {LeftPropertyPath} {Operator.Description} {RightPropertyPath}"; return $"[Static] {LeftPropertyPath} {Operator.Description} {RightStaticValue}"; } internal override void Save() { Entity.PredicateType = (int) PredicateType; Entity.LeftDataModelGuid = LeftDataModel?.PluginInfo?.Guid; Entity.LeftPropertyPath = LeftPropertyPath; Entity.RightDataModelGuid = RightDataModel?.PluginInfo?.Guid; Entity.RightPropertyPath = RightPropertyPath; Entity.RightStaticValue = JsonConvert.SerializeObject(RightStaticValue); Entity.OperatorPluginGuid = Operator?.PluginInfo?.Guid; Entity.OperatorType = Operator?.GetType().Name; } internal void Initialize() { DataModelStore.DataModelAdded += DataModelStoreOnDataModelAdded; DataModelStore.DataModelRemoved += DataModelStoreOnDataModelRemoved; ConditionOperatorStore.ConditionOperatorAdded += ConditionOperatorStoreOnConditionOperatorAdded; ConditionOperatorStore.ConditionOperatorRemoved += ConditionOperatorStoreOnConditionOperatorRemoved; // Left side if (Entity.LeftDataModelGuid != null) { DataModel dataModel = DataModelStore.Get(Entity.LeftDataModelGuid.Value)?.DataModel; if (dataModel != null && dataModel.ContainsPath(Entity.LeftPropertyPath)) UpdateLeftSide(dataModel, Entity.LeftPropertyPath); } // Operator if (Entity.OperatorPluginGuid != null) { ConditionOperator conditionOperator = ConditionOperatorStore.Get(Entity.OperatorPluginGuid.Value, Entity.OperatorType)?.ConditionOperator; if (conditionOperator != null) UpdateOperator(conditionOperator); } // Right side dynamic if (PredicateType == ProfileRightSideType.Dynamic && Entity.RightDataModelGuid != null) { DataModel dataModel = DataModelStore.Get(Entity.RightDataModelGuid.Value)?.DataModel; if (dataModel != null && dataModel.ContainsPath(Entity.RightPropertyPath)) UpdateRightSide(dataModel, Entity.RightPropertyPath); } // Right side static else if (PredicateType == ProfileRightSideType.Static && Entity.RightStaticValue != null) { try { if (LeftDataModel != null) { // Use the left side type so JSON.NET has a better idea what to do Type leftSideType = LeftDataModel.GetTypeAtPath(LeftPropertyPath); object rightSideValue; try { rightSideValue = JsonConvert.DeserializeObject(Entity.RightStaticValue, leftSideType); } // If deserialization fails, use the type's default catch (JsonSerializationException e) { DeserializationLogger.LogPredicateDeserializationFailure(this, e); rightSideValue = Activator.CreateInstance(leftSideType); } UpdateRightSide(rightSideValue); } else { // Hope for the best... UpdateRightSide(JsonConvert.DeserializeObject(Entity.RightStaticValue)); } } catch (JsonReaderException) { // ignored // TODO: Some logging would be nice } } } internal override DataModelConditionPartEntity GetEntity() { return Entity; } private void CreateAccessors() { if (Operator == null) return; // If the operator does not support a right side, create a static expression because the right side will simply be null if (PredicateType == ProfileRightSideType.Dynamic && Operator.SupportsRightSide) CreateDynamicAccessors(); else CreateStaticExpression(); } private void ValidateOperator() { if (LeftDataModel == null || Operator == null) return; Type leftType = LeftDataModel.GetTypeAtPath(LeftPropertyPath); if (!Operator.SupportsType(leftType)) Operator = null; } private void ValidateRightSide() { Type leftSideType = LeftDataModel.GetTypeAtPath(LeftPropertyPath); if (PredicateType == ProfileRightSideType.Dynamic) { if (RightDataModel == null) return; Type rightSideType = RightDataModel.GetTypeAtPath(RightPropertyPath); if (!leftSideType.IsCastableFrom(rightSideType)) UpdateRightSide(null, null); } else { if (RightStaticValue != null && leftSideType.IsCastableFrom(RightStaticValue.GetType())) UpdateRightSide(RightStaticValue); else UpdateRightSide(null); } } private void CreateDynamicAccessors() { if (LeftDataModel == null || RightDataModel == null || Operator == null) return; Expression leftSideAccessor = ExpressionUtilities.CreateDataModelAccessor(LeftDataModel, LeftPropertyPath, "left", out ParameterExpression leftSideParameter); Expression rightSideAccessor = ExpressionUtilities.CreateDataModelAccessor(RightDataModel, RightPropertyPath, "right", out ParameterExpression rightSideParameter); // A conversion may be required if the types differ // This can cause issues if the DataModelConditionOperator wasn't accurate in it's supported types but that is not a concern here if (rightSideAccessor.Type != leftSideAccessor.Type) rightSideAccessor = Expression.Convert(rightSideAccessor, leftSideAccessor.Type); LeftSideAccessor = Expression.Lambda>(leftSideAccessor, leftSideParameter).Compile(); RightSideAccessor = Expression.Lambda>(rightSideAccessor, rightSideParameter).Compile(); } private void CreateStaticExpression() { if (LeftDataModel == null || Operator == null) return; UnaryExpression leftSideAccessor = Expression.Convert( ExpressionUtilities.CreateDataModelAccessor(LeftDataModel, LeftPropertyPath, "left", out ParameterExpression leftSideParameter), typeof(object) ); // If the left side is a value type but the input is empty, this isn't a valid expression if (leftSideAccessor.Type.IsValueType && RightStaticValue == null) return; LeftSideAccessor = Expression.Lambda>(leftSideAccessor, leftSideParameter).Compile(); RightSideAccessor = null; } #region Event handlers private void DataModelStoreOnDataModelAdded(object sender, DataModelStoreEvent e) { DataModel dataModel = e.Registration.DataModel; if (dataModel.PluginInfo.Guid == Entity.LeftDataModelGuid && dataModel.ContainsPath(Entity.LeftPropertyPath)) UpdateLeftSide(dataModel, Entity.LeftPropertyPath); if (dataModel.PluginInfo.Guid == Entity.RightDataModelGuid && dataModel.ContainsPath(Entity.RightPropertyPath)) UpdateRightSide(dataModel, Entity.RightPropertyPath); } private void DataModelStoreOnDataModelRemoved(object sender, DataModelStoreEvent e) { if (LeftDataModel == e.Registration.DataModel) { LeftSideAccessor = null; LeftDataModel = null; } if (RightDataModel == e.Registration.DataModel) { RightSideAccessor = null; RightDataModel = null; } } private void ConditionOperatorStoreOnConditionOperatorAdded(object sender, ConditionOperatorStoreEvent e) { ConditionOperator conditionOperator = e.Registration.ConditionOperator; if (Entity.OperatorPluginGuid == conditionOperator.PluginInfo.Guid && Entity.OperatorType == conditionOperator.GetType().Name) UpdateOperator(conditionOperator); } private void ConditionOperatorStoreOnConditionOperatorRemoved(object sender, ConditionOperatorStoreEvent e) { if (e.Registration.ConditionOperator != Operator) return; Operator = null; } #endregion /// protected override void Dispose(bool disposing) { DataModelStore.DataModelAdded -= DataModelStoreOnDataModelAdded; DataModelStore.DataModelRemoved -= DataModelStoreOnDataModelRemoved; ConditionOperatorStore.ConditionOperatorAdded -= ConditionOperatorStoreOnConditionOperatorAdded; ConditionOperatorStore.ConditionOperatorRemoved -= ConditionOperatorStoreOnConditionOperatorRemoved; base.Dispose(disposing); } } }