Verarbeitungsmethoden XSLT Streaming Betrachten wir folgendes Problem. Es soll ein kommaseparierter Report aus dieser XML Quelle generiert werden.
<status-report> <status-change> <billing_id>360788</billing_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</billing_id> <claim_ids>967382,673647</claim_ids> <status>open</status> <time_stamp>2019-02-22T13:53:34.605Z</status_time> </status-change> [...]Plain Text
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>Plain Text
Diesen Code kann man vereinfachen: Da von einer Datei eingelesen wird, brauchen wir kein Named-Template. Statt der Schleife können wir uns auch auf den rekursiven Abstieg des XSLT Prozessors verlassen:
<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>Plain Text
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 zu den
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 probeweise 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>Plain Text
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 19Plain TextIn 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>Plain Text
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.
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.