HotPDF validerer EN 16931 e-fakturaens forretningsregler gennem HPDFEInvoiceValidator, en Schematron-assertionsmotor biblioteket selv implementerer oven på MSXML's XPath 1.0-understøttelse frem for en licenseret XSLT 2.0-processor. HPDFEInvoiceValidator parser de officielle Factur-X .sch-regelfiler, evaluerer hver assertion, den kan udtrykke i XPath 1.0, og markerer resten som sprunget over i stedet for at lade et ikke-understøttet udtryk kaste en undtagelse midt i kørslen
Omfanget her forbliver inden i den motor: hvordan loaderen omsætter Schematron-XML til regelposter, hvordan assert- og report-evaluering rent faktisk afgør bestået eller ikke, hvordan XPath 2.0-hullet opdages og springes over, og hvordan den samme unit stadig kompilerer på Delphi 7. PDF/A-3-indlejring, factur-x.xml/xrechnung.xml-container-mekanikken og ZUGFeRD 2.5-versionshistorien findes i følgeartiklen om ZUGFeRD og Factur-X e-fakturaer i Delphi med HotPDF, som dette stykke bevidst ikke gentager
Hvorfor byggede HotPDF sin egen EN 16931 Schematron-motor
HotPDF byggede sin egen Schematron-motor, fordi regelfilens erklærede binding overvurderer, hvad dens assertioner rent faktisk kræver: filen sætter queryBinding="xslt2" øverst og beder teknisk om en fuld XSLT 2.0-/XPath 2.0-processor, men at læse selve assertionerne igennem viser, at det store flertal kun kalder XPath 1.0-funktioner såsom string-length og substring-after. Windows' indbyggede MSXML DOM — den ene XML-motor garanteret at findes på enhver understøttet Delphi-installation uden at tilføje en tredjepartsafhængighed — implementerer netop den delmængde, XPath 1.0, hvilket er det, der gjorde en native motor praktisk i stedet for at licensere en separat XSLT 2.0-runtime. HPDFSchematronFileForProfile mapper et registreret Factur-X-konformitetsniveau til en af fem medfølgende regelfiler, denne motor kan indlæse — MINIMUM, BASIC WL, BASIC, EN 16931 og EXTENDED — og hver af dem bedømmer kun den udtrukne faktura-XML, aldrig den omgivende PDF; om den PDF selv er en strukturelt gyldig PDF/A-3-fil, er et separat spørgsmål besvaret gennem HotPDFs PDF/A-, PDF/X- og PDF/UA-konformitetstjek andre steder i biblioteket
Hvordan omsætter motoren en .sch-fil til regelposter?
THPDFMSXMLSchematronEngine.Load starter med at kalde CoInitializeEx(nil, COINIT_MULTITHREADED), før den opretter noget som helst, fordi en konsol- eller service-vært, der aldrig kaldte Application.Initialize, endnu ikke har et COM-lejlighed (apartment), mens en GUI VCL-vært allerede har; motoren behandler det S_FALSE- eller RPC_E_CHANGED_MODE-resultat, det kald kan returnere på en tråd allerede inde i et lejlighed, som lige så fint som en fejl. Den parser derefter Schematron-filen med et MSXML 6.0 DOM-dokument (CoDOMDocument60) og setProperty('SelectionLanguage', 'XPath'), da MSXML som standard bruger sin ældre XSL-Pattern-dialekt, medmindre en kalder eksplicit vælger XPath til. Derfra kalder loaderen dog aldrig selectNodes for at gennemgå .sch-filens egen struktur — hvert <pattern>-, <rule>-, <assert>- og <report>-element findes ved at gennemgå firstChild/nextSibling i hånden og sammenligne hver nodes lokale navn og navnerum-URI med den bogstavelige streng http://purl.oclc.org/dsdl/schematron
Den manuelle gennemgangsmetode findes på grund af et hønen-og-ægget-problem i <ns prefix="ram" uri="..."/>-bindingerne, som hver Factur-X-Schematron-fil erklærer på forhånd. At løse et præfikset XPath-udtryk som ram:Name mod de bindinger kræver, at MSXML's SelectionNamespaces-egenskab allerede indeholder dem, men at opdage bindingerne i første omgang ville normalt betyde at køre en XPath-forespørgsel som //ns:ns — som selv kræver SelectionNamespaces sat først. HPDFEInvoiceValidator bryder den cyklus ved at indsamle hvert <ns>-element gennem den samme manuelle underknude-gennemgang, før den overhovedet rører selectNodes, og folder derefter de indhøstede præfiks/URI-par ind i én SelectionNamespaces-streng, som både .sch-strukturgennemgangen og hver senere regelevaluering genbruger
// Schematron <ns> bindings must be known before any prefixed XPath can
// run, so this walk cannot itself use selectNodes -- it is done by hand.
ChildNode := Root.firstChild;
while ChildNode <> nil do
begin
if (ChildNode.baseName = 'ns') and
(ChildNode.namespaceURI = 'http://purl.oclc.org/dsdl/schematron') then
AddNamespace(AttrValue(ChildNode, 'prefix'), AttrValue(ChildNode, 'uri'));
ChildNode := ChildNode.nextSibling;
end;
Doc.setProperty('SelectionNamespaces', BuildSelectorNamespaces);
Assert versus report: hvad udløser egentlig en overtrædelse?
Schematron giver assert og report modsat polaritet, og motoren skal bevare det skel præcist, ellers betyder dens overtrædelsestal ingenting. Et <assert test="X"> erklærer, at X skal holde for hver node, der matcher reglens kontekststi, så EvaluateAssert registrerer en overtrædelse, når testudtrykkets resultat-node-sæt kommer tomt tilbage; et <report test="X"> er spejlbilledet og markerer et problem, når X er sandt, så EvaluateReport registrerer en overtrædelse, når testresultatet i stedet er ikke-tomt. Begge indgangspunkter deler den samme to-trins form nedenunder — Doc.selectNodes(Entry.Context) først, for at finde hver node reglen gælder for, derefter ContextNode.selectNodes(Entry.Test) mod hver af dem i tur — hvilket er præcis den kontekst-så-test-model, en rigtig Schematron-processor bruger, blot drevet af MSXML's XPath 1.0 selectNodes i stedet for en Schematron-bevidst udførelsesmotor
Hvordan springer motoren XPath 2.0 over uden at crashe kørslen?
HPDFEInvoiceValidator forsvarer sig mod ikke-understøttet XPath 2.0-syntaks i to lag, og det første lader aldrig MSXML se udtrykket overhovedet. Før den evaluerer nogen assert eller report, skanner XPath2Detected den rå test-udtryksstreng for seks bogstavelige tokens — xs:decimal, xs:integer, xs:string, upper-case, lower-case og exists( — og hvis en af dem er til stede, markeres reglen straks som Skipped med stsInfo-alvorlighed, med den begrundelse at MSXML aldrig bør overdrages et udtryk, det allerede vides at afvise
const
// MSXML implements XPath 1.0 only; presence of any of these tokens marks
// the assertion as skipped instead of letting MSXML reject the expression.
XPATH2_TOKENS: array[0..5] of string = ('xs:decimal', 'xs:integer',
'xs:string', 'upper-case', 'lower-case', 'exists(');
function XPath2Detected(const TestExpr: string): Boolean;
var
Token: string;
begin
Result := False;
for Token in XPATH2_TOKENS do
if Pos(Token, TestExpr) > 0 then
Exit(True);
end;
Det andet lag opfanger, hvad den statiske token-liste går glip af. Både kontekst-selectNodes-kaldet og det per-node test-selectNodes-kald kører inde i en try/except-blok; når MSXML kaster en undtagelse på et udtryk, token-skanningen lod slippe igennem — en konstruktion uden for de seks kendte tokens, eller en kontekststi den ikke kan løse — fanges undtagelsen, og reglen registreres som Skipped i stedet for at blive videresendt til kalderen. Det to-lags-design er grunden til, at en XPath 2.0-konstruktion noget sted i regelsættet aldrig kaster en undtagelse forbi HPDFValidateEInvoice: hver af dens 424 assertioner enten evalueres, fejler, eller markeres sprunget over, og et internt skøn mod den regelfil satte den XPath-1.0-udførbare andel til omtrent 350 af de 424 — nok til at delvis evaluering er værd at gøre frem for at falde tilbage til et rent container-tjek, i det øjeblik en enkelt XPath 2.0-assertion dukker op
At fodre UTF-8-faktura-XML til MSXML uden at ødelægge den
THPDFMSXMLSchematronEngine.Validate overdrager ikke den udtrukne faktura-bytes til IXMLDOMDocument.loadXML, fordi den metode forventer en BSTR — UTF-16 — og ville genfortolke et rå UTF-8-byte-array under den antagelse, uanset hvad dokumentets egen <?xml encoding="UTF-8"?>-erklæring siger. HPDFEInvoiceValidator kopierer i stedet bytene ind i en GlobalAlloc'et HGLOBAL, pakker den ind i en IStream via CreateStreamOnHGlobal, og indlæser den stream via IPersistStreamInit.Load, en vej MSXML respekterer ved at læse encoding-erklæringen fra selve byte-strømmen i stedet for at antage UTF-16 på forhånd. Den samme metode genopbygger SelectionNamespaces fra de præfiksbindinger, loaderen allerede indhøstede under parsingen af .sch-filen, så en regel skrevet mod et præfiks som ram: løses korrekt mod faktura-XML'ens eget navnerum ved hver evaluering, ikke kun da regelfilen først blev parset
HMem := GlobalAlloc(GMEM_MOVEABLE, Length(XMLBytes));
P := GlobalLock(HMem);
Move(XMLBytes[0], P^, Length(XMLBytes));
GlobalUnlock(HMem);
CreateStreamOnHGlobal(HMem, True, Stream); // stream owns HMem from here
(Doc as IPersistStreamInit).Load(Stream); // honours the XML encoding declaration
At holde én unit kompilerende fra Delphi 7 til i dag
HPDFEInvoiceValidator.pas skal kompilere på hver Delphi-version, HotPDF understøtter, inklusive udgivelser helt uden nogen XML- eller XPath-binding, så dens interface-sektion eksponerer kun almindelige værditype: records, dynamiske arrays og én enkelt grænseflade, IHPDFESchematronEngine, med metoderne Load, Validate og LastSummary. Hver MSXML-specifik type — IXMLDOMDocument2, Winapi.msxml-importen, THPDFMSXMLSchematronEngine selv — sidder inde i én {$IFDEF XE2+}-blok i implementeringssektionen, usynlig for kaldere og for kompileren på ældre toolchains alike
{$IFDEF XE2+}
function HPDFCreateSchematronEngine: IHPDFESchematronEngine;
begin
Result := THPDFMSXMLSchematronEngine.Create; // real MSXML-backed engine
end;
{$ELSE}
function HPDFCreateSchematronEngine: IHPDFESchematronEngine;
begin
Result := THPDFStubSchematronEngine.Create; // Delphi 7: reports itself unavailable
end;
{$ENDIF}
På Delphi 7 og ældre overdrager HPDFCreateSchematronEngine i stedet THPDFStubSchematronEngine: dens Load returnerer altid False med en ErrorText, der navngiver det reelle hul — MSXML DOM-binding kræver XE2 eller nyere — og peger mod en ekstern validator såsom veraPDF, Mustang eller et ZUGFeRD-konformitetsværktøj til fuld dækning i mellemtiden. Dens Validate returnerer et enkelt syntetisk resultat med RuleID 'ENGINE' og Skipped sat, så kode der gennemløber BusinessRules ikke behøver en separat gren for "motoren kunne ikke køre" versus "hver regel tilfældigvis blev sprunget over" — begge ser samme form ud for kalderen. HPDFValidateEInvoice folder også det elegant ind i sin dom: den boolean, den returnerer, er ContainerValid and ((not BusinessRulesEvaluated) or (BusinessRuleViolations = 0)), så en utilgængelig motor nedgraderer resultatet til et rent container-tjek i stedet for at tvinge en hård fejl på en kompiler, der aldrig kom til at køre Schematron-regler i første omgang
HPDFEInvoiceValidators XPath 1.0-motor erstatter ikke en fuld Schematron/XSLT 2.0-processor, og det var aldrig meningen: en motor begrænset til XPath 1.0 vil altid efterlade en håndfuld EN 16931-assertioner uevalueret, hvilket er præcis, hvad Skipped-flaget på hvert resultat findes for at gøre synligt frem for at skjule. Det, motoren giver, er forretningsregel-feedback, der kører hvor som helst HotPDF allerede kører, uden nogen ekstern proces at kalde ud til og ingen XSLT 2.0-runtime at licensere. Denne motor leveres som en del af HotPDF PDF-komponenten til Delphi og C++Builder, sammen med det container-niveau Factur-X- og PDF/A-værktøj, den bygger på