HotPDF validuje obchodné pravidlá elektronickej faktúry podľa EN 16931 prostredníctvom HPDFEInvoiceValidator, engine na vyhodnocovanie tvrdení Schematron, ktorý si knižnica implementuje sama nad podporou XPath 1.0 v MSXML namiesto licencovaného procesora XSLT 2.0. HPDFEInvoiceValidator parsuje oficiálne súbory pravidiel .sch z Factur-X, vyhodnotí každé tvrdenie, ktoré dokáže vyjadriť v XPath 1.0, a zvyšok označí ako preskočený namiesto toho, aby nechal nepodporovaný výraz vyvolať výnimku uprostred behu
Rozsah tohto článku ostáva vnútri tohto enginu: ako loader premieňa XML Schematron na záznamy pravidiel, ako vyhodnotenie assert a report skutočne rozhoduje o úspechu alebo zlyhaní, ako sa detekuje a preskakuje medzera XPath 2.0, a ako tá istá jednotka stále kompiluje aj na Delphi 7. Vkladanie do PDF/A-3, mechanika kontajnera factur-x.xml / xrechnung.xml a príbeh verziovania ZUGFeRD 2.5 žijú v sprievodnom článku o elektronických faktúrach ZUGFeRD a Factur-X v Delphi s HotPDF, ktorý tento text zámerne neopakuje
Prečo si HotPDF postavil vlastný engine Schematron pre EN 16931
HotPDF si postavil vlastný engine Schematron preto, lebo deklarovaná väzba súboru pravidiel EN 16931 prehnane odhaduje, čo jeho tvrdenia v skutočnosti potrebujú: súbor na začiatku nastavuje queryBinding="xslt2", čím technicky žiada plný procesor XSLT 2.0 / XPath 2.0, no pri prečítaní samotných tvrdení sa ukáže, že veľká väčšina volá iba funkcie XPath 1.0, ako string-length a substring-after. Vstavaný DOM MSXML vo Windows — jediný XML engine, ktorý je zaručene prítomný na každej podporovanej inštalácii Delphi bez pridania závislosti na tretej strane — implementuje práve túto podmnožinu, XPath 1.0, čo je dôvod, prečo bol natívny engine prakticky realizovateľný namiesto licencovania samostatného runtime XSLT 2.0. HPDFSchematronFileForProfile mapuje zistenú úroveň zhody Factur-X na jeden z piatich dodávaných súborov pravidiel, ktoré tento engine dokáže načítať — MINIMUM, BASIC WL, BASIC, EN 16931 a EXTENDED — a každý z nich posudzuje iba extrahované XML faktúry, nikdy okolité PDF; či je toto PDF samotné štrukturálne platným súborom PDF/A-3, je samostatná otázka, na ktorú inde v knižnici odpovedajú kontroly zhody s PDF/A, PDF/X a PDF/UA v HotPDF
Ako engine premieňa súbor .sch na záznamy pravidiel?
THPDFMSXMLSchematronEngine.Load začína volaním CoInitializeEx(nil, COINIT_MULTITHREADED) ešte pred vytvorením čohokoľvek, pretože konzolový alebo servisný hostiteľ, ktorý nikdy nezavolal Application.Initialize, ešte nemá apartmán COM, zatiaľ čo grafický hostiteľ VCL ho už má; engine považuje výsledok S_FALSE alebo RPC_E_CHANGED_MODE, ktorý toto volanie môže vrátiť na vlákne už vnútri apartmánu, rovnako za v poriadku, nie za chybu. Následne parsuje súbor Schematron pomocou DOM dokumentu MSXML 6.0 (CoDOMDocument60) a setProperty('SelectionLanguage', 'XPath'), keďže MSXML predvolene používa svoj starší dialekt XSL-Pattern, ak sa volajúci výslovne neprihlási na XPath. Odtiaľto však loader nikdy nevolá selectNodes na prechod vlastnej štruktúry súboru .sch — každý element <pattern>, <rule>, <assert> a <report> sa nájde ručným prechodom firstChild / nextSibling, pričom sa lokálny názov a URI menného priestoru každého uzla porovnáva s doslovným reťazcom http://purl.oclc.org/dsdl/schematron
Tento prístup s ručným prechodom existuje kvôli problému typu sliepka a vajce vo väzbách <ns prefix="ram" uri="..."/>, ktoré na začiatku deklaruje každý súbor Schematron z Factur-X. Vyriešenie výrazu XPath s prefixom, ako ram:Name, voči týmto väzbám vyžaduje, aby vlastnosť SelectionNamespaces v MSXML už tieto väzby obsahovala, no zistenie väzieb ako prvý krok by za normálnych okolností znamenalo spustenie dotazu XPath, ako //ns:ns — ktorý sám osebe potrebuje najprv nastavenú SelectionNamespaces. HPDFEInvoiceValidator tento cyklus preruší tak, že zozbiera každý element <ns> tým istým ručným prechodom podriadených uzlov ešte predtým, než sa vôbec dotkne selectNodes, a potom zozbierané dvojice prefix/URI zloží do jedného reťazca SelectionNamespaces, ktorý znova použije ako prechod štruktúry .sch, tak aj každé neskoršie vyhodnotenie pravidla
// 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 oproti report: čo v skutočnosti vyvolá porušenie?
Schematron dáva assert a report opačnú polaritu a engine musí toto rozlíšenie zachovať presne, inak počty porušení nič neznamenajú. <assert test="X"> deklaruje, že X musí platiť pre každý uzol zodpovedajúci kontextovej ceste pravidla, takže EvaluateAssert zaznamená porušenie, keď sa množina uzlov výsledku testovacieho výrazu vráti prázdna; <report test="X"> je zrkadlový obraz, označuje problém, keď je X pravdivé, takže EvaluateReport namiesto toho zaznamená porušenie, keď je výsledok testu neprázdny. Oba vstupné body zdieľajú rovnaký dvojstupňový tvar pod kapotou — najprv Doc.selectNodes(Entry.Context), na nájdenie každého uzla, na ktorý sa pravidlo vzťahuje, potom ContextNode.selectNodes(Entry.Test) postupne voči každému z nich — čo je presne model kontext-a-potom-test, aký používa skutočný procesor Schematron, len riadený cez selectNodes XPath 1.0 z MSXML namiesto enginu, ktorý pozná Schematron
Ako engine preskočí XPath 2.0 bez pádu behu?
HPDFEInvoiceValidator sa bráni pred nepodporovanou syntaxou XPath 2.0 v dvoch vrstvách, a prvá z nich nedovolí MSXML výraz vôbec vidieť. Pred vyhodnotením akéhokoľvek assert alebo report XPath2Detected preskenuje surový reťazec testovacieho výrazu na šesť doslovných tokenov — xs:decimal, xs:integer, xs:string, upper-case, lower-case a exists( — a ak je prítomný ktorýkoľvek z nich, pravidlo sa okamžite označí ako Skipped so závažnosťou stsInfo, na základe úvahy, že MSXML by nikdy nemalo dostať výraz, o ktorom sa už vopred vie, že ho odmietne
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;
Druhá vrstva zachytí to, čo statický zoznam tokenov prehliadne. Volanie selectNodes pre kontext aj volanie selectNodes pre test na úrovni jednotlivého uzla bežia vnútri bloku try/except; keď MSXML vyvolá výnimku na výraze, ktorý skenovanie tokenov prepustilo — konštrukcia mimo šiestich známych tokenov, alebo kontextová cesta, ktorú nedokáže vyriešiť — výnimka sa zachytí a pravidlo sa zaznamená ako Skipped namiesto toho, aby sa vyvolala ďalej k volajúcemu. Práve tento dvojvrstvový návrh je dôvodom, prečo konštrukcia XPath 2.0 kdekoľvek v súbore pravidiel nikdy nevyvolá výnimku ponad HPDFValidateEInvoice: každé z jeho 424 tvrdení sa buď vyhodnotí, zlyhá, alebo sa označí ako preskočené, a interný odhad voči tomuto súboru pravidiel odhadol podiel spustiteľný cez XPath 1.0 na približne 350 zo 424 — dosť na to, aby sa oplatilo robiť čiastočné vyhodnotenie namiesto toho, aby sa vo chvíli, keď sa objaví čo i len jedno tvrdenie XPath 2.0, spadlo späť na kontrolu len na úrovni kontajnera
Ako sa XML faktúry v UTF-8 odovzdáva do MSXML bez jeho pokazenia
THPDFMSXMLSchematronEngine.Validate neposiela extrahované bajty faktúry do IXMLDOMDocument.loadXML, pretože táto metóda očakáva BSTR — teda UTF-16 — a pri tomto predpoklade by prepočítala surové pole bajtov v UTF-8 nesprávne, bez ohľadu na to, čo hovorí vlastná deklarácia dokumentu <?xml encoding="UTF-8"?>. HPDFEInvoiceValidator namiesto toho skopíruje bajty do HGLOBAL alokovaného cez GlobalAlloc, obalí ho do IStream pomocou CreateStreamOnHGlobal a načíta tento prúd cez IPersistStreamInit.Load, cestu, ktorú MSXML rešpektuje tak, že prečíta deklaráciu kódovania priamo z bajtového prúdu namiesto toho, aby vopred predpokladalo UTF-16. Tá istá metóda znova zostaví SelectionNamespaces z väzieb prefixov, ktoré loader už zozbieral počas parsovania súboru .sch, takže pravidlo napísané voči prefixu, ako ram:, sa pri každom vyhodnotení správne vyrieši voči vlastnému mennému priestoru XML faktúry, nielen vo chvíli, keď bol súbor pravidiel prvýkrát parsovaný
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
Ako udržať jednu jednotku kompilovateľnú od Delphi 7 až po súčasnosť
HPDFEInvoiceValidator.pas sa musí kompilovať na každej verzii Delphi, ktorú HotPDF podporuje, vrátane vydaní úplne bez akejkoľvek väzby na XML alebo XPath, takže jej sekcia interface sprístupňuje iba obyčajné hodnotové typy: záznamy, dynamické polia a jediné rozhranie, IHPDFESchematronEngine, s metódami Load, Validate a LastSummary. Každý typ špecifický pre MSXML — IXMLDOMDocument2, import Winapi.msxml, samotná THPDFMSXMLSchematronEngine — sedí vnútri jedného bloku {$IFDEF XE2+} v sekcii implementation, neviditeľný pre volajúcich aj pre kompilátor na starších reťazcoch nástrojov rovnako
{$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}
Na Delphi 7 a starších HPDFCreateSchematronEngine namiesto toho vráti THPDFStubSchematronEngine: jej Load vždy vráti False s ErrorText, ktorý pomenúva skutočnú medzeru — väzba na DOM MSXML vyžaduje XE2 alebo novšiu verziu — a medzičasom ukazuje na externý validátor, ako veraPDF, Mustang, alebo nástroj na zhodu s ZUGFeRD pre plné pokrytie. Jej Validate vráti jediný syntetický výsledok s RuleID 'ENGINE' a nastaveným Skipped, takže kód, ktorý prechádza BusinessRules, nepotrebuje samostatnú vetvu pre „engine sa nedal spustiť“ oproti „všetky pravidlá boli náhodou preskočené“ — obe vyzerajú pre volajúceho rovnako. HPDFValidateEInvoice to tiež elegantne zohľadňuje vo svojom výroku: booleovská hodnota, ktorú vracia, je ContainerValid and ((not BusinessRulesEvaluated) or (BusinessRuleViolations = 0)), takže nedostupný engine zníži výsledok na kontrolu iba na úrovni kontajnera namiesto toho, aby vynútil tvrdé zlyhanie na kompilátore, ktorý aj tak nikdy nemal spúšťať pravidlá Schematron
Engine XPath 1.0 v HPDFEInvoiceValidator nenahrádza plný procesor Schematron/XSLT 2.0 a ani to nikdy nebolo jeho cieľom: engine obmedzený na XPath 1.0 vždy ponechá hŕstku tvrdení EN 16931 nevyhodnotených, čo je presne to, čo má príznak Skipped pri každom výsledku sprístupniť, nie skryť. Čo engine skutočne prináša, je spätná väzba k obchodným pravidlám, ktorá beží všade, kde už beží HotPDF, bez akéhokoľvek externého procesu, na ktorý by bolo treba volať cez shell, a bez potreby licencovať runtime XSLT 2.0. Tento engine sa dodáva ako súčasť komponentu HotPDF PDF pre Delphi a C++Builder, spolu s nástrojmi na úrovni kontajnera pre Factur-X a PDF/A, na ktorých je postavený