Műszaki cikk

EN 16931 Schematron szabálymotor Delphiben a HotPDF-fel

A HotPDF a HPDFEInvoiceValidator-on keresztül érvényesíti az EN 16931 elektronikusszámla-üzleti szabályokat, ez egy Schematron állítás-motor, amelyet a könyvtár maga valósít meg az MSXML XPath 1.0 támogatására építve, nem pedig egy licencelt XSLT 2.0 processzoron. A HPDFEInvoiceValidator feldolgozza a hivatalos Factur-X .sch szabályfájlokat, kiértékel minden állítást, amelyet ki tud fejezni XPath 1.0-ban, a többit pedig kihagyottként jelöli meg ahelyett, hogy egy nem támogatott kifejezés kivételt dobna a futás közepén

A hatókör itt ezen a motoron belül marad: hogyan alakítja a betöltő a Schematron XML-t szabálybejegyzésekké, hogyan dönti el ténylegesen az assert és report kiértékelés a sikert vagy bukást, hogyan kerül felismerésre és kihagyásra az XPath 2.0-s hiányosság, és hogyan fordul le ugyanaz az unit még Delphi 7 alatt is. A PDF/A-3 beágyazás, a factur-x.xml / xrechnung.xml konténer-mechanika, és a ZUGFeRD 2.5 verziózási történet a ZUGFeRD-ről és Factur-X e-számlákról Delphiben a HotPDF-fel szóló kísérőcikkben él, amelyet ez az írás szándékosan nem ismétel meg

Miért építette a HotPDF a saját EN 16931 Schematron motorját

A HotPDF azért építette meg saját Schematron motorját, mert az EN 16931 szabályfájl deklarált kötése túlbecsüli, amire állításainak ténylegesen szükségük van: a fájl tetején queryBinding="xslt2" szerepel, technikailag egy teljes XSLT 2.0 / XPath 2.0 processzort kérve, de magukat az állításokat átolvasva kiderül, hogy a túlnyomó többség csak XPath 1.0 függvényeket hív, mint a string-length és a substring-after. A Windows beépített MSXML DOM-ja — az az egyetlen XML-motor, amely garantáltan létezik minden támogatott Delphi-telepítésen, harmadik féltől származó függőség hozzáadása nélkül — véletlenül pontosan ezt a részhalmazt valósítja meg, az XPath 1.0-t, és ez tette gyakorlativá egy natív motort ahelyett, hogy egy külön XSLT 2.0 futtatókörnyezetet kellett volna licencelni. A HPDFSchematronFileForProfile a felismert Factur-X megfelelőségi szintet öt kiszállított szabályfájl egyikéhez képezi le, amelyet ez a motor be tud tölteni — MINIMUM, BASIC WL, BASIC, EN 16931, és EXTENDED —, és mindegyikük csak a kinyert számla-XML-t bírálja el, soha nem a körülvevő PDF-et; hogy az a PDF maga strukturálisan érvényes PDF/A-3 fájl-e, az egy külön kérdés, amelyre a könyvtár máshol, a HotPDF PDF/A, PDF/X és PDF/UA megfelelőségi ellenőrzései adnak választ

Hogyan alakítja a motor egy .sch fájlt szabálybejegyzésekké?

A THPDFMSXMLSchematronEngine.Load azzal kezdi, hogy meghívja a CoInitializeEx(nil, COINIT_MULTITHREADED)-et, mielőtt bármit létrehozna, mert egy konzol- vagy szolgáltatás-hoszt, amely soha nem hívta meg az Application.Initialize-t, még nem rendelkezik COM-apartmenttel, míg egy GUI VCL-hoszt már rendelkezik; a motor az S_FALSE vagy RPC_E_CHANGED_MODE eredményt, amelyet ez a hívás egy már egy apartmenten belüli szálon visszaadhat, ugyanolyan rendben lévőnek kezeli, mint egy hibát. Ezután feldolgozza a Schematron-fájlt egy MSXML 6.0 DOM-dokumentummal (CoDOMDocument60) és a setProperty('SelectionLanguage', 'XPath')-val, mivel az MSXML alapértelmezetten a régebbi XSL-Pattern dialektusát használja, hacsak a hívó kifejezetten nem az XPath-ot választja. Innentől kezdve azonban a betöltő soha nem hívja a selectNodes-t a .sch fájl saját szerkezetének bejárásához — minden <pattern>, <rule>, <assert>, és <report> elemet kézzel talál meg, a firstChild / nextSibling-en keresztül bejárva, minden csomópont helyi nevét és névtér-URI-ját a http://purl.oclc.org/dsdl/schematron szó szerinti sztringgel összehasonlítva

Ez a kézi bejárási megközelítés a tyúk-és-tojás probléma miatt létezik a <ns prefix="ram" uri="..."/> kötésekben, amelyeket minden Factur-X Schematron-fájl előre deklarál. Egy előtaggal ellátott XPath-kifejezés, mint a ram:Name, feloldásához ezekhez a kötésekhez az MSXML SelectionNamespaces tulajdonságának már tartalmaznia kell azokat, de a kötések eredeti felfedezéséhez normál esetben egy XPath-lekérdezést kellene futtatni, mint például a //ns:ns — ami maga is igényli, hogy a SelectionNamespaces előbb be legyen állítva. A HPDFEInvoiceValidator úgy töri meg ezt a kört, hogy minden egyes <ns> elemet ugyanazzal a kézi gyerekcsomópont-bejárással gyűjt össze, mielőtt egyáltalán hozzányúlna a selectNodes-hoz, majd az összegyűjtött előtag/URI párokat egyetlen SelectionNamespaces sztringgé gyúrja, amelyet mind a .sch szerkezet bejárása, mind minden későbbi szabálykiértékelés újrahasznosít

// 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 kontra report: mi váltja ki ténylegesen a szabálysértést?

A Schematron ellentétes polaritást ad az assert-nek és a report-nak, és a motornak pontosan meg kell őriznie ezt a megkülönböztetést, különben a szabálysértés-számlálásai semmit nem jelentenek. Egy <assert test="X"> azt deklarálja, hogy X-nek fenn kell állnia minden csomópontra, amely illeszkedik a szabály kontextusútvonalára, így az EvaluateAssert szabálysértést rögzít, amikor a teszt-kifejezés eredmény-csomóponthalmaza üresen jön vissza; egy <report test="X"> a tükörképe, egy problémát akkor jelez, amikor X igaz, így az EvaluateReport szabálysértést rögzít, amikor a teszteredmény nem üres. Mindkét belépési pont ugyanazt a kétszakaszos alakot osztja meg alul — először a Doc.selectNodes(Entry.Context), hogy megtaláljon minden csomópontot, amelyre a szabály vonatkozik, majd a ContextNode.selectNodes(Entry.Test) mindegyikük ellen sorban —, ami pontosan az a kontextus-majd-teszt modell, amit egy valódi Schematron-processzor használ, csak az MSXML XPath 1.0-s selectNodes-a hajtja, nem pedig egy Schematron-tudatos végrehajtó motor

Hogyan hagyja ki a motor az XPath 2.0-t anélkül, hogy összeomlasztaná a futást?

A HPDFEInvoiceValidator két rétegben védekezik a nem támogatott XPath 2.0 szintaxis ellen, és az első soha nem hagyja, hogy az MSXML egyáltalán lássa a kifejezést. Bármely assert vagy report kiértékelése előtt az XPath2Detected átvizsgálja a nyers teszt-kifejezés sztringjét hat szó szerinti tokenért — xs:decimal, xs:integer, xs:string, upper-case, lower-case, és exists( —, és ha bármelyik jelen van, a szabály azonnal Skipped-ként kerül megjelölésre stsInfo súlyossággal, azon a megfontoláson alapulva, hogy az MSXML-nek soha nem szabadna olyan kifejezést kapnia, amelyről már tudott, hogy elutasítja

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;

A második réteg elkapja, amit a statikus token-lista elmulaszt. Mind a kontextus selectNodes hívás, mind a csomópontonkénti teszt selectNodes hívás egy try/except blokkon belül fut; amikor az MSXML kivételt dob egy olyan kifejezésen, amelyet a token-vizsgálat átengedett — egy konstrukció, amely a hat ismert tokenen kívül esik, vagy egy kontextusútvonal, amelyet nem tud feloldani —, a kivétel elkapásra kerül, és a szabály Skipped-ként kerül rögzítésre, ahelyett hogy továbbterjedne a hívóhoz. Ez a kétrétegű felépítés az oka annak, hogy egy XPath 2.0-s konstrukció a szabálykészletben sehol nem dob kivételt a HPDFValidateEInvoice-on túl: mind a 424 állítása vagy kiértékelődik, vagy elbukik, vagy kihagyottként kerül megjelölésre, és egy belső becslés e szabályfájl ellen az XPath-1.0-val végrehajtható részt nagyjából 424-ből 350-re tette — épp elég ahhoz, hogy a részleges kiértékelés megérje, ahelyett hogy egy csak-konténer ellenőrzésre esne vissza abban a pillanatban, amint egyetlen XPath 2.0-s állítás felbukkan

UTF-8 számla-XML táplálása az MSXML-nek a szétzúzás nélkül

A THPDFMSXMLSchematronEngine.Validate nem adja át a kinyert számla-bájtokat az IXMLDOMDocument.loadXML-nek, mert az a metódus BSTR-t vár — UTF-16-ot —, és ez alapján a feltételezés alapján újraértelmezne egy nyers UTF-8 bájttömböt, függetlenül attól, mit mond a dokumentum saját <?xml encoding="UTF-8"?> deklarációja. A HPDFEInvoiceValidator ehelyett átmásolja a bájtokat egy GlobalAlloc-kal létrehozott HGLOBAL-ba, becsomagolja egy IStream-be a CreateStreamOnHGlobal-on keresztül, és betölti azt a streamet az IPersistStreamInit.Load-on keresztül, egy útvonal, amelyet az MSXML úgy tisztel, hogy magából a bájtfolyamból olvassa be a kódolási deklarációt, ahelyett hogy előre UTF-16-ot feltételezne. Ugyanez a metódus újraépíti a SelectionNamespaces-t az előtag-kötésekből, amelyeket a betöltő már összegyűjtött a .sch fájl feldolgozása közben, így egy olyan előtaggal írt szabály, mint a ram:, minden kiértékeléskor helyesen oldódik fel a számla-XML saját névtere ellen, nem csak akkor, amikor a szabályfájlt először feldolgozták

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

Egyetlen unit fordíthatóságának megtartása Delphi 7-től máig

A HPDFEInvoiceValidator.pas-nak minden Delphi-verzión le kell fordulnia, amelyet a HotPDF támogat, beleértve az olyan kiadásokat is, amelyeknek egyáltalán nincs XML- vagy XPath-kötése, így az interfész szakasza csak egyszerű értéktípusokat tesz elérhetővé: rekordokat, dinamikus tömböket, és egyetlen interfészt, az IHPDFESchematronEngine-t, Load, Validate, és LastSummary metódusokkal. Minden MSXML-specifikus típus — az IXMLDOMDocument2, a Winapi.msxml import, maga a THPDFMSXMLSchematronEngine — egyetlen {$IFDEF XE2+} blokkon belül ül a megvalósítási szakaszban, láthatatlan a hívók és a régebbi eszközláncokon lévő fordító számára egyaránt

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

Delphi 7-en és korábbi verziókon a HPDFCreateSchematronEngine ehelyett a THPDFStubSchematronEngine-t adja vissza: a Load-ja mindig False-t ad vissza egy ErrorText-tel, amely megnevezi a valódi hiányosságot — az MSXML DOM-kötés XE2-t vagy újabbat igényel —, és egy külső érvényesítőre mutat, mint a veraPDF, a Mustang, vagy egy ZUGFeRD-megfelelőségi eszköz, teljes lefedettséghez addig is. A Validate-je egyetlen szintetikus eredményt ad vissza 'ENGINE' RuleID-vel és Skipped beállítva, így az a kód, amely a BusinessRules-on iterál, nem igényel külön ágat "a motor nem tudott futni" versus "minden szabály véletlenül kihagyásra került" esetekre — mindkettő ugyanolyan alakúnak látszik a hívó számára. A HPDFValidateEInvoice ezt is elegánsan beépíti a végítéletébe: a visszaadott logikai érték ContainerValid and ((not BusinessRulesEvaluated) or (BusinessRuleViolations = 0)), így egy elérhetetlen motor egy csak-konténer ellenőrzésre fokozza le az eredményt ahelyett, hogy kemény hibát kényszerítene ki egy olyan fordítón, amely eleve soha nem futtatott volna Schematron-szabályokat

A HPDFEInvoiceValidator XPath 1.0-s motorja nem helyettesít egy teljes Schematron/XSLT 2.0 processzort, és soha nem is ez volt a célja: egy XPath 1.0-ra korlátozott motor mindig ki fog hagyni néhány EN 16931 állítást kiértékeletlenül, és pontosan ezt hivatott láthatóvá tenni, nem elrejteni, a Skipped jelző minden eredményen. Amit a motor ténylegesen ad, az egy üzletiszabály-visszajelzés, amely mindenhol fut, ahol a HotPDF már fut, külső folyamat elindítása nélkül, és XSLT 2.0 futtatókörnyezet licencelése nélkül. Ez a motor a Delphihez és C++Builderhez készült HotPDF PDF komponens részeként érkezik, azon konténer-szintű Factur-X és PDF/A eszközök mellett, amelyekre épül