Tehnični članak

Mehanizem pravil Schematron EN 16931 v Delphiju s HotPDF

HotPDF preverja poslovna pravila elektronskih računov EN 16931 prek HPDFEInvoiceValidator, mehanizma za uveljavljanje trditev Schematron, ki ga knjižnica sama izvaja na podpori XPath 1.0 v MSXML namesto prek licenciranega procesorja XSLT 2.0. HPDFEInvoiceValidator razčleni uradne datoteke pravil Factur-X .sch, ovrednoti vsako trditev, ki jo lahko izrazi v XPath 1.0, preostale pa označi kot preskočene, namesto da bi nepodprt izraz sredi izvajanja povzročil izjemo

Obseg tega članka ostaja znotraj tega mehanizma: kako nalagalnik pretvori XML Schematron v vnose pravil, kako vrednotenje assert in report dejansko odloča o uspehu ali neuspehu, kako se zazna in preskoči vrzel XPath 2.0 ter kako se ista enota še vedno prevede v Delphiju 7. Vdelava PDF/A-3, mehanika vsebnika factur-x.xml / xrechnung.xml in zgodba različic ZUGFeRD 2.5 so opisane v spremljevalnem članku o elektronskih računih ZUGFeRD in Factur-X v Delphiju s HotPDF, ki ga ta članek namenoma ne ponavlja

Zakaj je HotPDF izdelal lasten mehanizem Schematron EN 16931

HotPDF je izdelal lasten mehanizem Schematron, ker deklarirana vezava datoteke pravil EN 16931 preceni dejanske potrebe svojih trditev: datoteka na vrhu nastavi queryBinding="xslt2", s čimer tehnično zahteva popoln procesor XSLT 2.0 / XPath 2.0, vendar branje samih trditev pokaže, da velika večina kliče samo funkcije XPath 1.0, kot sta string-length in substring-after. Vgrajeni MSXML DOM sistema Windows — edini XML-mehanizem, za katerega je zagotovljeno, da obstaja v vsaki podprti namestitvi Delphija brez dodajanja odvisnosti tretje osebe — izvaja natanko to podmnožico, XPath 1.0, zato je bil izvorni mehanizem praktičen namesto licenciranja ločenega izvajalnega okolja XSLT 2.0. HPDFSchematronFileForProfile preslikuje zaznano raven skladnosti Factur-X v eno od petih priloženih datotek pravil, ki jih ta mehanizem lahko naloži — MINIMUM, BASIC WL, BASIC, EN 16931 in EXTENDED — vsaka od njih pa presoja samo izločeni XML računa, nikoli okoliškega PDF; ali je ta PDF sam po sebi strukturno veljavna datoteka PDF/A-3, je ločeno vprašanje, na katerega drugje v knjižnici odgovarjajo preverjanja skladnosti HotPDF za PDF/A, PDF/X in PDF/UA

Kako mehanizem pretvori datoteko .sch v vnose pravil?

THPDFMSXMLSchematronEngine.Load začne tako, da pred ustvarjanjem česar koli pokliče CoInitializeEx(nil, COINIT_MULTITHREADED), saj gostitelj konzole ali storitve, ki nikoli ni poklical Application.Initialize, še nima okolja COM, medtem ko ga gostitelj grafičnega VCL že ima; mehanizem rezultat S_FALSE ali RPC_E_CHANGED_MODE, ki ga lahko ta klic vrne v niti, ki je že v okolju, obravnava kot ustreznega in ne kot napako. Nato datoteko Schematron razčleni z dokumentom DOM MSXML 6.0 (CoDOMDocument60) in setProperty('SelectionLanguage', 'XPath'), saj MSXML privzeto uporablja starejšo narečje XSL-Pattern, če klicatelj izrecno ne izbere XPath. Od tam nalagalnik nikoli ne kliče selectNodes za sprehod po lastni strukturi datoteke .sch — vsak element <pattern>, <rule>, <assert> in <report> najde z ročnim sprehodom prek firstChild / nextSibling, pri čemer primerja lokalno ime vozlišča in URI imenskega prostora z dobesednim nizom http://purl.oclc.org/dsdl/schematron

Ta pristop z ročnim sprehodom obstaja zaradi problema kokoši in jajca pri vezavah <ns prefix="ram" uri="..."/>, ki jih vsaka datoteka Schematron Factur-X deklarira vnaprej. Razreševanje izraza XPath s predpono, kot je ram:Name, glede na te vezave zahteva, da MSXML-ova lastnost SelectionNamespaces že vsebuje vezave, vendar bi odkrivanje vezav na začetku običajno pomenilo izvajanje poizvedbe XPath, kot je //ns:ns — ta pa sama potrebuje predhodno nastavljeno lastnost SelectionNamespaces. HPDFEInvoiceValidator prekine ta krog tako, da z istim ročnim sprehodom podrejenih vozlišč zbere vsak element <ns>, preden se sploh dotakne selectNodes, nato pa zbrane pare predpone/URI združi v en niz SelectionNamespaces, ki ga ponovno uporabita sprehod po strukturi .sch in vsako poznejše vrednotenje 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 proti report: kaj dejansko sproži kršitev?

Schematron daje assert in report nasprotno polarnost, mehanizem pa mora to razliko ohraniti natančno, sicer število kršitev ne pomeni ničesar. <assert test="X"> določa, da mora X veljati za vsako vozlišče, ki ustreza kontekstni poti pravila, zato EvaluateAssert zabeleži kršitev, ko se nastali nabor vozlišč testnega izraza vrne prazen; <report test="X"> je zrcalna različica, ki težavo označi, ko je X resničen, zato EvaluateReport zabeleži kršitev, ko rezultat testa ni prazen. Obe vstopni točki si spodaj delita isto dvostopenjsko obliko — najprej Doc.selectNodes(Entry.Context), da najdeta vsako vozlišče, za katero velja pravilo, nato pa ContextNode.selectNodes(Entry.Test) za vsako od njih — kar je natanko model kontekst in nato test, ki ga uporablja pravi procesor Schematron, le da ga poganja MSXML-ov XPath 1.0 selectNodes namesto mehanizma izvajanja, ki pozna Schematron

Kako mehanizem preskoči XPath 2.0, ne da bi se izvajanje zrušilo?

HPDFEInvoiceValidator se pred nepodprto skladnjo XPath 2.0 brani v dveh plasteh, pri čemer prva MSXML-u sploh ne dovoli videti izraza. Pred vrednotenjem vsakega assert ali report XPath2Detected pregleda surovi niz testnega izraza za šest dobesednih žetonov — xs:decimal, xs:integer, xs:string, upper-case, lower-case in exists( — in če je prisoten kateri koli od njih, se pravilo takoj označi kot Skipped z resnostjo stsInfo, saj MSXML-u ne smemo predati izraza, za katerega že vemo, da ga bo zavrnil

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;

Druga plast ujame vse, kar statični seznam žetonov spregleda. Klic kontekstnega selectNodes in klic selectNodes za test posameznega vozlišča se izvedeta znotraj bloka try/except; ko MSXML sproži napako pri izrazu, ki ga je pregled žetonov spustil skozi — pri konstruktu zunaj šestih znanih žetonov ali kontekstni poti, ki je ne more razrešiti — se izjema ujame in pravilo zabeleži kot Skipped, namesto da bi se posredovala klicatelju. Ta dvoplastna zasnova je razlog, da konstrukcija XPath 2.0 kjer koli v naboru pravil nikoli ne uide iz HPDFValidateEInvoice: vseh 424 trditev se bodisi ovrednoti, odpove ali označi kot preskočenih, interna ocena te datoteke pravil pa je delež izvedljivih v XPath 1.0 ocenila na približno 350 od 424 — dovolj, da se delno vrednotenje splača, namesto da bi se ob pojavu ene same trditve XPath 2.0 takoj vrnili k preverjanju samo vsebnika

Podajanje XML-ja računa v UTF-8 MSXML-u brez popačenja

THPDFMSXMLSchematronEngine.Validate izločenih bajtov računa ne poda v IXMLDOMDocument.loadXML, ker ta metoda pričakuje BSTR — UTF-16 — in bi surovo polje bajtov ponovno razlagala na podlagi te predpostavke ne glede na to, kaj pravi deklaracija <?xml encoding="UTF-8"?> v dokumentu. HPDFEInvoiceValidator namesto tega kopira bajte v GlobalAlloc'iran HGLOBAL, jih ovije v IStream prek CreateStreamOnHGlobal in ta tok naloži prek IPersistStreamInit.Load, kar je pot, pri kateri MSXML prebere deklaracijo kodiranja neposredno iz toka bajtov, namesto da bi vnaprej predpostavil UTF-16. Ista metoda znova sestavi SelectionNamespaces iz vezav predpon, ki jih je nalagalnik že zbral pri razčlenjevanju datoteke .sch, zato se pravilo, zapisano s predpono, kot je ram:, pri vsakem vrednotenju pravilno razreši glede na lastni imenski prostor XML-ja računa in ne le takrat, ko je bila datoteka pravil prvič razčlenjena

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

Ohranjanje prevajanja ene enote od Delphija 7 do danes

HPDFEInvoiceValidator.pas se mora prevesti v vsaki različici Delphija, ki jo HotPDF podpira, vključno z izdajami brez kakršne koli vezave XML ali XPath, zato njegov vmesniški odsek izpostavlja samo preproste tipe vrednosti: zapise, dinamična polja in en vmesnik, IHPDFESchematronEngine, z metodami Load, Validate in LastSummary. Vsak tip, značilen za MSXML — IXMLDOMDocument2, uvoz Winapi.msxml, sam THPDFMSXMLSchematronEngine — je v enem bloku {$IFDEF XE2+} v izvedbenem odseku, zato je neviden klicateljem in prevajalniku v starejših verigah orodij

{$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}

V Delphiju 7 in starejših različicah HPDFCreateSchematronEngine namesto tega vrne THPDFStubSchematronEngine: njegov Load vedno vrne False z ErrorText, ki poimenuje dejansko vrzel — vezava DOM MSXML zahteva XE2 ali novejši — in medtem usmeri k zunanjemu preverjalniku, kot so veraPDF, Mustang ali orodje za skladnost ZUGFeRD, za popolno pokritost. Njegov Validate vrne en sam sintetični rezultat z RuleID 'ENGINE' in nastavljenim Skipped, zato kodi, ki iterira po BusinessRules, ni treba ločene veje za primer »mehanizma ni bilo mogoče zagnati« v primerjavi s primerom »vsako pravilo je bilo po naključju preskočeno« — klicatelju sta videti enako. HPDFValidateEInvoice to brez težav vključi tudi v svojo presojo: logična vrednost, ki jo vrne, je ContainerValid and ((not BusinessRulesEvaluated) or (BusinessRuleViolations = 0)), zato nedosegljiv mehanizem zniža rezultat na preverjanje samo vsebnika, namesto da bi povzročil trd neuspeh pri prevajalniku, ki pravil Schematron tako ali tako ne bi mogel izvajati

Mehanizem XPath 1.0 v HPDFEInvoiceValidator ne nadomešča popolnega procesorja Schematron/XSLT 2.0 in temu nikoli ni bil namenjen: mehanizem, omejen na XPath 1.0, bo vedno pustil nekaj trditev EN 16931 neovrednotenih, prav zato pa obstaja zastavica Skipped pri vsakem rezultatu, da to pokaže namesto skrije. Prednost mehanizma je povratna informacija o poslovnih pravilih, ki deluje povsod, kjer že deluje HotPDF, brez zunanjega procesa, ki bi ga bilo treba zagnati, in brez licenciranja izvajalnega okolja XSLT 2.0. Ta mehanizem je priložen kot del komponente HotPDF PDF za Delphi in C++Builder, skupaj z orodji na ravni vsebnika Factur-X in PDF/A, na katerih temelji