HotPDF validează regulile de business ale facturii electronice EN 16931 prin HPDFEInvoiceValidator, un motor de asertare Schematron pe care biblioteca îl implementează ea însăși peste suportul XPath 1.0 al MSXML, în loc de un procesor XSLT 2.0 licențiat. HPDFEInvoiceValidator analizează fișierele oficiale de reguli .sch Factur-X, evaluează fiecare asertare pe care o poate exprima în XPath 1.0 și marchează restul ca omise (skipped), în loc să lase o expresie neacceptată să ridice o excepție în mijlocul rulării
Sfera acestui articol rămâne în interiorul acelui motor: cum transformă încărcătorul XML-ul Schematron în intrări de reguli, cum decide efectiv evaluarea assert și report trecerea sau eșecul, cum este detectat și omis golul XPath 2.0 și cum aceeași unitate încă se compilează pe Delphi 7. Încorporarea PDF/A-3, mecanica containerului factur-x.xml / xrechnung.xml și povestea versiunilor ZUGFeRD 2.5 trăiesc în articolul complementar despre facturile electronice ZUGFeRD și Factur-X în Delphi cu HotPDF, pe care acest articol nu îl repetă în mod deliberat
De ce și-a construit HotPDF propriul motor Schematron EN 16931
HotPDF și-a construit propriul motor Schematron pentru că legarea declarată a fișierului de reguli EN 16931 supraestimează ce au nevoie de fapt asertările sale: fișierul setează queryBinding="xslt2" la început, cerând tehnic un procesor complet XSLT 2.0 / XPath 2.0, dar parcurgerea asertărilor propriu-zise arată că marea majoritate apelează doar funcții XPath 1.0 precum string-length și substring-after. DOM-ul MSXML încorporat în Windows — singurul motor XML garantat să existe pe fiecare instalare Delphi acceptată, fără a adăuga o dependență terță — implementează exact acel subset, XPath 1.0, ceea ce a făcut ca un motor nativ să fie practic, în loc să se licențieze un runtime XSLT 2.0 separat. HPDFSchematronFileForProfile mapează un nivel de conformitate Factur-X detectat pe unul din cele cinci fișiere de reguli livrate pe care acest motor le poate încărca — MINIMUM, BASIC WL, BASIC, EN 16931 și EXTENDED — iar fiecare dintre ele judecă doar XML-ul facturii extrase, niciodată PDF-ul înconjurător; dacă acel PDF este el însuși un fișier PDF/A-3 valid structural este o întrebare separată, la care se răspunde prin verificările de conformitate PDF/A, PDF/X și PDF/UA ale HotPDF din altă parte a bibliotecii
Cum transformă motorul un fișier .sch în intrări de reguli?
THPDFMSXMLSchematronEngine.Load începe apelând CoInitializeEx(nil, COINIT_MULTITHREADED) înainte de a crea orice, pentru că o gazdă de tip consolă sau serviciu care nu a apelat niciodată Application.Initialize nu are încă un apartament COM, în timp ce o gazdă VCL GUI îl are deja; motorul tratează rezultatul S_FALSE sau RPC_E_CHANGED_MODE pe care acel apel îl poate returna pe un thread deja aflat într-un apartament ca fiind la fel de acceptabil, nu ca o eroare. Apoi analizează fișierul Schematron cu un document DOM MSXML 6.0 (CoDOMDocument60) și setProperty('SelectionLanguage', 'XPath'), pentru că MSXML implicit folosește dialectul său mai vechi XSL-Pattern, decât dacă un apelant optează explicit pentru XPath. De acolo, totuși, încărcătorul nu apelează niciodată selectNodes pentru a parcurge propria structură a fișierului .sch — fiecare element <pattern>, <rule>, <assert> și <report> este găsit parcurgând manual firstChild / nextSibling, comparând numele local și URI-ul de spațiu de nume ale fiecărui nod cu șirul literal http://purl.oclc.org/dsdl/schematron
Această abordare de parcurgere manuală există din cauza unei probleme de tip ou-și-găină în legăturile <ns prefix="ram" uri="..."/> pe care fiecare fișier Schematron Factur-X le declară la început. Rezolvarea unei expresii XPath cu prefix precum ram:Name față de acele legături necesită ca proprietatea SelectionNamespaces a MSXML să le conțină deja, dar descoperirea legăturilor în primul rând ar însemna în mod normal rularea unei interogări XPath precum //ns:ns — care ea însăși are nevoie ca SelectionNamespaces să fie deja setat. HPDFEInvoiceValidator rupe acel ciclu colectând fiecare element <ns> prin aceeași parcurgere manuală a nodurilor copil înainte de a atinge deloc selectNodes, apoi pliază perechile prefix/URI adunate într-un singur șir SelectionNamespaces pe care atât parcurgerea structurii .sch, cât și fiecare evaluare ulterioară de regulă îl refolosesc
// 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: ce declanșează efectiv o încălcare?
Schematron atribuie assert și report o polaritate opusă, iar motorul trebuie să păstreze exact acea distincție, altfel numărătorile de încălcări nu înseamnă nimic. Un <assert test="X"> declară că X trebuie să fie adevărat pentru fiecare nod care se potrivește cu calea de context a regulii, așa că EvaluateAssert înregistrează o încălcare atunci când setul de noduri rezultat al expresiei de test revine gol; un <report test="X"> este imaginea în oglindă, semnalând o problemă atunci când X este adevărat, așa că EvaluateReport înregistrează o încălcare atunci când rezultatul testului este ne-gol în schimb. Ambele puncte de intrare împart aceeași formă în doi pași dedesubt — mai întâi Doc.selectNodes(Entry.Context), pentru a găsi fiecare nod căruia i se aplică regula, apoi ContextNode.selectNodes(Entry.Test) pentru fiecare dintre ele pe rând — ceea ce este exact modelul context-apoi-test pe care îl folosește un procesor Schematron real, doar condus de selectNodes XPath 1.0 al MSXML, în loc de un motor de execuție conștient de Schematron
Cum omite motorul XPath 2.0 fără să prăbușească rularea?
HPDFEInvoiceValidator se apără împotriva sintaxei XPath 2.0 neacceptate în două straturi, iar primul nu lasă niciodată MSXML să vadă deloc expresia. Înainte de a evalua orice assert sau report, XPath2Detected scanează șirul brut al expresiei de test pentru șase token-uri literale — xs:decimal, xs:integer, xs:string, upper-case, lower-case și exists( — iar dacă oricare dintre ele este prezent, regula este marcată Skipped cu severitatea stsInfo imediat, pe raționamentul că MSXML nu ar trebui niciodată să primească o expresie despre care se știe deja că o va respinge
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;
Al doilea strat prinde tot ce ratează lista statică de token-uri. Atât apelul selectNodes pentru context, cât și apelul selectNodes pentru testul per-nod rulează în interiorul unui bloc try/except; când MSXML ridică o excepție pe o expresie pe care scanarea de token-uri a lăsat-o să treacă — o construcție în afara celor șase token-uri cunoscute, sau o cale de context pe care nu o poate rezolva — excepția este prinsă și regula este înregistrată ca Skipped, în loc să fie propagată apelantului. Acest design pe două straturi este motivul pentru care o construcție XPath 2.0 oriunde în setul de reguli nu ridică niciodată o excepție dincolo de HPDFValidateEInvoice: fiecare din cele 424 de asertări ale sale fie se evaluează, fie eșuează, fie este marcată omisă, iar o estimare internă pe acel fișier de reguli a plasat partea executabilă în XPath 1.0 la aproximativ 350 din cele 424 — suficient de mult încât evaluarea parțială merită făcută, în loc de a reveni la o verificare doar la nivel de container în clipa în care apare o singură asertare XPath 2.0
Alimentarea XML-ului de factură UTF-8 către MSXML fără a-l deteriora
THPDFMSXMLSchematronEngine.Validate nu predă octeții facturii extrase către IXMLDOMDocument.loadXML, pentru că acea metodă așteaptă un BSTR — UTF-16 — și ar reinterpreta un tablou brut de octeți UTF-8 sub acea presupunere, indiferent de ce spune propria declarație <?xml encoding="UTF-8"?> a documentului. HPDFEInvoiceValidator copiază în schimb octeții într-un HGLOBAL alocat cu GlobalAlloc, îl învelește într-un IStream prin CreateStreamOnHGlobal, și încarcă acel flux via IPersistStreamInit.Load, o cale pe care MSXML o respectă citind declarația de codificare direct din fluxul de octeți, în loc să presupună UTF-16 de la început. Aceeași metodă reconstruiește SelectionNamespaces din legăturile de prefix pe care încărcătorul le-a adunat deja în timp ce analiza fișierul .sch, astfel încât o regulă scrisă cu un prefix precum ram: se rezolvă corect față de propriul spațiu de nume al XML-ului facturii la fiecare evaluare, nu doar atunci când fișierul de reguli a fost analizat prima dată
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
Păstrarea unei singure unități care se compilează de la Delphi 7 până azi
HPDFEInvoiceValidator.pas trebuie să se compileze pe fiecare versiune de Delphi pe care HotPDF o acceptă, inclusiv lansări fără nicio legătură XML sau XPath deloc, așa că secțiunea sa de interfață expune doar tipuri simple de valori: înregistrări, tablouri dinamice și o singură interfață, IHPDFESchematronEngine, cu metodele Load, Validate și LastSummary. Fiecare tip specific MSXML — IXMLDOMDocument2, importul Winapi.msxml, THPDFMSXMLSchematronEngine însuși — stă în interiorul unui singur bloc {$IFDEF XE2+} din secțiunea de implementare, invizibil deopotrivă pentru apelanți și pentru compilator pe lanțurile de instrumente mai vechi
{$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}
Pe Delphi 7 și anterior, HPDFCreateSchematronEngine returnează în schimb THPDFStubSchematronEngine: Load-ul său returnează întotdeauna False cu un ErrorText care numește golul real — legarea DOM MSXML necesită XE2 sau ulterior — și îndreaptă între timp către un validator extern precum veraPDF, Mustang sau un instrument de conformitate ZUGFeRD pentru acoperire completă. Validate-ul său returnează un singur rezultat sintetic cu RuleID 'ENGINE' și Skipped setat, așa că un cod care iterează BusinessRules nu are nevoie de o ramură separată pentru „motorul nu a putut rula” versus „fiecare regulă s-a întâmplat să fie omisă” — ambele arată aceeași formă pentru apelant. HPDFValidateEInvoice pliază asta grațios și în verdictul său: booleanul pe care îl returnează este ContainerValid and ((not BusinessRulesEvaluated) or (BusinessRuleViolations = 0)), astfel încât un motor indisponibil retrogradează rezultatul la o verificare doar la nivel de container, în loc să forțeze un eșec dur pe un compilator care oricum nu avea să ruleze niciodată reguli Schematron
Motorul XPath 1.0 al HPDFEInvoiceValidator nu înlocuiește un procesor complet Schematron/XSLT 2.0, și nici nu a avut vreodată această intenție: un motor limitat la XPath 1.0 va lăsa întotdeauna câteva asertări EN 16931 neevaluate, ceea ce este exact ce marcajul Skipped de pe fiecare rezultat există pentru a face vizibil, nu pentru a ascunde. Ce câștigă efectiv motorul este feedback de reguli de business care rulează oriunde HotPDF rulează deja, fără niciun proces extern la care să se apeleze și fără niciun runtime XSLT 2.0 de licențiat. Acest motor este livrat ca parte a componentei PDF HotPDF pentru Delphi și C++Builder, alături de instrumentele Factur-X și PDF/A la nivel de container pe care le construiește