Archiv für Juli, 2010

XSLT/XPath: arithmetische Operationen mit einer Leersequenz

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.

Keine Kommentare

Java in XSLT verwenden

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

2 Kommentare