مقاله فنی

موتور قواعد Schematron مطابق EN 16931 در Delphi با HotPDF

HotPDF قواعد کسب‌وکاری فاکتور الکترونیک مطابق EN 16931 را از طریق HPDFEInvoiceValidator اعتبارسنجی می‌کند، یک موتور تأیید Schematron که کتابخانه آن را خودش روی پشتیبانی XPath 1.0 از MSXML پیاده‌سازی کرده، نه یک پردازشگر XSLT 2.0 دارای مجوز. HPDFEInvoiceValidator فایل‌های قاعده‌ی رسمی .sch از Factur-X را تجزیه می‌کند، هر تأییدی را که بتواند در XPath 1.0 بیان کند ارزیابی می‌کند، و بقیه را به‌جای اینکه اجازه دهد یک عبارت پشتیبانی‌نشده در میانه‌ی اجرا یک exception پرتاب کند، به‌عنوان رد‌شده علامت می‌زند

دامنه‌ی این مقاله درون همان موتور می‌ماند: بارگذار چطور XML از نوع Schematron را به ورودی‌های قاعده تبدیل می‌کند، ارزیابی assert و report چطور واقعاً قبولی یا رد را تعیین می‌کند، شکاف XPath 2.0 چطور تشخیص داده و رد می‌شود، و همین واحد چطور همچنان روی Delphi 7 کامپایل می‌شود. جاسازی PDF/A-3، جزئیات کانتینر factur-x.xml / xrechnung.xml، و داستان نسخه‌بندی ZUGFeRD 2.5 در مقاله‌ی همراه درباره‌ی فاکتورهای الکترونیکی ZUGFeRD و Factur-X در Delphi با HotPDF زندگی می‌کنند، که این مقاله عمداً آن‌ها را تکرار نمی‌کند

چرا HotPDF موتور Schematron مطابق EN 16931 خودش را ساخت

HotPDF موتور Schematron خودش را ساخت چون binding اعلان‌شده‌ی فایل قاعده‌ی EN 16931 چیزی بیش از آنچه تأییدهایش واقعاً نیاز دارند ادعا می‌کند: فایل در بالای خودش queryBinding="xslt2" را تنظیم می‌کند، که از نظر فنی یک پردازشگر کامل XSLT 2.0 / XPath 2.0 می‌خواهد، اما خواندن خودِ تأییدها نشان می‌دهد که اکثریت قریب به اتفاق فقط توابع XPath 1.0 مانند string-length و substring-after را فرا می‌خوانند. DOM MSXML توکار ویندوز — تنها موتور XMLای که تضمین می‌شود روی هر نصب پشتیبانی‌شده‌ی Delphi بدون افزودن یک وابستگی شخص ثالث وجود داشته باشد — دقیقاً همان زیرمجموعه، XPath 1.0، را پیاده‌سازی می‌کند، و همین چیزی است که یک موتور بومی را عملی کرد به‌جای دریافت مجوز یک زمان‌اجرای جداگانه‌ی XSLT 2.0. HPDFSchematronFileForProfile یک سطح مطابقت Factur-X تشخیص‌داده‌شده را به یکی از پنج فایل قاعده‌ی عرضه‌شده که این موتور می‌تواند بار کند نگاشت می‌کند — MINIMUM، BASIC WL، BASIC، EN 16931، و EXTENDED — و هر یک از آن‌ها فقط XML فاکتور استخراج‌شده را قضاوت می‌کند، هرگز خودِ PDF پیرامون را؛ اینکه آیا آن PDF خودش یک فایل PDF/A-3 با ساختار معتبر است یک سؤال جداگانه است که از طریق بررسی‌های مطابقت PDF/A، PDF/X، و PDF/UA در HotPDF در جای دیگری از کتابخانه پاسخ داده می‌شود

موتور چطور یک فایل .sch را به ورودی‌های قاعده تبدیل می‌کند؟

THPDFMSXMLSchematronEngine.Load با فراخوانی CoInitializeEx(nil, COINIT_MULTITHREADED) پیش از ساختن هر چیزی شروع می‌شود، چون یک میزبان کنسول یا سرویس که هرگز Application.Initialize را فراخوانی نکرده، هنوز هیچ apartment از COM ندارد، درحالی‌که یک میزبان VCL گرافیکی از پیش دارد؛ موتور نتیجه‌ی S_FALSE یا RPC_E_CHANGED_MODE که آن فراخوانی روی یک رشته‌ی از پیش درون یک apartment می‌تواند برگرداند را به‌همان اندازه مطلوب تلقی می‌کند، نه به‌عنوان یک خطا. سپس فایل Schematron را با یک سند DOM از MSXML 6.0 (‏CoDOMDocument60) و setProperty('SelectionLanguage', 'XPath') تجزیه می‌کند، چون MSXML به‌طور پیش‌فرض به گویش قدیمی‌تر XSL-Pattern خودش برمی‌گردد مگر اینکه یک فراخواننده صراحتاً وارد XPath شود. از آنجا، هرچند، بارگذار هرگز selectNodes را برای پیمایش ساختار خودِ فایل .sch فرا نمی‌خواند — هر عنصر <pattern>، <rule>، <assert>، و <report> با پیمایش دستی firstChild / nextSibling یافت می‌شود، با مقایسه‌ی نام محلی و URI فضای‌نام هر گره در برابر رشته‌ی لفظی http://purl.oclc.org/dsdl/schematron

آن رویکرد پیمایش دستی به‌دلیل یک مسئله‌ی مرغ‌وتخم‌مرغ در binding‌های <ns prefix="ram" uri="..."/>ای وجود دارد که هر فایل Schematron از Factur-X از پیش اعلان می‌کند. حل یک عبارت XPath پیشوند‌دار مثل ram:Name در برابر آن bindingها به این نیاز دارد که ویژگی SelectionNamespaces در MSXML از پیش آن‌ها را داشته باشد، اما کشف bindingها در وهله‌ی اول معمولاً یعنی اجرای یک پرس‌وجوی XPath مثل //ns:ns — که خودش نیاز دارد SelectionNamespaces از پیش تنظیم شده باشد. HPDFEInvoiceValidator آن چرخه را با جمع‌آوری هر عنصر <ns> از طریق همان پیمایش دستی گره‌ی فرزند پیش از لمس‌کردن اصلاً selectNodes می‌شکند، سپس جفت‌های پیشوند/URI برداشت‌شده را در یک رشته‌ی SelectionNamespaces تا می‌کند که هم پیمایش ساختار .sch و هم هر ارزیابی قاعده‌ی بعدی دوباره از آن استفاده می‌کنند

// 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 در برابر report: واقعاً چه چیزی یک نقض را شلیک می‌کند؟

Schematron به assert و report قطبیت متضادی می‌دهد، و موتور باید آن تمایز را دقیقاً حفظ کند وگرنه شمارش نقض‌هایش هیچ معنایی نخواهد داشت. یک <assert test="X"> اعلان می‌کند که X باید برای هر گره‌ای که با مسیر context قاعده مطابقت دارد برقرار باشد، پس EvaluateAssert وقتی مجموعه‌گره‌ی نتیجه‌ی عبارت تست خالی برگردد یک نقض ثبت می‌کند؛ یک <report test="X"> تصویر آینه‌ای آن است، وقتی X درست باشد یک مسئله را پرچم‌گذاری می‌کند، پس EvaluateReport وقتی نتیجه‌ی تست غیرخالی باشد یک نقض ثبت می‌کند. هر دو نقطه ورودی زیرِ خودشان همان شکل دومرحله‌ای را به اشتراک می‌گذارند — ابتدا Doc.selectNodes(Entry.Context)، برای یافتن هر گره‌ای که قاعده روی آن اعمال می‌شود، سپس ContextNode.selectNodes(Entry.Test) در برابر هر یک به‌نوبت — که دقیقاً همان مدل context-سپس-test است که یک پردازشگر واقعی Schematron استفاده می‌کند، فقط به‌جای یک موتور اجرای آگاه از Schematron، توسط selectNodes از XPath 1.0 در MSXML هدایت می‌شود

موتور چطور XPath 2.0 را بدون خراب‌کردن اجرا رد می‌کند؟

HPDFEInvoiceValidator در برابر نحو پشتیبانی‌نشده‌ی XPath 2.0 در دو لایه دفاع می‌کند، و لایه‌ی اول اصلاً اجازه نمی‌دهد MSXML آن عبارت را ببیند. پیش از ارزیابی هر assert یا report، XPath2Detected رشته‌ی خام عبارت تست را برای شش توکن لفظی اسکن می‌کند — xs:decimal، xs:integer، xs:string، upper-case، lower-case، و exists( — و اگر هرکدام از آن‌ها حاضر باشد، قاعده بلافاصله با شدت stsInfo به‌عنوان Skipped علامت می‌خورد، با این استدلال که MSXML هرگز نباید عبارتی داده شود که از پیش دانسته می‌شود آن را رد می‌کند

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;

لایه‌ی دوم هر چیزی را که فهرست توکن ایستا از قلم می‌اندازد می‌گیرد. هم فراخوانی selectNodes context و هم فراخوانی selectNodes تست به‌ازای هر گره درون یک بلوک try/except اجرا می‌شوند؛ وقتی MSXML روی عبارتی که اسکن توکن اجازه‌ی عبورش را داده raise می‌کند — یک ساخت خارج از آن شش توکن شناخته‌شده، یا یک مسیر context که نمی‌تواند حل کند — استثنا گرفته می‌شود و قاعده به‌جای انتشار به فراخواننده به‌عنوان Skipped ثبت می‌شود. این طراحی دولایه دلیل آن است که یک ساخت XPath 2.0 در هر جایی از مجموعه قاعده هرگز فراتر از HPDFValidateEInvoice raise نمی‌شود: هر یک از ۴۲۴ تأیید آن یا ارزیابی می‌شود، شکست می‌خورد، یا به‌عنوان رد‌شده علامت می‌خورد، و یک تخمین داخلی در برابر آن فایل قاعده، سهم قابل‌اجرا-در-XPath-1.0 را حدود ۳۵۰ از ۴۲۴ قرار داد — به‌اندازه‌ی کافی که ارزیابی جزئی به‌جای فروبازگشت به یک بررسی فقط-کانتینر همان لحظه‌ای که یک تأیید تکی از XPath 2.0 ظاهر شود، ارزش انجام‌دادن دارد

تغذیه‌ی XML فاکتور UTF-8 به MSXML بدون خراب‌کردنش

THPDFMSXMLSchematronEngine.Validate بایت‌های فاکتور استخراج‌شده را به IXMLDOMDocument.loadXML نمی‌سپارد، چون آن متد یک BSTR — UTF-16 — انتظار دارد و یک آرایه‌ی بایت خام UTF-8 را صرف‌نظر از آنچه اعلان خودِ <?xml encoding="UTF-8"?> سند می‌گوید، تحت آن فرض دوباره تفسیر می‌کند. در عوض HPDFEInvoiceValidator بایت‌ها را به یک HGLOBAL ساخته‌شده با GlobalAlloc کپی می‌کند، آن را در یک IStream از طریق CreateStreamOnHGlobal می‌پیچد، و آن جریان را از طریق IPersistStreamInit.Load بار می‌کند، مسیری که MSXML آن را با خواندن اعلان encoding از خودِ جریان بایت به‌جای فرض‌کردن UTF-16 از قبل، رعایت می‌کند. همان متد SelectionNamespaces را از bindingهای پیشوندی که بارگذار از پیش هنگام تجزیه‌ی فایل .sch برداشت کرده بازمی‌سازد، پس قاعده‌ای که در برابر یک پیشوند مثل ram: نوشته شده، در هر ارزیابی، نه فقط زمانی که فایل قاعده اول بار تجزیه شد، در برابر فضای‌نام خودِ XML فاکتور به‌درستی حل می‌شود

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

نگه‌داشتن کامپایل یک واحد از Delphi 7 تا امروز

HPDFEInvoiceValidator.pas باید روی هر نسخه‌ی Delphi که HotPDF پشتیبانی می‌کند کامپایل شود، از جمله نسخه‌هایی بدون هیچ binding XML یا XPathای اصلاً، پس بخش interface آن فقط انواع مقدار ساده را در معرض دید می‌گذارد: رکوردها، آرایه‌های پویا، و یک اینترفیس تکی، IHPDFESchematronEngine، با متدهای Load، Validate، و LastSummary. هر نوع مختص MSXML — IXMLDOMDocument2، ایمپورت Winapi.msxml، خودِ THPDFMSXMLSchematronEngine — درون یک بلوک تکی {$IFDEF XE2+} در بخش پیاده‌سازی می‌نشیند، هم برای فراخوانندگان و هم برای کامپایلر روی زنجیره‌ابزارهای قدیمی‌تر نامرئی

{$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 و قدیمی‌تر، HPDFCreateSchematronEngine در عوض THPDFStubSchematronEngine را پس می‌دهد: Load آن همیشه False را با یک ErrorText که شکاف واقعی را نام می‌برد — binding از نوع DOM در MSXML به XE2 یا جدیدتر نیاز دارد — برمی‌گرداند و در همین حین به یک اعتبارسنج خارجی مثل veraPDF، Mustang، یا یک ابزار مطابقت ZUGFeRD برای پوشش کامل اشاره می‌کند. Validate آن یک نتیجه‌ی مصنوعی تکی با RuleID برابر 'ENGINE' و Skipped تنظیم‌شده برمی‌گرداند، پس کدی که BusinessRules را پیمایش می‌کند نیازی به یک شاخه‌ی جداگانه برای «موتور نتوانست اجرا شود» در مقابل «همه‌ی قواعد اتفاقی رد شدند» ندارد — هر دو از دید فراخواننده یک شکل به‌نظر می‌رسند. HPDFValidateEInvoice این را هم به‌آرامی در حکم خودش تا می‌کند: مقدار بولی‌ای که برمی‌گرداند ContainerValid and ((not BusinessRulesEvaluated) or (BusinessRuleViolations = 0)) است، پس یک موتور در‌دسترس‌نبوده نتیجه را به یک بررسی فقط-کانتینر تنزل می‌دهد به‌جای اینکه یک شکست سخت را روی کامپایلری تحمیل کند که از ابتدا هرگز قرار نبود قواعد Schematron را اجرا کند

موتور XPath 1.0 در HPDFEInvoiceValidator جایگزین یک پردازشگر کامل Schematron/XSLT 2.0 نمی‌شود، و هرگز قرار هم نبود باشد: موتوری که به XPath 1.0 محدود شده همیشه چند تأیید از EN 16931 را ارزیابی‌نشده باقی می‌گذارد، که دقیقاً همان چیزی است که پرچم Skipped روی هر نتیجه برای نمایان‌کردن آن به‌جای پنهان‌کردنش وجود دارد. آنچه این موتور واقعاً می‌خرد، بازخورد قاعده‌ی کسب‌وکاری‌ای است که هر جا HotPDF از پیش اجرا می‌شود اجرا شود، بدون هیچ فرآیند خارجی‌ای برای shell کردن و بدون هیچ زمان‌اجرای XSLT 2.0ای برای گرفتن مجوز. این موتور به‌عنوان بخشی از کامپوننت PDF از HotPDF برای Delphi و C++Builder عرضه می‌شود، در کنار ابزارهای سطح کانتینر Factur-X و PDF/A که روی آن‌ها ساخته شده