Named-Templates, die direkt über den Saxon Aufruf
saxon -it:main
aufgerufen werden, sind dann
brauchbar, wenn keine eindeutige Eingabequelle vorhanden ist, bspw. weil aus mehreren
Quellen eingelesen werden soll, falls die Eingabe von einem Webservice kommt oder
vom XSLT Skript selbst erzeugt wird.
4.1.3.2 XSLT Iterator
Betrachten wir folgendes Problem. Es soll ein kommaseparierter Report aus dieser XML Quelle generiert werden.
<status-report> <status-change> <billing_id>360788</dentaltrac_encounter_id> <claim_ids>967382,673647</claim_ids> <status>open</status> <time_stamp>2019-02-22T13:53:34.605Z</status_time> </status-change> <status-change> <billing_id>360788</dentaltrac_encounter_id> <claim_ids>967382,673647</claim_ids> <status>open</status> <time_stamp>2019-02-22T13:53:34.605Z</status_time> </status-change> [...]
Mit einer
for-each
Loop und einem Named-Template würde das so gehen:
<xsl:template name="main"> <xsl:for-each select="$input-file/status-report/status-change"> <xsl:value-of select="concat(billing_id,',')"/> <xsl:value-of select="concat(claim_ids,',')"/> <xsl:value-of select="concat(status,',')"/> <xsl:value-of select="concat(format-dateTime(xs:dateTime(time_stamp), '[Y]-[M]-[D] [H]:[m]'),' ')"/> </xsl:for-each> </xsl:template>
Hinweis
NOTIZ
Im vorliegenden Fall wird von einer Datei eingelesen - wir brauchen also kein Named-Template. Statt der Schleife können wir
uns auch auf den rekursiven Abstieg des XSLT Prozessors
verlassen, was den Code weiter vereinfacht:
<xsl:template match="/status-report/status-change"> <xsl:value-of select="concat(billing_id,',')"/> <xsl:value-of select="concat(claim_ids,',')"/> <xsl:value-of select="concat(status,',')"/> <xsl:value-of select="concat(format-dateTime(xs:dateTime(time_stamp), '[Y]-[M]-[D] [H]:[m]'),' ')"/> </xsl:template>
Wollen wir große Datenmengen schnell verarbeiten - mit ein paar Hundert MB, so ist es sinnvoll
auf die neue XSLT3.0 Streaming Option umzuschalten, weil dadurch kein Eingabebaum in-Memory
aufgebaut wird. Wie schon im Kapitel XSLT Akkumulator angesprochen, gibt es dazu mehrere Möglichkeiten.
Wir betrachten hier das
xsl:iterator
(Doku) ↗↗
Konstrukt und stossen dabei auf einige Fallstricke. Zunächst einmal unsere Settings:
-
Wir benutzen xsl:source-document in Verbindung mit dem streamable='yes' Attribut, um dem Prozessor mitzuteilen, dass er im Streaming Modus arbeiten soll.
-
Wenn wir die Quelle über einen Parameter einlesen, dann müssen wir auch die Transformation über ein Named-Template starten.
Ohne zu wissen, wie XSLT Streaming genau funktioniert, setzen wir eine Reihe von
value-of select
statements in den Iterator:
<xsl:template name="main"> <xsl:source-document href="{$input-file}" streamable='yes'> <xsl:iterate select="status-report/status-change"> <xsl:value-of select="concat(billing_id,',')"/> <xsl:value-of select="concat(claim_ids,',')"/> <xsl:value-of select="concat(status,',')"/> <xsl:value-of select="concat(format-dateTime(xs:dateTime(time_stamp), '[Y]-[M]-[D] [H]:[m]'),' ')"/> </xsl:iterate> </xsl:source-document> </xsl:template>
und werden dafür prompt mit einer Fehlermeldung belohnt:
Static error on line 16 column 64 of report.xsl: XTSE3430: The body of the xsl:stream instruction is not streamable * There is more than one consuming operand: {xsl:value-of} on line 18, and {xsl:value-of} on line 19
In diesem Iterator ist also nur eine "konsumierende"
value-of
Operation erlaubt. Um nur einmal zu selektieren, müssen wir - auf Kosten der Lesbarkeit - ziemlich umbauen. Eine Lösung könnte
z.B. so aussehen:
<?xml version="1.0" encoding="UTF-8"?> <xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:xs="http://www.w3.org/2001/XMLSchema" exclude-result-prefixes="xs" xpath-default-namespace="https://tekturcms.de/schema/status-report/1.0" version="3.0"> <xsl:param name="input-file" required="yes"/> <xsl:output method="text"/> <!-- https://www.saxonica.com/html/documentation/xsl-elements/iterate.html --> <xsl:template name="main"> <xsl:source-document href="{$input-file}" streamable='yes'> <xsl:iterate select="status-report/status-change/*"> <xsl:choose> <xsl:when test="name()='time_stamp'"> <xsl:value-of select="concat(format-dateTime(xs:dateTime(time_stamp), '[Y]-[M]-[D] [H]:[m]'),' ')"/> </xsl:when> <xsl:otherwise> <xsl:value-of select="concat(.,',')"/> </xsl:otherwise> </xsl:choose> </xsl:iterate> </xsl:source-document> </xsl:template> </xsl:stylesheet>
Hier wird davon ausgegangen, dass das Element mit Namen 'time_stamp' als letztes in der Sequenz vorkommt und beim Auftreten
(
) wird ein Zeilenumbruch gesetzt. Der deklarative Ansatz aus dem ersten Beispiel geht dabei verloren.
Hinweis
NOTIZ
Logisch wird beim XSLT Streaming auf einer niedrigeren Abstraktionsebene programmiert, um den Anforderungen des Prozessors
gerecht zu werden.
Für eine 1.6 GB Datei benötigt das obige Skript auf meinem Rechner gute drei Minuten. Der traditionelle template-match Ansatz bricht mit einer Out-of-Memory Exception ab, selbst wenn man den Java Heap Size
auf 4GB einstellt.