Tehnički članak

EN 16931 Schematron mehanizam pravila u Delphiju

HotPDF proverava poslovna pravila elektronskih faktura EN 16931 pomoću HPDFEInvoiceValidator, Schematron mehanizma za tvrdnje koji biblioteka sama implementira iznad MSXML podrške za XPath 1.0, umesto licenciranog XSLT 2.0 procesora. HPDFEInvoiceValidator obrađuje zvanične Factur-X .sch datoteke sa pravilima, izračunava svaku tvrdnju koju može da izrazi u XPath 1.0 i preostale označava kao preskočene, umesto da nepodržani izraz izazove izuzetak usred rada

Ovaj tekst ostaje u okviru tog mehanizma: objašnjava kako učitavač pretvara Schematron XML u stavke pravila, kako procena assert i report elemenata stvarno odlučuje da li pravilo prolazi ili pada, kako se otkriva i preskače razlika u odnosu na XPath 2.0 i kako se ista jedinica i dalje kompajlira na Delphi 7. Ugrađivanje PDF/A-3, mehanika kontejnera factur-x.xml / xrechnung.xml i verzionisanje ZUGFeRD 2.5 opisani su u pratećem članku o ZUGFeRD i Factur-X e-fakturama u Delphiju uz HotPDF, pa ih ovaj tekst namerno ne ponavlja

Zašto je HotPDF napravio sopstveni EN 16931 Schematron mehanizam

HotPDF je napravio sopstveni Schematron mehanizam zato što deklarisano povezivanje u datoteci sa pravilima EN 16931 preuveličava ono što tvrdnje zaista zahtevaju: datoteka na vrhu postavlja queryBinding="xslt2", čime tehnički traži puni XSLT 2.0 / XPath 2.0 procesor, ali pregled samih tvrdnji pokazuje da velika većina koristi samo funkcije XPath 1.0 kao što su string-length i substring-after. Ugrađeni Windows MSXML DOM — jedini XML mehanizam za koji je zagarantovano da postoji na svakoj podržanoj Delphi instalaciji bez dodavanja zavisnosti treće strane — upravo implementira taj podskup, XPath 1.0, pa je izvorni mehanizam bio praktičniji od licenciranja posebnog XSLT 2.0 okruženja. HPDFSchematronFileForProfile preslikava otkriveni nivo usklađenosti Factur-X na jednu od pet isporučenih datoteka sa pravilima koje ovaj mehanizam može da učita — MINIMUM, BASIC WL, BASIC, EN 16931 i EXTENDED — a svaka od njih proverava samo izdvojeni XML fakture, nikada okolni PDF; da li je taj PDF strukturalno ispravan PDF/A-3 proverava se odvojeno kroz HotPDF provere usklađenosti sa PDF/A, PDF/X i PDF/UA na drugom mestu u biblioteci

Kako mehanizam pretvara .sch datoteku u stavke pravila?

THPDFMSXMLSchematronEngine.Load najpre poziva CoInitializeEx(nil, COINIT_MULTITHREADED) pre nego što bilo šta kreira, jer konzolni ili servisni host koji nikada nije pozvao Application.Initialize još nema COM apartman, dok ga grafički VCL host već ima; mehanizam rezultate S_FALSE ili RPC_E_CHANGED_MODE, koje taj poziv može da vrati na niti koja je već u apartmanu, podjednako smatra prihvatljivim umesto greškom. Zatim obrađuje Schematron datoteku pomoću MSXML 6.0 DOM dokumenta (CoDOMDocument60) i setProperty('SelectionLanguage', 'XPath'), jer MSXML podrazumevano koristi stariji XSL-Pattern dijalekt dok pozivalac izričito ne izabere XPath. Od tog trenutka učitavač ipak nikada ne poziva selectNodes za obilazak sopstvene strukture .sch datoteke — svaki element <pattern>, <rule>, <assert> i <report> pronalazi ručnim obilaskom firstChild / nextSibling, upoređujući lokalni naziv čvora i URI prostora imena sa literalnim nizom http://purl.oclc.org/dsdl/schematron

Ovakav ručni obilazak postoji zbog problema kokoške i jajeta u vezama <ns prefix="ram" uri="..."/> koje svaka Factur-X Schematron datoteka navodi na početku. Razrešavanje XPath izraza sa prefiksom kao što je ram:Name zahteva da svojstvo SelectionNamespaces u MSXML-u već sadrži te veze, ali njihovo prvo pronalaženje obično bi zahtevalo XPath upit poput //ns:ns — a i njemu je potrebno da SelectionNamespaces najpre bude postavljen. HPDFEInvoiceValidator prekida taj krug tako što istim ručnim obilaskom podčvorova prikuplja svaki element <ns> pre nego što uopšte dodirne selectNodes, a zatim prikupljene parove prefiksa i URI-ja spaja u jedan niz za SelectionNamespaces koji ponovo koriste i obilazak strukture .sch datoteke i svaka kasnija procena pravila

// 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 nasuprot report elementu: šta zapravo pokreće prekršaj?

Schematron daje elementima assert i report suprotan smisao, a mehanizam mora da sačuva tu razliku do poslednjeg detalja, inače broj prekršaja nema značenje. Element <assert test="X"> izjavljuje da X mora da važi za svaki čvor koji odgovara kontekstnoj putanji pravila, pa EvaluateAssert beleži prekršaj kada je skup čvorova koji vraća testni izraz prazan; element <report test="X"> predstavlja suprotan slučaj i označava problem kada je X tačan, pa EvaluateReport beleži prekršaj kada rezultat testa nije prazan. Obe ulazne tačke ispod koriste istu dvostepenu strukturu — najpre Doc.selectNodes(Entry.Context) da pronađu svaki čvor na koji se pravilo primenjuje, zatim ContextNode.selectNodes(Entry.Test) nad svakim od njih — što je upravo model kontekst-pa-test koji koristi pravi Schematron procesor, samo pokretan MSXML XPath 1.0 pozivom selectNodes umesto mehanizmom za izvršavanje Schematrona

Kako mehanizam preskače XPath 2.0 bez rušenja obrade?

HPDFEInvoiceValidator se od nepodržane sintakse XPath 2.0 štiti u dva sloja, a prvi sloj uopšte ne dopušta da MSXML vidi takav izraz. Pre procene bilo kog assert ili report elementa, XPath2Detected pretražuje sirovi niz testnog izraza tražeći šest literalnih tokena — xs:decimal, xs:integer, xs:string, upper-case, lower-case i exists( — pa se pravilo, ako je neki od njih prisutan, odmah označava kao Skipped sa ozbiljnošću stsInfo, jer MSXML-u ne treba prosleđivati izraz za koji se već zna da će ga odbiti

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;

Drugi sloj hvata sve što statička lista tokena propusti. I poziv selectNodes za kontekst i poziv selectNodes za test po čvoru izvršavaju se unutar bloka try/except; kada MSXML podigne izuzetak zbog izraza koji je prošao proveru tokena — konstrukcije izvan šest poznatih tokena ili kontekstne putanje koju nije moguće razrešiti — izuzetak se hvata, a pravilo se beleži kao Skipped umesto da se prosledi pozivaocu. Zbog tog dvostrukog dizajna konstrukcija XPath 2.0 bilo gde u skupu pravila nikada ne prolazi izvan HPDFValidateEInvoice: svaka od 424 tvrdnje se izračuna, padne ili označi kao preskočena, a interna procena te datoteke sa pravilima stavila je udeo izvršiv u XPath 1.0 na približno 350 od 424 tvrdnje — dovoljno da delimična procena vredi više od provere samo kontejnera čim se pojavi jedna tvrdnja XPath 2.0

Prosleđivanje UTF-8 XML-a fakture u MSXML bez narušavanja sadržaja

THPDFMSXMLSchematronEngine.Validate ne prosleđuje izdvojene bajtove fakture metodi IXMLDOMDocument.loadXML, jer ta metoda očekuje BSTR — UTF-16 — i sirovi niz bajtova UTF-8 tumačila bi pod tom pretpostavkom bez obzira na deklaraciju <?xml encoding="UTF-8"?> u samom dokumentu. HPDFEInvoiceValidator umesto toga kopira bajtove u HGLOBAL dobijen preko GlobalAlloc, obmotava ga u IStream pomoću CreateStreamOnHGlobal i učitava taj tok preko IPersistStreamInit.Load, što je putanja kojom MSXML čita deklaraciju kodiranja iz samog toka bajtova umesto da unapred pretpostavi UTF-16. Ista metoda ponovo gradi SelectionNamespaces iz veza prefiksa koje je učitavač već prikupio tokom obrade .sch datoteke, pa se pravilo napisano sa prefiksom kao što je ram: pravilno razrešava prema sopstvenom prostoru imena XML-a fakture pri svakoj proceni, a ne samo kada je datoteka sa pravilima prvi put obrađena

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

Kako održati istu jedinicu kompatibilnom od Delphi 7 do danas?

HPDFEInvoiceValidator.pas mora da se kompajlira na svakoj verziji Delphija koju HotPDF podržava, uključujući izdanja bez ikakvog XML ili XPath povezivanja, pa njegov interfejs izlaže samo obične tipove vrednosti: zapise, dinamičke nizove i jedan interfejs, IHPDFESchematronEngine, sa metodama Load, Validate i LastSummary. Svaki tip specifičan za MSXML — IXMLDOMDocument2, uvoz Winapi.msxml i sam THPDFMSXMLSchematronEngine — nalazi se unutar jednog bloka {$IFDEF XE2+} u implementacionom odeljku, nevidljiv pozivaocima i kompajleru na starijim alatnim lancima

{$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 i starijim verzijama HPDFCreateSchematronEngine umesto toga vraća THPDFStubSchematronEngine: njegov Load uvek vraća False uz ErrorText koji navodi stvarno ograničenje — povezivanje sa MSXML DOM-om zahteva XE2 ili noviji — i za to vreme upućuje na spoljni validator kao što su veraPDF, Mustang ili alat za usklađenost sa ZUGFeRD radi potpune provere. Njegov Validate vraća jedan sintetički rezultat sa RuleID 'ENGINE' i postavljenom vrednošću Skipped, pa kod koji prolazi kroz BusinessRules ne mora da ima posebnu granu za slučaj „mehanizam nije mogao da se pokrene“ nasuprot slučaju „svako pravilo je preskočeno“ — oba slučaja pozivaocu izgledaju isto. HPDFValidateEInvoice to bez problema uključuje i u konačnu procenu: Bulova vrednost koju vraća jeste ContainerValid and ((not BusinessRulesEvaluated) or (BusinessRuleViolations = 0)), pa nedostupan mehanizam spušta rezultat na proveru samo kontejnera umesto da izazove tvrdi pad na kompajleru koji ionako nije mogao da izvrši Schematron pravila

XPath 1.0 mehanizam u HPDFEInvoiceValidator ne zamenjuje puni Schematron/XSLT 2.0 procesor, niti je to bila njegova namena: mehanizam ograničen na XPath 1.0 uvek će ostaviti nekoliko tvrdnji EN 16931 neizračunatim, a upravo zato zastavica Skipped na svakom rezultatu postoji da bi to prikazala umesto da sakrije. Ono što ovaj mehanizam pruža jeste povratna informacija o poslovnim pravilima koja radi svuda gde već radi HotPDF, bez pokretanja spoljnog procesa i bez licenciranja XSLT 2.0 okruženja. Mehanizam se isporučuje u okviru HotPDF PDF komponente za Delphi i C++Builder, zajedno sa alatima za rad sa Factur-X kontejnerom i PDF/A formatom na kojima se zasniva