From 524c429de4c469122baf4046f4218205bc38a3d1 Mon Sep 17 00:00:00 2001 From: Jonathan Jenne Date: Thu, 22 Jun 2023 10:51:43 +0200 Subject: [PATCH] ZUGFeRD: Validate errors in xml and throw ValidationException --- Interfaces/Interfaces.vbproj | 1 + Interfaces/ZUGFeRDInterface.vb | 129 ++++------------------ Interfaces/ZUGFeRDInterface/Exceptions.vb | 14 ++- Interfaces/ZUGFeRDInterface/Validator.vb | 98 ++++++++++++++++ Jobs/Exceptions.vb | 10 -- Jobs/ZUGFeRD/ImportZUGFeRDFiles.vb | 16 +-- 6 files changed, 139 insertions(+), 129 deletions(-) create mode 100644 Interfaces/ZUGFeRDInterface/Validator.vb diff --git a/Interfaces/Interfaces.vbproj b/Interfaces/Interfaces.vbproj index 49f37819..e9ae530f 100644 --- a/Interfaces/Interfaces.vbproj +++ b/Interfaces/Interfaces.vbproj @@ -111,6 +111,7 @@ Settings.settings True + diff --git a/Interfaces/ZUGFeRDInterface.vb b/Interfaces/ZUGFeRDInterface.vb index 8588efbb..cc3dccf9 100644 --- a/Interfaces/ZUGFeRDInterface.vb +++ b/Interfaces/ZUGFeRDInterface.vb @@ -15,6 +15,7 @@ Public Class ZUGFeRDInterface Private ReadOnly _logConfig As LogConfig Private ReadOnly _logger As Logger Private ReadOnly _Options As ZugferdOptions + Private ReadOnly _Validator As Validator ' These constants define the specification markers for the different ' zugferd document schema versions. These markers need to be used to @@ -39,7 +40,6 @@ Public Class ZUGFeRDInterface UnsupportedFormat FileTooBig UnknownError - ValidationFailed End Enum Public ReadOnly Property FileGroup As FileGroups @@ -54,7 +54,6 @@ Public Class ZUGFeRDInterface Public Class ZugferdResult Public Property DataFileName As String - Public Property XPathObject As XPathDocument Public Property XElementObject As XElement Public Property SchemaObject As Object Public Property Specification As String @@ -76,6 +75,7 @@ Public Class ZUGFeRDInterface Public Sub New(pLogConfig As LogConfig, pGDPictureKey As String, Optional pOptions As ZugferdOptions = Nothing) _logConfig = pLogConfig _logger = _logConfig.GetLogger() + _Validator = New Validator(_logConfig) If pOptions Is Nothing Then _Options = New ZugferdOptions() @@ -138,9 +138,11 @@ Public Class ZUGFeRDInterface Dim oResult = ValidateZUGFeRDFileWithGDPicture(Path) oResult = ValidateZUGFeRDDocument(oResult) - 'If IsNothing(oResult.SchemaObject) Then - Throw New ZUGFeRDExecption(ErrorType.NoZugferd, "Datei ist keine ZUGFeRD Datei.") - 'End If + If oResult.ValidationErrors.Any() Then + Throw New ValidationException() With { + .ValidationErrors = oResult.ValidationErrors + } + End If Return SerializeZUGFeRDDocument(oResult) End Function @@ -154,9 +156,16 @@ Public Class ZUGFeRDInterface Dim oResult = ValidateZUGFeRDFileWithGDPicture(Stream) oResult = ValidateZUGFeRDDocument(oResult) - 'If IsNothing(oResult.SchemaObject) Then - ' Throw New ZUGFeRDExecption(ErrorType.NoZugferd, "Datei ist keine ZUGFeRD Datei.") - 'End If + If oResult.ValidationErrors.Any() Then + _logger.Info("Validation found [{0}] errors", oResult.ValidationErrors.Count) + oResult.ValidationErrors.ForEach( + Sub(e) _logger.Info("Field [{0}] with value [{1}] has error: [{2}]", e.ElementName, e.ElementValue, e.ErrorMessage) + ) + + Throw New ValidationException() With { + .ValidationErrors = oResult.ValidationErrors + } + End If Return SerializeZUGFeRDDocument(oResult) End Function @@ -248,22 +257,13 @@ Public Class ZUGFeRDInterface End If Try - Dim oXPathObject As XPathDocument = Nothing Using oStream As New MemoryStream(oAllowedResult.FileContents) - oXPathObject = New XPathDocument(oStream) + Return New ZugferdResult With { + .DataFileName = oAllowedResult.FileName, + .XElementObject = XElement.Load(oStream) + } End Using - Dim oXElementObject As XElement = Nothing - Using oStream As New MemoryStream(oAllowedResult.FileContents) - oXElementObject = XElement.Load(oStream) - End Using - - Return New ZugferdResult With { - .DataFileName = oAllowedResult.FileName, - .XElementObject = oXElementObject, - .XPathObject = oXPathObject - } - Catch ex As ZUGFeRDExecption ' Don't log ZUGFeRD Exceptions here, they should be handled by the calling code. ' It also produces misleading error messages when checking if an attachment is a zugferd file. @@ -281,93 +281,11 @@ Public Class ZUGFeRDInterface End Class Public Function ValidateZUGFeRDDocument(pResult As ZugferdResult) As ZugferdResult - Dim oNavigator As XPathNavigator = pResult.XPathObject.CreateNavigator() - Dim oNamespaceManager As New XmlNamespaceManager(oNavigator.NameTable) - - oNamespaceManager.AddNamespace("ram", "urn:un:unece:uncefact:data:standard:ReusableAggregateBusinessInformationEntity:12") - oNamespaceManager.AddNamespace("rsm", "urn:ferd:CrossIndustryDocument:invoice:1p0") - - Try - Dim oDecimalNodes = pResult.XElementObject.Descendants(). - Where(Function(n) n.Name.ToString.EndsWith("Amount") Or n.Name.ToString.EndsWith("Percent")) - - For Each oNode As XElement In oDecimalNodes - Dim oParsedValue As Decimal = 0.0 - If Decimal.TryParse(oNode.Value, oParsedValue) = False Then - pResult.ValidationErrors.Add(New ZugferdValidationError() With { - .ElementName = oNode.Name.LocalName, - .ElementValue = oNode.Value, - .ErrorMessage = "Value could not be parsed as Decimal" - }) - End If - Next - - Catch ex As Exception - _logger.Error(ex) - End Try - - ' CurrencyCode Nodes - Try - Dim oCurrencyCodeNodes = pResult.XElementObject.Descendants(). - Where(Function(n) n.Name.ToString.EndsWith("CurrencyCode")) - - For Each oNode As XElement In oCurrencyCodeNodes - Dim oValid = ValidateCurrencyCode(oNode.Value) - If oValid = False Then - pResult.ValidationErrors.Add(New ZugferdValidationError() With { - .ElementName = oNode.Name.LocalName, - .ElementValue = oNode.Value, - .ErrorMessage = "Invalid CurrencyCode. Only 3-Character codes are allowed." - }) - End If - Next - Catch ex As Exception - _logger.Error(ex) - End Try - - ' currencyID - Try - Dim oCurrencyIDNodes = pResult.XElementObject.Descendants(). - Where(Function(n) n.Attributes.Any(Function(a) a.Name.LocalName = "currencyID")) - - For Each oNode As XElement In oCurrencyIDNodes - Dim oCurrencyID As String = oNode.Attribute("currencyID")?.Value - - ' CurrencyID is optional per spec - If String.IsNullOrWhiteSpace(oCurrencyID) Then - Continue For - End If - - Dim oValid = ValidateCurrencyCode(oCurrencyID) - If oValid = False Then - pResult.ValidationErrors.Add(New ZugferdValidationError() With { - .ElementName = oNode.Name.LocalName, - .ElementValue = oCurrencyID, - .ErrorMessage = "Invalid currencyID. Only 3-Character codes or empty values are allowed." - }) - End If - Next - - Catch ex As Exception - _logger.Error(ex) - End Try - - Return pResult - End Function - - Private Function ValidateCurrencyCode(pValue As String) As Boolean - Dim oValueRegex As New Text.RegularExpressions.Regex("[A-Z]{3}") - - If oValueRegex.IsMatch(pValue) = False Then - Return False - End If - - Return True + Return _Validator.ValidateZUGFeRDDocument(pResult) End Function Public Function SerializeZUGFeRDDocument(pResult As ZugferdResult) As ZugferdResult Try - Dim oNavigator As XPathNavigator = pResult.XPathObject.CreateNavigator() Dim oReader As XmlReader Dim oObject As Object = Nothing @@ -405,8 +323,7 @@ Public Class ZUGFeRDInterface _logger.Debug("Trying Type [{0}]", oTypeName) Try - oReader = oNavigator.ReadSubtree() - + oReader = pResult.XElementObject.CreateReader() oObject = oSerializer.Deserialize(oReader) oSpecification = oType.Specification diff --git a/Interfaces/ZUGFeRDInterface/Exceptions.vb b/Interfaces/ZUGFeRDInterface/Exceptions.vb index e7de05f0..1efedde1 100644 --- a/Interfaces/ZUGFeRDInterface/Exceptions.vb +++ b/Interfaces/ZUGFeRDInterface/Exceptions.vb @@ -1,4 +1,6 @@ -Public Class Exceptions +Imports DigitalData.Modules.Interfaces.ZUGFeRDInterface + +Public Class Exceptions Public Class ZUGFeRDExecption Inherits ApplicationException @@ -23,4 +25,14 @@ _XmlFile = pXmlFileName End Sub End Class + + Public Class ValidationException + Inherits ApplicationException + + Public ValidationErrors As List(Of ZugferdValidationError) + + Public Sub New() + MyBase.New("ZUGFeRD document found but validation failed!") + End Sub + End Class End Class diff --git a/Interfaces/ZUGFeRDInterface/Validator.vb b/Interfaces/ZUGFeRDInterface/Validator.vb new file mode 100644 index 00000000..bef87b34 --- /dev/null +++ b/Interfaces/ZUGFeRDInterface/Validator.vb @@ -0,0 +1,98 @@ +Imports DigitalData.Modules.Interfaces.ZUGFeRDInterface +Imports DigitalData.Modules.Logging + +Public Class Validator + Private ReadOnly _logConfig As LogConfig + Private ReadOnly _logger As Logger + + Public Sub New(pLogConfig As LogConfig) + _logConfig = pLogConfig + _logger = pLogConfig.GetLogger() + End Sub + + Public Function ValidateZUGFeRDDocument(pResult As ZugferdResult) As ZugferdResult + ValidateDecimalNodes(pResult) + ValidateCurrencyNodes(pResult) + + Return pResult + End Function + + Private Sub ValidateDecimalNodes(ByRef pResult As ZugferdResult) + Try + Dim oDecimalNodes = pResult.XElementObject.Descendants(). + Where(Function(n) n.Name.ToString.EndsWith("Amount") Or n.Name.ToString.EndsWith("Percent")) + + For Each oNode As XElement In oDecimalNodes + Dim oParsedValue As Decimal = 0.0 + If Decimal.TryParse(oNode.Value, oParsedValue) = False Then + pResult.ValidationErrors.Add(New ZugferdValidationError() With { + .ElementName = oNode.Name.LocalName, + .ElementValue = oNode.Value, + .ErrorMessage = "Value could not be parsed as Decimal" + }) + End If + Next + + Catch ex As Exception + _logger.Error(ex) + End Try + End Sub + + Private Sub ValidateCurrencyNodes(ByRef pResult As ZugferdResult) + ' CurrencyCode Nodes + Try + Dim oCurrencyCodeNodes = pResult.XElementObject.Descendants(). + Where(Function(n) n.Name.ToString.EndsWith("CurrencyCode")) + + For Each oNode As XElement In oCurrencyCodeNodes + Dim oValid = ValidateCurrencyCode(oNode.Value) + If oValid = False Then + pResult.ValidationErrors.Add(New ZugferdValidationError() With { + .ElementName = oNode.Name.LocalName, + .ElementValue = oNode.Value, + .ErrorMessage = "Invalid CurrencyCode. Only 3-Character codes are allowed." + }) + End If + Next + Catch ex As Exception + _logger.Error(ex) + End Try + + ' currencyID + Try + Dim oCurrencyIDNodes = pResult.XElementObject.Descendants(). + Where(Function(n) n.Attributes.Any(Function(a) a.Name.LocalName = "currencyID")) + + For Each oNode As XElement In oCurrencyIDNodes + Dim oCurrencyID As String = oNode.Attribute("currencyID")?.Value + + ' CurrencyID is optional per spec + If String.IsNullOrWhiteSpace(oCurrencyID) Then + Continue For + End If + + Dim oValid = ValidateCurrencyCode(oCurrencyID) + If oValid = False Then + pResult.ValidationErrors.Add(New ZugferdValidationError() With { + .ElementName = oNode.Name.LocalName, + .ElementValue = oCurrencyID, + .ErrorMessage = "Invalid currencyID. Only 3-Character codes or empty values are allowed." + }) + End If + Next + + Catch ex As Exception + _logger.Error(ex) + End Try + End Sub + + Private Function ValidateCurrencyCode(pValue As String) As Boolean + Dim oValueRegex As New Text.RegularExpressions.Regex("[A-Z]{3}") + + If oValueRegex.IsMatch(pValue) = False Then + Return False + End If + + Return True + End Function +End Class diff --git a/Jobs/Exceptions.vb b/Jobs/Exceptions.vb index 13ba4b2b..03038289 100644 --- a/Jobs/Exceptions.vb +++ b/Jobs/Exceptions.vb @@ -77,14 +77,4 @@ Public Class Exceptions MyBase.New(pInfo) End Sub End Class - - Public Class ValidationException - Inherits ApplicationException - - Public ValidationErrors As List(Of ZugferdValidationError) - - Public Sub New() - MyBase.New("ZUGFeRD document found but validation failed!") - End Sub - End Class End Class diff --git a/Jobs/ZUGFeRD/ImportZUGFeRDFiles.vb b/Jobs/ZUGFeRD/ImportZUGFeRDFiles.vb index 2fb8dee2..d68b4348 100644 --- a/Jobs/ZUGFeRD/ImportZUGFeRDFiles.vb +++ b/Jobs/ZUGFeRD/ImportZUGFeRDFiles.vb @@ -255,6 +255,9 @@ Public Class ImportZUGFeRDFiles Try oDocument = _zugferd.ExtractZUGFeRDFileWithGDPicture(oFile.FullName) + Catch ex As ValidationException + Throw ex + Catch ex As ZUGFeRDExecption Select Case ex.ErrorType Case ZUGFeRDInterface.ErrorType.NoZugferd @@ -267,7 +270,7 @@ Public Class ImportZUGFeRDFiles Throw New UnsupportedFerdException(ex.XmlFile) Case ZUGFeRDInterface.ErrorType.NoValidZugferd - _logger.Warn("File [{0}] is an Incorrectly formatted ZUGFeRD document!", oFile.Name) + _logger.Info("File [{0}] is an Incorrectly formatted ZUGFeRD document!", oFile.Name) Throw New InvalidFerdException() Case Else @@ -276,17 +279,6 @@ Public Class ImportZUGFeRDFiles End Select End Try - ' These validation errors check the document according to the specification. - ' Things like this will be checked: - ' - Currency Codes - ' - Country Codes - ' - DateTime Formats - If oDocument.ValidationErrors.Any() Then - Throw New ValidationException() With { - .ValidationErrors = oDocument.ValidationErrors - } - End If - ' Extract all attachments with the extensions specified in `AllowedExtensions`. ' If you need to extract and use embedded xml files, you need to filter out the zugferd-invoice.xml yourself. ' Right now the zugferd-invoice.xml is filtered out because `AllowedExtensions` does not contain `xml`.