Imports System.Collections.Generic Imports System.IO Imports System.Reflection.Emit Imports System.Xml Imports System.Xml.Serialization Imports System.Xml.XPath Imports System.Xml.Xsl Imports DigitalData.Modules.Interfaces.Exceptions Imports DigitalData.Modules.Interfaces.ZUGFeRD Imports DigitalData.Modules.Interfaces.ZUGFeRDInterface Imports DigitalData.Modules.Logging Imports GdPicture14 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 ' define the property map in the database (column SPECIFICATION). Public Const ZUGFERD_SPEC_DEFAULT = "DEFAULT" Public Const ZUGFERD_SPEC_10 = "ZUGFERD_10" Public Const ZUGFERD_SPEC_2x = "ZUGFERD_2x" Private ReadOnly ValidFilenames As New List(Of String) From { PDFEmbeds.ZUGFERD_XML_FILENAME.ToUpper, PDFEmbeds.FACTUR_X_XML_FILENAME_DE.ToUpper, PDFEmbeds.FACTUR_X_XML_FILENAME_FR.ToUpper } Private AllowedFilenames As New List(Of String) Public Enum ErrorType NoValidFile NoZugferd NoValidZugferd MissingProperties UnsupportedFormat FileTooBig UnknownError End Enum Public ReadOnly Property FileGroup As FileGroups Public ReadOnly Property PropertyValues As PropertyValues Public Class ZugferdOptions Public Property AllowFacturX_Filename As Boolean = True Public Property AllowXRechnung_Filename As Boolean = True Public Property AllowZugferd_1_0_Schema As Boolean = True Public Property AllowZugferd_2_x_Schema As Boolean = True End Class Public Class ZugferdResult Public Property DataFileName As String Public Property XElementObject As XElement Public Property SchemaObject As Object Public Property Specification As String Public Property ValidationErrors As New List(Of ZugferdValidationError) End Class Public Class ZugferdValidationError Public ElementName As String Public ElementValue As String Public ErrorMessage As String End Class ''' ''' Create a new instance of ZUGFeRDInterface ''' ''' A LogConfig object ''' A valid GDPicture License ''' Optional parameters to control various settings 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() Else _Options = pOptions End If ApplyFilenameOptions(_Options) FileGroup = New FileGroups(_logConfig) PropertyValues = New PropertyValues(_logConfig) Try Dim oLicenseManager As New LicenseManager oLicenseManager.RegisterKEY(pGDPictureKey) Catch ex As Exception _logger.Warn("GDPicture License could not be registered!") _logger.Error(ex) End Try End Sub Private Sub ApplyFilenameOptions(pOptions As ZugferdOptions) Dim oAllowedFilenames As List(Of String) = ValidFilenames If pOptions.AllowFacturX_Filename = False Then oAllowedFilenames = oAllowedFilenames. Except(New List(Of String) From {PDFEmbeds.FACTUR_X_XML_FILENAME_FR}).ToList() End If If pOptions.AllowXRechnung_Filename = False Then oAllowedFilenames = oAllowedFilenames. Except(New List(Of String) From {PDFEmbeds.FACTUR_X_XML_FILENAME_DE}).ToList() End If AllowedFilenames = oAllowedFilenames End Sub Public Function FilterPropertyMap(pPropertyMap As Dictionary(Of String, XmlItemProperty), pSpecification As String) As Dictionary(Of String, XmlItemProperty) _logger.Debug("Filtering Property map for Specification [{0}]", pSpecification) If pSpecification = ZUGFERD_SPEC_10 Then _logger.Debug("Special Case [{0}], including [{1}]", ZUGFERD_SPEC_10, ZUGFERD_SPEC_DEFAULT) Return pPropertyMap. Where(Function(kv) kv.Value.Specification = pSpecification Or kv.Value.Specification = ZUGFERD_SPEC_DEFAULT). ToDictionary(Function(kv) kv.Key, Function(kv) kv.Value) Else _logger.Debug("Using Specification [{0}]", pSpecification) Return pPropertyMap. Where(Function(kv) kv.Value.Specification = pSpecification). ToDictionary(Function(kv) kv.Key, Function(kv) kv.Value) End If End Function ''' ''' Validates a ZUGFeRD File and extracts the XML Document from it ''' ''' ''' Public Function ExtractZUGFeRDFileWithGDPicture(Path As String) As ZugferdResult Dim oResult = ValidateZUGFeRDFileWithGDPicture(Path) oResult = ValidateZUGFeRDDocument(oResult) If oResult.ValidationErrors.Any() Then Throw New ValidationException() With { .ValidationErrors = oResult.ValidationErrors } End If Return SerializeZUGFeRDDocument(oResult) End Function ''' ''' Validates a ZUGFeRD File and extracts the XML Document from it ''' ''' ''' Public Function ExtractZUGFeRDFileWithGDPicture(Stream As Stream) As ZugferdResult Dim oResult = ValidateZUGFeRDFileWithGDPicture(Stream) oResult = ValidateZUGFeRDDocument(oResult) 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 ''' ''' Validates a ZUGFeRD File and extracts the XML Document from it ''' ''' ''' ''' The embedded xml data as an XPath document Public Function ValidateZUGFeRDFileWithGDPicture(pStream As Stream) As ZugferdResult Dim oEmbedExtractor = New PDFEmbeds(_logConfig) Try ' Extract XML attachments only! Dim oFiles = oEmbedExtractor.Extract(pStream, New List(Of String) From {"xml"}) ' Attachments are in this case the files that are embedded into a pdf file, ' like for example the zugferd-invoice.xml file Return HandleEmbeddedFiles(oFiles) 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. Throw ex Catch ex As Exception _logger.Warn("Error while validating ZUGFeRD file with GDPicture") _logger.Error(ex) Throw ex End Try End Function ''' ''' Validates a ZUGFeRD File and extracts the XML Document from it ''' ''' ''' ''' The embedded xml data as an XPath document Public Function ValidateZUGFeRDFileWithGDPicture(pPath As String) As ZugferdResult Dim oEmbedExtractor = New PDFEmbeds(_logConfig) Try ' Extract XML attachments only! Dim oFiles = oEmbedExtractor.Extract(pPath, New List(Of String) From {"xml"}) ' Attachments are in this case the files that are embedded into a pdf file, ' like for example the zugferd-invoice.xml file Return HandleEmbeddedFiles(oFiles) 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. Throw ex Catch ex As Exception _logger.Warn("Error while validating ZUGFeRD file with GDPicture") _logger.Error(ex) Throw ex End Try End Function Private Function HandleEmbeddedFiles(pResults As List(Of PDFEmbeds.EmbeddedFile)) As ZugferdResult If pResults Is Nothing Then Throw New ZUGFeRDExecption(ErrorType.NoZugferd, "Datei ist keine ZUGFeRD Datei, weil die Attachments nicht gelesen werden konnten.") End If If pResults.Count = 0 Then Throw New ZUGFeRDExecption(ErrorType.NoZugferd, "Datei ist keine ZUGFeRD Datei, weil sie keine Attachments enthält.") End If ' Find the first file which filename matches the valid filenames for embedded invoice files Dim oValidResult As PDFEmbeds.EmbeddedFile = pResults. Where(Function(f) ValidFilenames.Contains(f.FileName.ToUpper)). FirstOrDefault() If oValidResult Is Nothing Then Throw New ZUGFeRDExecption(ErrorType.NoZugferd, "Datei ist keine ZUGFeRD Datei, weil keine entsprechende XML-Datei gefunden wurde.") End If ' Search the embedded files for the ones which are allowed as per the configuration. ' The config might say, allow ZUGFeRD but not Factur-X. Dim oAllowedResult As PDFEmbeds.EmbeddedFile = pResults. Where(Function(f) AllowedFilenames.Contains(f.FileName.ToUpper)). FirstOrDefault() If oAllowedResult Is Nothing Then Throw New ZUGFeRDExecption(ErrorType.UnsupportedFormat, "Datei ist eine ZUGFeRD Datei, aber das Format wird nicht unterstützt.", oAllowedResult.FileName) End If Try Using oStream As New MemoryStream(oAllowedResult.FileContents) Return New ZugferdResult With { .DataFileName = oAllowedResult.FileName, .XElementObject = XElement.Load(oStream) } End Using 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. Throw ex Catch ex As Exception _logger.Error(ex) Throw New ZUGFeRDExecption(ErrorType.NoValidZugferd, "Datei ist eine ungültige ZUGFeRD Datei.") End Try End Function Private Class AllowedType Public SchemaType As Type Public Specification As String End Class Public Function ValidateZUGFeRDDocument(pResult As ZugferdResult) As ZugferdResult Return _Validator.ValidateZUGFeRDDocument(pResult) End Function Public Function SerializeZUGFeRDDocument(pResult As ZugferdResult) As ZugferdResult Try Dim oReader As XmlReader Dim oObject As Object = Nothing Dim oSpecification As String = Nothing Dim oAllowedTypes As New List(Of AllowedType) If _Options.AllowZugferd_1_0_Schema Then oAllowedTypes.Add(New AllowedType With { .SchemaType = GetType(Version1_0.CrossIndustryDocumentType), .Specification = ZUGFERD_SPEC_10 }) End If If _Options.AllowZugferd_2_x_Schema Then oAllowedTypes.AddRange(New List(Of AllowedType) From { New AllowedType With { .SchemaType = GetType(Version2_0.CrossIndustryInvoiceType), .Specification = ZUGFERD_SPEC_2x }, New AllowedType With { .SchemaType = GetType(Version2_1_1.CrossIndustryInvoiceType), .Specification = ZUGFERD_SPEC_2x }, New AllowedType With { .SchemaType = GetType(Version2_2_FacturX.CrossIndustryInvoiceType), .Specification = ZUGFERD_SPEC_2x } }) End If For Each oType In oAllowedTypes Dim oTypeName As String = oType.SchemaType.FullName Dim oSerializer As New XmlSerializer(oType.SchemaType) _logger.Debug("Trying Type [{0}]", oTypeName) Try oReader = pResult.XElementObject.CreateReader() oObject = oSerializer.Deserialize(oReader) oSpecification = oType.Specification _logger.Debug("Serializing with type [{0}] succeeded", oTypeName) Exit For Catch ex As Exception _logger.Debug("Serializing with type [{0}] failed", oTypeName) _logger.Debug(ex.Message) If IsNothing(ex.InnerException) = False Then _logger.Debug(ex.InnerException.Message) End If End Try Next If oObject Is Nothing Then 'Throw New ApplicationException("No Types matched the given document. Document could not be serialized.") Throw New ZUGFeRDExecption(ErrorType.UnsupportedFormat, "Unsupported Format") End If pResult.Specification = oSpecification pResult.SchemaObject = oObject Return pResult Catch ex As ZUGFeRDExecption _logger.Error(ex) Throw ex Catch ex As Exception _logger.Error(ex) Dim oMessage = "Datei ist eine ungültige ZUGFeRD Datei oder das Format wird nicht unterstüzt, oder das Format ist deaktiviert." Throw New ZUGFeRDExecption(ErrorType.NoValidZugferd, oMessage) End Try End Function End Class