XSL-Übersicht / Arbeiten mit optionalen Elementen

Arbeiten mit optionalen Elementen

Arbeiten mit optionalen Elementen

➪ Bei der Arbeit mit optionalen Elementen / optionalen Attributen können Programmierfehler zu Informationsverlust führen.

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 wir es uns nicht so einfach machen. Zulässiges Fehlen kann zu realen Fehlern führen. Wir können legitim fehlende Elemente programmiertechnisch nicht genauso behandeln, als wären sie real vorhanden. Das möchte ich an folgenden Beispielen zeigen:

Gehen wir 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 Childelement "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 wir dieses XML-Dokument gegen das folgende XML Schema Dokument validieren, dann erhielten wir 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>

XPath-Adressierung optionaler Werte

Möchten wir alle "Ort"-Elemente haben, deren Childnode "PLZ" leer oder nicht vorhanden ist, so können wir 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, daß 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 Sequence zu , 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 ""):


<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>

Gruppierung optionaler Werte

Wenn wir 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 wir (logischerweise) 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 wir dagegen mit "xsl:for-each-group" und gruppieren wir ü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, die das optionale Element "PLZ" nicht aufweisen, ausgeblendet. Das Ergebnis lautet:


<ergebnis>
   <PLZ info="61000">
      <Ort>Darmstadt</Ort>
   </PLZ>
</ergebnis>

Dasselbe Ergebnis erreichen wir auch mit der -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 wir uns 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 unserem Listing zu berücksichtigen, können wir für einen modifizierten XML Input sorgen, der in jedem Fall über ein "PLZ"-Element verfügt. Dies mag geschehen durch eine separate Variable, 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 wir diese temporäre Variable "vort" als Input nehmen, können wir mit der Anweisung <xsl:for-each-group select="$vort/Ort" group-by="PLZ"> dafür sorgen, daß 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 wir mit der -Konvertierung des Gruppierungsschlüssels erreichen, auch ohne eine temporäre Variable "$vort" anlegen zu müssen. Wichtig ist, daß 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 -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 / 17. März 2018



Fragen? Anmerkungen? Tips?

Bitte nehmen Sie Kontakt zu mir auf:

Vorname
Nachname
Mailadresse







Vielen Dank für Ihr Interesse an meiner Arbeit.


V.i.S.d.P.: Wilfried Grupe * Klus 6 * 37643 Negenborn

☎ 0151. 750 360 61 * eMail: info10@wilfried-grupe.de

www.wilfried-grupe.de/optionaleElemente.html