// // Class: RuleEngine // Current CLR: 4.0.30319.34209 // System: Microsoft Visual Studio 10.0 // Author: dani // Created: 9/7/2015 8:16:42 AM // // Copyright (c) 2015 Informatikbüro Daniel Schick. All rights reserved. using System; using System.Collections.Generic; using System.Linq; using System.Reflection; using System.Text.RegularExpressions; using log4net; namespace bsmd.database { public class RuleEngine { #region german LOCODE's static definition private static readonly List gerLocodeList = new List() { "DE001", "DEAND", "DEBDF", "DEBZR", "DEBMK", "DEBKE", "DEBRE", "DEBRV", "DEBRB", "DE651", "DEBUM", "DEBUZ", "DECUX", "DEDMG", "DEDTM", "DEDUI", "DEDUS", "DEECK", "DEELS", "DEEME", "DEEMM", "DEFLF", "DEGEK", "DEGLU", "DEGRD", "DEHAM", "DEHHF", "DEHGL", "DEHUS", "DEITZ", "DEKEL", "DE136", "DEKCH", "DEKLE", "DECGN", "DEKRE", "DE241", "DELEE", "DELEW", "DELIS", "DELBC", "DELBM", "DE002", "DEMHG", "DEMOZ", "DEMUK", "DEMUH", "DENSS", "DENHO", "DENHA", "DEOLO", "DETRD", "DEPAP", "DEPEF", "DEPUT", "DEREC", "DEREN", "DERHB", "DERSK", "DESAS", "DESTL", "DEUCK", "DE585", "DEVRD", "DEWED", "DEWVN", "DEWIS", "DE003", "DEWOL", "DEWYK", "DEMAG" }; #endregion public enum LocodeMode { STANDARD, NO_PORT_FLAG, SSN }; public delegate bool LocodeValidHandler(string locode, LocodeMode mode); public delegate bool PortAreaValidHandler(string locode, string portArea); public delegate bool NationalityValidHandler(string nationality); private static readonly ILog log = LogManager.GetLogger(typeof(RuleEngine)); private static Dictionary errorTextList = null; private static Dictionary violationTextList = null; private static LocodeValidHandler _locodeChecker = null; private static PortAreaValidHandler _portAreaChecker = null; private static NationalityValidHandler _nationalityChecker = null; public RuleEngine() { if (RuleEngine.errorTextList == null) RuleEngine.errorTextList = DBManager.Instance.LoadErrorTexts(); if (RuleEngine.violationTextList == null) RuleEngine.violationTextList = DBManager.Instance.LoadViolationTexts(); } public Dictionary> ErrorDict { get; } = new Dictionary>(); public Dictionary> ViolationDict { get; } = new Dictionary>(); public static PortAreaValidHandler PortAreaChecker { get { return _portAreaChecker; } } public static LocodeValidHandler LocodeChecker { get { return _locodeChecker; } } public static NationalityValidHandler NationalityChecker { get { return _nationalityChecker; } } #region public static property validation public static void RegisterLocodeChecker(LocodeValidHandler handler) { _locodeChecker = handler; } public static void RegisterPortAreaChecker(PortAreaValidHandler handler) { _portAreaChecker = handler; } public static void RegisterNationalityChecker(NationalityValidHandler handler) { _nationalityChecker = handler; } /// /// Test function checks decorated properties on an entity for errors (only errors, violations cannot /// happen here) /// /// /// public static void ValidateProperties(DatabaseEntity entity, List errors, List violations) { string identifier = null; if (entity is ISublistElement sublistElement) identifier = (sublistElement).Identifier; Type objType = entity.GetType(); Type attribType = typeof(Validation1Attribute); if (entity.GetValidationBlock() == DatabaseEntity.ValidationBlock.BLOCK2) attribType = typeof(Validation2Attribute); List props = new List(); // add "generic" validation properties to check list props.AddRange(objType.GetProperties().Where(prop => Attribute.IsDefined(prop, typeof(ValidationAttribute)))); // add "block" validation properties to check list props.AddRange(objType.GetProperties().Where( prop => Attribute.IsDefined(prop, attribType)) ); // alle "MaxLength" Properties hinzufügen die sonst keine Validierung haben props.AddRange(objType.GetProperties().Where(prop => (!Attribute.IsDefined(prop, typeof(ValidationAttribute)) && !Attribute.IsDefined(prop, attribType) && Attribute.IsDefined(prop, typeof(MaxLengthAttribute))))); foreach (PropertyInfo property in props) { object propValue = property.GetValue(entity, null); string value = (propValue == null) ? string.Empty : propValue.ToString(); ValidationCode validationCode = ValidationCode.NONE; int maxlen = 0; if (Attribute.IsDefined(property, attribType)) { if (entity.GetValidationBlock() == DatabaseEntity.ValidationBlock.BLOCK1) { Validation1Attribute validationAttribute = Attribute.GetCustomAttribute(property, typeof(Validation1Attribute)) as Validation1Attribute; validationCode = validationAttribute.Code; maxlen = validationAttribute.MaxLen; } else { Validation2Attribute validationAttribute = Attribute.GetCustomAttribute(property, typeof(Validation2Attribute)) as Validation2Attribute; validationCode = validationAttribute.Code; maxlen = validationAttribute.MaxLen; } } if (validationCode == ValidationCode.NONE) { if (Attribute.IsDefined(property, typeof(ValidationAttribute))) { ValidationAttribute validationAttribute = Attribute.GetCustomAttribute(property, typeof(ValidationAttribute)) as ValidationAttribute; validationCode = validationAttribute.Code; maxlen = validationAttribute.MaxLen; } } /// check for truncation warnings if (Attribute.IsDefined(property, typeof(MaxLengthAttribute))) { MaxLengthAttribute mla = Attribute.GetCustomAttribute(property, typeof(MaxLengthAttribute)) as MaxLengthAttribute; bool isStandardML = (mla.MaxLength == 99) || (mla.MaxLength == 100); if((value.Length >= 90) && ((mla.MaxLength == value.Length) || isStandardML)) { // put out a warning this might be truncated violations.Add(RuleEngine.CreateViolation(ValidationCode.TRUNCATE, property.Name, value, entity.Title, identifier, entity.Tablename)); } } /// check for max printable word size (report table overflow) if (Attribute.IsDefined(property, typeof(MaxWordLengthAttribute))) { MaxWordLengthAttribute mwla = Attribute.GetCustomAttribute(property, typeof(MaxWordLengthAttribute)) as MaxWordLengthAttribute; if (value.MaxLenNoWS() > mwla.MaxWordLength) { // put out a warning this might be overflowing in the report violations.Add(RuleEngine.CreateViolation(ValidationCode.WORDOVERFLOW, property.Name, value, entity.Title, identifier, entity.Tablename)); } } // check properties switch (validationCode) { case ValidationCode.NOT_NULL: if (value.Length == 0) errors.Add(RuleEngine.CreateError(validationCode, property.Name, value, entity.Title, identifier, entity.Tablename)); break; case ValidationCode.LOCODE: { Regex rgx = new Regex("^[A-Z]{2}[A-Z0-9]{3}$"); if (!rgx.IsMatch(value)) { errors.Add(RuleEngine.CreateError(validationCode, property.Name, value, entity.Title, identifier, entity.Tablename)); } else if (_locodeChecker != null) { if (!_locodeChecker(value, LocodeMode.STANDARD)) errors.Add(RuleEngine.CreateError(validationCode, property.Name, value, entity.Title, identifier, entity.Tablename)); } } break; case ValidationCode.LOCODE_NOPORT: { Regex rgx = new Regex("^[A-Z]{2}[A-Z0-9]{3}$"); if (!rgx.IsMatch(value)) errors.Add(RuleEngine.CreateError(validationCode, property.Name, value, entity.Title, identifier, entity.Tablename)); if (_locodeChecker != null) if (!_locodeChecker(value, LocodeMode.NO_PORT_FLAG)) errors.Add(RuleEngine.CreateError(validationCode, property.Name, value, entity.Title, identifier, entity.Tablename)); } break; case ValidationCode.LOCODE_SSN: { Regex rgx = new Regex("^[A-Z]{2}[A-Z0-9]{3}$"); if (!rgx.IsMatch(value)) errors.Add(RuleEngine.CreateError(validationCode, property.Name, value, entity.Title, identifier, entity.Tablename)); if (_locodeChecker != null) if (!_locodeChecker(value, LocodeMode.SSN)) errors.Add(RuleEngine.CreateError(validationCode, property.Name, value, entity.Title, identifier, entity.Tablename)); } break; case ValidationCode.LOCODE_GER: { if(!RuleEngine.gerLocodeList.Contains(value)) errors.Add(RuleEngine.CreateError(validationCode, property.Name, value, entity.Title, identifier, entity.Tablename)); } break; case ValidationCode.INT_GT_ZERO: { if (!Int32.TryParse(value, out int intVal) || intVal <= 0) errors.Add(RuleEngine.CreateError(validationCode, property.Name, value, entity.Title, identifier, entity.Tablename)); } break; case ValidationCode.DOUBLE_GT_ZERO: { if (!Double.TryParse(value, out double dVal) || dVal <= 0) errors.Add(RuleEngine.CreateError(validationCode, property.Name, value, entity.Title, identifier, entity.Tablename)); } break; case ValidationCode.GISIS: { Regex rgx = new Regex("[0-9]{4}"); if (!rgx.IsMatch(value)) errors.Add(RuleEngine.CreateError(validationCode, property.Name, value, entity.Title, identifier, entity.Tablename)); } break; case ValidationCode.FLAG_CODE: { if(!RuleEngine.NationalityChecker(value)) errors.Add(RuleEngine.CreateError(validationCode, property.Name, value, entity.Title, identifier, entity.Tablename)); } break; case ValidationCode.OPTIONAL_FLAG_CODE: { if(!value.IsNullOrEmpty()) { if (!RuleEngine.NationalityChecker(value)) errors.Add(RuleEngine.CreateError(validationCode, property.Name, value, entity.Title, identifier, entity.Tablename)); } } break; case ValidationCode.TWO_DIGIT: { Regex rgx = new Regex("[0-9]{2}"); if (!rgx.IsMatch(value)) errors.Add(RuleEngine.CreateError(validationCode, property.Name, value, entity.Title, identifier, entity.Tablename)); } break; case ValidationCode.STRING_EXACT_LEN: { if (!value.IsNullOrEmpty()) { if (value.Length != maxlen) errors.Add(RuleEngine.CreateError(validationCode, property.Name, value, entity.Title, identifier, entity.Tablename)); } } break; case ValidationCode.STRING_MAXLEN: { if (value.Length > maxlen) errors.Add(RuleEngine.CreateError(validationCode, property.Name, value, entity.Title, identifier, entity.Tablename)); } break; case ValidationCode.STRING_IMOCLASS: { Regex rgx = new Regex(@"[1-9]{1}(\.[1-9]{1})?"); if (!rgx.IsMatch(value)) errors.Add(RuleEngine.CreateError(validationCode, property.Name, value, entity.Title, identifier, entity.Tablename)); } break; case ValidationCode.STRING_UNNUMBER: { Regex rgx = new Regex("[0-9]{4}"); if (!rgx.IsMatch(value)) errors.Add(RuleEngine.CreateError(validationCode, property.Name, value, entity.Title, identifier, entity.Tablename)); } break; case ValidationCode.DRAUGHT_IMPLAUSIBLE: { if (!Double.TryParse(value, out double dVal) || dVal <= 0) errors.Add(RuleEngine.CreateError(ValidationCode.DOUBLE_GT_ZERO, property.Name, value, entity.Title, identifier, entity.Tablename)); else if ((dVal < 20) || (dVal > 150)) errors.Add(RuleEngine.CreateError(validationCode, property.Name, value, entity.Title, identifier, entity.Tablename)); } break; case ValidationCode.TIME_IMPLAUSIBLE: { if (value.Length == 0) errors.Add(RuleEngine.CreateError(ValidationCode.NOT_NULL, property.Name, value, entity.Title, identifier, entity.Tablename)); if (DateTime.TryParse(value, out DateTime aTime)) { if ((aTime - DateTime.UtcNow).Minutes > 15) errors.Add(RuleEngine.CreateError(validationCode, property.Name, value, entity.Title, identifier, entity.Tablename)); } } break; case ValidationCode.DOT_NO_COMMA: { if(!value.IsNullOrEmpty() && (value.Contains(","))) { errors.Add(RuleEngine.CreateError(validationCode, property.Name, value, entity.Title, identifier, entity.Tablename)); } if(!value.IsNullOrEmpty() && !value.Trim().Any(char.IsDigit)) // falls "-" oder keine Zahl enthalten, Fehler! { errors.Add(RuleEngine.CreateError(ValidationCode.IMPLAUSIBLE, property.Name, value, entity.Title, identifier, entity.Tablename)); } break; } case ValidationCode.VESSEL_TYPE: { if((value.Length == 0) || (STAT.VesselTypeDict?.ContainsKey(value) == false)) { errors.Add(RuleEngine.CreateError(ValidationCode.NOT_NULL, property.Name, value, entity.Title, identifier, entity.Tablename)); } break; } case ValidationCode.NOT_NULL_MAX_LEN: if ((value.Length > maxlen) || (value.Length == 0)) errors.Add(RuleEngine.CreateError(validationCode, property.Name, value, entity.Title, identifier, entity.Tablename)); break; case ValidationCode.FRZ: { Regex rgx = new Regex("^[A-Z,a-z,0-9]{4,7}$"); if (!rgx.IsMatch(value)) errors.Add(RuleEngine.CreateError(validationCode, property.Name, value, entity.Title, identifier, entity.Tablename)); break; } case ValidationCode.MMSI: { Regex rgx = new Regex("^[0-9]{9}$"); if (!rgx.IsMatch(value)) errors.Add(RuleEngine.CreateError(validationCode, property.Name, value, entity.Title, identifier, entity.Tablename)); break; } case ValidationCode.INVALID_NUMBER_CHARS: { string[] elems = value.Split('\n'); char[] invalidChars = { '\'', '[', '^', ',', '_', '&', ']' }; for(int i=0;i 50) || (elems[i].IndexOfAny(invalidChars) >= 0)) { errors.Add(RuleEngine.CreateError(validationCode, property.Name, value, entity.Title, identifier, entity.Tablename)); break; } } break; } default: break; } } } #endregion #region public methods public List ValidateMessage(Message aMessage, out List errors, out List violations) { List result = new List(); errors = new List(); violations = new List(); foreach (DatabaseEntity derivedEntity in aMessage.Elements) { // individuelle Fehler nach Nachrichtenklasse prüfen derivedEntity.MessageCore = aMessage.MessageCore; // some instance we need info from core (NOA / Transit) if ((derivedEntity is LADG) && aMessage.MessageCore.IsTransit) continue; // kein error reporting für LADG bei Transit (CH, 1.2.16) if ((derivedEntity is SEC) && aMessage.MessageCore.IsSmallShip) continue; // keine SEC Validierung für kleine Schiffe (CH, 1.11.18) RuleEngine.ValidateProperties(derivedEntity, errors, violations); derivedEntity.Validate(errors, violations); } return result; } /// /// Diese Funktion wird für Nachrichtenklassen (MDH, SEC,.. usw.) aufgerufen. Error in eingebetteten /// Klassen werden dann der Nachrichtenklasse zugeordnet (können dann logischerweise mehrfach auftreten) /// public void Validate(DatabaseEntity entity) { if (!(entity is Message)) return; List errors = new List(); List violations = new List(); this.ErrorDict[entity] = errors; this.ViolationDict[entity] = violations; foreach (DatabaseEntity derivedEntity in ((Message)entity).Elements) { // individuelle Fehler nach Nachrichtenklasse prüfen derivedEntity.MessageCore = entity.MessageCore; // some instance we need info from core (NOA / Transit) if ((derivedEntity is LADG) && entity.MessageCore.IsTransit) continue; // kein error reporting für LADG bei Transit (CH, 1.2.16) if ((derivedEntity is SEC) && entity.MessageCore.IsSmallShip) continue; // keine STAT Validierung für kleine Schiffe (CH, 1.11.18) ValidateProperties(derivedEntity, errors, violations); derivedEntity.Validate(errors, violations); } foreach (MessageError error in errors) { error.MessageHeaderId = entity.Id.Value; DBManager.Instance.Save(error); } foreach (MessageViolation violation in violations) { violation.MessageHeaderId = entity.Id.Value; DBManager.Instance.Save(violation); } log.InfoFormat("Msg Id {0} Type {1} has {2} errors and {3} violations", entity.Id, entity.MessageNotificationClass, errors.Count, violations.Count); if (errors.Count > 0) { ((Message)entity).InternalStatus = Message.BSMDStatus.ERROR; DBManager.Instance.Save(entity); } else if (violations.Count > 0) { ((Message)entity).InternalStatus = Message.BSMDStatus.VIOLATION; DBManager.Instance.Save(entity); } } public static bool IsGermanLocode(string val) { return gerLocodeList.Contains(val.ToUpper()); } #endregion #region private helper internal static MessageError CreateError(ValidationCode validationCode, string p, string value, string entityName, string identifier = "", string notificationClass = "") { MessageError error = new MessageError(); if (identifier.IsNullOrEmpty()) error.FullName = string.Format("{0}.{1}", entityName, p); else error.FullName = string.Format("{0}.{1}_{2}", entityName, p, identifier); error.ErrorCode = (int)validationCode; error.Identifier = identifier; error.PropertyName = p; var match = Regex.Match(notificationClass, @"\[*\]\.\[(.*)\]"); if (match.Success) error.NotificationClass = match.Groups[1].Value; else error.NotificationClass = notificationClass; if (errorTextList.ContainsKey((int)validationCode)) { error.ErrorText = string.Format(errorTextList[(int)validationCode], p, value); } else { error.ErrorText = p; if (value != null) error.ErrorText += " - " + value; } return error; } public static MessageViolation CreateViolation(ValidationCode validationCode, string p, string value, string entityName, string identifier = "", string notificationClass = "") { MessageViolation violation = new MessageViolation(); if (identifier.IsNullOrEmpty()) violation.FullName = string.Format("{0}.{1}", entityName, p); else violation.FullName = string.Format("{0}.{1}_{2}", entityName, p, identifier); violation.ViolationCode = (int)validationCode; violation.Identifier = identifier; violation.PropertyName = p; var match = Regex.Match(notificationClass, @"\[*\]\.\[(.*)\]"); if (match.Success) violation.NotificationClass = match.Groups[1].Value; else violation.NotificationClass = notificationClass; if (violationTextList.ContainsKey((int)validationCode)) { violation.ViolationText = string.Format(violationTextList[(int)validationCode], p, value); } else { violation.ViolationText = p; if (value != null) violation.ViolationText += " - " + value; } return violation; } #endregion } }