HotPDF valida le regole di business della fattura elettronica EN 16931 tramite HPDFEInvoiceValidator, un motore di asserzioni Schematron che la libreria implementa autonomamente sopra il supporto XPath 1.0 di MSXML, invece di un processore XSLT 2.0 concesso su licenza. HPDFEInvoiceValidator analizza i file di regole .sch ufficiali di Factur-X, valuta ogni asserzione esprimibile in XPath 1.0 e marca il resto come saltato invece di lasciare che un'espressione non supportata sollevi un'eccezione a metà esecuzione
L'ambito qui resta interno a quel motore: come il loader trasforma l'XML Schematron in voci di regola, come la valutazione di assert e report decide realmente successo o fallimento, come viene rilevata e saltata la lacuna XPath 2.0, e come la stessa unit continui a compilare su Delphi 7. L'incorporamento PDF/A-3, le meccaniche del contenitore factur-x.xml / xrechnung.xml e la storia del versionamento ZUGFeRD 2.5 risiedono nell'articolo di approfondimento su ZUGFeRD e Factur-X in Delphi con HotPDF, che questo pezzo deliberatamente non ripete
Perché HotPDF ha costruito un proprio motore Schematron EN 16931
HotPDF ha costruito un proprio motore Schematron perché il binding dichiarato nel file di regole EN 16931 sopravvaluta ciò di cui le sue asserzioni hanno realmente bisogno: il file imposta queryBinding="xslt2" in cima, richiedendo tecnicamente un processore completo XSLT 2.0 / XPath 2.0, ma leggendo le asserzioni stesse emerge che la grande maggioranza richiama solo funzioni XPath 1.0 come string-length e substring-after. Il DOM MSXML integrato di Windows — l'unico motore XML garantito presente su ogni installazione Delphi supportata senza aggiungere una dipendenza di terze parti — implementa esattamente quel sottoinsieme, XPath 1.0, ed è ciò che ha reso praticabile un motore nativo invece di concedere in licenza un runtime XSLT 2.0 separato. HPDFSchematronFileForProfile mappa un livello di conformità Factur-X rilevato su uno dei cinque file di regole forniti che questo motore può caricare — MINIMUM, BASIC WL, BASIC, EN 16931 ed EXTENDED — e ognuno di essi giudica solo l'XML della fattura estratto, mai il PDF circostante; se quel PDF sia esso stesso un file PDF/A-3 strutturalmente valido è una questione separata, a cui rispondono altrove nella libreria i controlli di conformità PDF/A, PDF/X e PDF/UA di HotPDF
Come trasforma il motore un file .sch in voci di regola?
THPDFMSXMLSchematronEngine.Load inizia chiamando CoInitializeEx(nil, COINIT_MULTITHREADED) prima di creare qualsiasi cosa, perché un host console o servizio che non ha mai chiamato Application.Initialize non ha ancora alcun apartment COM, mentre un host GUI VCL lo ha già; il motore tratta il risultato S_FALSE o RPC_E_CHANGED_MODE che quella chiamata può restituire su un thread già dentro un apartment come ugualmente accettabile invece che come un errore. Analizza quindi il file Schematron con un documento DOM MSXML 6.0 (CoDOMDocument60) e setProperty('SelectionLanguage', 'XPath'), poiché MSXML usa per default il proprio dialetto XSL-Pattern più vecchio a meno che un chiamante non opti esplicitamente per XPath. Da lì in poi, tuttavia, il loader non chiama mai selectNodes per percorrere la struttura propria del file .sch — ogni elemento <pattern>, <rule>, <assert> e <report> viene trovato percorrendo firstChild / nextSibling a mano, confrontando il nome locale e l'URI del namespace di ciascun nodo con la stringa letterale http://purl.oclc.org/dsdl/schematron
Questo approccio di percorrenza manuale esiste per un problema dell'uovo e della gallina nei binding <ns prefix="ram" uri="..."/> che ogni file Schematron Factur-X dichiara in apertura. Risolvere un'espressione XPath con prefisso come ram:Name rispetto a quei binding richiede che la proprietà SelectionNamespaces di MSXML li contenga già, ma scoprire i binding in primo luogo normalmente significherebbe eseguire una query XPath come //ns:ns — che a sua volta richiede che SelectionNamespaces sia già impostata. HPDFEInvoiceValidator spezza quel ciclo raccogliendo ogni elemento <ns> tramite la stessa percorrenza manuale dei nodi figlio prima ancora di toccare selectNodes, quindi condensa le coppie prefisso/URI raccolte in un'unica stringa SelectionNamespaces che sia la percorrenza della struttura .sch sia ogni successiva valutazione di regola riutilizzano
// 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 contro report: cosa fa scattare realmente una violazione?
Schematron assegna ad assert e report polarità opposta, e il motore deve preservare esattamente quella distinzione, altrimenti i suoi conteggi di violazioni non significano nulla. Un <assert test="X"> dichiara che X deve valere per ogni nodo che corrisponde al percorso di contesto della regola, quindi EvaluateAssert registra una violazione quando l'insieme di nodi risultante dall'espressione di test torna vuoto; un <report test="X"> è l'immagine speculare, segnalando un problema quando X è vero, quindi EvaluateReport registra invece una violazione quando il risultato del test non è vuoto. Entrambi i punti di ingresso condividono sotto il cofano la stessa forma a due fasi — prima Doc.selectNodes(Entry.Context), per trovare ogni nodo a cui la regola si applica, poi ContextNode.selectNodes(Entry.Test) su ciascuno a turno — che è esattamente il modello contesto-poi-test usato da un vero processore Schematron, solo pilotato dal selectNodes XPath 1.0 di MSXML invece che da un motore di esecuzione consapevole di Schematron
Come salta il motore XPath 2.0 senza far crollare l'esecuzione?
HPDFEInvoiceValidator si difende dalla sintassi XPath 2.0 non supportata su due livelli, e il primo non lascia mai che MSXML veda affatto l'espressione. Prima di valutare qualsiasi assert o report, XPath2Detected scansiona la stringa grezza dell'espressione di test alla ricerca di sei token letterali — xs:decimal, xs:integer, xs:string, upper-case, lower-case ed exists( — e se uno qualsiasi di essi è presente la regola viene immediatamente marcata Skipped con severità stsInfo, sul ragionamento che MSXML non dovrebbe mai ricevere un'espressione già nota per essere rifiutata
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;
Il secondo livello cattura tutto ciò che l'elenco statico di token si lascia sfuggire. Sia la chiamata selectNodes del contesto sia la chiamata selectNodes del test per singolo nodo vengono eseguite dentro un blocco try/except; quando MSXML solleva un'eccezione su un'espressione che la scansione dei token ha lasciato passare — un costrutto al di fuori dei sei token noti, o un percorso di contesto che non riesce a risolvere — l'eccezione viene catturata e la regola viene registrata come Skipped invece di propagarsi al chiamante. Questo design a due livelli è il motivo per cui un costrutto XPath 2.0 in qualsiasi punto del set di regole non solleva mai un'eccezione oltre HPDFValidateEInvoice: ognuna delle sue 424 asserzioni viene valutata, fallisce, oppure viene marcata come saltata, e una stima interna su quel file di regole ha collocato la quota eseguibile in XPath 1.0 a circa 350 delle 424 — abbastanza da rendere utile una valutazione parziale invece di ricadere su un controllo limitato al solo contenitore nel momento in cui compare una singola asserzione XPath 2.0
Passare a MSXML l'XML della fattura in UTF-8 senza corromperlo
THPDFMSXMLSchematronEngine.Validate non passa i byte della fattura estratta a IXMLDOMDocument.loadXML, perché quel metodo si aspetta un BSTR — UTF-16 — e reinterpreterebbe un array di byte UTF-8 grezzo sotto quel presupposto indipendentemente da cosa dica la dichiarazione <?xml encoding="UTF-8"?> propria del documento. HPDFEInvoiceValidator copia invece i byte in un HGLOBAL allocato con GlobalAlloc, lo incapsula in un IStream tramite CreateStreamOnHGlobal, e carica quello stream tramite IPersistStreamInit.Load, un percorso che MSXML rispetta leggendo la dichiarazione di codifica direttamente dal flusso di byte invece di presupporre UTF-16 in partenza. Lo stesso metodo ricostruisce SelectionNamespaces a partire dai binding di prefisso già raccolti dal loader durante l'analisi del file .sch, cosicché una regola scritta contro un prefisso come ram: si risolva correttamente rispetto al namespace proprio dell'XML della fattura a ogni valutazione, non solo quando il file di regole è stato analizzato per la prima volta
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
Mantenere una singola unit compilabile da Delphi 7 a oggi
HPDFEInvoiceValidator.pas deve compilare su ogni versione di Delphi supportata da HotPDF, incluse release senza alcun binding XML o XPath, quindi la sua sezione interface espone solo tipi valore semplici: record, array dinamici e una singola interfaccia, IHPDFESchematronEngine, con i metodi Load, Validate e LastSummary. Ogni tipo specifico di MSXML — IXMLDOMDocument2, l'import Winapi.msxml, THPDFMSXMLSchematronEngine stessa — risiede dentro un unico blocco {$IFDEF XE2+} nella sezione implementation, invisibile sia ai chiamanti sia al compilatore sui toolchain più datati
{$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 e versioni precedenti, HPDFCreateSchematronEngine restituisce invece THPDFStubSchematronEngine: il suo Load restituisce sempre False con un ErrorText che indica la lacuna reale — il binding DOM MSXML richiede XE2 o versioni successive — e indirizza, nel frattempo, verso un validatore esterno come veraPDF, Mustang o uno strumento di conformità ZUGFeRD per la copertura completa. Il suo Validate restituisce un singolo risultato sintetico con RuleID pari a 'ENGINE' e Skipped impostato, cosicché il codice che itera BusinessRules non necessiti di un ramo separato per "il motore non è riuscito a partire" rispetto a "ogni regola è risultata saltata" — entrambi presentano al chiamante la stessa forma. HPDFValidateEInvoice integra con eleganza anche questo nel proprio verdetto: il booleano che restituisce è ContainerValid and ((not BusinessRulesEvaluated) or (BusinessRuleViolations = 0)), cosicché un motore non disponibile declassi il risultato a un controllo limitato al solo contenitore invece di forzare un fallimento netto su un compilatore che non avrebbe mai eseguito regole Schematron in primo luogo
Il motore XPath 1.0 di HPDFEInvoiceValidator non sostituisce un processore Schematron/XSLT 2.0 completo, e non è mai stato pensato per farlo: un motore limitato a XPath 1.0 lascerà sempre una manciata di asserzioni EN 16931 non valutate, ed è esattamente ciò che il flag Skipped su ciascun risultato esiste per rendere visibile piuttosto che nascondere. Ciò che il motore offre è un feedback sulle regole di business che funziona ovunque HotPDF già funzioni, senza alcun processo esterno da invocare e senza alcun runtime XSLT 2.0 da concedere in licenza. Questo motore fa parte del componente PDF HotPDF per Delphi e C++Builder, insieme agli strumenti Factur-X e PDF/A a livello di contenitore su cui si basa