Archiv für Kategorie Beispiele

Ein universelles XSL-Stylesheet zum Aufteilen von XML-Dokumenten

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 gleich 20, dann sind mögliche Gruppenanfänge die Elemente mit der Position 1, 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.

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).

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 http://www.w3schools.com/xsl/func_formatnumber.asp). Wird kein Wert übergeben, werden führende Nullen entsprechend der Anzahl der Elemente im Ausgangsdokument ausgegeben.

Extras

wiederhole-initiale-nodes
(xs:boolean) Steht dieser Parameter auf true, werden nodes() vor dem ersten Element (Kommentare und/oder Processing-Instructions) in jede Ausgabe-Datei an den Anfang kopiert. Bei false 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.

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.

»crosslinked«

Keine Kommentare

Benutzerdefinierte Funktionen und externe Funktionsbibliotheken in Schematron

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>

OxygenXML-Einstellungsdialog für SchematronBei 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.

1 Kommentar

XSLT-SB – eine Standard-Bibliothek für XSLT

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.

Keine Kommentare

EPUB-Beispiel aus Wikipedia

Die Beispiele aus dem Wikipedia-Artikel EPUB ergeben ein vollständiges E-Book. Leider kann man bei Commons noch keine EPUBs hochladen, siehe https://bugzilla.wikimedia.org/show_bug.cgi?id=17858. Ich habe deshalb die dazugehörigen Dateien und das EPUB in der Beispielsammlung abgelegt.

Keine Kommentare

Binärdateien mit Saxon schreiben

XSLT bietet von Haus aus keine Möglichkeit zur Ausgabe von Binärdateien wie Bildern. Aber zumindest Saxon-PE/Saxon-EE ab Version 9.2 bietet eine Erweiterung, um diese Beschränkung zu umgehen. Nach einigen Tests ist es mir gelungen, sowohl jpeg als auch png zu schreiben. Das Beispiel-Stylesheet hat folgenden Abschnitt:

<xsl:variable name="image" as="xs:hexBinary">89504E470D0A1A0A0000000D494844520000000D0000000F0403000000CD0D5844000000017352474200AECE1CE900000018504C544562129D80418A8851829E7972B09266DCD946F4FF39FEFFFC482C1765000000564944415408D725CD2B12C0200C04D030133C0A5D55DD2354A1A3D069A17BFF237403316FF217AC70190ABCD905A5E36ED4CE2FB18EA14F0E51AEB6B484AD6E0F6ED0A95643AB53B1EF94EEC2842517363912F3EBDF0F99462041E4FA77630000000049454E44AE426082</xsl:variable>
<xsl:result-document method="text" encoding="iso-8859-1" href="x.png" saxon:recognize-binary="yes">
	<xsl:processing-instruction name="hex" select="$image"/>
</xsl:result-document>

Ich hatte auf einem Mac mit <OxygenXML/> kein Glück bei der expliziten Angabe des Encodings, etwa hex.utf8, aber vielleicht habe ich nicht konsequent genug probiert.

Das vollständige Stylesheet steht unter http://www.expedimentum.org/example/xslt/binaerdaten-ausgabe.xsl zum Download bereit.

Nachtrag
Wenn man schon Erweiterungs-Funktionen bemüht, kann man die Arbeit auch gleich Java erledigen lassen. Damit ist man nicht mehr auf eine der »großen« Saxon-Versionen beschränkt; auch AltovaXML und Saxon-B 8.9 und 9.1 lassen sich zur Mitarbeit überreden. Ich habe zwei Varianten entwickelt: binaerdaten-ausgabe_java.xsl »borgt« sich nur den FileOutputStream von Java und wandelt den HexBinary-String per XSLT in eine Sequenz von Bytes um. binaerdaten-ausgabe_java_extern.xsl bedient sich der externen Klasse HexBinaryFileWriter, die die komplette Konvertierung und Ausgabe abwickelt. Die Erklärung folgt im nächsten Blog-Beitrag.

1 Kommentar

XSLT-Dokumentation mit XSLStyle™

Leider gibt es für XSLT keine standardisierten oder auch nur weitverbreiteten Regeln für die Dokumentation der Stylesheets. Ein Grund dafür könnte sein, dass einfach zu bedienende Werkzeuge nicht verfügbar sind. Für die Bespiele auf dieser Website habe ich mir noch einmal ein paar Ansätze angeschaut und mich dann für XSLStyle™ von G. Ken Holman entschieden, weil a) dieses Tool als XSL-Transformation direkt auf das Stylesheet selbst angewandt werden kann und b) XSLStyle™ auch noch ein paar Tests auf guten Programmierstil durchführt.

<OxygenXML/>-Transformations-Szenario für XSLStyle™Die Installation ist recht einfach: zip-Datei herunterladen, in einem beliebigen Verzeichnis entpacken, fertig. XSLStyle™ kann über die Kommandozeile auf das zu dokumentierende Stylesheet angewandt werden, bequemer ist natürlich, in <OxygenXML/> ein Transformations-Szenario einzurichten.

Die Dokumentation erfolgt über Elemente im http://www.CraneSoftwrights.com/ns/xslstyle-Namensraum, die unmittelbar vor dem zu dokumentierenden XSL-Element stehen. XSLStyle™ kann bei fehlender Dokumentation warnen. Weil bei mir das von XSLStyle™ vorgegebene Präfix xs: immer für den http://www.w3.org/2001/XMLSchema-Namensraum steht, habe ich das Präfix doc: verwendet.

Innerhalb der XSLStyle™-Elemente kann die Dokumentation in beliebigem Markup erfolgen, G. Ken Holman gibt Stylesheets für DocBook und DITA mit. Ich habe mich pragmatisch für DocBook entschieden, weil ich damit aus einem früheren Projekt etwas Erfahrung habe. Es steht ein großer Teil des DocBook-Vokabulars zur Verfügung; nachdem meine Experimente mit einem recht elaboriertem Markup (wie refentry und funcsynopsis) primär aufgeblähten Code hervorgebracht haben, schien mir eine stark vereinfachte Version jedoch die bessere Variante. Der Code aus einem der letzten Posts zum Dateien-Testen sieht mit Dokumentation so aus:

<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet
	version="2.0"
	xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
	xmlns:xs="http://www.w3.org/2001/XMLSchema"
	xmlns:misc="http://www.expedimentum.org/XSLT/Misc"
	xmlns:saxon="http://saxon.sf.net/"
	xmlns:doc="http://www.CraneSoftwrights.com/ns/xslstyle"
	xmlns:docv="http://www.CraneSoftwrights.com/ns/xslstyle/vocabulary"
	exclude-result-prefixes="doc docv"
	extension-element-prefixes="saxon"
	>
	<!--  -->
	<doc:doc filename="files.xsl" internal-ns="docv" global-ns="doc misc" vocabulary="DocBook" info="$Revision: 29 $, $Date: 2009-05-03 02:11:03 +0200 (So, 03 Mai 2009) $">
		<doc:title>Dateien und Dateisystem</doc:title>
		<para>Dieses Stylesheet enthält Funktionen rund um das Dateisystem.</para>
		<para>Dieses Stylesheet unterliegt  mangels Schöpungshöhe keinen urheberrechtlichen Beschränkungen.</para>
		<revhistory>
			<revision>
				<revnumber>0.28</revnumber>
				<date>2008-05-03</date>
				<authorinitials>Stf</authorinitials>
				<revremark>erste Version mit Dokumentation</revremark>
			</revision>
		</revhistory>
	</doc:doc>
	<!--  -->
	<!-- __________     misc:file-exist()     __________ -->
	<doc:function>
		<doc:param name="href"><para>Pfad zur zu überprüfenden Datei (URI-encoded)</para></doc:param>
		<para>Diese Funktion überprüft, ob die angegebene Datei vorhanden ist. Das Ergebnis ist ein Wahrheitswert 
			(<code>xs:boolean</code>, <code>true()</code> oder <code>false()</code>). 
			<emphasis role="bold">Achtung!</emphasis> Diese Funktion erfordert bei Ausführung Saxon, 
			da die Saxon-spezifische Erweiterung <code>saxon:file-last-modified()</code>  verwendet wird.</para>
	</doc:function>
	<xsl:function name="misc:file-exists" as="xs:boolean">
		<xsl:param name="href" as="xs:string?"/>
		<xsl:value-of select="boolean(normalize-space(string(saxon:file-last-modified($href))))"/>
	</xsl:function>
	<!--  -->
</xsl:stylesheet>

Damit XSLStyle™ eine Dokumentation ausgibt, müssen der http://www.CraneSoftwrights.com/ns/xslstyle und ein weiter Namensraum für interne Zwecke (http://www.CraneSoftwrights.com/ns/xslstyle/vocabulary) im zu dokumentierenden Stylesheet deklariert werden. Außerdem erwartet XSLStyle™ am Anfang der Dokumentation ein <doc:doc/>-Element mit Basisinformationen, fehlt es (oder auch nur einzelne Attribute), gibt es bei mir merkwürdige Effekte.

Das vollständige Beispiel habe ich unter http://www.expedimentum.org/example/xslt/files.xsl abgelegt, die resultierende Dokumentation liegt unter http://www.expedimentum.org/example/xslt/doc/files.html. Hier bzw. hier gibt es ein etwas umfangreicheres Beispiel mit importierten Stylesheets.

Zum Schluss folgt noch eine Sammlung zufälliger Links zur XSL-Dokumentation, die bei der Recherche entstanden ist. Falls XSLStyle™ nicht passt, findet sich sicher eine geignetere Lösung. Mir hat XSLTdoc relativ gut gefallen, und mit einem Ant-Script lässt sich auch die umständliche Handhabung bändigen, leider lässt es sich nicht direkt als Transformation auf das zu dokumentierende Stylesheet anwenden.

siehe auch: Florent Georges: XSLStyle and oXygen (englisch)

Keine Kommentare

Encoding und Zeilenumbrüche ändern mit XSLT

Beim Datenaustausch mit XML entsteht immer wieder das Problem, dass eine bestimmte Anwendung nur Daten in einem bestimmten Encoding »versteht«, es ist also gelegentlich eine Konvertierung des Encodings notwendig. Ich verwende dazu eine »Identity Transformation«, die bis auf das Encoding tatsächlich überhaupt keine Änderungen vornimmt:

<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet
	version="1.0"
	xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
	>
	<!--  -->
	<xsl:output encoding="Windows-1252" indent="no" method="xml"/>
	<!--<xsl:output encoding="UTF-8" indent="no" method="xml"/>-->
	<!--<xsl:output encoding="UTF-16" indent="no" method="xml"/>-->
	<!--<xsl:output encoding="ISO-8859-1" indent="no" method="xml"/>-->
	<!--  -->
	<xsl:preserve-space elements="*"/>
	<!--  -->
	<xsl:template match="@* | node()">
		<xsl:copy>
			<xsl:apply-templates select="@* | node()"/>
		</xsl:copy>
	</xsl:template>
	<!--  -->
</xsl:stylesheet>

In XSLT kann man das Encoding des Output-Dokumentes mit <xsl:output encoding="xxx"/> festlegen. Im obigen Beispiel habe ich die gängigen Encodings vorgegeben, zur Anwendung müssen nur die jeweilige Zeile vom Kommentar befreit und die Zeile auskommentiert werden.

Die unterstützen Encodings sind vom jeweiligen XSLT-Prozessor und im Fall von Saxon zudem von der darunter liegenden Java-Installation abhängig. Obwohl laut XML-Standard nur UTF-8 und UTF-16 unterstützt werden müssen, kommen viele XSLT-Prozessoren auch mit ISO 8859-1, Windows-1252 und anderen zurecht.

Zeilenumbrüche

Im vorigen Post habe ich das Problem der falschen Zeilenumbrüche angesprochen, aber keine Lösung angeboten. Die folgt nun hier, allerdings muss ich dazu etwas weiter ausholen:

Zeilenumbrüche sind sogenannte Steuerzeichen, d.h. Zeichen, die normalerweise nicht dargestellt werden, sondern die Darstellung steuern. Historisch haben sich drei Varianten etabliert: Unix verwendet das Steuerzeichen LF (engl. line feed, hexadezimal &0A;), der Mac bis Mac OS 9 CR (engl. carriage return, hex &0D;) und Windows die Sequenz CRLF (&0A;&0D;). Unicode und alle Encodings kennen beide Zeichen, allerding müssen laut XML-Standard bei der Verarbeitung von XML-Dokumenten CR und CRLF zu LF normalisiert werden, so dass ausgegebene Dokumente normalerweise Unix-Zeilenumbrüche enthalten.

Um trotzdem Windows-Zeilenumbrüche in der Ausgabe zu erhalten, muss der XSLT-Prozessor bei der Ausgabe LF wieder in CRLF umwandeln. Soweit ich weiß, gibt es in Saxon keine Möglichkeit dazu, allerdings schreibt Microsofts msxsl.exe Windows-Zeilenumbrüche. <OxygenXML/> erlaubt in der Konfiguration von Transformations-Szenarien die Auswahl von MSXML4.0 als Transformator (sic!). Einzige Einschränkung: Microsoft unterstützt nur XSLT in der Version 1.0.

Ich habe das Stylesheet im Beispiel-Ordner hinterlegt und das Beispieldokument um Testfälle ergänzt. Auch die <OxygenXML/>-Projekt-Datei liegt in einer ergänzten Version vor.

Keine Kommentare

Mit XSLT testen, ob eine Datei existiert

XSLT bietet recht wenig Möglichkeiten, mit der Außenwelt zu kommunizieren. Informationen über die Umwelt zur Laufzeit des Stylesheets lassen sich mit Bordmitteln nur in sehr geringem Umfang ermitteln, Wirkungen nur über die Dateiausgabe erzielen. Dies mag daher kommen, dass das funktionale Paradigma von XSLT Seiteneffekte verbietet, führt aber in der Praxis gelegentlich zu merkwürdigen Verrenkungen. Beispielsweise habe ich öfters das Problem, Stylesheets abhängig von der Existenz einer externen Datei aufzurufen. Bisher habe ich dazu in einer externen Anwendung ein XML mit Informationen zum Dateisystem erzeugt und dieses dann im XSLT ausgewertet. Das funktioniert ganz gut mit statischen Daten, bei häufigen Änderungen im Dateisystem ist dieses Verfahren aber zu umständlich.

XPath 2.0 bietet mit doc-available() zwar eine dedizierte Funktion, diese liefert aber nur bei der Existenz eines wohlgeformten XML-Dokumentes ein true(). Ebenso hängt ein logisches wahr bei unparsed-text-available() von der Existenz eines Textes ab. Was aber, wenn ich wissen will, ob eine Bild-Datei (nein, nicht SVG ;-)) oder eine leere Datei existiert? Die wenigen Lösungsvorschläge verweisen auf xslt-externe Erweiterungsfunktionen. Diese setzen in der Regel die Existenz von Java und manchmal auch zusätzliche Java-Klassen voraus, was die Weiterverwendung vorhandener Stylesheets auf verschiedenen Rechnern erheblich erschwert.

Da in meiner Umgebung ohnehin meist Saxon eingesetzt wird, bietet es sich an, die Saxon-eigenen Erweiterungsfunktionen zu benutzen. Zwar bietet Saxon nicht den gesuchten Test, aber mit file-last-modified() eine Funktion, die einen Leerstring zurückgibt, wenn die Datei nicht vorhanden ist. Damit war die gesuchte Funktion schnell geschrieben:

<xsl:function name="misc:file-exists" as="xs:boolean" xmlns:saxon="http://saxon.sf.net/" extension-element-prefixes="saxon">
	<xsl:param name="href" as="xs:string?"/>
	<xsl:value-of select="boolean(normalize-space(string(saxon:file-last-modified($href))))"/>
</xsl:function>

Zu beachten ist, dass diese Funktion nur mit Saxon funktioniert und keine Fehlerbehandlung stattfindet. Außerdem muss der Parameter URI-codiert werden (z.B. Leerzeichen durch %20 ersetzen). Es gibt also noch genug Raum für Optimierungen.

Nachtrag: Diese Funktion ist nun in der Beispielsammlung abgelegt.

Nachtrag II: saxon:file-last-modified() ist spezifisch für Saxon 9.1. In Saxon 9.2 PE/EE wurde diese Funktion in saxon:last-modified() integriert, in Saxon HE stehen keine Erweiterungsfunktionen zur Verfügung (vgl. Post von Michael Kay auf der Saxon-Mailingliste). Ich habe das Stylesheet entsprechend aktualisiert.

Nachtrag III: Eine Lösung unter Verwendung von Java, die weniger prozessorabhängig ist, habe ich hier beschrieben.

Nachtrag IV: Ich habe die verschiedenen Lösungen in der XSLT-SB zusammengeführt. Der Code kann in der Beispielsammlung eingesehen werden.

Keine Kommentare

Die einfachste XSL-Transformation

Meine ersten Gehversuche mit XSLT hatten das Ziel, kleine Änderungen an XML-Dokumenten vorzunehmen: einzelne Elemente löschen, Werte neu berechnen und ähnliches. Die Lösung dafür ist die sogenannte »Identity Transformation«:

<xsl:stylesheet
	version="2.0"
	xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
	>
	<!--  -->
	<xsl:template match="@* | node()">
		<xsl:copy>
			<xsl:apply-templates select="@* | node()"/>
		</xsl:copy>
	</xsl:template>
	<!--  -->
</xsl:stylesheet>

Dieses Stylesheet kopiert das Eingabedokument vollständig in das Ausgabedokument. Das einzige Template matcht alle Attribute (@*) und alle Knoten (node(), das sind Elemente, Text, Kommentare und sogenannte Processing Instructions).

Wie kann man nun Änderungen am Dokument vornehmen? Ganz einfach: Es werden zusätzliche Templates eingefügt, die nur die zu ändernden Knoten beeinflussen. Sollen zum Beispiel aus einem XHTML-Dokument alle <code/>-Elemente gelöscht werden, hilft folgendes Stylesheet:

<xsl:stylesheet
	version="2.0"
	xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
	xmlns:xhtml="http://www.w3.org/1999/xhtml"
	>
	<!--  -->
	<xsl:template match="@* | node()">
		<xsl:copy>
			<xsl:apply-templates select="@* | node()"/>
		</xsl:copy>
	</xsl:template>
	<!--  -->
	<xsl:template match="xhtml:code"/>
	<!--  -->
</xsl:stylesheet>

Leider wird jetzt der enthaltene Text ebenfalls gelöscht. Wenn die Kind-Knoten (z.B. text()-Knoten) erhalten werden sollen, muss für diese die allgemeine Kopierregel angewendet werden, am einfachsten durch ein <xsl:apply-templates/>:

<xsl:stylesheet
	version="2.0"
	xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
	xmlns:xhtml="http://www.w3.org/1999/xhtml"
	>
	<!--  -->
	<xsl:template match="@* | node()">
		<xsl:copy>
			<xsl:apply-templates select="@* | node()"/>
		</xsl:copy>
	</xsl:template>
	<!--  -->
	<xsl:template match="xhtml:code">
		<xsl:apply-templates/>
	</xsl:template>
	<!--  -->
</xsl:stylesheet>

Voilà! Alle <code/>-Tags sind entfernt, der Inhalt ist noch da, und alles ohne Suchen&Ersetzen mit regulären Ausdrücken.

Letztes Beispiel: die <code/>-Tags sollen durch <span class="code"/>-Tags ersetzt werden. Dazu werden hier die neuen Elemente einfach als sogenannte literale Elemente (englisch Literal Result Elements) in das Template geschrieben:

<xsl:stylesheet
	version="2.0"
	xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
	xmlns="http://www.w3.org/1999/xhtml"
	xpath-default-namespace="http://www.w3.org/1999/xhtml"
	exclude-result-prefixes="#default"
	>
	<!--  -->
	<xsl:template match="@* | node()">
		<xsl:copy>
			<xsl:apply-templates select="@* | node()"/>
		</xsl:copy>
	</xsl:template>
	<!--  -->
	<xsl:template match="code">
		<span class="code"><xsl:apply-templates/></span>
	</xsl:template>
	<!--  -->
</xsl:stylesheet>

Man beachte die Verwendung von xpath-default-namespace und exclude-result-prefixes. Dies sorgt dafür, dass das Namespace-Präfix nicht mehr explizit z.B. in das match-Attribut geschrieben werden muss und keine unnötigen Namespace-Angaben in die Ausgabedatei geschrieben werden.

Das letzte Stylesheet gibt es zum Herunterladen, ein passendes Transformations-Szenario für OxygenXML in der Projekt-Datei example.xpr

Tipp: Ich habe die Identity Transformation als Dokumentenvorlage in OxygenXML hinterlegt, weil ich sie sehr oft brauche. Dann sind Mini-Stylesheets sehr schnell geschrieben.

Nachtrag: Die oben angebotene Identity-Transformation hat einige Nebeneffekte: Standard-Attribute aus der DTD werden ergänzt, Zeilenumbrüche werden nicht genau rekonstruiert, Numeric Character References werden in Zeichen umgewandelt. Eine kompliziertere Alternative wird unter http://www.xmlplease.com/identity-template (englisch) vorgestellt.

Keine Kommentare

Römische Zahlen in Integer konvertieren

Für ein Projekt stand ich vor der Aufgabe, a) einen String darauf zu testen, ob er eine römische Zahl ist und b) und diesen String dann in einen Integer zu konvertieren. Ich kannte mich mit römischen Zahlen nicht wirklich aus, also erst einmal in der Wikipedia nachschlagen. Dabei lernte ich gleich, dass „römische Zahlen“ auf englisch „roman numerals“ heißen – gut, dass konnte ich in der anschließenden Google-Suche gebrauchen.

Im ersten Anlauf fand ich in Sal Manganos XSLT Cookbook eine angestaubte XSLT 1.0-Lösung, die mir überhaupt nicht gefiel. Der Test auf römische Zahlen ist ein Test auf gültige Zeichen, und anschließend wird mangels Funktionen eine Menge mit rekursiven Templates gemacht. Mit XSLT 2.0 muss das doch eleganter gehen. Die freche Lösung – allerdings ohne Gültigkeitstest – liefert Mukul Gandhi in der segensreichen xsl-Mailing-Liste ab. Er zählt solange von 1 bis 10000, bis das Ergebnis von <xsl:number/> gleich dem Input-String ist:

<xsl:stylesheet
	version="2.0"
	xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
	xmlns:xs="http://www.w3.org/2001/XMLSchema"
	xmlns:num="http://whatever">
	<!--  -->
	<xsl:output method="text"/>
	<!--  -->
	<xsl:variable name="max" select="10000"/>
	<!--  -->
	<xsl:template match="/">
		<xsl:call-template name="RomanToInteger">
			<xsl:with-param name="roman_number" select="'IIX'"/>
		</xsl:call-template>
	</xsl:template>
	<!--  -->
	<xsl:template name="RomanToInteger">
		<xsl:param name="roman_number"/>
		<xsl:for-each select="1 to $max">
			<xsl:if test="num:toRoman(.) = $roman_number">
				<xsl:value-of select="."/>
			</xsl:if>
		</xsl:for-each>
	</xsl:template>
	<!--  -->
	<xsl:function name="num:toRoman" as="xs:string">
		<xsl:param name="value" as="xs:integer"/>
		<xsl:number value="$value" format="I"/>
	</xsl:function>
	<!--  -->
</xsl:stylesheet>

Nachteil 1: Bei großen Zahlen kann dieser Algorithmus nicht effektiv sein. Nachteil 2: Die Beschränkung auf 10000 ist ein Schutz vor ungültigen Eingaben und der daraus resultierenden Endlosschleife, größere Zahlen sind aber vorstellbar. Also selbst entwickeln.

a) einen String darauf testen, ob er eine gültige römische Zahl ist

Der von Mangano angebotene Test auf die gültigen Zeichen „I“, „V“, „X“, „L“, „C“, „D“, „M“, „i“, „v“, „x“, „l“, „c“, „d“, „m“ ist nicht ausreichend, weil damit auch ungültige Kombinationen wie „IIX“ möglich sind (die unter anderem Mukul Gandhis Algorithmus bis zum Abbruch laufen lassen). Besser ist ein regulärer Ausdruck, dank Google gefunden bei regexlib.com:

<xsl:function name="misc:IsRomanNumeral" as="xs:boolean">
	<xsl:param name="Input" as="xs:string?"/>
	<xsl:variable name="temp" as="xs:string?" select="normalize-space(upper-case($Input))"/>
	<xsl:value-of select="not(matches($temp,'(([IXCM])2{3,})|[^IVXLCDM]|([IL][LCDM])|([XD][DM])|(V[VXLCDM])|(IX[VXLC])|(VI[VX])|(XC[LCDM])|(LX[LC])|((CM|DC)[DM])|(I[VX]I)|(X[CL]X)|(C[DM]C)|(I{2,}[VX])|(X{2,}[CL])|(C{2,}[DM])'))"/>
</xsl:function>

In der Variablen $temp wird der eingegebene String vorbereitet. Da der reguläre Ausdruck auf ungültige Kombinationen testet, war noch ein zusätzliches not() notwendig.

b) einen String mit einer römischen Zahl in einen Integer umwandeln

In der Wikipedia ist der Algorithmus beschrieben: Die Buchstaben werden durch ihre Integer-Werte ersetzt (dafür nehme ich eine Hilfsfunktion) und dann aufaddiert. Eine Ausnahme ist die Subtraktionsschreibweise. Vereinfacht: Steht genau eine kleinere Ziffer vor einer größeren, wird deren Wert von der Summe abgezogen. Ich setze das geradlinig um:

<xsl:function name="misc:GetIntegerFromRomanNumeral" as="xs:integer">
	<xsl:param name="Input" as="xs:string?"/>
	<xsl:variable name="temp" as="xs:string?" select="normalize-space(upper-case($Input))"/>
	<xsl:choose>
		<xsl:when test="misc:IsRomanNumeral($temp)">
			<xsl:variable name="Values" as="xs:integer*">
				<xsl:for-each select="for $i in 1 to string-length($temp) return $i">
					<xsl:variable name="CharValue" as="xs:integer"
						select="misc:GetIntegerFromRomanNumberChar(substring($temp, position(), 1))"/>
					<xsl:variable name="NextCharValue" as="xs:integer"
						select="misc:GetIntegerFromRomanNumberChar(substring($temp, position() + 1, 1) )"/>
					<xsl:choose>
						<xsl:when test="$CharValue lt $NextCharValue">
							<xsl:value-of select="- $CharValue"/>
						</xsl:when>
						<xsl:otherwise>
							<xsl:value-of select="$CharValue"/>
						</xsl:otherwise>
					</xsl:choose>
				</xsl:for-each>
			</xsl:variable>
			<xsl:value-of select="sum($Values)"/>
		</xsl:when>
		<xsl:otherwise>0</xsl:otherwise>
	</xsl:choose>
</xsl:function>
<!--  -->
<xsl:function name="misc:GetIntegerFromRomanNumberChar" as="xs:integer">
	<xsl:param name="Input" as="xs:string?"/>
	<xsl:variable name="temp" as="xs:string?" select="upper-case(normalize-space($Input))"/>
	<xsl:choose>
		<xsl:when test="$temp = 'I' ">1</xsl:when>
		<xsl:when test="$temp = 'V' ">5</xsl:when>
		<xsl:when test="$temp = 'X' ">10</xsl:when>
		<xsl:when test="$temp = 'L' ">50</xsl:when>
		<xsl:when test="$temp = 'C' ">100</xsl:when>
		<xsl:when test="$temp = 'D' ">500</xsl:when>
		<xsl:when test="$temp = 'M' ">1000</xsl:when>
		<xsl:otherwise>0</xsl:otherwise>
	</xsl:choose>
</xsl:function>

In der Variablen $Values wird für jedes Zeichen des Strings ein Wert erzeugt, am Ende enthält die Variable eine Sequenz von Werten. Diese Sequenz wird mit sum() einfach aufaddiert. Ein Test, ob bei $NextCharValue das nächste Zeichen hinter dem letzen Zeichen liegt, ist hier übrigens nicht notwendig, weil in diesem Fall substring() einen Leerstring zurückgibt, dessen Wert in misc:GetIntegerFromRomanNumberChar() als 0 definiert ist.

Nachtrag: Diese Funktionen sind nun in der Beispielsammlung abgelegt.

Keine Kommentare