Archiv für Kategorie XSLT und XPath
Ein universelles XSL-Stylesheet zum Aufteilen von XML-Dokumenten
Verfasst von Stf unter Beispiele, XML, XSLT und XPath am 1. September 2013
Manchmal müssen große XML-Dokumente in kleinere Portionen zerlegt werden. In der Beispielsammlung gibt es dafür ein generisches Stylesheet (xml-aufteilen.xsl). Es teilt XML-Dateien auf der zweiten Ebene (unterhalb des Wurzelementes) in gleich große Gruppen mit bspw. 20 Elementen, umschließt diese Gruppen jeweils mit dem rekonstruierten Wurzelelement und ggfs. führenden Kommentaren und Processing Instructions und gibt die Gruppen als Einzeldateien aus.
Die Implementierung ist pures XSLT 2.0 auf der Grundlage von xsl:for-each-group group-adjacent="…"
und xsl:result-document
ohne Besonderheiten, deshalb gehe ich hier nicht näher auf den Code ein. Mit dem Stylesheet wird die zu teilende Eingabedatei transformiert, es entstehen eine Protokolldatei sowie die geteilten Dateien. Zahlreiche Parameter erlauben die Steuerung:
Auswahl der auszugebenden Elemente
- elemente-pro-datei
(xs:integer)
Anzahl der Elemente pro Ausgabedatei. Dieser Wert bestimmt die Größe der Gruppen und damit auch, welche Elemente als Gruppenanfang in Frage kommen. Ist bspw.$elemente-pro-datei
gleich20
, dann sind mögliche Gruppenanfänge die Elemente mit der Position1
,21
,41
usw. Damit wird sichergestellt, dass die Ausgabedateien (mit Ausnahme der letzten) immer vollständige Gruppen enthalten.- Mit einem Wert von
1
wird je Element eine Datei ausgegeben. - erstes-element
(xs:integer)
Position des ersten zu kopierenden Elementes. Ohne Angabe eines Wertes wird das erste Element der zweiten Ebene gewählt. Fällt die angegebene Position nicht auf den Anfang einer Gruppe, der sich aus dem Wert für$elemente-pro-datei
ergibt, wird das nächste vorhergehende Element, das ein Gruppenanfang ist, gewählt.- letztes-element
(xs:integer)
Position des letzten zu kopierenden Elementes. Ohne Angabe eines Wertes wird das letzte Element der zweiten Ebene gewählt. Fällt die angegebene Position nicht auf das Ende einer Gruppe, der sich aus dem Wert für$elemente-pro-datei
ergibt, wird das nächst folgende Element, das ein Gruppenende ist, gewählt.- ausgabeordner
(xs:string)
Ordner für die Ausgabe der geteilten Dateien als URI, ohne schließenden Schrägstrich. Ohne Angabe eines Ausgabeordners werden die Dateien in den Ordner des Ausgangsdokumentes in einen Unterordner »geteilt
« geschrieben.- ausgabedatei-präfix
(xs:string)
Präfix für die Ausgabedateien. Ohne Angabe eines Wertes wird der Name der Ausgangsdatei (ohne Dateierweiterung) mit einem nachgestellten Unterstrich verwendet.- ausgabedatei-erweiterung
(xs:string)
Dateityp-Erweiterung für die Ausgabedateien. Ohne Angabe eines Wertes wird die Dateierweiterung der Ausgangsdatei verwendet.- picture-string
(xs:string)
Formatierung der Zählstellen, als Picture-String (siehe https://wiki.selfhtml.org/wiki/XML/XSL/XPath/Funktionen#format-number.28.29_.28Zahl_in_Zeichenkette_umwandeln.29). Wird kein Wert übergeben, werden führende Nullen entsprechend der Anzahl der Elemente im Ausgangsdokument ausgegeben.- wiederhole-initiale-nodes
(xs:boolean)
Steht dieser Parameter auftrue
, werden nodes() vor dem ersten Element (Kommentare und/oder Processing-Instructions) in jede Ausgabe-Datei an den Anfang kopiert. Beifalse
werden entsprechende Knoten nur in die erste Datei kopiert.- Vor dem Wurzelelement stehen oft Lizenzangaben in Kommentaren oder Processing Instructions wie bspw. das xml-model (siehe die W3C Working Group Note Associating Schemas with XML documents). Es kann wünschenswert sein, diese Informationen zu wiederholen.
- schuetze-vorhandene-dateien
(xs:boolean)
Verhindert, dass vorhandene Dateien überschrieben werden.- Um die Existenz einer Datei zu prüfen, muss die Datei geparst werden. Bei vielen und/oder großen bereits vorhandenen Ausgabedokumenten kann das sehr zeitaufwändig sein. In diesen Fällen empfiehlt es sich, erhaltenswerte Dokumente zu sichern und den Parameter auf
false
zu setzen.
Pfad und Name der Ausgabe-Dateien
Der vollständige Pfad zu den Ausgabedateien setzt sich aus einem für alle Dateien gleichem Pfad zum Ausgabeordner, einem Dateinamen-Präfix, den Zählstellen sowie der Dateityp-Erweiterung zusammen. Aus einer Eingabedatei mit Namen »daten.xml
« entstehen bspw. »daten_001.xml
«, »daten_002.xml
« usw. (bei $elemente-pro-datei = 1
) bzw. »daten_001-020.xml
«, »daten_021-040.xml
« usw. (bei $elemente-pro-datei = 20
).
Extras
Um das Stylesheet einfach an eigene Anforderungen anpassen zu können, wurden wichtige Subroutinen als Funktionen und benannte Templates angelegt. Diese können leicht überschrieben werden, um z.B. die auszugebenden Nodes zu filtern (Funktion local:nodes-auswaehlen()
oder um ein eigenes Benennungsschema (Funktion local:dateiname()
) zu realisieren.
- Das Stylesheet in der Beispielsammlung: http://www.expedimentum.org/example/xslt/xml_aufteilen.xsl
XSLT-SB 0.2.50: Bessere Dokumentation und neue Funktionen
Verfasst von Stf unter XSLT und XPath, XSLT-SB am 28. Mai 2012
Die neuen Funktionen – xsb:sort()
, xsb:replace()
, xsb:integer-to-hex()
, xsb:hex-to-integer()
, xsb:twos-complement()
, xsb:reverse-twos-complement()
, xsb:fill-left()
, xsb:fill-right()
, xsb:escape-for-regex()
, xsb:escape-for-replacement()
, xsb:count-matches()
, xsb:index-of-first-match()
und xsb:decode-from-url()
haben sich im Laufe einiger Projekte bei mir angesammelt und sind nun in die XSLT-SB gewandert; einige möchte ich im Folgenden näher beschreiben.
xsb:sort()
Diese Funktion sortiert atomic values, also Zahlen, Strings usw. Das Fehlen dieser Funktion in den XPath-Funktionen ist ein Rätsel, deshalb liefert der XSLT-Standard wohl das Gerüst der Implementierung – als Wrapper für xsl:perform-sort
– gleich mit. Ich habe das Beispiel um die Möglichkeit ergänzt, die Reihenfolge (aufsteigend/absteigend, englisch ascending
/descending
) in Funktionsaufruf zu übergeben. Das sieht dann so aus:
<xsl:function name="xsb:sort" as="xs:anyAtomicType*" intern:solved="EmptySequenceAllowed"> <xsl:param name="input-sequence" as="xs:anyAtomicType*"/> <xsl:param name="order" as="xs:string"/> <xsl:perform-sort select="$input-sequence"> <xsl:sort select="." order="{$order}"/> </xsl:perform-sort> </xsl:function> |
Sortiert werden können nur Werte, die mit dem lt
-Operator verglichen werden können, also nur Sequenzen aus Strings, Zahlen, Daten; nicht aber gemischte Sequenzen aus diesen Typen. Gemischte Sequenzen können aber auf string gecastet werden, etwa mit for $i in $sequence return string($i)
.
Zur bequemeren Benutzung gibt es auch eine Version mit nur einem Argument, dann ist die Reihenfolge auf aufsteigend resp. ascending
festgelegt.
xsb:index-of-first-match()
Diese Funktion ermittelt die Position des ersten Auftretens eines RegEx-Patterns in einem String. Mit fn:tokenize()
wird der erste Teilstring vor dem Pattern ermittelt und zu dessen Länge 1
addiert:
<xsl:function name="xsb:index-of-first-match" as="xs:integer"> <xsl:param name="input" as="xs:string?"/> <xsl:param name="pattern" as="xs:string?"/> <xsl:param name="flags" as="xs:string?"/> <xsl:choose> <xsl:when test="normalize-space($pattern) and matches($input, $pattern, $flags)"> <xsl:sequence select="string-length(tokenize($input, $pattern, $flags)[1]) + 1"/> </xsl:when> <xsl:otherwise>0</xsl:otherwise> </xsl:choose> </xsl:function> |
Auch hier gibt es wieder die »bequeme« Version ohne flags
.
xsb:replace()
xsb:replace()
erweitert fn:replace()
um die Möglichkeit, Listen (genauer Sequenzen) als Argumente für Such-Pattern und Ersetzungszeichenfolgen zu übergeben. Das ist praktisch, wenn in einem String paarweise verschiedene Fundstellen durch korrespondierende Texte ersetzt werden sollen, also z.B. alle »Jan.« durch »Januar«, »Feb.« durch »Februar«, »Apr.« durch »April« usw. Ohne benutzerdefinierte Funktionen müssen mehrere fn:replace()
geschachtelt werden, es entstehen dann Ungetüme wie:
replace( replace( replace($input, 'Apr\.', 'April'), Feb\.', 'Februar'), 'Jan\.', 'Januar') |
Viel übersichtlicher ist es dann doch so:
xsb:replace($input, ('Jan\.', 'Feb.\', 'Apr\.'), ('Januar', 'Februar', 'April') ) |
Neben der Übersichtlichkeit ist ein zweiter wesentlicher Vorteil, dass mit xsb:replace()
die Länge der Sequenzen resp. die Anzahl der Ersetzungspaare nicht schon beim Schreiben des Stylesheets bekannt sein müssen. Dazu folgt weiter unter ein Beispiel. Aber erst noch ein paar Worte zur Implementierung, die so aussieht:
<xsl:function name="xsb:replace" as="xs:string"> <xsl:param name="input" as="xs:string?"/> <xsl:param name="pattern" as="xs:string*"/> <xsl:param name="replacement" as="xs:string*"/> <xsl:param name="flags" as="xs:string?"/> <xsl:choose> <xsl:when test="exists($pattern[1])"> <xsl:sequence select=" xsb:replace( if (boolean($pattern[1]) ) then replace($input, $pattern[1], string($replacement[1]), $flags) else $input, $pattern[position() gt 1], $replacement[position() gt 1], $flags )"/> </xsl:when> <xsl:otherwise> <xsl:sequence select="concat('', $input)"/> </xsl:otherwise> </xsl:choose> </xsl:function> |
Es handelt sich um eine rekursive Funktionsdefinition. Die Implementierung ist nicht ganz geradlinig, weil Leerstrings und Leersquenzen sinnvoll behandelt werden müssen. Der Reihe nach:
exists($pattern[1])
testet, ob ein weiterespattern
-Argument vorhanden ist. Wichtig ist, dassfn:exists()
(im Unterschied zufn:boolean()
) den Leerstring zutrue()
evaluiert. Ist kein weiterespattern
-Argument vorhanden, sind die Ersetzungen abgeschlossen, und das Ergebnis der Funktion wird imxsl:otherwise
-Zweig zurückgegeben. (Das Ergänzen eines Leerstrings dort stellt sicher, dass keine Leersequenz, sondern mindestens ein Leerstring ausgegeben wird.)- Wenn ein weiteres
pattern
-Argument (das auch ein Leerstring sein kann) vorhanden ist, wirdxsb:replace()
rekursiv aufgerufen- Für das zu übergebende
input
-Argument wird mitboolean($pattern[1])
geprüft, ob das aktuelle Such-Pattern Zeichen enthält, und nur in diesem Fall wird einfn:replace()
mit den aktuellenpattern
undreplacement
aufgerufen. Im Fall eines Leerstrings wirdinput
unverändert weitergereicht. Diese Akrobatik ist notwendig, weil der Leerstring kein gültiger Suchstring infn:replace()
ist, ich mir aber an dieser Stelle etwas Fehlertoleranz gewünscht habe. - Die in den
pattern
– undreplacement
-Sequenzen auf den jeweils ersten Wert folgenden Werte werden als neuepattern
undreplacement
übergeben. Wenn keine weiteren Argumente vorhanden sind, wird halt eine Leersequenz weitergereicht, was im Fall vonpattern
im nächsten Durchlauf zum Abbruch der Rekursion und zur Ausgabe der Ergebnisses führt. flags
wird unverändert durchgereicht.
- Für das zu übergebende
Leerstrings in der pattern
-Sequenz werden als »nichts suchen« interpretiert und samt dem zugehörigen replacement
übersprungen.
Sind mehr pattern
-Werte als replacement
-Werte vorhanden, werden die Fundstellen der »überzähligen« pattern
-Werte gelöscht: xsb:replace('Affe Bär Elefant', ('Affe', 'Elefant') , ('monkey') )
ergibt »monkey Bär
«. Dieses Verhalten entspricht dem des guten alten fn:translate()
.
xsb:replace
kann man wunderbar zum Suchen-und-Ersetzen an Hand einer Ersetzungstabelle verwenden. Im folgenden Beispiel wird die Ersetzungstabelle als externes Dokument verwaltet, dass ggfs. unabhängig vom Stylesheet bearbeitet werden kann.
Ersetzungstabelle (search-and-replace_list.xml
):
<root> <pair> <pattern>Affe</pattern> <replacement>monkey</replacement> </pair> <pair> <pattern>Wolf</pattern> <replacement>wolf</replacement> </pair> </root> |
Stylesheet:
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsb="http://www.expedimentum.org/XSLT/SB" exclude-result-prefixes="xs xsb" version="2.0"> <xsl:import href="strings.xsl"/> <xsl:param name="search-and-replace-list">search-and-replace_list.xml</xsl:param> <xsl:template match="p"> <xsl:copy> <xsl:value-of select="xsb:replace( ., doc($search-and-replace-list)//pair/string(pattern), doc($search-and-replace-list)//pair/string(replacement) )"/> </xsl:copy> </xsl:template> <xsl:template match="@*|node()"> <xsl:copy> <xsl:apply-templates select="@*, node()"/> </xsl:copy> </xsl:template> </xsl:stylesheet> |
Beispieldokument:
<root> <p>Affe</p> <p>Wolf</p> <p>Zebra</p> </root> |
Transformationsergebnis:
<root> <p>monkey</p> <p>wolf</p> <p>Zebra</p> </root> |
Achtung! Leersequenzen innerhalb der pattern
-Sequenz (bzw. fehlende pattern
-Elemente in der Ersetzungstabelle) führen dazu, dass die folgenden Werte innerhalb der pattern
-Sequenz nach links rücken: xsb:replace('Affe Wolf Zebra', ('Affe', (), 'Zebra'), ('monkey', 'wolf', 'zebra') )
ergibt – ebenso wie xsb:replace('Affe Wolf Zebra', ('Affe', 'Zebra'), ('monkey', 'wolf', 'zebra') )
– »monkey Wolf wolf
«. Das gilt analog auch für die replacement
-Sequenz bzw. replacement
-Elemente der Ersetzungstabelle. Diesem Problem kann man mit einem rigiden Schema samt Validierung oder durch geschickte Konstruktion der pattern
– und replacement
-Sequenzen begegnen: im obigen Stylesheet wurde nicht das naheliegende doc($search-and-replace-list)//pattern
verwendet, sondern statt dessen doc($search-and-replace-list)//pair/string(pattern)
, womit bei fehlendem pattern
-Element mit fn:string()
ein Leerstring erzeugt wird.
Exkurs: Im Gegensatz zu fn:string()
evaluiert xs:string()
die Leersequenz zu einer Leersequenz. Ich bin mir sicher, dass es für diesen Unterschied eine sachliche Begründung gibt, aber solche Inkonsistenzen machen das Programmieren nicht nur für Einsteiger unnötig kompliziert.
Durch die Verwendung von fn:replace()
werden die Suchstrings als reguläre Ausdrücke interpretiert. Entsprechend wird im Beispiel oben der Punkt mit dem Backslash escapet. Wenn das händische Escapen nicht sinnvoll oder möglich ist, hilft die Funktion xsb:escape-for-regex()
. Analog dazu gibt es auch im Ersetzungstext Steuerzeichen, die mit xsb:escape-for-replacement()
escapet werden können.
Auch von xsb:replace()
gibt es die »bequeme« Version ohne flags
.
xsb:escape-for-regex()
und xsb:escape-for-replacement()
Die Funktion xsb:escape-for-regex()
escapet in Strings Steuerzeichen für reguläre Ausdrücke mit einem Backslash (\
). Damit können Strings, die Steuerzeichen enthalten, als Suchmuster in regulären Ausdrücken verwendet werden. Im Beispiel oben musste beispielsweise der Punkt (steht in regulären Ausdrücken für ein beliebiges Zeichen) in Jan.
escapet werden (Jan\.
), damit nicht auch »Jana« ersetzt wird. Die Implementierung ist sehr simpel:
<xsl:function name="xsb:escape-for-regex" as="xs:string"> <xsl:param name="input" as="xs:string?"/> <xsl:sequence select="concat('', replace($input, '[\\*.+?\^\$()\[\]{}|]', '\\$0') )"/> </xsl:function> |
Im pattern
von fn:replace()
werden die »verbotenen« Zeichen gesucht. Bemerkenswert ist vielleicht das replacement
: der doppelte Backslash ist ein escapeter einfacher Backslash, und $0
steht für den gesamten gematchten Teilstring. Es wird also ein Backslash ausgegeben und anschließend die Fundstelle wiederholt.
Da es also auch im Ersetzungstext von fn:replace()
Steuerzeichen – »\
« und »$
« – geben kann, macht eine Funktione zu Escapen eben dieses Ersetzungstextes das Leben einfacher: xsb:escape-for-replacement()
.
Ein Beispiel für die Nutzung der beiden Funktionen ergibt sich im Zusammenhang mit der oben beschriebenen Ersetzungstabelle. Wenn man davon ausgehen kann, dass in search-and-replace_list.xml
keine regulären Ausdrücke notiert werden, kann das Stylesheet zur Absicherung modifiziert werden:
<xsl:template match="p"> <xsl:copy> <xsl:value-of select="xsb:replace( ., doc($search-and-replace-list)//pair/xsb:escape-for-regex(string(pattern)), doc($search-and-replace-list)//pair/xsb:escape-for-replacement(string(replacement)) )"/> </xsl:copy> </xsl:template> |
Quelldokument, Ersetzungstabelle und Stylesheet habe ich wie üblich in der Beispielsammlung abgelegt.
Weblinks:
- Einen guten praktischen Überblick bietet der Abschnitt Regulärer Ausdrücke in der Praxis im Wikipedia-Artikel »Regulärer Ausdruck«
- Kapitel 7.6 String Functions that Use Pattern Matching in XQuery 1.0 and XPath 2.0 Functions and Operators (Second Edition) definiert reguläre Ausdrücke und die Funktionen
fn:match()
,fn:tokenize()
undfn:replace()
(W3C-Standard, englisch)
Benutzerdefinierte Funktionen und externe Funktionsbibliotheken in Schematron
Verfasst von Stf unter Beispiele, Schematron, XML-Validierung, XSLT und XPath am 1. Mai 2012
In manchen Situationen reicht der Umfang von XPath oder auch von XPath 2.0 nicht aus, um die gewünschten Tests zu formulieren, etwa wenn rekursive Funktionsaufrufe nötig sind. In anderen Situationen möchte man Algorithmen in verschiedenen Tests wiederverwenden. In solchen Situationen helfen benutzerdefinierte Funktionen weiter. Mit XSLT 2.0 geht das recht einfach:
<schema xmlns="http://purl.oclc.org/dsdl/schematron" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" queryBinding="xslt2" > <ns prefix="my" uri="test"/> <xsl:function name="my:literal-autor" as="xs:string?"> <xsl:param name="vorname" as="xs:string?"/> <xsl:param name="nachname" as="xs:string?"/> <xsl:sequence select="concat($nachname, ', ', $vorname)"/> </xsl:function> <pattern id="p3"> <rule context="autor"> <assert test="my:literal-autor(//person[@xml:id eq current()/@ref]/vorname, //person[@xml:id eq current()/@ref]/nachname) eq .">[p3] autor muss eine gültige Kombination aus person/vorname und person/nachname sein.</assert> </rule> </pattern> </schema> |
Im äußersten schema
-Element wird der XSL-Namespace definiert und mit queryBinding="xslt2"
XSLT 2.0 als Abfragesprache festgelegt. Mit dem ns
-Element wird analog zu XSLT 2.0 der Namespace für die benutzerdefinierte Funktionen deklariert.
Anschließend folgt die Funktionsdefinition 1:1 wie in XSLT 2.0. Das Beispiel fügt Nachname und Vorname – getrennt durch ein Komma – zusammen. xsl:function
-Elemente können an beliebiger Position direkt unterhalb von schema
stehen.
Schließlich wird im assert
die so definierte Funktion verwendet. Das Beispiel testet, ob der Inhalt des autor
-Elements mit dem referenzierten person
-Element korrespondiert.
Das dazugehörige XML könnte so aussehen:
<literatur> <buecher> <buch xml:id="b1"> <autor ref="p1">Mann, Thomas</autor> <titel>Der Zauberberg</titel> <isbn>978-3-596-29433-6</isbn> <href>http://d-nb.info/942764498</href> </buch> <buch xml:id="b2"> <autor ref="p2">Mann,Klaus</autor> <titel>Mephisto</titel> <isbn>3-10-046705-1</isbn> <href>http://d nb.info/959653694</href> </buch> <buch xml:id="b3"> <autor ref="b1"></autor> <titel></titel> </buch> </buecher> <autoren> <person xml:id="p1"> <vorname>Thomas</vorname> <nachname>Mann</nachname> </person> <person xml:id="p2"> <vorname>Klaus</vorname> <nachname>Mann</nachname> </person> </autoren> </literatur> |
Bei buch xml:id="b2"
wird ein Fehler gemeldet, weil das Leerzeichen nach dem Komma fehlt, bei buch xml:id="b3"
wegen des fehlenden Inhaltes. Schema und XML habe ich in der Beispielsammlung abgelegt.
In OxygenXML muss die Verarbeitung von XSLT innerhalb von Schematron ggfs. erst aktiviert werden. Dazu muss in den Einstellungen unter XML ⇒ XML-Parser
ein Häkchen bei ISO Schematron ⇒ Fremde Elemente erlauben (allow-foreign)
gesetzt werden, vgl. Bild rechts. [Edit: Ein Hinweis darauf, dass das Häkchen fehlt, ist die Fehlermeldung »unrecognized element … from namespace http://www.w3.org/1999/XSL/Transform
«, wobei an Stelle der drei Pünktchen der Name eines XSL-Elements steht, bspw. xsl:function
oder xsl:include
]
externe Funktionsbibliotheken
Oft liegen die benötigten Funktionen bereits in einer Bibliothek vor. Beispielsweise lassen sich URLs mit misc:is-url()
aus der XSLT-SB auf Gültigkeit testen. Auch das Einbinden externen Bibliotheken geht mit Schematron recht einfach:
<schema xmlns="http://purl.oclc.org/dsdl/schematron" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" queryBinding="xslt2" > <xsl:include href="http://www.expedimentum.org/example/xslt/xslt-sb/files.xsl"/> <ns prefix="xsb" uri="http://www.expedimentum.org/XSLT/SB"/> <pattern id="p4"> <rule context="href"> <assert test="xsb:is-url(.)">[p4] href muss eine gültige URL beinhalten</assert> </rule> </pattern> </schema> |
Mit diesem Schema wird das href
-Element bei buch xml:id="b2"
bemängelt, da ein Leerzeichen kein gültiges Zeichen in einer URL ist.
Übrigens hatte ich mit <xsl:import/>
keinen Erfolg; mir fällt aber kein Beispiel ein, wo man nicht statt dessen per <xsl:include/>
ein (ggfs. angepasstes) externes Stylesheet verwenden könnte. Über Beispiele und/oder Hinweise zur Lösung würde ich mich freuen.
Auch dieses Schema habe ich in der Beispielsammlung abgelegt.
xsl:key in Schematron verwenden
Verfasst von Stf unter Schematron, XML-Validierung, XSLT und XPath am 1. Mai 2012
Schlüssel (xsl:key
) sind eine Möglichkeit, um Transformationen über größere Dokumente mit vielen (internen oder externen) Verweisen zu beschleunigen. Im Sprachumfang von Schematron sind Daten-Schlüssel nicht enthalten, allerdings ist es sehr einfach, xsl:key
zu verwenden. Ein einfaches Beispiel: in einer Literaturliste verweisen die autor
-Elemente über das ref
-Attribut auf ein korrespondierendes person
-Element aus einer Liste:
<literatur> <buecher> <buch xml:id="b1"> <autor ref="p1">Mann, Thomas</autor> <titel>Der Zauberberg</titel> <isbn>978-3-596-29433-6</isbn> <href>http://d-nb.info/942764498</href> </buch> <buch xml:id="b2"> <autor ref="p2">Mann,Klaus</autor> <titel>Mephisto</titel> <isbn>3-10-046705-1</isbn> <href>http://d nb.info/959653694</href> </buch> <buch xml:id="b3"> <autor ref="b1"></autor> <titel></titel> </buch> </buecher> <autoren> <person xml:id="p1"> <vorname>Thomas</vorname> <nachname>Mann</nachname> </person> <person xml:id="p2"> <vorname>Klaus</vorname> <nachname>Mann</nachname> </person> </autoren> </literatur> |
Natürlich lässt sich die Referenz problemlos über XPath-Lokalisierungsschritte wie //person[@xml:id eq current()/@ref]
prüfen, aber eleganter und vermutlich schneller geht das mit Schlüsseln. Um xsl:key
in Schematron zu verwenden, muss das queryBinding
im äußersten schema
-Element auf xslt
(das ist ohnehin der Standardwert) oder xslt2
gesetzt und der XSL-Namespace deklariert werden, danach können xsl:key
-Elemente und die key()
-Funktion wie in XSLT verwendet werden:
<schema xmlns="http://purl.oclc.org/dsdl/schematron" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" queryBinding="xslt" > <xsl:key name="myKey" match="person" use="@xml:id"/> <pattern id="p3"> <rule context="autor"> <assert test="key('myKey', @ref)">[p3] autor/@ref muss auf ein person/@xml:id verweisen</assert> </rule> </pattern> </schema> |
Das assert
überprüft, ob jedes autor
-Element ein ref
-Attribut hat, und ob dieses ref
-Attribut auf ein person
-Element in der Autorenliste verweist. Entsprechend wird im obigen XML bei buch xml:id="b3"
die Referenz auf b1
als Fehler gemeldet.
xsl:key
-Elemente sollen im Schema vor dem ersten pattern
-Element stehen. Leider ist der Schematron-Standard diesbezüglich (und wie so oft) nicht sehr präzise; bei einem kurzen Test mit OxygenXML hat aber auch ein xsl:key
nach dem pattern
funktioniert.
XML-Beispiel und Schematron habe ich in der Beispielsammlung abgelegt.
Weblink
- Dave Pawson, Roger Costello, Florent Georges: ISO Schematron tutorial. Using keys for assertions (englisch)
xsb:random(): Zufallszahlen mit XSLT
Verfasst von Stf unter Grundlagen, XSLT-SB am 24. März 2012
»Der Laie staunt, der Fachmann wundert sich.« Zufallszahlen in XSLT sind soetwas wie die Quadratur des Kreises. Warum? Eine der absoluten Grundlagen funktionaler Programmiersprachen, zu denen auch XPath und XSLT gehören, ist der Verzicht auf Seiteneffekte, d.h. das Ergebnis einer Operation oder Funktion hängt ausschließlich von den übergebenen Parametern ab und nicht von anderen Programmzuständen, die beispielsweise in Variablen gespeichert und extern verändert werden könnten. Ebenso werden während der Berechnung einer Funktion oder der Ausführung eines Templates keine Programmzustände geändert. Der Vorteil dieser Restriktion ist, dass Programmzustände wesentlich leichter vorhergesagt und damit die Ausführung von Programmen gut optimiert werden können: wenn bspw. einmal log10($input)
berechnet wurde, kann der Prozessor beim nächster Auftreten von log10($input)
auf das letzte Ergebnis zurückgreifen, weil sich $input
per funktionalem Paradigma nicht ändern darf. Daraus folgt, dass eine Funktion random()
, die bei jedem Aufruf ein anderes Ergebnis liefert, ein Paradox ist.
XSLT und XPath sind (manchmal zum Verzweifeln) strikt in Bezug auf Seiteneffektfreiheit. Ein Loch im System – das sogar durch den XSLT-Standard gedeckt ist – fand aber Vladimir Nesterovsky: generate-id()
muss für jeden Knoten eine andere ID liefern; das schließt im Stylesheet generierte temporäre Knoten ein. Eine Funktion, die einen temporären Knoten erzeugt und für diesen generate-id()
aufruft, gibt deshalb bei jedem Aufruf ein anderes Ergebnis zurück:
<xsl:stylesheet version="2.0" xmlns:f="data:,f" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"> <xsl:template match="/"> <xsl:message select=" for $i in 1 to 8 return f:fun()"/> </xsl:template> <xsl:function name="f:fun" as="xs:string"> <xsl:variable name="x">!</xsl:variable> <xsl:sequence select="generate-id($x)"/> </xsl:function> </xsl:stylesheet> |
Das Ergebnis mit Saxon ist bspw. tt2 tt3 tt4 tt5 tt6 tt7 tt8 tt9
.
Damit kann man nun einen Pseudozufallsszahlengenerator füttern. Ich habe mich für einen linearen Kongruenzgenerator entschieden, weil er sich recht einfach implementieren lässt:
<xsl:function name="intern:linear-congruential-generator" as="xs:integer+"> <xsl:param name="length" as="xs:integer"/> <xsl:param name="vortrag" as="xs:integer+"/> <xsl:choose> <xsl:when test="$length eq 0"> <xsl:sequence select="$vortrag[position() gt 1]"/> </xsl:when> <xsl:otherwise> <xsl:sequence select="intern:linear-congruential-generator($length - 1, ($vortrag, (1103515245 *$vortrag[last()] + 12345) mod 4294967296) )"/> </xsl:otherwise> </xsl:choose> </xsl:function> |
Die numerischen Konstanten werden so bei der glibc
verwendet, ich habe sie einfach übernommen. Die Funktion ist rekursiv, sie fügt bei jedem Aufruf an die vortrag
-Sequenz (die mit einem möglichst zufälligem Startwert – englisch seed – initialisiert wird) eine Pseudozufallszahl an. Der Startwert wird aus der Rückgabe entfernt; so kann man diese Funktion (mit einer length
von 1
) zum einfachen Umwandeln eines Eingabewertes benutzen. intern:linear-congruential-generator(5, 1)
ergibt bspw. 1103527590 2524885223 662824084 3295386429 4182499122
. Dieses Ergebnis ist reproduzierbar, d.h. jeder Aufruf mit den Parametern 5, 1
liefert genau diese Sequenz zurück.
An diesem Punkt kommt die oben besprochene Funktion zum Erzeugen veränderlicher Werte ins Spiel. Ich habe sie um eine Verarbeitung von Datum und Uhrzeit ergänzt, damit bei jedem Aufruf des Stylesheets neue Pseudozufallswerte erzeugt werden:
<xsl:function name="intern:seed" as="xs:integer"> <xsl:param name="seed" as="xs:anyAtomicType"/> <xsl:variable name="integer-of-seed" as="xs:integer" select="if ($seed castable as xs:integer) then xs:integer($seed) else sum(string-to-codepoints(string($seed) ) )"/> <xsl:variable name="integer-of-current-date" as="xs:integer" select="xs:integer(format-dateTime(current-dateTime(), '[Y][d][H][m][s][f]') )"/> <xsl:variable name="temporary_node" as="text()">?</xsl:variable> <xsl:variable name="sequence-of-weighted-id-integers" as="xs:integer+"> <xsl:for-each select="string-to-codepoints(generate-id($temporary_node) )"> <xsl:sequence select="intern:power(xs:integer(10), position()) * xs:integer(.)"/> </xsl:for-each> </xsl:variable> <xsl:sequence select="intern:linear-congruential-generator(1, $integer-of-seed + $integer-of-current-date + xs:integer(sum($sequence-of-weighted-id-integers) ) )"/> </xsl:function> |
Disclaimer: Ich bin kein Mathematiker und kann deshalb die Güte der Zufallszahlen nicht beurteilen. Sie eignen sich sicher nicht für kryptographische Anwendungen. Auf der anderen Seite zeigt ein Plot eine recht gleichmäßige Verteilung, so dass sie für die meisten Zwecke ausreichen dürften.
Bei den ersten Tests sahen die Ergebnisse sehr gut aus, aber dann schlug die Optimierung von Saxon zu: for $i in 1 to 100 return string(xsb:random(1))
lieferte 100 Mal den selben zufälligen Wert. Die Lösung ist, die Evaluierung der Funktion zu erzwingen, indem als Argument ein möglichst veränderlicher Wert übergeben wird. Das ist hier naheliegend die Zählvariable $i
: for $i in 1 to 100 return string(xsb:random($i))
liefert wieder recht zufällige Werte. Aus pädagogischen Gründen habe ich deshalb in der XSLT-SB keine xsb:random()
-Funktion ohne Argument implementiert. Natürlich könnte ein sehr intelligenter Prozessor auch hier wieder optimieren, aber je schwerer man ihm das macht, umso unwahrscheinlicher ist sein Erfolg.
siehe auch
- Dimitre Novatchev: Casting the Dice with FXSL: Random Number Generation Functions in XSLT (englisch)
XSLT-SB: asin(), acos(), atan(), atan2(), dynamische Typung
Verfasst von Stf unter Allgemeines, XSLT-SB am 27. Juni 2011
Mit dem Sprung auf Version 0.2.37 kann die XSLT-SB, genauer math.xsl
, jetzt auch die Arkusfunktionen und dynamische Typung. In Ergänzung der knappen Release-Notes hier ein paar Anmerkungen dazu.
Arkusfunktionen
Die Gruppe der Arkusfunktion lässt sich einfach auf der Grundlage von atan()
implementieren – wenn man den richtigen Ansatz gefunden hat. Meine ersten Versuche, asin()
und atan()
über Taylor-Reihen zu implementierten scheiterten daran, dass diese Reihen zu langsam konvergieren. Zusammen mit den in der Reihenbildung verwendeten rekursiven Funktionen (Fakultät und Potenz) war schnell das Limit der Rekursionstiefe erreicht: Saxon bricht die Verarbeitung mit Hinweis auf das Überschreiten der maximalen Rekursionstiefe ab.
Die Lösung brachte ein alternativer Algorithmus zur Berechnung von atan()
, den ich bei WolframMathWorld fand (Gleichungen Nr. 44 bis 48). Mittels des Arkustangens lässt sich einfach der Arkussinus und Arkuskosinus berechnen, so dass diese Implementierung ein Kinderspiel war (siehe Wikipedia).
dynamische Typung
Zweites großes Thema der Überarbeitung von math.xsl
war die dynamische Typung. Die mathematischen Operatoren und Funktionen in XPath geben ihr Ergebnis i.d.R. mit dem Typ der Argumente zurück. Zum Beispiel ist das Ergebnis von fn:round-half-to-even()
mit einem Argument vom Typ xs:decimal
vom Typ xs:decimal
, während mit einem xs:double
-Argument das Ergebnis vom Typ xs:double
ist.
Die Implementierung dieses Verhaltens barg einige Schwierigkeiten, weil manche Funktionen als Ergebnis NaN
, -INF
oder INF
zurückgeben können. Diese speziellen Werte sind zwar mit den Typen xs:float
und xs:double
darstellbar, nicht aber mit xs:decimal
(das aber genauere Ergebnisse als xs:double
liefert) und xs:integer
. Das gewünschte Verhalten ist wohl sehr vom Einsatzzweck der Funktionen abhängig. Der jetzige Kompromiss ist daher, dass bei diesen Werten der Cast von NaN
, -INF
oder INF
auf ungeeignete Typen scheitert; wer Wert auf hohe Genauigkeit legt, kann vielleicht auch die kritischen Werte umschiffen.
Genauigkeit
Laut Standard müssen XPath-Implementierungen mit 18 signifikanten Stellen rechnen. Für die XSLT-SB habe ich mittels intern:round()
die Genauigkeit auf 16 Stellen begrenzt, weil damit die meisten Tests erfolgreich absolviert werden. Trotzdem wird für manche Testwerte (etwa exp(100)
) nicht das richtige Ergebnis ermittelt (bei vielen Berechnungsschritten summieren sich die Fehler halt). In diesen Fällen wird im Testprotokoll (hier z.B. für Saxon EE) eine Warnung (orange hinterlegt) ausgegeben. Diese sind zu unterscheiden von den gelb hinterlegten Fällen, bei denen ein Cast der Ergebnisse auf xs:decimal
(den Typ der Tests) nicht möglich ist, siehe oben. In den Tests nicht berücksichtigt sind die Fälle, für die eine Funktion nicht definiert ist, etwa asin(3)
.
Intern (d.h. bei den Funktionen mit intern:
-Prefix) wird mit einer höhren Stellenzahl gerechnet, diese müssen aber weder signifikant noch richtig sein.
XSLT-SB – eine Standard-Bibliothek für XSLT
Verfasst von Stf unter Beispiele, XSLT und XPath, XSLT-SB am 14. Mai 2011
Es ist vollbracht. Nach ein paar Wochenenden mit Feinschliff und letzten Test habe ich Version 0.2 von XSLT-SB – einer Standard-Bibliothek für XSLT – veröffentlicht.
Was ist XSLT-SB?
Die XSLT-Standard-Bibliothek (XSLT-SB) beinhaltet nützliche, immer wieder gebrauchte Funktionen und Templates. Gleichzeitig dient sie als beispielhafte Implementierung bestimmter Techniken. Sie wendet sich als Beispielsammlung vor allem an deutschsprachige Entwickler, um für diese die Einstiegshürden zu senken.
Die XSLT-SB hat zwei Quellen: einerseits habe ich zeitig angefangen, immer wieder gebrauchte Funktionen und Templates in produktive Bibliothek-Stylesheets auszulagern. Für die XSLT-SB habe ich einige davon übernommen. Beispiele dafür sind xsb:force-cast-to-integer()
und xsb:parse-string-to-boolean()
sowie die Grundlagen des Logging-Systems. Andererseits habe ich aus Spaß (oder so) mal die eine oder andere Funktion implementiert, bspw. ist files.xsl
wesentlich umfangreicher ausgefallen, als es für die eigentliche Aufgabe notwendig gewesen wäre.
Templates und Funktionen der XSLT-SB entstanden also nicht systematisch, sondern nach Bedarf oder Interesse. Im besonderen habe ich nicht versucht, bestehende Bibliotheken wie EXSLT zu ersetzen. Deshalb kann die XSLT-SB mit Fug und Recht als lückenhaft bezeichnet werden.
Ich habe die XSLT-SB in einigen kleineren Projekten produktiv eingesetzt, aber der dauernde Härtetest steht noch aus. Außerdem sind die Stylesheets durch Dokumentation und Tests recht umfangreich; und ich habe sie auch nicht auf eine hohe Ausführungsgeschwindigkeit optimiert. Deshalb möchte ich heute von einem produktiven Einsatz abraten, aber selbstverständlich können einzelne Templates oder Funktionen gezielt in eigene Projekte übernommen werden. Die Veröffentlichung der Stylesheets macht einen breiteren Einsatz möglich, und ich freue mich auf das Feedback. Abhängig davon mag sie sich in die eine oder andere Richtung entwickeln – mehr Beispielsammlung oder mehr produktive Bibliothek.
Drei Besonderheiten der XSLT-SB möchte ich hervorheben: files.xsl
, das Logging-System und die Testumgebung.
files.xsl
files.xsl
bündelt Funktionen rund um URLs. Da xs:anyURI
kaum geprüft wird, habe ich die Regeln von RFC 1808 (URL) in diverse String-Tests gegossen und darauf aufbauend Funktionen zum Ermitteln von Dateiname, Dateipfad, Dateierweiterung usw. entwickelt. Ergänzt wird das Stylesheet durch Funktionen wie xsb:file-exists()
und xsb:mediatype-from-url()
.
Das Stylesheet demonstriert einige spezielle XML- und XSLT-Techniken, etwa benannte Entities für lesbare reguläre Ausdrücke, von Systemeigenschaften abhängige Funktionen (mit use-when
) und die Verwendung von Java-Funktionen.
Logging-System
Die XSLT-SB implementiert ein konfigurierbares Logging-System, um Nachrichten des Stylesheets während der Verarbeitung einfach und flexibel auszugeben. Meldungen können per xsl:message
oder (soweit der Rückgabetyp einer Funktion oder eines Templates das zulässt) als Kommentar, XML-Element oder HTML ausgegeben werden, unterschiedliche Dringlichkeitsstufen werden unterstützt. Die XSLT-SB nutzt das Logging-System intensiv für die Selbsttests der Funktionen, für den Einstieg lohnt ein Blick auf xsb:internals.Error
bzw. xsb:internals.FunctionError
in internals.xsl.
Testumgebung
Für Funktionstest habe ich eine Testumgebung entwickelt (siehe internals.testing.xsl). Tests werden in Templates zusammengefasst, die im Stylesheet selbst oder in externen Teststylesheets abgelegt werden können und per initialem Mode oder initialem Template aufgerufen werden. Einige Funktionen und Templates helfen beim Vergleich von erwarteten und berechneten Werten und kümmern sich um die Protokollierung. Interessanterweise haben mir die Test nicht nur beim nachträglichen Absichern der Stylesheets geholfen, sondern ich bin relativ schnell auf eine testgetriebene Entwicklung umgestiegen. Diesen Aspekt möchte ich in meiner täglichen Arbeit nicht mehr missen.
Die Testumgebung wird durch formale Tests der Stylesheets selbst ergänzt (internals.stylecheck.xsl, Template intern:internals.Stylecheck
). Sie warnen bei fehlender Typung von Variablen, Parametern und Funktionen, fehlender Dokumentation u.a. und listen ToDos.
Ein Beispiel für absolvierte Tests und den Stylecheck, ausgegeben über das Logging-System als HTML, sind die Testergebnisse für files.xml
unter Saxon-HE.
Wie kann die XSLT-SB benutzt werden?
Wie ich oben schrieb, kann ich die XSLT-SB im Moment nicht für den produktiven Einsatz empfehlen. Wer es trotzdem wagen möchte, kann sich die Stylesheets herunterladen und in eigene Projekte einbinden. Ein neues Projekt kann einfach auf der Grundlage von pattern+includes.xsl begonnen werden. Natürlich kann man die Stylesheets auch zum Nachschauen oder für Kopieren & Einfügen verwenden.
Die Verwendung der meisten Funktionen sollte selbsterklärend sein, für die Logging- und Testumgebung können die XSLT-SB-Stylesheets als Beispiel herangezogen werden. Die Stylesheets sind dokumentiert; eine HTML-Version der Dokumentation liegt im doc
-Verzeichnis der Distribution und – meist aktueller – online.
Lizenz
Die Stylesheets und das Drumherum sind dual lizenziert: EXPAT (MIT) für den Einsatz als Software und CC-by 3.0, so dass einer Verwendung keine rechtlichen Hürden im Weg stehen sollten.
Was kommt als nächstes?
Das hängt vom Feedback ab – oder von überraschenden neuen Projekten. Auszug aus meiner ToDo-List:
auf Intel SOAE zum laufen bringen, im Moment stürzt dieser Prozessor einfach ab[Nachtrag: Ich habe das Problem eingegrenzt und im Intel-Forum dargestellt. In neueren Versionen des Prozessors tritt es wohl nicht mehr auf, allerdings plant Intel keine Veröffentlichung einer neuen Version, siehe hier. Damit sind mir hier wohl die Hände gebunden …]- Dokumentation verbessern, z.B. Liste der Funktionen mit Kurzbeschreibung erstellen. [Nachtrag: Im Projektwiki gibt es jetzt aus den Stylesheets heraus generierte Übersichten.]
- zusätzliche Funktionen implementieren, bspw. habe ich gerade wieder mal das
p:directory-list
aus XProc vermisst - Kompakt-Distribution ohne geschwätzige Kommentare und Dokumentation erzeugen, um die Startgeschwindigkeit zu erhöhen
Links
Ich habe das Projekt bei Google-Code eingestellt, dort gibt es sowohl ein SVN-Repository als auch fertige Distributionen, die gelegentlich dem aktuellen Entwicklungsstand hinterherhinken können. Auf den Expedimentum-Seiten gibt es ein aktuelles Checkout aus dem Trunk, hier kann man auch online die Dokumentation einsehen. Kommentare und Fehlermeldungen sollten über die Google-Seiten laufen.
- Download: http://code.google.com/p/xslt-sb/downloads/list
- Projektseite bei Google Code: http://code.google.com/p/xslt-sb/
- aktuelles Checkout: http://www.expedimentum.org/example/xslt/xslt-sb/
Apache Ant: Initialen Mode und Initiales Template für XSLT 2.0 mit Saxon setzen
Verfasst von Stf unter Apache Ant, XSLT und XPath am 2. April 2011
Ein schönes Feature von XSLT 2.0 ist, dass man ein Stylesheet in einem initialen Mode oder mit einem initialen Template starten kann (siehe Standard). Ich benutze diese Möglichkeit gern, um Selbsttests direkt im Stylesheet unterzubringen und über einen initialen Mode auszuführen – ohne die eigentliche Logik des Stylesheets zu beeinflussen.
In OxygenXML kann man den initialen Mode oder das initiale Template prima im Transformationsszenario einstellen, der kleine Button mit dem rasenden Zahnrädchen neben der Auswahlliste für die Transformations-Engine öffnet den passenden Einstellungs-Dialog (siehe Dokumentation).
Schon vor geraumer Zeit wollte ich diese Feature auch mit Apache Ant nutzen, bin aber wegen fehlender Unterstützung durch Saxon und einen zwischenzeitlichen Ant-Bug in Version 1.8.1 nicht weitergekommen. Inzwischen wurden Saxon und Ant aktualisiert, so dass es Zeit für einen neuen Anlauf war. Die größte Herausforderung war, die jeweils passende URI für die beiden Features zu finden, letztendlich habe ich in die Saxon-Quelltexte geschaut. So geht’s:
<target name="test"> <xslt in="input.xml" out="output.xml" style="stylesheet.xsl"> <!-- Pfad zu Saxon an lokale Installation anpassen! --> <classpath location="saxon9he.jar" /> <factory name="net.sf.saxon.TransformerFactoryImpl"> <!-- hier ggfs. "http://saxon.sf.net/feature/initialTemplate" einsetzen --> <attribute name="http://saxon.sf.net/feature/initialMode" value="MyMode"/> </factory> </xslt> </target> |
Über die Factory-Attribute können auch viele weitere Saxon-Optionen – die oft auch über die Kommandozeile zu erreichen sind – von Ant aus gesteuert werden, etwa der Umgang mit Whitespace (http://saxon.sf.net/feature/strip-whitespace
) oder die Zeilennummerierung (http://saxon.sf.net/feature/linenumbering
). Das habe ich allerdings nicht getestet.
Quellen:
- http://ant.apache.org/manual/Tasks/style.html (unter »factory (‚trax‘ processors only)« und »Using factory settings«)
- http://saxonica.com/documentation/configuration/config-interfaces/jaxp-configuration.xml
- http://saxonica.com/documentation/javadoc/constant-values.html in Verbindung mit http://saxonica.com/documentation/javadoc/net/sf/saxon/TransformerFactoryImpl.html und http://saxonica.com/documentation/javadoc/net/sf/saxon/lib/FeatureKeys.html
XSLT/XPath: arithmetische Operationen mit einer Leersequenz
Verfasst von Stf unter XSLT und XPath am 17. Juli 2010
Neues aus der Serie »Man lernt nie aus…«: Ist bei arithmetischen Operationen einer der Operanden die empty sequence
, so ist das Ergebnis bei XSLT/XPath 1.0 NaN
(»not a number«), bei XSLT/XPath 2.0 aber die empty sequence
. Dieses Verhalten ist im XPath-Standard unter 3.4 Arithmetic Expressions definiert.
Ich stand vor dem Problem, eine Variable, die entweder eine xs:decimal
-Zahl oder aber auch die Leersequenz enthält, in eine gültige Instanz von xs:decimal
umzuwandeln, d.h. statt der Leersequenz sollte 0 geliefert werden. Es ist gängige Programmiertechnik, eine Leersequenz durch Anhängen eines Leerstrings – z.B. xs:string( ( (), '') )
– in eine gültige Instanz von xs:string
(d.h. einen Leerstring) umzuwandeln. Die analoge Technik – Addieren einer Null zu einer Leersequenz – funktioniert aber wegen des im Standard definierten Verhaltens nicht. Abhilfe schafft bei XSLT 2.0 eine spezielle Funktion:
<xsl:function name="xsb:force-cast-to-integer" as="xs:decimal"> <xsl:param name="input" as="xs:string?"/> <xsl:choose> <xsl:when test="$input castable as xs:decimal"> <xsl:sequence select="xs:decimal($input)"/> </xsl:when> <xsl:otherwise> <xsl:sequence select="0"/> </xsl:otherwise> </xsl:choose> </xsl:function> |
oder kürzer:
<xsl:function name="xsb:force-cast-to-integer" as="xs:decimal"> <xsl:param name="input" as="xs:string?"/> <xsl:sequence select="if ($input castable as xs:decimal) then xs:decimal($input) else 0"/> </xsl:function> |
Außer bei Leerstring und Leersequenz gibt diese Funktion auch bei nicht konvertierbaren Strings (wie z.B. römischen Zahlen) 0
zurück (was bei mir häufig das gewünschte Verhalten ist), aber die Beschränkung auf Leerstring und Leersequenz lässt sich einfach durch Ersetzen von $input castable as xs:decimal
mit normalize-space($input)
erzielen.
Nachtrag:
Thomas Meinicke merkte zum Rechnen mit Leersequenzen an, dass man zuerst mit exists()
oder empty()
prüfen kann, ob eine Sequenz leer ist, um dann den resultierenden Wahrheitswert in xs:decimal
zu casten, etwa so: xs:decimal(exists( () ) )
. Achtung: Dabei wird die Leersequenz zu 0
, während der Leerstring zu 1
wird.
Nachtrag II:
Leersequenzen führen auch bei Vergleichs-Operationen zur Rückgabe einer Leersequenz, siehe im XPath-Standard unter 3.5.1 Value Comparisons. Da Leersequenzen zu false()
evaluiert werden, ergibt beispielsweise () eq ()
immer false()
. Um zu testen, ob eine Leersequenz vorliegt, muss deshalb empty()
oder exist()
verwendet werden.
Java in XSLT verwenden
Verfasst von Stf unter Java, XSLT und XPath am 9. Juli 2010
Es gibt immer wieder Programmieraufgaben, bei denen XSLT allein zur Lösung nicht ausreicht. In diesen Fällen kann – vorausgesetzt, dass der XSLT-Prozessor mitspielt – die Einbindung von Java helfen. XSLT bietet dafür zwei Mechanismen: Erweiterungsfunktionen (Extension Functions) und Erweiterungsbefehle (Extension Instructions, in XSLT 1.0 als Extension Elements bezeichnet). Im Folgenden werden nur Erweiterungsfunktionen betrachtet.
Nutzung von Standard-Java-Methoden
In einem früheren Post habe ich ein Verfahren beschrieben, um mit Saxon-Erweiterungsfunktionen die Existenz einer Datei zu testen. Natürlich lässt sich diese Aufgabe auch mit Java erledigen:
<xsl:stylesheet version="2.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:test="something" xmlns:java-file="java:java.io.File" xmlns:java-uri="java:java.net.URI"> <xsl:function name="test:file-exists" as="xs:boolean"> <xsl:param name="fileURL" as="xs:string"/> <xsl:sequence select="java-file:exists(java-file:new(java-uri:new($fileURL)))"/> </xsl:function> </xsl:stylesheet> |
Die Funktion test:file-exists
nutzt die Methode exists()
aus der Klasse java.io.File
. Dazu wird in der xsl:stylesheet
-Deklaration der Namespace java.io.File
(der Klassenname) definiert und an das Präfix java-file
gebunden. In der Zeile <xsl:sequence ...
wird dann mit java-file:new(...)
eine neue Instanz erzeugt und mit java-file:exists(...)
die Methode aufgerufen. Die merkwürdige Syntax ist dem Umstand geschuldet, dass der Aufruf von Java wie eine XPath-Funktion aussehen soll, um problemlos mit XSLT verarbeitet werden zu können.
Die Klasse java.net.URI
verwende ich zusätzlich, um den übergebenen String auf Gültigkeit zu überprüfen – bei einem ungültigen String schlägt schon die Erzeugung der URI fehl. Zudem erzwingt der Konstruktor von java.net.URI
die Angabe einer absoluten URI, was in der Praxis hilft, Programmierfehler zu vermeiden. Relative Pfade lassen sich einfach mit der XPath-Funktion resolve-uri()
– ggfs. in Verbindung mit base-uri()
– in absolute Pfade umwandeln. Die Typung des Funktionsparameters fileURL
als xs:anyURI
würde hier übrigens nicht wirklich weiter helfen, da xs:anyURI
per Standard nur recht schwache Gültigkeitskriterien hat. Wenn dieser zusätzliche Test nicht benötigt wird, kann das File-Objekt aber auch direkt aus dem Funktionsparameter erzeugt werden.
Das vollständige Stylesheet steht in der Beispielsammlung zum Download bereit.
Voraussetzungen und Konfiguration
Um Java in XSLT verwenden zu können, muss der jeweilige XSLT-Prozessor dies unterstützen. Nachdem Michael Kay neue Saxon-Pakete zusammengestellt hat, bietet sich folgendes Bild: Saxon-B und Saxon-SA bis einschließlich Version 9.1 unterstützen die Einbindung von Java-Funktionen, ab Version 9.2 funktioniert dies nur noch mit den kostenpflichtigen PE- und EE-Versionen. Bei AltovaXML hatte ich mit dem Build vom 23.10.2009 sowie der Version 2010 rel. 3 sp 1 Erfolg, allerdings musste ich nach der Fehlermeldung »JVM dll kann nicht geladen werden« bzw. »Can’t load JVM DLL« erst ein JDK installieren und die Path
-Umgebungsvariable um das bin
-Verzeichnis im JDK ergänzen.
Ein weiteres Beispiel
Im letzten Blog-Beitrag wurde die Ausgabe von Binärdaten in eine Datei angesprochen. Eine erste Java-Lösung sieht so aus:
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:test="something" xmlns:java-file="java:java.io.File" xmlns:java-fos="java:java.io.FileOutputStream" xmlns:java-uri="java:java.net.URI" version="2.0"> <xsl:function name="test:write-hexBinary-to-file"> <xsl:param name="hexBin" as="xs:hexBinary?"/> <xsl:param name="OutputFileURI" as="xs:anyURI"/> <xsl:variable name="fostream" select="java-fos:new(java-file:new(java-uri:new($OutputFileURI) ) )"/> <xsl:value-of select="java-fos:write($fostream, test:tokenize-HexBinary($hexBin) )"/> </xsl:function> […] </xsl:stylesheet> |
Zuerst wird ein FileOutputStream
-Objekt erzeugt und einer Variablen zugewiesen. Danach wird die write()
-Methode des Objektes aufgerufen. Interessant ist die Syntax dieses Methodenaufrufes: der scheinbaren Funktion java-fos:write()
wird als erstes Argument das Objekt, dessen Methode aufgerufen werden soll, übergeben.
Die Umwandlung des HexBin-Strings in eine Sequenz von Bytes habe ich in die XSLT-Funktion test:tokenize-HexBinary
samt zweier Hilfsfunktionen ausgelagert, die Details lassen sich im Beispiel-Stylesheet nachschlagen.
Einbindung externer Klassen
Das FileOutputStream
-Beispiel funktioniert zwar, aber es ist eine ziemliche XSLT-Akrobatik für das Umwandeln von HexBinary in Bytes notwendig. Wenn man schon Java benutzt, kann man das Problem auch gleich ganz an Java delegieren. Unter Verwendung einer Lösung von Dave L. habe ich diese Klasse geschrieben:
package org.expedimentum.example.java; import java.io.File; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; public class HexBinaryFileWriter { public void write (String hexbinary, File file) throws FileNotFoundException, IOException { FileOutputStream fos = new FileOutputStream(file); for (int i = 0; i < hexbinary.length(); i += 2) { fos.write( (Character.digit(hexbinary.charAt(i), 16) << 4) + Character.digit(hexbinary.charAt(i+1), 16)); } } } |
Diese Klasse ist eine ganz normale Java-Klasse, sie beinhaltet keinen XSLT-bezogenen Code. Sie kann nun wie in den obigen Beispielen beschrieben in ein Stylesheet eingebunden werden:
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:test="something" xmlns:java-hbfw="java:org.expedimentum.example.java.HexBinaryFileWriter" xmlns:java-file="java:java.io.File" xmlns:java-uri="java:java.net.URI" version="2.0"> <xsl:function name="test:write-hexBinary-to-file"> <xsl:param name="hexBin" as="xs:hexBinary?"/> <xsl:param name="OutputFileURI" as="xs:anyURI"/> <xsl:variable name="HexBinaryFileWriter" select="java-hbfw:new()"/> <xsl:value-of select="java-hbfw:write($HexBinaryFileWriter, string($hexBin), java-file:new(java-uri:new($OutputFileURI) ) )"/> </xsl:function> </xsl:stylesheet> |
Unter <OxygenXML/> und Saxon muss das jar-File mit der HexBinaryFileWriter
-Klasse im CLASSPATH
liegen. Bei Saxon darf die Transformation nicht mit java -jar ...
gestartet werden, vielmehr muss der Aufruf über java -cp ... net.sf.saxon.Transform
erfolgen. AltovaXML wünscht die Übergabe des Pfades zum jar in der Deklaration des Namespace, was dann bspw. so aussehen könnte: xmlns:java-hbfw="java:org.expedimentum.example.java.HexBinaryFileWriter?path=jar:file:///C:/example/Java/dist/Beispiele.jar!/"
.
Der Quelltext der Klasse und ein jar liegen in der Beispielsammlung, ebenso das vollständige Stylesheet.
Noch ein Tipp zum Schluss: einfacher als das Schreiben, Einbinden und Warten von Java-Erweiterungen dürfte in vielen Fällen die Nutzung prozessorspezifischer Erweiterungsfunktionen – wie sie z.B. Saxon und AltovaXML anbieten – oder von XSLT-Bibliotheken wie EXSLT sein.
Weblinks
- XSL Transformations (XSLT) Version 2.0. 18 Extensibility and Fallback – http://www.w3.org/TR/xslt20/#extension (englisch)
- XSLT blooms with Java. Use Java in your stylesheets when XSLT won’t do the trick – http://www.javaworld.com/javaworld/jw-12-2001/jw-1221-xslt.html?page=1 (englisch)
- Extending XSLT with Java – http://cafeconleche.org/books/xmljava/chapters/ch17s03.html (englisch)
- Saxonica: XSLT and XQuery Processing: Writing reflexive extension functions in Java – http://www.saxonica.com/documentation/extensibility/functions.html (englisch)
- AltovaXML 2010: Java Extension Functions – http://manual.altova.com/AltovaXML/index.html?xextjava.htm (englisch)