Eine XML-Hierarchie vertiefen

(Auszug aus "XSLT Kochbuch" von Sal Mangano)

Problem

Sie haben ein schlecht gestaltetes Dokument, das zusätzliche Struktur gebrauchen kann. (In Hinblick auf bestimmte Ziele mag es gut gestaltet sein, dies sind jedoch nicht Ihre Ziele.)

Lösung

Dieses Problem ist demjenigen aus dem vorherigen Rezept Eine XML-Hierarchie reduzieren genau entgegengesetzt. Hier müssen Sie einem Dokument zusätzliche Struktur verleihen, möglicherweise, um seine Elemente mit Hilfe weiterer Kriterien zu organisieren.

Fügen Sie Struktur auf der Grundlage existierender Daten hinzu

Diese Art der Vertiefungstransformation macht die Reduktionstransformation rückgängig, die im vorherigen Rezept ausgeführt wurde:

<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
  <xsl:import href="copy.xslt"/>
  <xsl:output method="xml" version="1.0" encoding="UTF-8" indent="yes"/>
  <xsl:strip-space elements="*"/>
  <xsl:template match="people">
    <union>
      <xsl:apply-templates select="person[@class = 'union']" />
    </union>
    <salaried>
      <xsl:apply-templates select="person[@class = 'salaried']" />
    </salaried>
  </xsl:template>
</xsl:stylesheet>

Fügen Sie Struktur hinzu, um ein schlecht gestaltetes Dokument zu korrigieren

In einem fehlgeleiteten Versuch, XML zu vereinfachen, kodieren manche Leute Informationen, indem sie Geschwisterelemente anstelle von Elternelementen hinzufügen. (Um fair zu sein: Nicht jedes Auftreten dieser Technik ist fehlgeleitet. Entwurf ist die Gratwanderung zwischen widerstreitenden Nachteilen.)

Nehmen Sie beispielsweise an, jemand hätte auf folgende Weise zwischen Gewerkschafts- und Nichtgewerkschaftsmitgliedern unterschieden:

<people>
  <class name="union"/>
  <person>
    <firstname>Warren</firstname>
    <lastname>Rosenbaum</lastname>
    <age>37</age>
    <height>5.75</height>
  </person>
  ...
  <person>
    <firstname>Theresa</firstname>
    <lastname>Archul</lastname>
    <age>37</age>
    <height>5.5</height>
  </person>
  <class name="salaried"/>
  <person>
    <firstname>Sal</firstname>
    <lastname>Mangano</lastname>
    <age>37</age>
    <height>5.75</height>
  </person>
  ...
  <person>
    <firstname>James</firstname>
    <lastname>O'Riely</lastname>
    <age>33</age>
    <height>5.5</height>
  </person>
</people>

Beachten Sie, dass die Elemente, die union- und salaried-class-Elemente kennzeichnen, nun leer sind. Es ist beabsichtigt, dass alle nachfolgenden Geschwister eines class-Elements zu dieser Klasse gehören, bis ein neues class-Element entdeckt wird oder es keine weiteren Geschwister gibt. Diese Art der Kodierung ist einfach zu verstehen, für ein XSLT-Programm aber schwieriger zu verarbeiten. Um diese Darstellung zu korrigieren, müssen Sie ein Stylesheet erzeugen, das die Mengendifferenz zwischen allen person-Elementen, die dem ersten Vorkommen eines class-Elements folgen, und den person-Elementen, die dem nächsten Vorkommen eines class-Elements folgen, berechnet. XSLT 1.0 besitzt keine explizite Mengendifferenzfunktion. Sie erzielen prinzipiell die gleiche Wirkung und das sogar noch effizienter, wenn Sie alle Elemente betrachten, die einem class-Element folgen, dessen Position niedriger ist als die Position der Elemente, die dem nächsten class-Element folgen:

<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
  <xsl:import href="copy.xslt"/>
  <xsl:output method="xml" version="1.0" encoding="UTF-8" indent="yes"/>
  <xsl:strip-space elements="*"/>
  <!-- Gesamtanzahl an Leuten -->
  <xsl:variable name="num-people" select="count(/*/person)"/>
  <xsl:template match="class">
    <!--Die letzte Position, die wir betrachten wollen. -->
    <xsl:variable name="pos" select="$num-people - count(following-sibling::class/following-sibling::person)"/>
    <xsl:element name="{@name}">
      <!-- Kopieren der Leute, die dieser Klasse folgen, deren Position aber kleiner oder gleich $pos ist.-->
      <xsl:copy-of select="following-sibling::person[position( ) &lt;= $pos]"/>
    </xsl:element>
  </xsl:template>
  <!-- Ignorieren der person-Elemente. Sie wurden oben kopiert. -->
  <xsl:template match="person"/>
</xsl:stylesheet>

Es kann aber auch ein Schlüssel verwendet werden:

<xsl:key name="people" match="person" use="preceding-sibling::class[1]/@name" />
<xsl:template match="people">
  <people>
    <xsl:apply-templates select="class" />
  </people>
</xsl:template>
<xsl:template match="class">
  <xsl:element name="{@name}">
    <xsl:copy-of select="key('people', @name)" />
  </xsl:element>
</xsl:template>

Ein schrittweiser Ansatz ist eine weitere Alternative:

<xsl:template match="people">
  <people>
    <xsl:apply-templates select="class[1]" />
  </people>
</xsl:template>
<xsl:template match="class">
  <xsl:element name="{@name}">
    <xsl:apply-templates select="following-sibling::*[1][self::person]" />
  </xsl:element>
  <xsl:apply-templates select="following-sibling::class[1]" />
</xsl:template>
<xsl:template match="person">
  <xsl:copy-of select="." />
  <xsl:apply-templates select="following-sibling::*[1][self::person]" />
</xsl:template>

XSLT 2.0

Hinzufügen von Struktur auf der Grundlage existierender Daten

Mit der XSLT 2.0-Funktion xsl:for-each-group können Sie eine bessere (»generischere«) Lösung erzielen als mit XSLT 1.0. Es gibt zwar 1.0-Lösungen, die generisch sind (siehe Diskussion), aber keine ist so einfach:

<xsl:template match="people">
  <xsl:for-each-group select="person" group-by="preceding-sibling::class[1]/@name">
    <xsl:element name="{curent-grouping-key( )}">
      <xsl:apply-templates select="current-group( )" />
    </xsl:element>
  </xsl:for-each>
</xsl:template>

Hinzufügen von Struktur, um ein schlecht gestaltetes Dokument zu korrigieren

Sie können xsl:for-each-group mit der Option group-starting-with ausnutzen, um dieses Problem zu lösen:

<xsl:stylesheet version="2.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
  <xsl:import href="copy.xslt"/>
  <xsl:output method="xml" version="1.0" encoding="UTF-8" indent="yes"/>
  <xsl:template match="people">
    <xsl:copy>
      <xsl:for-each-group select="*" group-starting-with="class">
        <xsl:element name="{@name}">
          <xsl:apply-templates select="current-group( )[not(self::class)]"/>
        </xsl:element>
      </xsl:for-each-group>
    </xsl:copy>
  </xsl:template>
</xsl:stylesheet>

Diskussion

Hinzufügen von Struktur auf der Grundlage existierender Daten

Als Sie auf der Grundlage existierender Daten Struktur hinzufügten, haben Sie explizit auf die Kriterien verwiesen, die die interessierenden Kategorien geformt haben (z.B. union und salaried). Es wäre besser, wenn das Stylesheet diese Kategorien selbst herausfinden würde. Das Stylesheet wäre dann zwar komplexer, aber auch generischer:

<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
  <xsl:import href="copy.xslt"/>
  <xsl:output method="xml" version="1.0" encoding="UTF-8" indent="yes"/>
  <xsl:strip-space elements="*"/>
  <!-- Aufbauen einer eindeutigen Liste aller Klassen -->
  <xsl:variable name="classes" select="/*/*/@class[not(. = ../preceding-sibling::*/@class)]"/>
  <xsl:template match="/*">
    <!-- Für jede Klasse wird ein Element erzeugt, das nach der Klasse benannt wird, die Elemente dieser Klasse enthält -->
    <xsl:for-each select="$classes">
      <xsl:variable name="class-name" select="."/>
      <xsl:element name="{$class-name}">
        <xsl:for-each select="/*/*[@class=$class-name]">
          <xsl:copy>
            <xsl:apply-templates/>
          </xsl:copy>
        </xsl:for-each>
      </xsl:element>
    </xsl:for-each>
  </xsl:template>
</xsl:stylesheet>

Dieses Stylesheet ist zwar nicht 100% generisch, es vermeidet es aber zumindest, Annahmen darüber zu treffen, welche Arten von Klassen in dem Dokument existieren. Die einzige anwendungsspezifische Information in diesem Stylesheet ist die Tatsache, dass die Kategorien in einem Attribut @class kodiert sind und dass das Attribut in Elementen auftaucht, die zwei Ebenen unterhalb der Wurzel liegen.

Struktur hinzufügen, um ein schlecht gestaltetes Dokument zu korrigieren

Die Lösung kann explizit im Sinne der Mengendifferenz implementiert werden. Diese Lösung ist elegant, aber für große Dokumente mit vielen Kategorien unpraktisch. Der hier eingesetzte Trick zum Berechnen der Mengendifferenz wird im Rezept Mengenoperationen auf Knotenmengen ausführen erläutert:

<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
  <xsl:import href="copy.xslt"/>
  <xsl:output method="xml" version="1.0" encoding="UTF-8" indent="yes"/>
  <xsl:strip-space elements="*"/>
  <xsl:template match="class">
    <!--Alle Leute, die diesem class-Element folgen -->
    <xsl:variable name="nodes1" select="following-sibling::person"/>
    <!--Alle Leute, die dem nächsten class-Element folgen -->
    <xsl:variable name="nodes2" select="following-sibling::class/following-sibling::person"/>
    <xsl:element name="{@name}">
      <xsl:copy-of select="$nodes1[count(. | $nodes2) != count($nodes2)]"/>
    </xsl:element>
  </xsl:template>
  <xsl:template match="person"/>
</xsl:stylesheet>
Tipp der data2type-Redaktion:
Zum Thema XSLT bieten wir auch folgende Schulungen zur Vertiefung und professionellen Fortbildung an:

Copyright © 2006 O'Reilly Verlag GmbH & Co. KG
Für Ihren privaten Gebrauch dürfen Sie die Online-Version ausdrucken.
Ansonsten unterliegt dieses Kapitel aus dem Buch "XSLT Kochbuch" denselben Bestimmungen, wie die gebundene Ausgabe: Das Werk einschließlich aller seiner Teile ist urheberrechtlich geschützt. Alle Rechte vorbehalten einschließlich der Vervielfältigung, Übersetzung, Mikroverfilmung sowie Einspeicherung und Verarbeitung in elektronischen Systemen.

O'Reilly Verlag GmbH & Co. KG, Balthasarstraße 81, 50670 Köln, kommentar(at)oreilly.de