HotPDF, EN 16931 elektronik fatura iş kurallarını, lisanslı bir XSLT 2.0 işlemcisi yerine MSXML'in XPath 1.0 desteği üzerine kütüphanenin kendisinin uyguladığı bir Schematron doğrulama motoru olan HPDFEInvoiceValidator aracılığıyla doğrular. HPDFEInvoiceValidator, resmi Factur-X .sch kural dosyalarını ayrıştırır, XPath 1.0'da ifade edebildiği her doğrulamayı değerlendirir ve desteklenmeyen bir ifadenin çalışma ortasında bir istisna fırlatmasına izin vermek yerine geri kalanı atlandı olarak işaretler
Buradaki kapsam bu motorun içinde kalır: yükleyicinin Schematron XML'ini nasıl kural girdilerine dönüştürdüğü, assert ve report değerlendirmesinin geçme veya kalmaya gerçekte nasıl karar verdiği, XPath 2.0 boşluğunun nasıl tespit edilip atlandığı ve aynı birimin Delphi 7'de hâlâ nasıl derlendiği. PDF/A-3 gömme, factur-x.xml / xrechnung.xml konteyner mekaniği ve ZUGFeRD 2.5 sürümleme hikayesi, bu makalenin kasıtlı olarak tekrarlamadığı HotPDF ile Delphi'de ZUGFeRD ve Factur-X e-faturaları üzerine tamamlayıcı makalede yer alır
HotPDF neden kendi EN 16931 Schematron motorunu inşa etti
HotPDF kendi Schematron motorunu inşa etti çünkü EN 16931 kural dosyasının bildirilen bağlaması, doğrulamalarının gerçekte ihtiyaç duyduğundan fazlasını ifade eder: dosya tepede queryBinding="xslt2" ayarlar, teknik olarak tam bir XSLT 2.0 / XPath 2.0 işlemcisi ister, ancak doğrulamaların kendisini okumak, büyük çoğunluğunun yalnızca string-length ve substring-after gibi XPath 1.0 fonksiyonlarını çağırdığını gösterir. Windows'un yerleşik MSXML DOM'u — desteklenen her Delphi kurulumunda üçüncü taraf bir bağımlılık eklemeden var olması garanti edilen tek XML motoru — tam olarak bu alt kümeyi, XPath 1.0'ı uygular; bu da ayrı bir XSLT 2.0 çalışma zamanı lisanslamak yerine yerel bir motoru pratik kılan şeydir. HPDFSchematronFileForProfile, tespit edilen bir Factur-X uyumluluk seviyesini bu motorun yükleyebileceği beş gönderilen kural dosyasından birine eşler — MINIMUM, BASIC WL, BASIC, EN 16931 ve EXTENDED — ve bunların her biri yalnızca çıkarılan fatura XML'ini değerlendirir, çevreleyen PDF'i asla değerlendirmez; o PDF'in kendisinin yapısal olarak geçerli bir PDF/A-3 dosyası olup olmadığı, kütüphanede başka bir yerde HotPDF'in PDF/A, PDF/X ve PDF/UA uyumluluk kontrolleri aracılığıyla yanıtlanan ayrı bir sorudur
Motor bir .sch dosyasını kural girdilerine nasıl dönüştürür?
THPDFMSXMLSchematronEngine.Load, herhangi bir şey oluşturmadan önce CoInitializeEx(nil, COINIT_MULTITHREADED)'i çağırarak başlar, çünkü Application.Initialize'ı hiç çağırmamış bir konsol veya servis barındırıcısının henüz bir COM apartmanı yoktur, oysa bir GUI VCL barındırıcısının zaten vardır; motor, bir iş parçacığı zaten bir apartman içindeyken bu çağrının döndürebileceği S_FALSE veya RPC_E_CHANGED_MODE sonucunu bir hata yerine eşit derecede uygun olarak ele alır. Ardından Schematron dosyasını bir MSXML 6.0 DOM belgesiyle (CoDOMDocument60) ve setProperty('SelectionLanguage', 'XPath') ile ayrıştırır, çünkü bir çağıran açıkça XPath'e katılmadıkça MSXML varsayılan olarak daha eski XSL-Pattern lehçesini kullanır. Ancak buradan itibaren yükleyici, .sch dosyasının kendi yapısını gezmek için asla selectNodes'u çağırmaz — her <pattern>, <rule>, <assert> ve <report> öğesi, her bir düğümün yerel adını ve ad alanı URI'sini http://purl.oclc.org/dsdl/schematron literal dizesiyle karşılaştırarak firstChild / nextSibling'i elle gezerek bulunur
Bu elle gezme yaklaşımı, her Factur-X Schematron dosyasının önceden bildirdiği <ns prefix="ram" uri="..."/> bağlamalarındaki tavuk-yumurta sorunundan dolayı var. ram:Name gibi önekli bir XPath ifadesini bu bağlamalara karşı çözmek, MSXML'in SelectionNamespaces özelliğinin bunları zaten tutmasını gerektirir, ancak bağlamaları ilk etapta keşfetmek normalde //ns:ns gibi bir XPath sorgusu çalıştırmak anlamına gelirdi — bu da önce SelectionNamespaces'in ayarlanmış olmasını gerektirir. HPDFEInvoiceValidator bu döngüyü, selectNodes'a hiç dokunmadan önce her <ns> öğesini aynı elle çocuk düğüm gezintisiyle toplayarak kırar, ardından toplanan önek/URI çiftlerini hem .sch yapı gezintisinin hem de daha sonraki her kural değerlendirmesinin yeniden kullandığı tek bir SelectionNamespaces dizesine katlar
// 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 mi report mu: gerçekte bir ihlali ne tetikler?
Schematron, assert ve report'a zıt kutupluluk verir ve motorun bu ayrımı tam olarak koruması gerekir, yoksa ihlal sayıları hiçbir şey ifade etmez. Bir <assert test="X">, X'in kuralın bağlam yoluyla eşleşen her düğüm için geçerli olması gerektiğini bildirir; bu yüzden EvaluateAssert, test ifadesinin sonuç düğüm kümesi boş döndüğünde bir ihlal kaydeder; bir <report test="X"> ayna görüntüsüdür, X doğru olduğunda bir sorunu işaretler; bu yüzden EvaluateReport, test sonucu boş olmadığında bir ihlal kaydeder. Her iki giriş noktası da altta aynı iki aşamalı şekli paylaşır — önce Doc.selectNodes(Entry.Context), kuralın uygulandığı her düğümü bulmak için, ardından her birine karşı sırayla ContextNode.selectNodes(Entry.Test) — bu tam olarak gerçek bir Schematron işlemcisinin kullandığı bağlam-sonra-test modelidir, yalnızca Schematron farkında bir yürütme motoru yerine MSXML'in XPath 1.0 selectNodes'u tarafından yönlendirilir
Motor, çalışmayı çökertmeden XPath 2.0'ı nasıl atlar?
HPDFEInvoiceValidator, desteklenmeyen XPath 2.0 sözdizimine karşı iki katmanda savunma yapar ve ilki MSXML'in ifadeyi hiç görmesine izin vermez. Herhangi bir assert veya report'u değerlendirmeden önce, XPath2Detected ham test ifadesi dizesini altı literal jeton için tarar — xs:decimal, xs:integer, xs:string, upper-case, lower-case ve exists( — ve bunlardan herhangi biri varsa kural hemen stsInfo önem düzeyiyle Skipped olarak işaretlenir; gerekçe, MSXML'e zaten reddedeceği bilinen bir ifadenin asla verilmemesi gerektiğidir
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;
İkinci katman, statik jeton listesinin kaçırdığı her şeyi yakalar. Hem bağlam selectNodes çağrısı hem de düğüm başına test selectNodes çağrısı bir try/except bloğu içinde çalışır; MSXML, jeton taramasının geçirdiği bir ifadede — altı bilinen jetonun dışında bir yapı veya çözemediği bir bağlam yolu — bir istisna fırlattığında, istisna yakalanır ve kural çağırana yayılmak yerine Skipped olarak kaydedilir. Bu iki katmanlı tasarım, kural kümesindeki herhangi bir yerdeki bir XPath 2.0 yapısının HPDFValidateEInvoice'un ötesine asla bir istisna fırlatmamasının nedenidir: 424 doğrulamasının her biri ya değerlendirilir, ya başarısız olur ya da atlandı olarak işaretlenir ve o kural dosyasına karşı dahili bir tahmin, XPath-1.0-yürütülebilir payı 424'ün yaklaşık 350'sine koydu — bu, tek bir XPath 2.0 doğrulaması ortaya çıktığı anda yalnızca konteyner kontrolüne geri dönmek yerine kısmi değerlendirme yapmaya değecek kadar
UTF-8 fatura XML'ini MSXML'e bozmadan beslemek
THPDFMSXMLSchematronEngine.Validate, çıkarılan fatura byte'larını IXMLDOMDocument.loadXML'e vermez, çünkü bu yöntem bir BSTR — UTF-16 — bekler ve belgenin kendi <?xml encoding="UTF-8"?> bildiriminin ne dediğinden bağımsız olarak, ham bir UTF-8 byte dizisini bu varsayım altında yeniden yorumlardı. HPDFEInvoiceValidator bunun yerine byte'ları GlobalAlloc'lu bir HGLOBAL'a kopyalar, CreateStreamOnHGlobal aracılığıyla bir IStream'e sarar ve o akışı IPersistStreamInit.Load üzerinden yükler; bu, MSXML'in önceden UTF-16 varsaymak yerine kodlama bildirimini byte akışının kendisinden okuyarak onurlandırdığı bir yoldur. Aynı yöntem, .sch dosyasını ayrıştırırken yükleyicinin zaten topladığı önek bağlamalarından SelectionNamespaces'i yeniden inşa eder; bu yüzden ram: gibi bir öneke karşı yazılan bir kural, yalnızca kural dosyası ilk ayrıştırıldığında değil, her değerlendirmede fatura XML'inin kendi ad alanına karşı doğru çözülür
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
Tek bir birimi Delphi 7'den günümüze kadar derlenir tutmak
HPDFEInvoiceValidator.pas, hiç XML veya XPath bağlaması olmayan sürümler dahil, HotPDF'in desteklediği her Delphi sürümünde derlenmek zorundadır; bu yüzden arayüz bölümü yalnızca sade değer türlerini ortaya çıkarır: kayıtlar, dinamik diziler ve Load, Validate ve LastSummary yöntemleriyle tek bir arayüz, IHPDFESchematronEngine. MSXML'e özgü her tür — IXMLDOMDocument2, Winapi.msxml içe aktarımı, THPDFMSXMLSchematronEngine'in kendisi — uygulama bölümündeki tek bir {$IFDEF XE2+} bloğunun içinde oturur; çağıranlara ve daha eski araç zincirlerindeki derleyiciye eşit derecede görünmezdir
{$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 ve öncesinde, HPDFCreateSchematronEngine bunun yerine THPDFStubSchematronEngine'i geri verir: Load'u her zaman gerçek boşluğu adlandıran bir ErrorText ile False döndürür — MSXML DOM bağlaması XE2 veya sonrasını gerektirir — ve bu arada tam kapsam için veraPDF, Mustang veya bir ZUGFeRD uyumluluk aracı gibi harici bir doğrulayıcıya işaret eder. Validate'i, RuleID 'ENGINE' ve Skipped ayarlanmış tek bir sentetik sonuç döndürür; bu yüzden BusinessRules'u yineleyen kod, "motor çalışamadı" ile "her kural tesadüfen atlandı" için ayrı bir dal gerektirmez — ikisi de çağırana aynı şekilde görünür. HPDFValidateEInvoice bunu da hükmüne zarifçe katlar: döndürdüğü boolean ContainerValid and ((not BusinessRulesEvaluated) or (BusinessRuleViolations = 0))'dır; bu yüzden kullanılamayan bir motor, sonucu zaten hiçbir zaman Schematron kuralları çalıştırmayacak bir derleyicide sert bir başarısızlığı zorlamak yerine yalnızca konteyner kontrolüne düşürür
HPDFEInvoiceValidator'ın XPath 1.0 motoru tam bir Schematron/XSLT 2.0 işlemcisinin yerini almaz ve hiçbir zaman bunu amaçlamamıştır: yalnızca XPath 1.0 ile sınırlı bir motor her zaman bir avuç EN 16931 doğrulamasını değerlendirilmemiş bırakacaktır; bu tam olarak her sonuçtaki Skipped bayrağının gizlemek yerine görünür kılmak için var olduğu şeydir. Motorun kazandırdığı şey, HotPDF'in zaten çalıştığı her yerde çalışan, kabuğa çıkılacak harici bir süreç ve lisanslanacak bir XSLT 2.0 çalışma zamanı olmayan iş kuralı geri bildirimidir. Bu motor, üzerine inşa edildiği konteyner düzeyi Factur-X ve PDF/A araçlarının yanı sıra, Delphi ve C++Builder için HotPDF PDF bileşeninin bir parçası olarak gönderilir