git_bsmd/bsmd.database/RuleEngine.cs

559 lines
26 KiB
C#

//
// 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<string> gerLocodeList = new List<string>() {
"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<int, string> errorTextList = null;
private static Dictionary<int, string> 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<DatabaseEntity, List<MessageError>> ErrorDict { get; } = new Dictionary<DatabaseEntity, List<MessageError>>();
public Dictionary<DatabaseEntity, List<MessageViolation>> ViolationDict { get; } = new Dictionary<DatabaseEntity, List<MessageViolation>>();
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; }
/// <summary>
/// Test function checks decorated properties on an entity for errors (only errors, violations cannot
/// happen here)
/// </summary>
/// <param name="entity"></param>
/// <param name="errors"></param>
public static void ValidateProperties(DatabaseEntity entity, List<MessageError> errors, List<MessageViolation> 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<PropertyInfo> props = new List<PropertyInfo>();
// 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 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<elems.Length;i++)
{
if((elems[i].Length > 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<string> ValidateMessage(Message aMessage, out List<MessageError> errors, out List<MessageViolation> violations)
{
List<string> result = new List<string>();
errors = new List<MessageError>();
violations = new List<MessageViolation>();
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;
}
/// <summary>
/// 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)
/// </summary>
public void Validate(DatabaseEntity entity)
{
if (!(entity is Message)) return;
List<MessageError> errors = new List<MessageError>();
List<MessageViolation> violations = new List<MessageViolation>();
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)
RuleEngine.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
}
}