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.

Arbeiten mit optionalen Elementen

Es gibt Developer, die die Arbeit mit optionalen Elementen (minOccurs="0", analog optionale Attribute) im XML Input entspannt und sorgenfrei betrachten. Diese Sorglosigkeit kann Programmierfehler und Informationsverlust nach sich ziehen.

Ja, es stimmt: optionale Elemente können fehlen. Ganz legitim. Und wenn sie nicht da sind, werden sie auch nicht verarbeitet. Logisch.

Es kann aber Situationen geben, in denen zulässiges Fehlen zu realen Fehlern führt. Werden fehlende Elemente programmiertechnisch genauso behandelt, als wären sie real vorhanden, lauert darin das Risiko eines nachhaltigen Informationsverlustes. 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 das optionale 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: alles ist gut.


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

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>

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>

wg / 24. Dezember 2017



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

Mobil: 0151. 750 360 61 * eMail: info2018@wilfried-grupe.de

www.wilfried-grupe.de/optionaleElemente.html