XML | XML-Schema | XPath | XSL-T | XSL-FO | XQuery | XProc | SVG |
XSL-T / XSLT-Tipps / Arbeiten mit optionalen Elementen
![]() |
![]() |
➪ Bei der Arbeit mit optionalen Elementen/Attributen können Programmierfehler zu Informationsverlust führen.
Auf dieser Seite:Es gibt Developer, die die Arbeit mit optionalen XML-Elementen (minOccurs='0', analog optionale Attribute) entspannt und sorgenfrei betrachten. Diese Sorglosigkeit kann Programmierfehler und Informationsverlust nach sich ziehen.
Optionale Elemente können fehlen. Ganz legitim. Und wenn sie nicht da sind, werden sie auch nicht verarbeitet. Beruht die Programmierlogik jedoch wesentlich auf der möglichen Existenz der XML-Elemente, dann können Sie es sich nicht so einfach machen. Zulässiges Fehlen kann zu realen Fehlern führen. Sie können legitim fehlende Elemente/Attribute programmiertechnisch nicht genauso behandeln, als könnten Sie sich auf ihre Existenz verlassen. Das möchte ich an einigen Beispielen zeigen:
Gehen Sie von folgendem XML-Dokument aus, das jedem Ort-Element ein id-, ein name-, und optional ein PLZ-Element zuweist. Nur eines der Ort-Elemente weist das Child-Element PLZ auf, die beiden anderen nicht. Die Problematik besteht darin, auch jene Ort-Elemente im Fokus zu behalten, die nicht über den optionalen Childnode PLZ verfügen.
<Orte>
<Ort>
<id>1</id>
<name>Neustadt</name>
</Ort>
<Ort>
<id>2</id>
<name>Darmstadt</name>
<PLZ>61000</PLZ>
</Ort>
<Ort>
<id>3</id>
<name>Kapstadt</name>
</Ort>
</Orte>
Würden Sie dieses XML-Dokument gegen das folgende XML-Schema-Dokument validieren, dann erhielten Sie keine Fehlermeldung. Das minOccurs="0" bei PLZ deklariert dieses als optional. Ein mögliches Fehlen im XML-Dokument wäre legitim.
<?xml version="1.0" encoding="UTF-8"?>
<xs:schema
xmlns:xs="http://www.w3.org/2001/XMLSchema"
elementFormDefault="qualified"
attributeFormDefault="unqualified">
<xs:element name="Orte">
<xs:complexType>
<xs:sequence>
<xs:element ref="Ort" maxOccurs="unbounded"/>
</xs:sequence>
</xs:complexType>
</xs:element>
<xs:element name="Ort">
<xs:complexType>
<xs:sequence>
<xs:element ref="id"/>
<xs:element ref="name"/>
<xs:element ref="PLZ" minOccurs="0"/>
</xs:sequence>
</xs:complexType>
</xs:element>
<xs:element name="id"
type="xs:nonNegativeInteger"/>
<xs:element name="name"
type="xs:string"/>
<xs:element name="PLZ"
type="xs:string"/>
</xs:schema>
Möchten Sie alle Ort-Elemente haben, deren Childnode PLZ leer oder nicht vorhanden ist, so können Sie die folgende Logik nicht verwenden.
<ergebnis>
<xsl:for-each
select="Orte/Ort[PLZ = '']">
<PLZ info="{PLZ}">
<Ort>
<xsl:value-of select="name" />
</Ort>
</PLZ>
</xsl:for-each>
</ergebnis>
Das nicht gewünschte Resultat würde lauten:
<ergebnis/>
Der Grund für das fehlerhafte Ergebnis ist, dass XPath hier sämtliche Ort-Nodes adressiert, die einen Childnode PLZ mit leerem Inhalt haben. Das ist aber nirgendwo der Fall. Der Versuch, einen leeren String mit einer leeren Sequenz zu vergleichen, ergibt immer false().
Um das gewünschte Ergebnis
<ergebnis>
<PLZ info="">
<Ort>Neustadt</Ort>
</PLZ>
<PLZ info="">
<Ort>Kapstadt</Ort>
</PLZ>
</ergebnis>
zu erhalten, bietet sich folgende Lösung an (Orte/Ort[xs:string(PLZ) = ''] würde nicht funktionieren, siehe hierzu die Erläuterungen zu string):
<ergebnis>
<xsl:for-each select="Orte/Ort[string(PLZ) = '']">
<PLZ info="{PLZ}">
<Ort>
<xsl:value-of select="name" />
</Ort>
</PLZ>
</xsl:for-each>
</ergebnis>
... oder alternativ für alle, die gern mit erweiterter Syntax experimentieren:
<ergebnis>
<xsl:for-each select="for $yy in
Orte/Ort[xs:string(PLZ) = '' or not(PLZ)]
return $yy">
<PLZ info="{PLZ}">
<Ort>
<xsl:value-of select="name" />
</Ort>
</PLZ>
</xsl:for-each>
</ergebnis>
Wenn Sie das XML-Dokument mit dem folgenden XSL transformieren,
<ergebnis>
<xsl:for-each select="Orte/Ort">
<PLZ info="{PLZ}">
<xsl:value-of select="name" />
</PLZ>
</xsl:for-each>
</ergebnis>
... dann erhalten Sie ein Ergebnis, das zwar für jeden Ort ein PLZ-Element ausgibt, das aber nur dort mit Inhalt gefüllt ist, wo das XML-Input Dokument auch einen Childnode PLZ aufweist.
<ergebnis>
<PLZ info="">Neustadt</PLZ>
<PLZ info="61000">Darmstadt</PLZ>
<PLZ info="">Kapstadt</PLZ>
</ergebnis>
Arbeiten Sie dagegen mit xsl:for-each-group und gruppieren Sie über PLZ,
<ergebnis>
<xsl:for-each-group
select="/Orte/Ort"
group-by="PLZ">
<PLZ info="{current-grouping-key()}">
<xsl:for-each select="current-group()">
<Ort>
<xsl:value-of select="name" />
</Ort>
</xsl:for-each>
</PLZ>
</xsl:for-each-group>
</ergebnis>
so werden jene XML-Elemente ausgeblendet, die das optionale Element PLZ nicht aufweisen. Das Ergebnis lautet:
<ergebnis>
<PLZ info="61000">
<Ort>Darmstadt</Ort>
</PLZ>
</ergebnis>
Dasselbe Ergebnis erreichen Sie auch mit der xs:string-Konvertierung des Gruppierungsschlüssels:
<xsl:for-each-group
select="/Orte/Ort"
group-by="xs:string(PLZ)">
<PLZ info="{current-grouping-key()}">
<xsl:for-each select="current-group()">
<Ort><xsl:value-of select="name" />
</Ort>
</xsl:for-each>
</PLZ>
</xsl:for-each-group>
Das Ergebnis wäre hier faktisch dasselbe, als hätten Sie sich von vornherein auf die Nodelist Orte/Ort/PLZ konzentriert, die per Definition sämtliche Ort-Nodes ohne PLZ-Child ignoriert:
<ergebnis>
<xsl:for-each select="Orte/Ort/PLZ">
<PLZ info="{.}">
<Ort>
<xsl:value-of select="../name" />
</Ort>
</PLZ>
</xsl:for-each>
</ergebnis>
... oder auch mit Gruppierung:
<ergebnis>
<xsl:for-each-group
select="Orte/Ort/PLZ"
group-by=".">
<PLZ info="{.}">
<Ort>
<xsl:value-of select="../name" />
</Ort>
</PLZ>
</xsl:for-each-group>
</ergebnis>
Um auch ausgeblendete Ort-Elemente in dem Listing zu berücksichtigen, können Sie für einen modifizierten XML-Input sorgen, der in jedem Fall über ein PLZ-Element verfügt. Dies mag durch eine separate Variable geschehen, die ich hier vort genannt habe.
<xsl:variable name="vort">
<xsl:for-each select="Orte/Ort">
<Ort>
<id>
<xsl:value-of select="id" />
</id>
<name>
<xsl:value-of select="name" />
</name>
<PLZ>
<xsl:value-of select="PLZ" />
</PLZ>
</Ort>
</xsl:for-each>
</xsl:variable>
<ergebnis>
<xsl:for-each-group
select="$vort/Ort"
group-by="PLZ">
<PLZ info="{current-grouping-key()}">
<xsl:for-each select="current-group()">
<Ort><xsl:value-of select="name" /></Ort>
</xsl:for-each>
</PLZ>
</xsl:for-each-group>
</ergebnis>
Die temporäre Variable vort hat dann einen Aufbau, in dem jedes Ort-Element zuverlässig einen Childnode PLZ hat, auch wenn dessen Inhalt leer ist:
<Ort>
<id>1</id>
<name>Neustadt</name>
<PLZ/>
</Ort>
<Ort>
<id>2</id>
<name>Darmstadt</name>
<PLZ>61000</PLZ>
</Ort>
<Ort>
<id>3</id>
<name>Kapstadt</name>
<PLZ/>
</Ort>
Wenn Sie diese temporäre Variable vort als XML-Input nehmen, können Sie mit der Anweisung <xsl:for-each-group select="$vort/Ort" group-by="PLZ"> dafür sorgen, dass auch die vorher ausgeblendeten Elemente im Fokus bleiben.
<ergebnis>
<PLZ info="">
<Ort>Neustadt</Ort>
<Ort>Kapstadt</Ort>
</PLZ>
<PLZ info="61000">
<Ort>Darmstadt</Ort>
</PLZ>
</ergebnis>
Das vorstehende Ergebnis können Sie mit der string-Konvertierung des Gruppierungsschlüssels erreichen, auch ohne eine temporäre Variable $vort anlegen zu müssen. Wichtig ist, dass die group-by-Sequenz nicht leer ist. Daher würde die Alternative xs:string(PLZ) auch nicht funktionieren.
<xsl:for-each-group
select="/Orte/Ort"
group-by="string(PLZ)">
<PLZ info="{current-grouping-key()}">
...
</PLZ>
</xsl:for-each-group>
Eine weitere Alternative liegt in der Verwendung der exists-Funktion:
<xsl:for-each-group
select="$v1/Orte/Ort"
group-by="if (exists(PLZ)) then (PLZ) else ('')">
<PLZ info="{current-grouping-key()}">
...
</PLZ>
</xsl:for-each-group>
wg / 30. März 2018
Fragen? Anmerkungen? Tipps?
Bitte nehmen Sie Kontakt zu mir auf.
V.i.S.d.P.: Wilfried Grupe * Klus 6 * 37643 Negenborn
☎ 0151. 750 360 61 * eMail: info10@wilfried-grupe.de