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]'),'&#10;')"/>
  </xsl:for-each>
</xsl:template>
Hinweis
NOTIZ
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.
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]'),'&#10;')"/>
</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]'),'&#10;')"/>
    </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]'),'&#10;')"/>
          </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 ( &#10; ) 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.
Previous Page Next Page
Version: 93
Jan 25 2021