Το HotPDF επικυρώνει τους κανόνες επιχειρηματικής λογικής ηλεκτρονικού τιμολογίου EN 16931 μέσω του HPDFEInvoiceValidator, μιας μηχανής βεβαιώσεων Schematron που η ίδια η βιβλιοθήκη υλοποιεί πάνω στην υποστήριξη XPath 1.0 του MSXML αντί για έναν αδειοδοτημένο επεξεργαστή XSLT 2.0. Το HPDFEInvoiceValidator αναλύει τα επίσημα αρχεία κανόνων .sch του Factur-X, αξιολογεί κάθε βεβαίωση που μπορεί να εκφράσει σε XPath 1.0, και σημειώνει τις υπόλοιπες ως παραλειφθείσες αντί να αφήσει μια μη υποστηριζόμενη έκφραση να εγείρει εξαίρεση στη μέση της εκτέλεσης
Το πεδίο εφαρμογής εδώ παραμένει μέσα σε αυτή τη μηχανή: πώς ο φορτωτής μετατρέπει το 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 επειδή η δηλωμένη σύνδεση του αρχείου κανόνων EN 16931 υπερεκτιμά αυτό που πράγματι χρειάζονται οι βεβαιώσεις του: το αρχείο ορίζει queryBinding="xslt2" στην κορυφή, ζητώντας τεχνικά έναν πλήρη επεξεργαστή XSLT 2.0 / XPath 2.0, αλλά διαβάζοντας τις ίδιες τις βεβαιώσεις φαίνεται ότι η μεγάλη πλειονότητα καλεί μόνο συναρτήσεις XPath 1.0 όπως τα string-length και substring-after. Το ενσωματωμένο DOM MSXML των Windows — η μία μηχανή XML που είναι εγγυημένο ότι υπάρχει σε κάθε υποστηριζόμενη εγκατάσταση Delphi χωρίς να προσθέτει εξάρτηση τρίτου μέρους — τυχαίνει να υλοποιεί ακριβώς αυτό το υποσύνολο, το XPath 1.0, κάτι που έκανε πρακτική μια γηγενή μηχανή αντί για την αδειοδότηση ενός ξεχωριστού runtime 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) πριν δημιουργήσει οτιδήποτε, επειδή ένας host κονσόλας ή υπηρεσίας που ποτέ δεν κάλεσε το Application.Initialize δεν έχει ακόμη διαμέρισμα COM, ενώ ένας host GUI VCL το έχει ήδη· η μηχανή αντιμετωπίζει το αποτέλεσμα S_FALSE ή RPC_E_CHANGED_MODE που μπορεί να επιστρέψει εκείνη η κλήση σε ένα νήμα που βρίσκεται ήδη μέσα σε ένα διαμέρισμα ως εξίσου εντάξει αντί για σφάλμα. Έπειτα αναλύει το αρχείο Schematron με ένα έγγραφο DOM MSXML 6.0 (CoDOMDocument60) και setProperty('SelectionLanguage', 'XPath'), αφού το MSXML προεπιλέγει εξ ορισμού στην παλαιότερη διάλεκτό του XSL-Pattern εκτός αν ένας καλών επιλέξει ρητά XPath. Από εκεί όμως, ο φορτωτής ποτέ δεν καλεί το selectNodes για να διατρέξει τη δική του δομή του αρχείου .sch — κάθε στοιχείο <pattern>, <rule>, <assert>, και <report> βρίσκεται διατρέχοντας τα firstChild / nextSibling με το χέρι, συγκρίνοντας το τοπικό όνομα και το URI χώρου ονομάτων κάθε κόμβου με τη literal συμβολοσειρά http://purl.oclc.org/dsdl/schematron
Αυτή η προσέγγιση χειροκίνητης διάτρεξης υπάρχει λόγω ενός προβλήματος κότας-και-αυγού στις δεσμεύσεις <ns prefix="ram" uri="..."/> που δηλώνει εκ των προτέρων κάθε αρχείο Schematron του Factur-X. Η επίλυση μιας έκφρασης XPath με πρόθεμα όπως ram:Name έναντι αυτών των δεσμεύσεων απαιτεί η ιδιότητα SelectionNamespaces του MSXML να τις κρατά ήδη, αλλά η ανακάλυψη των δεσμεύσεων εξαρχής θα σήμαινε κανονικά την εκτέλεση ενός ερωτήματος 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 πρέπει να ισχύει για κάθε κόμβο που ταιριάζει με τη διαδρομή πλαισίου του κανόνα, οπότε το EvaluateAssert καταγράφει μια παράβαση όταν το σύνολο κόμβων αποτελέσματος της έκφρασης ελέγχου επιστρέφει άδειο· ένα <report test="X"> είναι η κατοπτρική εικόνα, σημειώνοντας ένα πρόβλημα όταν το X είναι αληθές, οπότε το EvaluateReport καταγράφει μια παράβαση όταν το αποτέλεσμα ελέγχου είναι μη κενό αντ' αυτού. Και τα δύο σημεία εισόδου μοιράζονται από κάτω το ίδιο σχήμα δύο σταδίων — πρώτα Doc.selectNodes(Entry.Context), για να βρεθεί κάθε κόμβος στον οποίο εφαρμόζεται ο κανόνας, έπειτα ContextNode.selectNodes(Entry.Test) έναντι καθενός με τη σειρά — που είναι ακριβώς το μοντέλο πλαίσιο-έπειτα-έλεγχος που χρησιμοποιεί ένας πραγματικός επεξεργαστής Schematron, απλώς οδηγούμενο από το selectNodes XPath 1.0 του MSXML αντί από μια μηχανή εκτέλεσης ενήμερη για Schematron
Πώς παραλείπει η μηχανή το XPath 2.0 χωρίς να καταρρεύσει η εκτέλεση;
Το HPDFEInvoiceValidator αμύνεται έναντι μη υποστηριζόμενης σύνταξης XPath 2.0 σε δύο επίπεδα, και το πρώτο ποτέ δεν αφήνει το MSXML να δει καθόλου την έκφραση. Πριν αξιολογήσει οποιοδήποτε assert ή report, το XPath2Detected σαρώνει την ακατέργαστη συμβολοσειρά έκφρασης ελέγχου για έξι literal tokens — xs:decimal, xs:integer, xs:string, upper-case, lower-case, και exists( — και αν κάποιο από αυτά είναι παρόν ο κανόνας σημειώνεται αμέσως ως Skipped με σοβαρότητα stsInfo, με το σκεπτικό ότι το 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;
Το δεύτερο επίπεδο πιάνει ό,τι διαφεύγει από τη στατική λίστα tokens. Τόσο η κλήση selectNodes πλαισίου όσο και η κλήση selectNodes ελέγχου ανά κόμβο εκτελούνται μέσα σε ένα μπλοκ try/except· όταν το MSXML εγείρει εξαίρεση σε μια έκφραση που άφησε να περάσει η σάρωση tokens — μια κατασκευή έξω από τα έξι γνωστά tokens, ή μια διαδρομή πλαισίου που δεν μπορεί να επιλύσει — η εξαίρεση συλλαμβάνεται και ο κανόνας καταγράφεται ως Skipped αντί να διαδοθεί στον καλούντα. Αυτός ο σχεδιασμός δύο επιπέδων είναι ο λόγος που μια κατασκευή XPath 2.0 οπουδήποτε στο σύνολο κανόνων ποτέ δεν εγείρει πέρα από το HPDFValidateEInvoice: κάθε μία από τις 424 βεβαιώσεις του είτε αξιολογείται, είτε αποτυγχάνει, είτε σημειώνεται ως παραλειφθείσα, και μια εσωτερική εκτίμηση έναντι εκείνου του αρχείου κανόνων έθεσε το εκτελέσιμο σε XPath-1.0 μερίδιο σε περίπου 350 από τις 424 — αρκετό ώστε η μερική αξιολόγηση να αξίζει τον κόπο αντί να επιστρέφει σε έναν έλεγχο μόνο δοχείου τη στιγμή που εμφανίζεται μία μόνο βεβαίωση XPath 2.0
Παράδοση UTF-8 XML τιμολογίου στο MSXML χωρίς αλλοίωση
Το THPDFMSXMLSchematronEngine.Validate δεν παραδίδει τα εξαγόμενα bytes τιμολογίου στο IXMLDOMDocument.loadXML, επειδή αυτή η μέθοδος αναμένει BSTR — UTF-16 — και θα επανερμήνευε έναν ακατέργαστο πίνακα byte UTF-8 κάτω από αυτή την υπόθεση ανεξάρτητα από το τι λέει η δική του δήλωση <?xml encoding="UTF-8"?> του εγγράφου. Το HPDFEInvoiceValidator αντ' αυτού αντιγράφει τα bytes σε ένα HGLOBAL δημιουργημένο με GlobalAlloc, το τυλίγει σε ένα IStream μέσω CreateStreamOnHGlobal, και φορτώνει αυτή τη ροή μέσω IPersistStreamInit.Load, μια διαδρομή που το MSXML τιμά διαβάζοντας τη δήλωση κωδικοποίησης από την ίδια τη ροή byte αντί να υποθέτει UTF-16 εκ των προτέρων. Η ίδια μέθοδος ανακατασκευάζει το SelectionNamespaces από τις δεσμεύσεις προθέματος που ο φορτωτής έχει ήδη συλλέξει κατά την ανάλυση του αρχείου .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, συμπεριλαμβανομένων εκδόσεων χωρίς καμία σύνδεση XML ή XPath καθόλου, οπότε το τμήμα διεπαφής του εκθέτει μόνο απλούς τύπους τιμών: εγγραφές, δυναμικούς πίνακες, και μία μοναδική διεπαφή, IHPDFESchematronEngine, με μεθόδους Load, Validate, και LastSummary. Κάθε τύπος ειδικός για MSXML — IXMLDOMDocument2, η εισαγωγή Winapi.msxml, το ίδιο το THPDFMSXMLSchematronEngine — βρίσκεται μέσα σε ένα μοναδικό μπλοκ {$IFDEF XE2+} στο τμήμα υλοποίησης, αόρατο τόσο στους καλούντες όσο και στον compiler σε παλαιότερες αλυσίδες εργαλείων
{$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 που ονομάζει το πραγματικό κενό — η σύνδεση DOM MSXML απαιτεί XE2 ή νεότερη — και δείχνει προς έναν εξωτερικό επικυρωτή όπως το veraPDF, το Mustang, ή ένα εργαλείο συμμόρφωσης ZUGFeRD για πλήρη κάλυψη εν τω μεταξύ. Το δικό του Validate επιστρέφει ένα μοναδικό συνθετικό αποτέλεσμα με RuleID 'ENGINE' και ορισμένο Skipped, οπότε κώδικας που επαναλαμβάνει το BusinessRules δεν χρειάζεται ξεχωριστό κλάδο για "η μηχανή δεν μπόρεσε να εκτελεστεί" έναντι "κάθε κανόνας έτυχε να παραλειφθεί" — και τα δύο φαίνονται με το ίδιο σχήμα στον καλούντα. Το HPDFValidateEInvoice το ενσωματώνει επίσης ομαλά στην ετυμηγορία του: το boolean που επιστρέφει είναι ContainerValid and ((not BusinessRulesEvaluated) or (BusinessRuleViolations = 0)), οπότε μια μη διαθέσιμη μηχανή υποβαθμίζει το αποτέλεσμα σε έναν έλεγχο μόνο δοχείου αντί να επιβάλλει σκληρή αποτυχία σε έναν compiler που ούτως ή άλλως ποτέ δεν επρόκειτο να εκτελέσει κανόνες Schematron εξαρχής
Η μηχανή XPath 1.0 του HPDFEInvoiceValidator δεν αντικαθιστά έναν πλήρη επεξεργαστή Schematron/XSLT 2.0, και ποτέ δεν προοριζόταν να το κάνει: μια μηχανή περιορισμένη σε XPath 1.0 πάντα θα αφήνει μια χούφτα βεβαιώσεων EN 16931 μη αξιολογημένες, κάτι που είναι ακριβώς αυτό που η σημαία Skipped σε κάθε αποτέλεσμα υπάρχει για να κάνει ορατό αντί να κρύψει. Αυτό που κερδίζει η μηχανή είναι ανατροφοδότηση επιχειρηματικών κανόνων που εκτελείται οπουδήποτε ήδη εκτελείται το HotPDF, χωρίς εξωτερική διεργασία προς κλήση και χωρίς runtime XSLT 2.0 προς αδειοδότηση. Αυτή η μηχανή διατίθεται ως μέρος του εξαρτήματος HotPDF PDF για Delphi και C++Builder, μαζί με τα εργαλεία Factur-X και PDF/A σε επίπεδο δοχείου πάνω στα οποία χτίζεται