HotPDF validerar EN 16931-affärsregler för elektronisk fakturering via HPDFEInvoiceValidator, en Schematron-assertionsmotor som biblioteket själv implementerar ovanpå MSXML:s XPath 1.0-stöd i stället för en licensierad XSLT 2.0-processor. HPDFEInvoiceValidator tolkar de officiella Factur-X .sch-regelfilerna, utvärderar varje assertion den kan uttrycka i XPath 1.0, och markerar resten som överhoppade i stället för att låta ett ostött uttryck kasta ett undantag mitt i körningen
Omfattningen här stannar inuti den motorn: hur laddaren omvandlar Schematron-XML till regelposter, hur assert- och report-utvärdering faktiskt avgör godkänt eller underkänt, hur XPath 2.0-luckan upptäcks och hoppas över, och hur samma enhet fortfarande kompilerar på Delphi 7. PDF/A-3-inbäddning, factur-x.xml/xrechnung.xml-behållarmekaniken, och ZUGFeRD 2.5-versionshistoriken lever i följdartikeln om ZUGFeRD och Factur-X e-fakturor i Delphi med HotPDF, som den här artikeln medvetet inte upprepar
Varför HotPDF byggde sin egen EN 16931 Schematron-motor
HotPDF byggde sin egen Schematron-motor eftersom regelfilens deklarerade bindning för EN 16931 överdriver vad dess assertioner faktiskt behöver: filen sätter queryBinding="xslt2" högst upp, vilket tekniskt sett begär en fullständig XSLT 2.0/XPath 2.0-processor, men att läsa igenom själva assertionerna visar att den stora majoriteten bara anropar XPath 1.0-funktioner som string-length och substring-after. Windows inbyggda MSXML DOM — den enda XML-motor som garanterat finns på varje Delphi-installation som stöds utan att lägga till ett tredjepartsberoende — implementerar råkar implementera exakt den delmängden, XPath 1.0, vilket är vad som gjorde en native motor praktisk i stället för att licensiera en separat XSLT 2.0-runtime. HPDFSchematronFileForProfile mappar en upptäckt Factur-X-konformansnivå till en av fem medskickade regelfiler den här motorn kan ladda — MINIMUM, BASIC WL, BASIC, EN 16931, och EXTENDED — och var och en av dem bedömer bara den extraherade faktura-XML:en, aldrig den omgivande PDF:en; huruvida den PDF:en själv är en strukturellt giltig PDF/A-3-fil är en separat fråga som besvaras via HotPDF:s konformanskontroller för PDF/A, PDF/X, och PDF/UA på annat håll i biblioteket
Hur omvandlar motorn en .sch-fil till regelposter?
THPDFMSXMLSchematronEngine.Load börjar med att anropa CoInitializeEx(nil, COINIT_MULTITHREADED) innan den skapar något, eftersom en konsol- eller tjänstvärd som aldrig anropat Application.Initialize ännu inte har någon COM-apartment, medan en GUI-VCL-värd redan har det; motorn behandlar resultatet S_FALSE eller RPC_E_CHANGED_MODE, som det anropet kan returnera på en tråd redan inuti en apartment, som lika godkänt snarare än som ett fel. Den tolkar sedan Schematron-filen med ett MSXML 6.0 DOM-dokument (CoDOMDocument60) och setProperty('SelectionLanguage', 'XPath'), eftersom MSXML som standard använder sin äldre XSL-Pattern-dialekt om inte en anropare uttryckligen väljer XPath. Därifrån anropar dock laddaren aldrig selectNodes för att gå igenom .sch-filens egen struktur — varje <pattern>-, <rule>-, <assert>- och <report>-element hittas genom att gå igenom firstChild/nextSibling för hand, och jämföra varje nods lokala namn och namnrymds-URI mot den bokstavliga strängen http://purl.oclc.org/dsdl/schematron
Den manuella genomgången finns på grund av ett hönan-och-ägget-problem i <ns prefix="ram" uri="..."/>-bindningarna varje Factur-X-Schematron-fil deklarerar i förväg. Att lösa ett prefixerat XPath-uttryck som ram:Name mot dessa bindningar kräver att MSXML:s SelectionNamespaces-egenskap redan innehåller dem, men att upptäcka bindningarna från början skulle normalt innebära att köra en XPath-fråga som //ns:ns — vilken i sig behöver SelectionNamespaces satt först. HPDFEInvoiceValidator bryter den cykeln genom att samla in varje <ns>-element via samma manuella barnnodgenomgång innan den ens rör selectNodes, och viker sedan in de insamlade prefix/URI-paren i en enda SelectionNamespaces-sträng som både .sch-strukturgenomgången och varje senare regelutvärdering återanvänder
// 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: vad utlöser egentligen en avvikelse?
Schematron ger assert och report motsatt polaritet, och motorn måste bevara den skillnaden exakt annars betyder dess avvikelseräkning ingenting. En <assert test="X"> deklarerar att X måste hålla för varje nod som matchar regelns kontextväg, så EvaluateAssert registrerar en avvikelse när testuttryckets resulterande nodmängd kommer tillbaka tom; en <report test="X"> är spegelbilden, och flaggar ett problem när X är sant, så EvaluateReport registrerar en avvikelse när testresultatet i stället är icke-tomt. Båda ingångspunkterna delar samma tvåstegsform under huven — Doc.selectNodes(Entry.Context) först, för att hitta varje nod regeln gäller, sedan ContextNode.selectNodes(Entry.Test) mot var och en i tur och ordning — vilket är exakt den kontext-sedan-test-modell en riktig Schematron-processor använder, bara driven av MSXML:s XPath 1.0-selectNodes i stället för en Schematron-medveten exekveringsmotor
Hur hoppar motorn över XPath 2.0 utan att krascha körningen?
HPDFEInvoiceValidator försvarar sig mot ostödd XPath 2.0-syntax i två lager, och det första låter aldrig MSXML se uttrycket alls. Innan den utvärderar någon assert eller report skannar XPath2Detected den råa testuttryckssträngen efter sex bokstavliga token — xs:decimal, xs:integer, xs:string, upper-case, lower-case, och exists( — och om någon av dem finns markeras regeln som Skipped med allvarlighetsgraden stsInfo omedelbart, med resonemanget att MSXML aldrig ska matas med ett uttryck det redan är känt att avvisa
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;
Det andra lagret fångar det som den statiska tokenlistan missar. Både kontext-selectNodes-anropet och det per-nod-testande selectNodes-anropet körs inuti ett try/except-block; när MSXML kastar på ett uttryck tokenskanningen släppte igenom — en konstruktion utanför de sex kända token, eller en kontextväg den inte kan lösa — fångas undantaget och regeln registreras som Skipped i stället för att fortplantas till anroparen. Den tvålagriga designen är varför en XPath 2.0-konstruktion någonstans i regeluppsättningen aldrig kastar förbi HPDFValidateEInvoice: var och en av dess 424 assertioner antingen utvärderas, misslyckas, eller markeras som överhoppad, och en intern uppskattning mot den regelfilen satte den XPath-1.0-exekverbara andelen till ungefär 350 av 424 — tillräckligt för att partiell utvärdering är värt att göra i stället för att falla tillbaka till en enbart-behållar-kontroll i samma stund en enda XPath 2.0-assertion dyker upp
Att mata UTF-8-faktura-XML till MSXML utan att fördärva den
THPDFMSXMLSchematronEngine.Validate lämnar inte de extraherade fakturabytesen till IXMLDOMDocument.loadXML, eftersom den metoden förväntar sig en BSTR — UTF-16 — och skulle omtolka en rå UTF-8-bytearray under det antagandet oavsett vad dokumentets egen <?xml encoding="UTF-8"?>-deklaration säger. HPDFEInvoiceValidator kopierar i stället bytesen till en GlobalAlloc:ad HGLOBAL, omsluter den i en IStream via CreateStreamOnHGlobal, och laddar den strömmen via IPersistStreamInit.Load, en väg MSXML respekterar genom att läsa kodningsdeklarationen från själva byteströmmen i stället för att anta UTF-16 i förväg. Samma metod bygger om SelectionNamespaces från de prefixbindningar laddaren redan samlade in medan den tolkade .sch-filen, så en regel skriven mot ett prefix som ram: löser sig korrekt mot faktura-XML:ens egen namnrymd vid varje utvärdering, inte bara när regelfilen först tolkades
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
Att hålla en enda enhet kompilerande från Delphi 7 till idag
HPDFEInvoiceValidator.pas måste kompilera på varje Delphi-version HotPDF stödjer, inklusive utgåvor helt utan XML- eller XPath-bindning, så dess interface-sektion exponerar bara vanliga värdetyper: poster, dynamiska arrayer, och ett enda gränssnitt, IHPDFESchematronEngine, med metoderna Load, Validate, och LastSummary. Varje MSXML-specifik typ — IXMLDOMDocument2, importen Winapi.msxml, THPDFMSXMLSchematronEngine själv — sitter inuti ett enda {$IFDEF XE2+}-block i implementationssektionen, osynligt för anropare och för kompilatorn på äldre verktygskedjor lika mycket
{$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}
På Delphi 7 och tidigare lämnar HPDFCreateSchematronEngine i stället tillbaka THPDFStubSchematronEngine: dess Load returnerar alltid False med en ErrorText som namnger den verkliga luckan — MSXML DOM-bindning kräver XE2 eller senare — och pekar mot en extern validerare som veraPDF, Mustang, eller ett ZUGFeRD-konformansverktyg för fullständig täckning under tiden. Dess Validate returnerar ett enda syntetiskt resultat med RuleID 'ENGINE' och Skipped satt, så kod som itererar över BusinessRules behöver ingen separat gren för "motorn kunde inte köras" jämfört med "varje regel råkade hoppas över" — båda ser likadana ut för anroparen. HPDFValidateEInvoice viker in det graciöst i sin dom också: den boolean den returnerar är ContainerValid and ((not BusinessRulesEvaluated) or (BusinessRuleViolations = 0)), så en otillgänglig motor nedgraderar resultatet till en enbart-behållar-kontroll i stället för att tvinga fram ett hårt fel på en kompilator som ändå aldrig skulle köra Schematron-regler från början
HPDFEInvoiceValidators XPath 1.0-motor ersätter inte en fullständig Schematron/XSLT 2.0-processor, och den var aldrig menad att göra det: en motor begränsad till XPath 1.0 kommer alltid att lämna en handfull EN 16931-assertioner outvärderade, vilket är exakt vad Skipped-flaggan på varje resultat finns för att göra synligt i stället för att dölja. Vad motorn ger är affärsregelåterkoppling som körs var HotPDF än redan körs, utan någon extern process att skala ut till och ingen XSLT 2.0-runtime att licensiera. Den här motorn levereras som en del av HotPDF PDF-komponenten för Delphi och C++Builder, tillsammans med behållarnivåverktygen för Factur-X och PDF/A den bygger på