Freitag, 13. Mai 2011

Resultatbaumfragmente sind keine Knotenmengen!

XML ist eine Auszeichnungssprache für strukturierte Daten. XSLT ist eine Programmiersprache zur Transformation von XML-Dokumenten. Wenn wir also ein XSLT-Programm schreiben und darin strukturierte Daten benötigen (in einer Situation, in der man in anderen Programmiersprachen eine auf Hashs, Arrays und Records aufgebaute Datenstruktur verwenden würde), liegt es nahe, auch diese Datenstruktur in XML-Form zu notieren.

Als triviales, illustrierendes Beispiel – ohne jeden praktischen Nutzen – nehmen wir an, wir wollen eine Übersetzung von Zahlwörtern in die Zahlen implementieren, für die sie stehen. Dann könnten wir mit einer Variablen $dict arbeiten, die einen XML-Baum als Inhalt hat:
<xsl:variable name="dict">
<one>1</one>
<two>2</two>
<three>3</three>
</xsl:variable>

Wir könnten versucht sein, den Zugriff auf diese Variable mit einem XPath-Ausdruck auszuführen: Wenn ein Parameter $key ein Zahlwort enthält, könnten wir probieren, die zugeordnete Zahl mit dem Ausdruck
<xsl:value-of select="$dict/*[name(.)=$key]"/>

zu ermitteln. Das sieht so aus, als könnte es funktionieren. Es funktioniert aber nur für einige XSLT-Prozessoren, nämlich im Internet Explorer und in ABAP.

Mehr noch: Gemäss XSLT-Spezifikation muss es auch gar nicht funktionieren! Denn der Inhalt einer <xsl:variable>-Anweisung evaluiert nicht zu einer Knotenmenge (worauf sich ein XPath-Ausdruck anwenden liesse), sondern zu einem Resultatbaumfragment (worauf sich ein XPath-Ausdruck nicht anwenden lässt).

Der folgende Code, von dem man erwarten könnte, dass er eine 2 ausgibt,
<xsl:variable name="key" select="'two'"/>
<xsl:value-of select="$dict/*[name(.)=$key]"/>

liefert beispielsweise auf dem in Java eingebauten Xalan-Prozessor nicht eine '2', sondern die nicht ganz klar nachvollziehbare Fehlermeldung
Fehler beim Überprüfen des Typs des Ausdrucks 'FilterParentPath(variable-ref(dict/result-tree), step("child", 1, pred(=(funcall(name, [step("self", -1)]), variable-ref(key/string)))))'.


Um dieses Problem zu lösen, gibt es eine Erweiterungsfunktion: Die unter dem Namen exslt (mit dem Namensraum-URI http://exslt.org/common) zusammengefassten Erweiterungen enthalten eine Funktion namens node-set( ), die ein Resultatbaumfragment in eine Knotenmenge konvertieren kann. Das obige Beispiel funktioniert also in Xalan, wenn wir es leicht modifizieren:

<xsl:variable name="key" select="'two'"/>
<xsl:value-of select="exslt:node-set($dict)/*[name(.)=$key]"/>


Das ist schön für Xalan, Google Chrome, Firefox und viele andere. Nicht so schön aber für Prozessoren, die die Erweiterungen von exslt nicht kennen, wozu der XSLT-Prozessor von ABAP und der des Microsoft Internet Explorers gehören.

Wenn es nur darum geht, auch noch Microsofts IE in den Kreis der Prozessoren aufzunehmen, die unser XSLT-Programm ausführen sollen, gibt es einen schönen Trick von David Carlisle. Er definiert die Funktion exslt:node-set() im Erweiterungsraum msxml von Microsoft:

<msxsl:script language="JScript" implements-prefix="exslt">
this['node-set'] = function (x) {
return x;
}
</msxsl:script>


Dabei macht er sich zunutze, dass die Variable $dict bei Microsoft ja bereits eine Knotenmenge ist. Daher ist die Implementierung von exslt:node-set() für diesen Prozessor einfach die identische Funktion.

Ein Trick, der aber leider nicht auf den ABAP-Prozessor anwendbar ist. Wenn wir eine Lösung für alle aufgeführten XSLT-Prozessoren suchen (die Prozessoren von Firefox, MSIE, ABAP und Java (Xalan) ), führt ein anderer Weg zum Erfolg, der leider nicht ganz so kurz ist: Wir packen für den Anfang unsere Beispielaufgabe in ein mit zwei Parametern versehenes Template: Der Parameter $dict enthält unsere strukturierten Daten als Knotenmenge. Der zweite Parameter $key ist ein String: Der Name des Elements, das wir suchen. Das Template stellt den Inhalt dieses Elements ins Ergebnis:
<xsl:template name="_getValue">
<xsl:param name="dict"/>
<xsl:param name="key"/>
<xsl:value-of select="$dict/*[local-name(.) = $key] "/>
</xsl:template>

Statt nun dieses Template direkt aufzurufen, rufen wir es indirekt über einen Wrapper auf, der den Parameter $dict je nach Prozessor zunächst mit exslt:node-set() in eine Knotenmenge verwandelt oder ihn direkt durchreicht (weil er schon eine Knotenmenge ist). Um prozessorabhängig festzustellen, welcher Fall vorliegt, können wir die Standardfunktion function-available() verwenden:
<xsl:template name="getValue">
<xsl:param name="dict"/>
<xsl:param name="key"/>
<xsl:choose>
<xsl:when test="function-available('exslt:node-set')">
<xsl:call-template name="_getValue">
<xsl:with-param name="dict" select="exslt:node-set($dict)"/>
<xsl:with-param name="key" select="$key"/>
</xsl:call-template>
</xsl:when>
<xsl:otherwise>
<xsl:call-template name="_getValue">
<xsl:with-param name="dict" select="$dict"/>
<xsl:with-param name="key" select="$key"/>
</xsl:call-template>
</xsl:otherwise>
</xsl:choose>
</xsl:template>

Dieses einhüllende Template ist also die Zwischenschicht, die von den konkreten Prozessorimplementierungen abstrahiert. Die implizite Annahme dieses Templates ist, dass ein Prozessor, der die Funktion exslt:node-set nicht kennt, einen Variableninhalt ohne weiteres Zutun als Knotenmenge behandeln kann. Für alle von mir betrachteten Prozessoren ist das der Fall.[1]

Der folgende Aufruf des Templates stellt also die Zahl 3 in die Ausgabe - sowohl wenn er auf dem Web Application Server als auch wenn es in einem Browser durchlaufen wird (und darum ging es mir):
<xsl:call-template name="getValue">
<xsl:with-param name="dict" select="$dict"/>
<xsl:with-param name="key" select="'three'"/>
</xsl:call-template>

Das Zwischen-Template kann man sich übrigens nicht ersparen. Eine direkte, bedingte Verwendung, je nach Fall von $dict oder exslt:node-set($dict) führt zu Fehlermeldungen: sobald der Prozessor herausfindet, dass die Variable $dict ein Resultatbaumfragment enthält, lehnt er es ab, sie in einem XPath-Ausdruck als Node-Set zu verwenden, auch wenn dieser Ausführungszweig ihn zur Laufzeit gar nicht betreffen wird. Nur durch den Templateaufruf wird eine Zwischenebene geschaffen: das Wissen um den Typ des Aktualparameters geht beim Abstieg im Callstack gewissermassen verloren, es darf bei der Prüfung des Templates selbst keine Rolle spielen. Nur dieser Tatsache ist es zu verdanken, dass die hier vorgestellte Lösung funktioniert.

Anmerkung am 21.8.2012 Mit dem IE9 hat Microsoft übrigens die XSLT-Engine intern auf das Objekt Msxml2.XSLTemplate.6.0 umgestellt. Diese hat eine Reihe sehr grundlegender Unterschiede zur bisherigen XSLT-Engine. Insbesondere entfällt die hier beschriebene automatische Umwandlung eines Variableninhaltes in eine Knotenmenge, wenn sie in einem Ausdruck verwendet wird. Das erfordert leider die Einführung eines weiteren Falls:
    <xsl:when test="function-available('msxsl:node-set')">
      <xsl:call-template name="_getValue">
        <xsl:with-param name="dict" select="msxsl:node-set($dict)"/>
      </xsl:call-template>
    </xsl:when>
Dabei steht msxsl für den Namensraum der Microsoft-XSLT-Erweiterungen mit dem URI urn:schemas-microsoft-com:xslt.


[1] "Alle von mir betrachteten Prozessoren" - als ich das schrieb, gab es den IE9 noch nicht. Der XSLT-Prozessor wurde zu IE9 umgestellt. Ab IE9 werden Resultatbaumfragmente auch vom Internet Explorer nicht mehr automatisch in Knotenmengen gewandelt, wenn der Kontext dies erfordert. Siehe meine Anmerkung vom 21.8.2012.

Keine Kommentare :