Techninis straipsnis

EN 16931 Schematron taisyklių variklis Delphi su HotPDF

HotPDF tikrina EN 16931 elektroninių sąskaitų verslo taisykles naudodamas HPDFEInvoiceValidator — Schematron teiginių variklį, kurį biblioteka įgyvendina pati, remdamasi MSXML XPath 1.0 palaikymu, o ne licencijuotu XSLT 2.0 procesoriumi. HPDFEInvoiceValidator analizuoja oficialius Factur-X .sch taisyklių failus, įvertina kiekvieną XPath 1.0 išreiškiamą teiginį, o likusius pažymi kaip praleistus, užuot leidusi nepalaikomai išraiškai vykdymo metu sukelti išimtį

Čia nagrinėjamas tik šis variklis: kaip įkėliklis Schematron XML paverčia taisyklių įrašais, kaip assert ir report vertinimas iš tikrųjų nusprendžia, ar patikra sėkminga, kaip aptinkamas ir praleidžiamas XPath 2.0 trūkumas ir kaip tas pats modulis kompiliuojamas su Delphi 7. PDF/A-3 įterpimas, factur-x.xml / xrechnung.xml konteinerio mechanika ir ZUGFeRD 2.5 versijų istorija aprašyti gretimame straipsnyje apie ZUGFeRD ir Factur-X elektronines sąskaitas Delphi su HotPDF, todėl šiame straipsnyje jie sąmoningai nekartojami

Kodėl HotPDF sukūrė savo EN 16931 Schematron variklį

HotPDF sukūrė savo Schematron variklį, nes EN 16931 taisyklių faile nurodytas susiejimas pervertina tai, ko iš tikrųjų reikia jo teiginiams: failo pradžioje nustatyta queryBinding="xslt2", techniškai prašanti visaverčio XSLT 2.0 / XPath 2.0 procesoriaus, tačiau pačių teiginių analizė rodo, kad didžioji dauguma naudoja tik XPath 1.0 funkcijas, tokias kaip string-length ir substring-after. Windows įtaisytas MSXML DOM — vienintelis XML variklis, garantuotai esantis kiekviename palaikomame Delphi dieginyje ir nereikalaujantis trečiosios šalies priklausomybės — kaip tik įgyvendina šį poaibį, XPath 1.0, todėl savasis variklis tapo praktiškas, užuot licencijavus atskirą XSLT 2.0 vykdymo aplinką. HPDFSchematronFileForProfile aptiktą Factur-X atitikties lygį susieja su vienu iš penkių pateikiamų taisyklių failų, kuriuos gali įkelti šis variklis — MINIMUM, BASIC WL, BASIC, EN 16931 ir EXTENDED — ir kiekvienas iš jų vertina tik ištrauktą sąskaitos XML, niekada neapdorodamas jį gaubiančio PDF; ar pats PDF yra struktūriškai galiojantis PDF/A-3 failas, yra atskiras klausimas, į kurį atsako HotPDF PDF/A, PDF/X ir PDF/UA atitikties patikros kitur bibliotekoje

Kaip variklis .sch failą paverčia taisyklių įrašais

THPDFMSXMLSchematronEngine.Load pradeda iškviesdamas CoInitializeEx(nil, COINIT_MULTITHREADED) prieš ką nors kurdamas, nes konsolės ar tarnybos procesas, kuris niekada neiškvietė Application.Initialize, dar neturi COM apartamentų, o grafinė VCL programa juos jau turi; variklis S_FALSE arba RPC_E_CHANGED_MODE rezultatą, kurį šis iškvietimas gali grąžinti gijoje, jau esančioje apartamentuose, traktuoja kaip tinkamą, o ne kaip klaidą. Tada jis analizuoja Schematron failą naudodamas MSXML 6.0 DOM dokumentą (CoDOMDocument60) ir setProperty('SelectionLanguage', 'XPath'), nes MSXML pagal numatytuosius nustatymus naudoja senesnę XSL-Pattern dialektą, jei iškvietėjas aiškiai nepasirenka XPath. Tačiau nuo šios vietos įkėliklis niekada nenaudoja selectNodes eidamas per paties .sch failo struktūrą — kiekvienas <pattern>, <rule>, <assert> ir <report> elementas surandamas rankiniu būdu einant per firstChild / nextSibling ir lyginant kiekvieno mazgo vietinį vardą bei vardų srities URI su literaline eilute http://purl.oclc.org/dsdl/schematron

Toks rankinio vaikščiojimo būdas reikalingas dėl vištos ir kiaušinio problemos, susijusios su <ns prefix="ram" uri="..."/> susiejimais, kuriuos kiekvienas Factur-X Schematron failas deklaruoja iš anksto. Norint išspręsti prefiksuotą XPath išraišką, pavyzdžiui, ram:Name, pagal šiuos susiejimus, MSXML SelectionNamespaces savybėje jie jau turi būti įrašyti, tačiau pirmiausia aptikti susiejimus įprastai reikštų vykdyti XPath užklausą, pavyzdžiui, //ns:ns, kuriai pačiai pirmiausia reikia nustatyti SelectionNamespaces. HPDFEInvoiceValidator nutraukia šį ciklą surinkdamas kiekvieną <ns> elementą tuo pačiu rankiniu vaikų mazgų apėjimu prieš paliesdamas selectNodes, tada surinktus prefikso ir URI porų duomenis sujungia į vieną SelectionNamespaces eilutę, kurią pakartotinai naudoja ir .sch struktūros apėjimas, ir kiekvienas vėlesnis taisyklės vertinimas

// 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 ir report: kas iš tikrųjų sukelia pažeidimą

Schematron suteikia assert ir report priešingas reikšmes, todėl variklis turi tiksliai išlaikyti šį skirtumą, kitaip pažeidimų skaičius nieko nereikštų. <assert test="X"> nurodo, kad X turi galioti kiekvienam mazgui, atitinkančiam taisyklės konteksto kelią, todėl EvaluateAssert užregistruoja pažeidimą, kai testo išraiškos rezultato mazgų aibė grįžta tuščia; <report test="X"> yra priešingas atvejis ir pažymi problemą, kai X yra teisinga, todėl EvaluateReport užregistruoja pažeidimą, kai testo rezultatas nėra tuščias. Abu įėjimo taškai viduje turi tą pačią dviejų etapų struktūrą — pirmiausia Doc.selectNodes(Entry.Context), kad būtų rasti visi mazgai, kuriems taikoma taisyklė, tada paeiliui ContextNode.selectNodes(Entry.Test) kiekvienam iš jų — būtent tokį konteksto ir testo modelį naudoja tikras Schematron procesorius, tik čia jį valdo MSXML XPath 1.0 selectNodes, o ne Schematron suprantantis vykdymo variklis

Kaip variklis praleidžia XPath 2.0 nesugadindamas vykdymo

HPDFEInvoiceValidator nuo nepalaikomos XPath 2.0 sintaksės ginasi dviem sluoksniais, o pirmasis iš jų apskritai neleidžia MSXML pamatyti išraiškos. Prieš vertinant bet kurį assert ar report, XPath2Detected neapdorotoje testo išraiškos eilutėje ieško šešių literalinių žetonų — xs:decimal, xs:integer, xs:string, upper-case, lower-case ir exists( — ir, radus bent vieną, taisyklė iš karto pažymima Skipped su stsInfo lygiu, nes MSXML neturėtų būti perduodama išraiška, kurią jis jau žinomai atmeta

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;

Antrasis sluoksnis pagauna tai, ko neaptiko statinis žetonų sąrašas. Ir konteksto selectNodes iškvietimas, ir kiekvieno mazgo testo selectNodes iškvietimas vykdomi try/except bloke; kai MSXML sukelia išimtį dėl išraiškos, kurios žetonų patikra nepraleido — konstrukcijos, nepatenkančios į šešis žinomus žetonus, arba neišsprendžiamo konteksto kelio — išimtis pagaunama, o taisyklė įrašoma kaip Skipped, užuot ją perdavus iškvietėjui. Dėl šios dviejų sluoksnių konstrukcijos XPath 2.0 konstrukcija bet kurioje taisyklių aibės vietoje niekada nepakelia išimties virš HPDFValidateEInvoice: kiekvienas iš 424 teiginių yra įvertinamas, neįvykdomas arba pažymimas kaip praleistas, o vidinis tos taisyklių rinkmenos įvertis nurodė, kad XPath 1.0 vykdomų teiginių dalis yra maždaug 350 iš 424 — pakankamai didelė, kad dalinis vertinimas būtų prasmingas, užuot vos pasirodžius vienam XPath 2.0 teiginiui iš karto grįžus prie tik konteinerio patikros

UTF-8 sąskaitos XML perdavimas MSXML jo nesugadinant

THPDFMSXMLSchematronEngine.Validate neperduoda ištrauktų sąskaitos baitų tiesiogiai į IXMLDOMDocument.loadXML, nes šis metodas tikisi BSTR — UTF-16 — ir taip interpretuotų neapdorotą UTF-8 baitų masyvą, nepaisydamas to, ką nurodo paties dokumento <?xml encoding="UTF-8"?> deklaracija. Vietoj to HPDFEInvoiceValidator nukopijuoja baitus į GlobalAlloc sukurtą HGLOBAL, apgaubia jį IStream per CreateStreamOnHGlobal ir įkelia srautą per IPersistStreamInit.Load; šį kelią MSXML gerbia, nes perskaito koduotės deklaraciją iš paties baitų srauto, o ne iš anksto numano UTF-16. Tas pats metodas iš naujo sukuria SelectionNamespaces iš prefiksų susiejimų, kuriuos įkėliklis jau surinko analizuodamas .sch failą, todėl pagal prefiksą, pavyzdžiui, ram:, parašyta taisyklė kiekvieno vertinimo metu teisingai susiejama su paties sąskaitos XML vardų sritimi, o ne tik tada, kai taisyklių failas buvo pirmą kartą analizuotas

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

Vieno modulio kompiliavimas nuo Delphi 7 iki šiandien

HPDFEInvoiceValidator.pas turi būti kompiliuojamas su kiekviena HotPDF palaikoma Delphi versija, įskaitant leidimus, neturinčius jokio XML ar XPath susiejimo, todėl jo sąsajos dalyje naudojami tik paprasti reikšmių tipai: įrašai, dinaminiai masyvai ir viena sąsaja IHPDFESchematronEngine su metodais Load, Validate ir LastSummary. Kiekvienas MSXML specifinis tipas — IXMLDOMDocument2, Winapi.msxml importas ir pats THPDFMSXMLSchematronEngine — yra viename {$IFDEF XE2+} bloke įgyvendinimo dalyje, nematomas iškvietėjams ir senesnių įrankių grandinių kompiliatoriui

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

Su Delphi 7 ir senesnėmis versijomis HPDFCreateSchematronEngine grąžina THPDFStubSchematronEngine: jo Load visada grąžina False su ErrorText, kuriame įvardijamas tikrasis trūkumas — MSXML DOM susiejimui reikia XE2 arba naujesnės versijos — ir nurodomas išorinis tikrintuvas, pavyzdžiui, veraPDF, Mustang arba ZUGFeRD atitikties įrankis, kad tuo metu būtų užtikrintas visas aprėpties lygis. Jo Validate grąžina vieną sintetinį rezultatą su RuleID 'ENGINE' ir nustatytu Skipped, todėl kodui, kuris pereina per BusinessRules, nereikia atskiros šakos atvejui „variklis negalėjo veikti“ ir atvejui „kiekviena taisyklė buvo praleista“ — iškvietėjui abu atrodo vienodai. HPDFValidateEInvoice tai taip pat tvarkingai įtraukia į savo išvadą: jo grąžinama loginė reikšmė yra ContainerValid and ((not BusinessRulesEvaluated) or (BusinessRuleViolations = 0)), todėl nepasiekiamas variklis rezultatą sumažina iki tik konteinerio patikros, o ne priverčia griežtai atmesti dokumentą kompiliatoriuje, kuris vis tiek negalėjo vykdyti Schematron taisyklių

HPDFEInvoiceValidator XPath 1.0 variklis nepakeičia visaverčio Schematron/XSLT 2.0 procesoriaus ir niekada nebuvo tam skirtas: tik XPath 1.0 apsiribojantis variklis visada paliks kelis EN 16931 teiginius neįvertintus, todėl kiekvieno rezultato Skipped vėliavėlė skirta tai padaryti matomą, o ne paslėpti. Variklis suteikia verslo taisyklių grįžtamąjį ryšį, veikiantį visur, kur jau veikia HotPDF, be išorinio proceso paleidimo ir be licencijuojamos XSLT 2.0 vykdymo aplinkos. Šis variklis pateikiamas kartu su HotPDF PDF komponentu, skirtu Delphi ir C++Builder, kartu su konteinerio lygio Factur-X ir PDF/A įrankiais, kuriais jis remiasi