HotPDF validerer EN 16931 forretningsregler for elektroniske fakturaer gjennom HPDFEInvoiceValidator, en Schematron-påstandsmotor biblioteket selv implementerer oppå MSXMLs XPath 1.0-støtte i stedet for en lisensiert XSLT 2.0-prosessor. HPDFEInvoiceValidator parser de offisielle Factur-X .sch-regelfilene, evaluerer hver påstand den kan uttrykke i XPath 1.0, og markerer resten som hoppet over i stedet for å la et ustøttet uttrykk kaste et unntak midt i kjøringen
Omfanget her holder seg innenfor den motoren: hvordan lasteren gjør Schematron-XML om til regeloppføringer, hvordan assert- og report-evaluering faktisk avgjør bestått eller ikke, hvordan XPath 2.0-gapet oppdages og hoppes over, og hvordan den samme enheten (unit) fortsatt kompilerer på Delphi 7. PDF/A-3-innbygging, container-mekanikken for factur-x.xml / xrechnung.xml, og historien om ZUGFeRD 2.5-versjonering hører hjemme i følgeartikkelen om ZUGFeRD- og Factur-X-e-fakturaer i Delphi med HotPDF, som denne artikkelen bevisst ikke gjentar
Hvorfor HotPDF bygde sin egen EN 16931 Schematron-motor
HotPDF bygde sin egen Schematron-motor fordi regelfilens deklarerte binding for EN 16931 overdriver hva påstandene dens faktisk trenger: filen setter queryBinding="xslt2" øverst, som teknisk sett ber om en full XSLT 2.0-/XPath 2.0-prosessor, men å lese gjennom selve påstandene viser at det store flertallet bare kaller XPath 1.0-funksjoner som string-length og substring-after. Windows' innebygde MSXML DOM — den ene XML-motoren garantert å finnes på hver eneste støttede Delphi-installasjon uten å legge til en tredjepartsavhengighet — implementerer nettopp den delmengden, XPath 1.0, noe som er det som gjorde en nativ motor praktisk i stedet for å lisensiere en separat XSLT 2.0-kjøretid. HPDFSchematronFileForProfile kartlegger et oppdaget Factur-X-konformitetsnivå til én av fem medfølgende regelfiler denne motoren kan laste — MINIMUM, BASIC WL, BASIC, EN 16931, og EXTENDED — og hver eneste av dem vurderer bare den utpakkede faktura-XML-en, aldri den omkringliggende PDF-en; hvorvidt den PDF-en selv er en strukturelt gyldig PDF/A-3-fil, er et separat spørsmål besvart gjennom HotPDFs PDF/A-, PDF/X-, og PDF/UA-konformitetssjekker andre steder i biblioteket
Hvordan gjør motoren en .sch-fil om til regeloppføringer?
THPDFMSXMLSchematronEngine.Load starter med å kalle CoInitializeEx(nil, COINIT_MULTITHREADED) før den oppretter noe som helst, fordi en konsoll- eller tjenestevert som aldri kalte Application.Initialize, ennå ikke har noen COM-apartment, mens en GUI VCL-vert allerede har det; motoren behandler S_FALSE- eller RPC_E_CHANGED_MODE-resultatet det kallet kan returnere på en tråd som allerede er inne i en apartment, som like greit som en feil. Deretter parser den Schematron-filen med et MSXML 6.0 DOM-dokument (CoDOMDocument60) og setProperty('SelectionLanguage', 'XPath'), ettersom MSXML som standard bruker sin eldre XSL-Pattern-dialekt med mindre en kaller eksplisitt velger XPath. Derfra kaller lasteren imidlertid aldri selectNodes for å gå gjennom .sch-filens egen struktur — hvert <pattern>-, <rule>-, <assert>-, og <report>-element finnes ved å gå gjennom firstChild / nextSibling for hånd, og sammenligne hver nodes lokale navn og navneromsadresse mot den bokstavelige strengen http://purl.oclc.org/dsdl/schematron
Denne manuelle gjennomgangs-tilnærmingen finnes på grunn av et høna-eller-egget-problem i <ns prefix="ram" uri="..."/>-bindingene hver Factur-X Schematron-fil deklarerer på forhånd. Å løse et prefikset XPath-uttrykk som ram:Name mot de bindingene krever at MSXMLs SelectionNamespaces-egenskap allerede holder dem, men å oppdage bindingene i utgangspunktet ville normalt bety å kjøre en XPath-spørring som //ns:ns — som selv trenger at SelectionNamespaces settes først. HPDFEInvoiceValidator bryter den syklusen ved å samle inn hvert <ns>-element gjennom den samme manuelle barnenode-gjennomgangen før den i det hele tatt rører selectNodes, og folder deretter de innhøstede prefiks/URI-parene inn i én SelectionNamespaces-streng som både .sch-strukturgjennomgangen og hver senere regelevaluering gjenbruker
// 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: hva utløser egentlig et brudd?
Schematron gir assert og report motsatt polaritet, og motoren må bevare det skillet nøyaktig, ellers betyr bruddtellingene ingenting. En <assert test="X"> erklærer at X må holde for hver node som matcher regelens kontekststi, så EvaluateAssert registrerer et brudd når testuttrykkets resultat-nodesett kommer tilbake tomt; en <report test="X"> er speilbildet, og flagger et problem når X er sann, så EvaluateReport registrerer i stedet et brudd når testresultatet er ikke-tomt. Begge inngangspunktene deler den samme to-trinns formen under: Doc.selectNodes(Entry.Context) først, for å finne hver node regelen gjelder for, deretter ContextNode.selectNodes(Entry.Test) mot hver av dem etter tur — noe som er nøyaktig kontekst-så-test-modellen en ekte Schematron-prosessor bruker, bare drevet av MSXMLs XPath 1.0-selectNodes i stedet for en Schematron-bevisst kjøremotor
Hvordan hopper motoren over XPath 2.0 uten å krasje kjøringen?
HPDFEInvoiceValidator forsvarer seg mot ustøttet XPath 2.0-syntaks i to lag, og det første lar aldri MSXML se uttrykket i det hele tatt. Før den evaluerer noen assert eller report, skanner XPath2Detected den rå test-uttrykksstrengen for seks bokstavelige tokens — xs:decimal, xs:integer, xs:string, upper-case, lower-case, og exists( — og hvis noen av dem er til stede, merkes regelen umiddelbart som Skipped med stsInfo-alvorlighetsgrad, ut fra resonnementet at MSXML aldri bør få overlevert et uttrykk det allerede er kjent for å avvise
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 andre laget fanger opp det den statiske token-listen bommer på. Både konteksten sin selectNodes-kall og test-selectNodes-kallet per node kjører inne i en try/except-blokk; når MSXML kaster et unntak på et uttrykk token-skanningen slapp gjennom — en konstruksjon utenfor de seks kjente tokenene, eller en kontekststi den ikke kan løse — fanges unntaket og regelen registreres som Skipped i stedet for å forplante seg til kalleren. Det to-lags-designet er grunnen til at en XPath 2.0-konstruksjon hvor som helst i regelsettet aldri kaster forbi HPDFValidateEInvoice: hver eneste av dens 424 påstander enten evalueres, feiler, eller merkes som hoppet over, og et internt estimat mot den regelfilen anslo den XPath-1.0-kjørbare andelen til rundt 350 av de 424 — nok til at delvis evaluering er verdt å gjøre i stedet for å falle tilbake til en ren container-sjekk i det øyeblikket én eneste XPath 2.0-påstand dukker opp
Å mate UTF-8-faktura-XML til MSXML uten å ødelegge den
THPDFMSXMLSchematronEngine.Validate overleverer ikke de utpakkede faktura-bytene til IXMLDOMDocument.loadXML, fordi den metoden forventer en BSTR — UTF-16 — og ville tolket et rått UTF-8-byte-array på nytt under den antakelsen, uansett hva dokumentets egen <?xml encoding="UTF-8"?>-deklarasjon sier. HPDFEInvoiceValidator kopierer i stedet bytene inn i en GlobalAlloc-et HGLOBAL, pakker den inn i en IStream gjennom CreateStreamOnHGlobal, og laster den strømmen via IPersistStreamInit.Load, en vei MSXML respekterer ved å lese encoding-deklarasjonen fra selve bytestrømmen i stedet for å anta UTF-16 på forhånd. Den samme metoden gjenoppbygger SelectionNamespaces fra prefiksbindingene lasteren allerede høstet mens den parset .sch-filen, slik at en regel skrevet mot et prefiks som ram: løses korrekt mot faktura-XML-ens egen navnerom ved hver evaluering, ikke bare da regelfilen først ble 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
Å holde én enhet (unit) kompilerbar fra Delphi 7 til i dag
HPDFEInvoiceValidator.pas må kompilere på hver Delphi-versjon HotPDF støtter, inkludert utgivelser uten noen XML- eller XPath-binding i det hele tatt, så grensesnittseksjonen dens eksponerer bare rene verditype: records, dynamiske arrays, og ett enkelt grensesnitt, IHPDFESchematronEngine, med Load-, Validate-, og LastSummary-metoder. Hver MSXML-spesifikke type — IXMLDOMDocument2, Winapi.msxml-importen, THPDFMSXMLSchematronEngine selv — sitter inne i én {$IFDEF XE2+}-blokk i implementasjonsseksjonen, usynlig for kallere og for kompilatoren på eldre verktøykjeder likt
{$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 eldre overleverer HPDFCreateSchematronEngine i stedet THPDFStubSchematronEngine: Load-en dens returnerer alltid False med en ErrorText som navngir det egentlige gapet — MSXML DOM-binding krever XE2 eller senere — og peker mot en ekstern validator som veraPDF, Mustang, eller et ZUGFeRD-konformitetsverktøy for full dekning i mellomtiden. Validate-en dens returnerer ett enkelt syntetisk resultat med RuleID 'ENGINE' og Skipped satt, slik at kode som itererer BusinessRules ikke trenger en separat gren for «motoren kunne ikke kjøre» versus «hver regel tilfeldigvis ble hoppet over» — begge ser like ut for kalleren. HPDFValidateEInvoice folder det elegant inn i sin dom også: den boolske verdien den returnerer, er ContainerValid and ((not BusinessRulesEvaluated) or (BusinessRuleViolations = 0)), slik at en utilgjengelig motor nedgraderer resultatet til en ren container-sjekk i stedet for å tvinge frem en hard feil på en kompilator som aldri kom til å kjøre Schematron-regler i utgangspunktet
HPDFEInvoiceValidators XPath 1.0-motor erstatter ikke en full Schematron-/XSLT 2.0-prosessor, og var aldri ment til det: en motor begrenset til XPath 1.0 vil alltid la en håndfull EN 16931-påstander stå uevaluert, noe som er nøyaktig det Skipped-flagget på hvert resultat finnes for å synliggjøre i stedet for å skjule. Det motoren derimot gir, er forretningsregel-tilbakemelding som kjører overalt HotPDF allerede kjører, uten noen ekstern prosess å skalle ut til og ingen XSLT 2.0-kjøretid å lisensiere. Denne motoren følger med som en del av HotPDF PDF-komponenten for Delphi og C++Builder, sammen med container-nivå Factur-X- og PDF/A-verktøyene den bygger på