550 lines
26 KiB
C#
550 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.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
|
|
|
|
}
|
|
}
|