Montag, 5. Mai 2008

Dynamische Elemente in XSLT

Ein XML-Dokument mit einem festen Aufbau ist in XSLT wesentlich leichter zu behandeln als eines mit dynamischen Elementen. Dennoch sind solche dynamischen Elemente gelegentlich nötig, also Elemente eines XML-Dokuments, deren Namen man beim Design des XSLT noch nicht kennt, auf die aber trotzdem im XSLT-Programm zugegriffen werden soll. Ich will hier ein Beispiel aus der Praxis schildern.

Im Retail Store, einer produktiven Web-Anwendung, gibt es eine Startseite mit Hyperlinks auf die einzelnen angebotenen Applikationen. Da diese Startseite im wesentlichen statisch ist, habe ich sie im Rahmen des letzten Release auf XSLT umgestellt. Der gesamte statische HTML-Code ist in ein Template menu.xsl umgezogen. Der Server sendet nur noch ein rund 300 Byte grosses XML-Dokument mit den dynamischen Daten. So realisieren wir serverseitige Antwortzeiten im Bereich von 50ms für das Menü!

Das wesentliche Element eines solchen XML-Menüs sah beispielsweise so aus:
  <status filiale="2700" client="520" system="D11"/>

Also ein einzelnes inhaltloses Status-Element, dessen Attribute die aktuellen, dynamischen Zustandsdaten dieser Filiale kennzeichnen.

Die Links waren nun als Daten-Teil des XSLT-Stylesheets z.B. wie der folgende definiert:
<link href="../zsrs_wsti">Inventur</link>

Aufwände gab es nun, weil bei Weiterentwicklungen immer neue Bedingungen an das Menü gestellt wurden. Unter bestimmten Bedingungen sollten bestimme Links angezeigt werden und andere gerade nicht.

Bislang habe ich zuerst dem Server für jede diese Bedingungen beibringen müssen, weitere Attribute zu rendern. Danach musste ich clientseitig, also im XSLT-Dokument, dieses Attribut abfragen, um den gewünschten Hyperlink bedingt darzustellen. Wenn es ein neues "Unit-ID"-Verfahren gab, an dem manche Filialen teilnahmen, so wurde für diese Filialen neu der Status
  <status filiale="2700" unit_id="X" client="520" system="D11"/>

gesendet. Wenn der Inventurlink nur für Filialen mit aktivem Unit-ID-Verfahren angezeigt werden sollte, pflegte man ihn wie folgt:
<link href="../zsrs_wsti" unit_id="X">Inventur</link>

Das XSLT-Stylesheet prüfte schliesslich, ob das neue Attribut unit_id im Link angegeben war. Wenn ja, wurde der Hyperlink nur gerendert, wenn der Attributwert des Links mit dem des Status-Elements des aktuellen XML-Dokuments übereinstimmt. So konnte man bestimmte Links für Unit-ID-aktive Filialen definieren und andere, die nur für die noch nicht an dieses Verfahren angeschlossenen Filialen gelten sollten.
Dieses Verfahren erwies sich als umständlich. Um einen neuen bedingten Statuswert zu definieren, musste man

  • Auf dem Server den neuen Attributwert ermitteln

  • Den Attributwert an den für das Menü zuständigen Controller propagieren

  • Den zu sendenden XML-View um das neue Attribut erweitern

  • Die XSLT-Transformation anpassen, um das Attribut auszuwerten

  • Die bedingten Hyperlinks in der XSLT-Transformation um das neue Attribut erweitern


Nach dem dritten neuen Attribut dieser Art wurde mir das Verfahren zu umständlich. Ich überlegte mir ein neues Verfahren, bei dem nur an den beiden Endpunkten der obigen Kette noch etwas zu tun ist:

  • Der Server ermittelt die für die Filiale gültigen Statuswerte: Hier muss in einer bestimmten Methode Code hinzugefügt werden, um den neuen Statuswert zu ermitteln.

  • In den in der XSLT-Transformation enthaltenen Link-Daten ist pro Link die Abhängigkeit von diesen Statuswerten anzugeben: Unter welchen Bedingungen soll der Link erscheinen bzw. nicht erscheinen.



Alle Zwischenschritte müssen bei Erweiterungen nicht mehr geändert werden.

Die Lösung mit XSLT-Mitteln erwies sich als besonders günstig, wenn die Zustandswerte als innere Elemente des <status>-Elements im XML-Dokument gesendet werden. Etwa so:
  <status filiale="2700" client="520" system="D11">
<UNIT_ID/>
<SIM/>
</status>

Die bedingten Hyperlinks haben neu die folgende Gestalt:
  <link href="../zsrs_wsti" if="SIM SB01" if_not="UNIT_ID STV">Inventur</link>  


Die Attribute if und if_not eines Links enthalten eine Liste von Tokens, die durch Leerraum getrennt sind. Ein Link wird genau dann dargestellt, wenn

  • die Liste des if-Attributs leer ist oder jedes aufgeführte Token Elementname eines Kindelements von <status> ist, und

  • die Liste des if_not leer ist oder keines der aufgeführten Tokens unter den Kindelementen von <status> vorkommt.


Beispiel: Der Link
  <link href="../zsrs_wsti" if="SIM SB01" if_not="UNIT_ID STV">Inventur</link>  

wird nur dargestellt, wenn die Elemente <SIM> und <SB01>, aber weder <UNIT_ID> noch <STV> unter den Kindknoten von <status> vorkommen.

Wie kann man nun dem XSLT-Programm diese Regeln beibringen? Dazu noch in der Sprachversion XSLT 1.0, damit es auch auf heute üblichen Browsern ausgeführt werden kann?

Hier meine Lösung. Ein (um mich nicht wiederholen zu müssen) rekursives Template singleLinkConditional, das zunächst für das if aufgerufen wird und sich selbst danach für das if_not-Attribut aufruft. Der Parameter $status enthält die Kindelemente von <status>. Um zu prüfen, wieviele dieser Kindelemente in der Tokenliste des if- oder if_not-Attributs vorkommen, ersetze ich mittels translate() die Tokenliste durch einen Ausdruck, in dem jedes Token durch die Zeichen / eingeschlossen ist, zum Beispiel
  /UNIT_ID/SIM/

Diesen String benenne ich durch die Variable $test_cond. Nun wird durch Iteration über die Kindknoten (also die Knotenmenge $status) geprüft, welcher Elementname in dieser Liste vorkommt. Die Zuweisung
<xsl:variable name="tokens_found" 
select="count($status[contains($test_cond,concat('/',local-name(),'/'))])"/>

zählt die Anzahl dieser Knoten. Das ist die entscheidende Anweisung des Templates, das ich zur besseren Illustration hier noch wiedergebe:
        <table>
<xsl:for-each select="$groupNode/link">
<%-- Menüpunkt nur angeben, wenn if- und if_not-Bedingungen passen --%>
<xsl:choose>
<xsl:when test="@if or @if_not">
<xsl:call-template name="singleLinkConditional">
<xsl:with-param name="mode" select="'entry'"/>
<xsl:with-param name="cond" select="@if"/>
<xsl:with-param name="if_not" select="@if_not"/>
<xsl:with-param name="status" select="$status"/>
</xsl:call-template>
</xsl:when>
<xsl:otherwise>
<xsl:call-template name="singleLink"/>
</xsl:otherwise>
</xsl:choose>
</xsl:for-each>
</table>

<xsl:template name="singleLinkConditional">
<xsl:param name="mode"/>
<xsl:param name="cond"/>
<xsl:param name="if_not"/>
<xsl:param name="status"/>
<xsl:variable name="test_cond"
select="concat('/',translate(normalize-space($cond),' ','/'),'/')"/>
<xsl:variable name="tokens_cond"
select="string-length($test_cond)-string-length(translate($test_cond,'/',''))-1"/>
<xsl:variable name="tokens_found"
select="count($status[contains($test_cond,concat('/',local-name(),'/'))])"/>
<xsl:choose>
<xsl:when test="($mode = 'entry') and ( not( $cond ) or ($tokens_found = $tokens_cond) )">
<xsl:call-template name="singleLinkConditional">
<xsl:with-param name="mode" select="'not'"/>
<xsl:with-param name="cond" select="$if_not"/>
<xsl:with-param name="status" select="$status"/>
</xsl:call-template>
</xsl:when>
<xsl:when test="($mode = 'not') and ( not( $cond ) or ($tokens_found = 0) )">
<xsl:call-template name="singleLink"/>
</xsl:when>
</xsl:choose>
</xsl:template>

<xsl:template name="singleLink">
<tr valign="top">
<td>
<img align="bottom" width="19" height="20" src="../zsrs_intro/arc1.gif"/>
</td>
<td nowrap="nowrap">
<span class="service" id="{@href}">
<xsl:value-of select="."/>
</span>
</td>
</tr>
</xsl:template>

Tragende Stütze dieser Lösung sind die XPath-Funktion count() und das XPath-Konstrukt node-set[condition] zur Bildung von Knotenmengen.

Die Pflege des Menüs hat sich durch diese Änderungen enorm vereinfacht. Das obige Template ist ja nun einmal geschrieben und muss bei Erweiterungen nicht mehr geändert werden. Änderungen sind nur noch dort nötig, wo man sie auch erwarten würde: in der Link-Definition und in der serverseitigen Definition der Statuswerte.

Fazit

Mit dieser Lösung habe ich also einen Teil des XSLT-Programms in ein XML-Dokument herausextrahiert: Die Konfiguration des Menüs. Das XSLT selbst ist dann nur noch für die Präsentierungsdetails des Menüs zuständig. Darüberhinaus nimmt das Konfigurations-XML auf Elementnamen aus den dynamischen Filialinfos Bezug, ohne dass das XSLT-Programm diese Namen kennt.

Kommentare :

alain hat gesagt…

Guck mal Matchers in XSL an, sind superpraktisch und vereinfachen die Templates massiv (imho). Ein foreach-Loop sollte eigentlich nie nötig sein :)

Rüdiger Plantiko hat gesagt…

Matcher kenne ich und nutze sie auch. Warum ich deshalb jedoch foreach-Konstrukte grundsätzlich vermeiden sollte, leuchtet mir nicht ein.