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

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

// 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 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 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;

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

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

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;   // real MSXML-backed engine
end;
{$ELSE}
function HPDFCreateSchematronEngine: IHPDFESchematronEngine;
begin
  Result := THPDFStubSchematronEngine.Create;    // Delphi 7: reports itself unavailable
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-PDF-komponenttia Delphille ja C++Builderille, yhdessä säiliötason Factur-X- ja PDF/A-työkalujen kanssa, joiden päälle se rakentuu