Tekninen artikkeli

EN 16931 Schematron-sääntömoottori Delphissä HotPDF:llä

HotPDF validoi EN 16931-sähköislaskujen liiketoimintasäännöt HPDFEInvoiceValidator-luokan kautta, Schematron-väittämämoottorin, jonka kirjasto toteuttaa itse MSXML:n XPath 1.0 -tuen päälle lisensoidun XSLT 2.0 -prosessorin sijaan. HPDFEInvoiceValidator jäsentää viralliset Factur-X .sch-sääntötiedostot, arvioi jokaisen väittämän, jonka se pystyy ilmaisemaan XPath 1.0:lla, ja merkitsee loput ohitetuiksi sen sijaan, että antaisi tukemattoman lausekkeen nostaa poikkeuksen kesken ajon

Tämän artikkelin laajuus pysyy tuon moottorin sisällä: miten lataaja muuttaa Schematron-XML:n sääntömerkinnöiksi, miten assert- ja report-arviointi todella päättää läpäisyn tai epäonnistumisen, miten XPath 2.0 -aukko havaitaan ja ohitetaan, ja miten sama yksikkö silti kääntyy Delphi 7:stä nykypäivään. PDF/A-3-upotus, factur-x.xml-/xrechnung.xml-säiliömekaniikka ja ZUGFeRD 2.5 -versiointitarina asuvat artikkelissa rinnakkaisartikkeli ZUGFeRD- ja Factur-X-sähköislaskuista Delphissä HotPDF:llä, jota tämä artikkeli ei tarkoituksella toista

Miksi HotPDF rakensi oman EN 16931 Schematron-moottorinsa

HotPDF rakensi oman Schematron-moottorinsa, koska EN 16931 -sääntötiedoston ilmoitettu sidonta liioittelee sitä, mitä sen väittämät todella tarvitsevat: tiedosto asettaa queryBinding="xslt2"-arvon alussa, teknisesti pyytäen täyttä XSLT 2.0- / XPath 2.0 -prosessoria, mutta itse väittämien lukeminen paljastaa, että valtaosa kutsuu vain XPath 1.0 -funktioita, kuten string-length ja substring-after. Windowsin sisäänrakennettu MSXML DOM — ainoa XML-moottori, jonka takuulla on olemassa jokaisessa tuetussa Delphi-asennuksessa ilman kolmannen osapuolen riippuvuuden lisäämistä — toteuttaa sattumalta juuri tuon osajoukon, XPath 1.0:n, mikä teki natiivista moottorista käytännöllisen sen sijaan, että olisi lisensoitu erillinen XSLT 2.0 -ajonaikainen ympäristö. HPDFSchematronFileForProfile kartoittaa havaitun Factur-X-yhdenmukaisuustason yhteen viidestä toimitetusta sääntötiedostosta, jotka tämä moottori voi ladata — MINIMUM, BASIC WL, BASIC, EN 16931 ja EXTENDED — ja jokainen niistä arvioi vain puretun laskun XML:n, ei koskaan ympäröivää PDF:ää; onko tuo PDF itsessään rakenteellisesti pätevä PDF/A-3-tiedosto, on erillinen kysymys, johon vastataan artikkelissa HotPDF:n PDF/A-, PDF/X- ja PDF/UA-yhdenmukaisuustarkistukset muualla kirjastossa

EN 16931 Schematron -validointiputki Delphissä HotPDF:llä: poimittu lasku-XML ja profiilimääritetty sääntötiedosto syöttävät natiiviin XPath 1.0 -moottoriin, jonka väittämät joko evaluoidaan tai merkitään ohitetuiksi ennen yhdistettyä tuomiota
Validointivirta päästä päähän: kaksi syötettä syöttää natiivin XPath 1.0 -moottorin, sen latausvaiheet vartioivat nimiavaruuden kana-muna-ongelmaa, ja jokainen assertio päätyy evaluoiduksi tai ohitetuksi ennen tuomion kaavan ajoa

Miten moottori muuttaa .sch-tiedoston sääntömerkinnöiksi?

THPDFMSXMLSchematronEngine.Load alkaa kutsumalla CoInitializeEx(nil, COINIT_MULTITHREADED) ennen minkään luomista, koska konsoli- tai palveluisäntä, joka ei ole koskaan kutsunut Application.Initialize-metodia, ei vielä omista COM-osastoa, kun taas graafinen VCL-isäntä jo omistaa; moottori kohtelee S_FALSE- tai RPC_E_CHANGED_MODE-tulosta, jonka tuo kutsu voi palauttaa säikeessä, joka on jo osaston sisällä, yhtä hyväksyttävänä kuin virheenä. Se jäsentää sitten Schematron-tiedoston MSXML 6.0 DOM -dokumentilla (CoDOMDocument60) ja kutsulla setProperty('SelectionLanguage', 'XPath'), koska MSXML käyttää oletuksena vanhempaa XSL-Pattern-murrettaan, ellei kutsuja nimenomaisesti valitse XPath:ia. Siitä eteenpäin lataaja ei kuitenkaan koskaan kutsu selectNodes-funktiota käydäkseen läpi .sch-tiedoston omaa rakennetta — jokainen <pattern>-, <rule>-, <assert>- ja <report>-elementti löydetään käymällä läpi firstChild/nextSibling-ketjua käsin, vertaamalla jokaisen solmun paikallista nimeä ja nimiavaruuden URI:a kirjaimelliseen merkkijonoon http://purl.oclc.org/dsdl/schematron

Tämä käsin tehtävän läpikäynnin lähestymistapa on olemassa muna-kana-ongelman vuoksi <ns prefix="ram" uri="..."/>-sidonnoissa, jotka jokainen Factur-X Schematron-tiedosto ilmoittaa etukäteen. Etuliitteellä varustetun XPath-lausekkeen, kuten ram:Name, ratkaiseminen noita sidontoja vasten edellyttää, että MSXML:n SelectionNamespaces-ominaisuus jo sisältää ne, mutta sidontojen löytäminen ensiksi tarkoittaisi normaalisti XPath-kyselyn, kuten //ns:ns, ajamista — mikä itsessään tarvitsee SelectionNamespaces-ominaisuuden asetettuna ensin. HPDFEInvoiceValidator murtaa tämän kehän keräämällä jokaisen <ns>-elementin saman käsin tehtävän lapsisolmuläpikäynnin kautta ennen kuin se koskettaa selectNodes-funktiota lainkaan, ja taittaa sitten kerätyt etuliite/URI-parit yhdeksi SelectionNamespaces-merkkijonoksi, jota sekä .sch-rakenteen läpikäynti että jokainen myöhempi säännön arviointi käyttävät uudelleen

// Schematronin <ns>-sidontojen on oltava tiedossa ennen kuin mikään etuliitteellinen XPath
// voi ajaa, joten tämä läpikäynti ei itse voi käyttää selectNodes-funktiota -- se tehdään käsin.
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 vai report: mikä todella laukaisee rikkomuksen?

Schematron antaa assert- ja report-elementeille vastakkaisen napaisuuden, ja moottorin on säilytettävä tuo ero tarkasti, tai muuten sen rikkomuslaskurit eivät tarkoita mitään. <assert test="X"> ilmoittaa, että X:n on pädettävä jokaiselle solmulle, joka täsmää säännön kontekstipolkuun, joten EvaluateAssert kirjaa rikkomuksen, kun testilausekkeen tulossolmujoukko palautuu tyhjänä; <report test="X"> on peilikuva, se merkitsee ongelman, kun X on tosi, joten EvaluateReport kirjaa rikkomuksen, kun testin tulos on epätyhjä sen sijaan. Molemmilla aloituspisteillä on sama kaksivaiheinen muoto altaan — ensin Doc.selectNodes(Entry.Context) löytääkseen jokaisen solmun, johon sääntöä sovelletaan, sitten ContextNode.selectNodes(Entry.Test) jokaista niistä vastaan vuorollaan — mikä on juuri se konteksti-sitten-testi-malli, jota oikea Schematron-prosessori käyttää, ajettuna vain MSXML:n XPath 1.0 selectNodes-funktiolla Schematron-tietoisen suoritusmoottorin sijaan

Miten moottori ohittaa XPath 2.0:n kaatamatta ajoa?

HPDFEInvoiceValidator puolustautuu tukemattomalta XPath 2.0 -syntaksilta kahdella kerroksella, ja ensimmäinen ei koskaan anna MSXML:n edes nähdä lauseketta. Ennen minkään assert- tai report-elementin arviointia XPath2Detected tarkistaa raa'an testilausekemerkkijonon kuuden kirjaimellisen tunnisteen varalta — xs:decimal, xs:integer, xs:string, upper-case, lower-case ja exists( — ja jos jokin niistä on läsnä, sääntö merkitään heti Skipped-tilaan stsInfo-vakavuudella, päättelyllä, ettei MSXML:lle koskaan pitäisi antaa lauseketta, jonka jo tiedetään hylkäävän sen

const
  // MSXML toteuttaa vain XPath 1.0:n; minkä tahansa näistä tunnisteista läsnäolo
  // merkitsee väittämän ohitetuksi sen sijaan, että MSXML hylkäisi lausekkeen.
  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;

Toinen kerros nappaa sen, mitä staattinen tunnisteluettelo ei havaitse. Sekä konteksti-selectNodes-kutsu että solmukohtainen testi-selectNodes-kutsu ajetaan try/except-lohkon sisällä; kun MSXML nostaa poikkeuksen lausekkeesta, jonka tunnistetarkistus päästi läpi — rakenne kuuden tunnetun tunnisteen ulkopuolelta, tai kontekstipolku, jota se ei voi ratkaista — poikkeus napataan ja sääntö kirjataan Skipped-tilaan sen sijaan, että se välitettäisiin kutsujalle. Tämä kaksikerroksinen suunnittelu on syy siihen, miksi XPath 2.0 -rakenne missä tahansa sääntöjoukossa ei koskaan nosta poikkeusta funktion HPDFValidateEInvoice ohi: jokainen sen 424 väittämästä joko arvioidaan, epäonnistuu tai merkitään ohitetuksi, ja sisäinen arvio tuota sääntötiedostoa vasten asetti XPath-1.0:lla suoritettavan osuuden noin 350:een 424:stä — riittävästi, jotta osittainen arviointi kannattaa tehdä sen sijaan, että palattaisiin pelkkään säiliötarkistukseen heti, kun yksikin XPath 2.0 -väittämä ilmestyy

UTF-8-laskun XML:n syöttäminen MSXML:lle sitä silpomatta

THPDFMSXMLSchematronEngine.Validate ei anna puretun laskun tavuja funktiolle IXMLDOMDocument.loadXML, koska tuo metodi odottaa BSTR:ää — UTF-16:ta — ja tulkitsisi raa'an UTF-8-tavutaulukon uudelleen tuolla oletuksella riippumatta siitä, mitä asiakirjan oma <?xml encoding="UTF-8"?>-ilmoitus sanoo. HPDFEInvoiceValidator kopioi sen sijaan tavut GlobalAllocilla varattuun HGLOBAL-lohkoon, kääri sen IStream-olioksi funktiolla CreateStreamOnHGlobal ja lataa tuon virran IPersistStreamInit.Load-metodilla, polulla, jota MSXML kunnioittaa lukemalla koodausilmoituksen itse tavuvirrasta sen sijaan, että olettaisi UTF-16:ta etukäteen. Sama metodi rakentaa SelectionNamespaces-arvon uudelleen etuliitesidonnoista, jotka lataaja on jo kerännyt jäsentäessään .sch-tiedostoa, joten sääntö, joka on kirjoitettu etuliitettä kuten ram: vastaan, ratkeaa oikein laskun XML:n omaa nimiavaruutta vasten jokaisella arvioinnilla, ei vain silloin, kun sääntötiedosto ensimmäisen kerran jäsennettiin

Vertailu UTF-8-lasku-XML:n lataamisesta Delphissä HotPDF:llä: loadXML BSTR -polku rikkoo raakoja UTF-8-tavuja, kun taas IStream-polku antaa MSXML:n kunnioittaa ilmoitettua merkistöä
Kaksi tapaa syöttää MSXML:lle samat tavut: loadXML olettaa UTF-16 BSTR:n ja sorkkii raakaa UTF-8:aa, kun taas HGLOBAL-virtapolku saa moottorin lukemaan ilmoitetun merkistön
HMem := GlobalAlloc(GMEM_MOVEABLE, Length(XMLBytes));
P := GlobalLock(HMem);
Move(XMLBytes[0], P^, Length(XMLBytes));
GlobalUnlock(HMem);
CreateStreamOnHGlobal(HMem, True, Stream);  // virta omistaa HMem:in tästä eteenpäin
(Doc as IPersistStreamInit).Load(Stream);   // kunnioittaa XML-koodausilmoitusta

Yhden yksikön kääntymisen pitäminen käynnissä Delphi 7:stä nykypäivään

HPDFEInvoiceValidator.pas:n on käännyttävä jokaisella Delphi-versiolla, jota HotPDF tukee, mukaan lukien julkaisuilla, joissa ei ole lainkaan XML- tai XPath-sidontaa, joten sen rajapintaosio paljastaa vain tavallisia arvotyyppejä: tietueita, dynaamisia taulukoita ja yhden rajapinnan, IHPDFESchematronEngine, jolla on Load-, Validate- ja LastSummary-metodit. Jokainen MSXML-spesifinen tyyppi — IXMLDOMDocument2, Winapi.msxml-tuonti, itse THPDFMSXMLSchematronEngine — istuu yhden {$IFDEF XE2+}-lohkon sisällä toteutusosiossa, näkymättömänä sekä kutsujille että kääntäjälle vanhemmissa työkaluketjuissa

{$IFDEF XE2+}
function HPDFCreateSchematronEngine: IHPDFESchematronEngine;
begin
  Result := THPDFMSXMLSchematronEngine.Create;   // aito MSXML-pohjainen moottori
end;
{$ELSE}
function HPDFCreateSchematronEngine: IHPDFESchematronEngine;
begin
  Result := THPDFStubSchematronEngine.Create;    // Delphi 7: ilmoittaa itsensä käyttökelvottomaksi
end;
{$ENDIF}

Delphi 7:ssä ja sitä vanhemmissa versioissa HPDFCreateSchematronEngine antaa sen sijaan takaisin THPDFStubSchematronEngine-olion: sen Load palauttaa aina False-arvon ErrorText-kentällä, joka nimeää todellisen aukon — MSXML DOM -sidonta vaatii XE2:n tai uudemman — ja osoittaa kohti ulkoista validaattoria, kuten veraPDF:ää, Mustangia tai ZUGFeRD-yhdenmukaisuustyökalua täydeksi kattavuudeksi sillä välin. Sen Validate palauttaa yhden synteettisen tuloksen, jonka RuleID on 'ENGINE' ja Skipped asetettu, joten koodi, joka iteroi BusinessRules-listaa, ei tarvitse erillistä haaraa "moottori ei voinut ajaa" -tilanteelle verrattuna "jokainen sääntö sattui olemaan ohitettu" -tilanteeseen — molemmat näyttävät kutsujalle samalta muodolta. HPDFValidateEInvoice taittaa myös tämän sulavasti tuomioonsa: totuusarvo, jonka se palauttaa, on ContainerValid and ((not BusinessRulesEvaluated) or (BusinessRuleViolations = 0)), joten käytettävissä olematon moottori alentaa tuloksen pelkäksi säiliötarkistukseksi sen sijaan, että pakottaisi kovan epäonnistumisen kääntäjällä, joka ei koskaan aikonutkaan ajaa Schematron-sääntöjä ylipäätään

HPDFEInvoiceValidator:n XPath 1.0 -moottori ei korvaa täyttä Schematron-/XSLT 2.0 -prosessoria, eikä se ollut koskaan tarkoitettukaan sellaiseksi: XPath 1.0:aan rajoittunut moottori jättää aina kourallisen EN 16931 -väittämiä arvioimatta, mikä on juuri se, minkä Skipped-lippu jokaisessa tuloksessa on olemassa tekemään näkyväksi eikä piilottamaan. Sen sijaan moottori tuo liiketoimintasääntöpalautteen, joka toimii kaikkialla, missä HotPDF muutenkin toimii, ilman ulkoista prosessia, jolle pitäisi kutsua komentotulkkia, ja ilman XSLT 2.0 -ajonaikaista ympäristöä lisensoitavaksi. Tämä moottori toimitetaan osana HotPDF-Delphi-PDF-komponenttia Delphille ja C++Builderille, yhdessä säiliötason Factur-X- ja PDF/A-työkalujen kanssa, joiden päälle se rakentuu