Im Katalog suchen Bücher A-Z
XSLT

XSLT

Doug Tidwell
1. Auflage März 2002
3-89721-292-7
544 Seiten

Kapitel 6

Sortieren und Gruppieren von Elementen

Ich hoffe, Sie sind jetzt davon überzeugt, daß Sie mit XSLT große Stapel XML-Daten in andere nützliche Dinge konvertieren können. Unsere Beispiele haben sich bisher beim Verarbeiten der XML-Quelle immer an die sogenannte Dokumentenreihenfolge gehalten. Nun jedoch wollen wir unsere XML-Dokumente auf verschiedene andere Weisen betrachten:

In diesem Kapitel werden wir Ihnen einige Beispiele für diese Vorgehensweisen liefern.

Daten mit < xsl:sort> sortieren

Die einfachste Methode zum Umarrangieren unserer XML-Elemente besteht darin, das Element <xsl:sort> einzusetzen. Dieses Element ordnet eine Sammlung von Elementen auf der Grundlage von Kriterien, die wir in unserem Stylesheet definieren, zeitweise neu an.

Unser erstes Beispiel

In unserem ersten Beispiel bedienen wir uns einer Gruppe von US-amerikanischen Postadressen, die wir sortieren wollen. Hier ist unser Originaldokument:

<?xml version="1.0"?>
<addressbook>
  <address>
    <name>
      <title>Mr.</title>
      <first-name>Chester Hasbrouck</first-name>
      <last-name>Frisby</last-name>
    </name>
    <street>1234 Main Street</street>
    <city>Sheboygan</city>
    <state>WI</state>
    <zip>48392</zip>
  </address>
  <address>
    <name>
      <first-name>Mary</first-name>
      <last-name>Backstayge</last-name>
    </name>
    <street>283 First Avenue</street>
    <city>Skunk Haven</city>
    <state>MA</state>
    <zip>02718</zip>
  </address>
  <address>
    <name>
      <title>Ms.</title>
      <first-name>Natalie</first-name>
      <last-name>Attired</last-name>
    </name>
    <street>707 Breitling Way</street>
    <city>Winter Harbor</city>
    <state>ME</state>
    <zip>00218</zip>
  </address>
  <address>
    <name>
      <first-name>Harry</first-name>
      <last-name>Backstayge</last-name>
    </name>
    <street>283 First Avenue</street>
    <city>Skunk Haven</city>
    <state>MA</state>
    <zip>02718</zip>
  </address>
  <address>
    <name>
      <first-name>Mary</first-name>
      <last-name>McGoon</last-name>
    </name>
    <street>103 Bryant Street</street>
    <city>Boylston</city>
    <state>VA</state>
    <zip>27318</zip>
  </address>
  <address>
    <name>
      <title>Ms.</title>
      <first-name>Amanda</first-name>
      <last-name>Reckonwith</last-name>
    </name>
    <street>930-A Chestnut Street</street>
    <city>Lynn</city>
    <state>MA</state>
    <zip>02930</zip>
  </address>
</addressbook>

Wir möchten nun eine Liste dieser Adressen erzeugen, die nach <last-name> sortiert ist. Um dieses Ziel zu erreichen, benutzen wir das magische Element <xsl:sort>. Unser Stylesheet sieht folgendermaßen aus:

<?xml version="1.0"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">

  <xsl:output method="text" indent="no"/>
  <xsl:strip-space elements="*"/>

  <xsl:variable name="newline">
<xsl:text>
</xsl:text>
  </xsl:variable>

  <xsl:template match="/">
    <xsl:for-each select="addressbook/address">
      <xsl:sort select="name/last-name"/>
      <xsl:value-of select="name/title"/>
      <xsl:text> </xsl:text>
      <xsl:value-of select="name/first-name"/>
      <xsl:text> </xsl:text>
      <xsl:value-of select="name/last-name"/>
      <xsl:value-of select="$newline"/>
      <xsl:value-of select="street"/>
      <xsl:value-of select="$newline"/>
      <xsl:value-of select="city"/>
      <xsl:text>, </xsl:text>
      <xsl:value-of select="state"/>
      <xsl:text>  </xsl:text>
      <xsl:value-of select="zip"/>
      <xsl:value-of select="$newline"/>
      <xsl:value-of select="$newline"/>
    </xsl:for-each>
  </xsl:template>
</xsl:stylesheet>

Das Herzstück unseres Stylesheets sind die Elemente <xsl:for-each> und <xsl:sort>. Das Element <xsl:for-each> wählt diejenigen Einträge aus, mit denen wir arbeiten werden. Das Element <xsl:sort> ordnet diese Einträge neu an, bevor wir sie ausgeben.

Beachten Sie, daß wir eine Textdatei erzeugen (<xsl:output method="text"/>). (Sie könnten auch eine HTML-Datei oder etwas noch Komplizierteres generieren, wenn Sie wollen.) Um die Stylesheet-Engine zu starten, führen wir folgenden Befehl aus:

java org.apache.xalan.xslt.Process -in names.xml -xsl namesorter1.xsl
  -out names.text

Bei unserem ersten Versuch mit der Sortierung erhalten wir diese Ergebnisse:

Ms. Natalie Attired
707 Breitling Way
Winter Harbor, ME  00218

 Mary Backstayge
283 First Avenue
Skunk Haven, MA  02718

 Harry Backstayge
283 First Avenue
Skunk Haven, MA  02718

Mr. Chester Hasbrouck Frisby
1234 Main Street
Sheboygan, WI  48392

 Mary McGoon
103 Bryant Street
Boylston, VA  27318

Ms. Amanda Reckonwith
930-A Chestnut Street
Lynn, MA  02930

Wie Sie an der Ausgabe erkennen können, wurden die Adressen aus unserem Originaldokument nach dem Nachnamen sortiert. Wir mußten lediglich das Element xsl:sort in unser Stylesheet einfügen, und alle Elemente wurden wie von Zauberhand neu angeordnet. Wenn Sie nicht davon überzeugt sind, daß XSLT Ihre Produktivität als Programmierer erhöhen kann, dann versuchen Sie einmal, den Java-Code und die DOM-Methodenaufrufe zu schreiben, mit denen Sie die gleiche Aktion ausführen.

Wir können unser ursprüngliches Stylesheet aber noch verbessern. Zum einen gibt es am Anfang jedes Namens, der kein <title>-Element enthält, noch ein störendes Leerzeichen. Eine deutlichere Verbesserung erhalten wir, indem wir die Adressen innerhalb des <last-name> (Nachnamen) noch nach dem <first-name> (Vornamen) sortieren. In unserem letzten Beispiel soll Mary Backstayge erst nach Harry Backstayge auftauchen. Wir müssen unser Stylesheet folgendermaßen modifizieren, um mehr als einen Sortierschlüssel zu verwenden:

<xsl:template match="/">
  <xsl:for-each select="addressbook/address">
    <xsl:sort select="name/last-name"/>
    <xsl:sort select="name/first-name"/>
    ...

Wir haben in unser Stylesheet einfach ein zweites <xsl:sort>-Element eingefügt. Dieses Element tut genau das, was wir wollen: Es sortiert die <address>-Elemente nach dem <first-name> innerhalb des <last-name>. Um unsere Ausgabe endgültig zu perfektionieren, können wir ein <xsl:if>-Element einsetzen, mit dessen Hilfe wir den störenden freien Platz vor Namen ohne <title>-Element loswerden:

<xsl:if test="name/title">
  <xsl:value-of select="name/title"/>
  <xsl:text> </xsl:text>
</xsl:if>

Jetzt ist unsere Ausgabe perfekt:

Ms. Natalie Attired
707 Breitling Way
Winter Harbor, ME  00218

Harry Backstayge
283 First Avenue
Skunk Haven, MA  02718

Mary Backstayge
283 First Avenue
Skunk Haven, MA  02718

Mr. Chester Hasbrouck Frisby
1234 Main Street
Sheboygan, WI  48392

Mary McGoon
103 Bryant Street
Boylston, VA  27318

Ms. Amanda Reckonwith
930-A Chestnut Street
Lynn, MA  02930

Nähere Informationen über das Element < xsl:sort>

Nachdem wir uns einige Beispiele für die Funktionsweise des Elements <xsl:sort> angeschaut haben, wollen wir uns mit seiner Syntax, seinen Attributen sowie seinen Einsatzmöglichkeiten befassen.

Was ist los mit seiner Syntax?

Ich bin so froh, daß Sie mich das fragen. Die XSLT-Arbeitsgruppe hätte so etwas tun können:

<xsl:for-each select="addressbook/address" sort-key-1="name/last-name"
  sort-key-2="name/first-name"/>

Das Problem bei diesem Vorgehen ist, daß – unabhängig davon, wieviele sort-key-x-Attribute Sie definieren – irgendjemand aus purer Bosheit rufen würde, daß er unbedingt das Attribut sort-key-8293 benötigt. Um dieses lästige Problem zu umgehen, beschlossen die XSLT-Entwickler, daß Sie die Sortierschlüssel festlegen können, indem Sie eine Anzahl von <xsl:sort>-Elementen benutzen. Der erste ist der primäre Sortierschlüssel, der zweite ist der sekundäre Sortierschlüssel, der 8293. ist der achttausendzweihundertunddreiundneunzigste Sortierschlüssel usw.

Nun, aus diesem Grund sieht die Syntax so aus, wie sie aussieht, aber funktioniert sie auch tatsächlich? Als ich diese Syntax

<xsl:for-each select="addressbook/address">
  <xsl:sort select="name/last-name"/>
  <xsl:sort select="name/first-name"/>
  <xsl:apply-templates select="."/>
</xsl:for-each>

zum ersten Mal sah, dachte ich, sie würde bedeuten, daß alle Knoten bei jedem Durchlauf des <xsl:for-each>-Elements sortiert werden würden. Das schien unglaublich ineffizient; weshalb sollten Sie, wenn Sie alle Knoten sortiert haben, sie jedesmal erneut durch das Element <xsl:for-each> sortieren lassen? Tatsächlich verarbeitet der XSLT-Prozessor alle <xsl:sort>-Elemente, bevor er etwas anderes tut. Anschließend betrachtet er die <xsl:for-each>-Elemente so, als gäbe es keine <xsl:sort>-Elemente.

Es ist zwar weniger effizient, aber vielleicht fühlen Sie sich besser, wenn Sie das Stylesheet so schreiben:

<xsl:template match="/">
  <xsl:for-each select="addressbook/address">
    <xsl:sort select="name/last-name"/>
    <xsl:sort select="name/first-name"/>
    <xsl:for-each select=".">  <!-- Dies ist langsamer, aber
  es funktioniert -->
      <xsl:apply-templates select="."/>
    </xsl:for-each>
  </xsl:for-each>
</xsl:template>

(Tun Sie es nicht wirklich. Ich versuche nur, einen Gedanken zu verdeutlichen.) Dieses Stylesheet erzeugt die gleichen Ergebnisse wie unser früheres Stylesheet.

Attribute

Das Element <xsl:sort> besitzt mehrere Attribute, die wir Ihnen hier vorstellen werden.

select

Das Attribut select definiert die beim Suchen eingesetzte Charakteristik. Es enthält einen XPath-Ausdruck, das heißt, Sie können Elemente, Text, Attribute, Kommentare, Vorfahren usw. auswählen. Wie üblich wird der XPath-Ausdruck, der in select definiert ist, in bezug auf den aktuellen Kontext ausgewertet.

data-type

Das Attribut data-type kann drei Werte enthalten:

  • data-type="text"

  • data-type="number"

  • data-type="QName", der einen bestimmten Datentyp kennzeichnet. Die XSLT-Arbeitsgruppe strebt an, daß hier irgendwann einmal die Datentypen unterstützt werden, die in der XML Schema-Spezifikation definiert sind.

Die XSLT-Spezifikation definiert das Verhalten für data-type="text" und data-type="number". Schauen Sie sich einmal dieses XML-Dokument an:

<?xml version="1.0"?>
<numberlist>
  <number>127</number>
  <number>23</number>
  <number>10</number>
</numberlist>

Wir verwenden beim Sortieren den vorgegebenen Wert (data-type="text"):

<?xml version="1.0"?>
<xsl:stylesheet version="1.0"
  xmlns:xsl="http://www.w3.org/1999/XSL/Transform">

  <xsl:output method="text" indent="no"/>
  <xsl:strip-space elements="*"/>

  <xsl:variable name="newline">
<xsl:text>
</xsl:text>
  </xsl:variable>

  <xsl:template match="/">
    <xsl:for-each select="numberlist/number">
      <xsl:sort select="."/>
      <xsl:value-of select="."/>
      <xsl:value-of select="$newline"/>
    </xsl:for-each>
  </xsl:template>
</xsl:stylesheet>

Wenn wir diese Elemente mit data-type="text" sortieren, erhalten wir folgendes:

10
127
23

Wir erhalten diese Ergebnisse, weil eine textbasierte Sortierung alles, was mit einer »1« beginnt, vor alles setzt, was mit einer »2« anfängt. Ändern wir das Element <xsl:sort> auf <xsl:sort select="." data-type="number"/>, erhalten wir diese Ergebnisse:

10
27
123

Wenn wir hier etwas anderes benutzen (zum Beispiel data-type="floating-point"), können wir nur raten, wie der XSLT-Prozessor reagiert. Die XSLT-Spezifikation läßt zwar andere Werte zu, es ist aber Sache des XSLT-Prozessors zu entscheiden, wie (oder ob) er diese Werte verarbeiten will. Schauen Sie in die Dokumentation Ihres Prozessors um festzustellen, ob dort etwas Sinnvolles zu anderen Werten als data-type="text" oder data-type="number" steht.

Ein letzter Hinweis: Wenn Sie data-type="number" verwenden und einige der Werte keine Zahlen sind, werden diese nicht-numerischen Werte vor den numerischen Werten sortiert. Somit kommen die nicht-numerischen Werte zuerst, wenn Sie order="ascending" benutzen; setzen Sie dagegen order="descending" ein, kommen die nicht-numerischen Werte zuletzt.

<?xml version="1.0"?>
<numberlist>
  <number>127</number>
  <number>23</number>
  <number>zzz</number>
  <number>10</number>
  <number>yyy</number>
</numberlist>

Hier sind die korrekt sortierten Ergebnisse für diese alles andere als perfekten Daten:

zzz
yyy
10
23
127

Beachten Sie, daß die nicht-numerischen Werte nicht sortiert waren; sie erscheinen im Ausgabedokument einfach in der Reihenfolge, in der sie im Originaldokument genannt wurden.

order

Sie können die Richtung der Sortierung als order="ascending" (aufsteigend) oder order="descending" (absteigend) festlegen. Vorgabe ist order="ascending".

case-order

Dieses Attribut kann zwei Werte annehmen. case-order="upper-first" bedeutet, daß Großbuchstaben vor Kleinbuchstaben sortiert werden. case-order="lower-first" gibt den umgekehrten Fall an. Das Attribut case-order wird nur berücksichtigt, wenn das data-type-Attribut text ist. Der Vorgabewert hängt vom Wert des im folgenden erläuterten Attributs lang ab.

lang

Dieses Attribut legt die Sprache der Sortierschlüssel fest. Gültige Werte für dieses Attribut sind die gleichen Werte wie für das Attribut xml:lang. Dessen Werte sind in Abschnitt 2.12 der XML 1.0-Spezifikation definiert. Die Sprachcodes entsprechen denjenigen, die in der Java-Programmierung, in Unix-Locales und an anderen Stellen verwendet werden, an denen ISO-Sprachen und Ländernamen festgelegt sind. So bedeutet lang="en" zum Beispiel »Englisch«, lang="en-US" bedeutet »US-Englisch« und lang="en-GB" bedeutet »britisches Englisch«. Ohne das Attribut lang (es wird in der Praxis selten eingesetzt), entnimmt der XSLT-Prozessor die vorgegebene Sprache der Systemumgebung.

Wo können Sie <xsl:sort> einsetzen?

Das Element <xsl:sort> kann innerhalb von zwei Elementen auftreten:

Wenn Sie ein <xsl:sort>-Element innerhalb von <xsl:for-each> einsetzen, muß das <xsl:sort>-Element (bzw. müssen die <xsl:sort>-Elemente, falls es mehrere gibt) als erstes auftauchen. Sollten Sie so etwas probieren, erhalten Sie vom XSLT-Prozessor eine Fehlermeldung:

<xsl:for-each select="addressbook/address">
  <xsl:sort select="name/last-name"/>
  <xsl:value-of select="name/title"/>
  <xsl:sort select="name/first-name"/> <!-- NICHT ZULÄSSIG! -->
  ...

Ein weiteres Beispiel

Wir haben das Element <xsl:sort> bis hierher schon ganz ausführlich beleuchtet. Um unserem Beispiel einen weiteren Kniff hinzuzufügen, ändern wir das Stylesheet so, daß das Element xsl:sort auf eine Teilmenge der Adressen angewendet wird und diese Teilmenge dann sortiert. Wir werden nur Adressen von Staaten sortieren, die mit dem Buchstaben M beginnen. Wie Sie sich vielleicht denken, erledigen wir diese kleine Zauberei mit einem XPath-Ausdruck, der die zu sortierenden Elemente einschränkt:

<?xml version="1.0"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
  <xsl:output method="text" indent="no"/>
  <xsl:strip-space elements="*"/>
  <xsl:variable name="newline">
<xsl:text>
</xsl:text>
  </xsl:variable>

  <xsl:template match="/">
    <xsl:for-each select="addressbook/address/[starts-with(state, 'M')]">
      <xsl:sort select="name/last-name"/>
      <xsl:sort select="name/first-name"/>
      <xsl:if test="name/title">
        <xsl:value-of select="name/title"/>
        <xsl:text> </xsl:text>
      </xsl:if>
      <xsl:value-of select="name/first-name"/>
      <xsl:text> </xsl:text>
      <xsl:value-of select="name/last-name"/>
      <xsl:value-of select="$newline"/>
      <xsl:value-of select="street"/>
      <xsl:value-of select="$newline"/>
      <xsl:value-of select="city"/>
      <xsl:text>, </xsl:text>
      <xsl:value-of select="state"/>
      <xsl:text>  </xsl:text>
      <xsl:value-of select="zip"/>
      <xsl:value-of select="$newline"/>
      <xsl:value-of select="$newline"/>
    </xsl:for-each>
  </xsl:template>
</xsl:stylesheet>

Hier sind die Ergebnisse: Adressen von Staaten, die mit dem Buchstaben M beginnen, sortiert nach dem Vornamen innerhalb des Nachnamens:

Ms. Natalie Attired
707 Breitling Way
Winter Harbor, ME  00218

Harry Backstayge
283 First Avenue
Skunk Haven, MA  02718

Mary Backstayge
283 First Avenue
Skunk Haven, MA  02718

Ms. Amanda Reckonwith
930-A Chestnut Street
Lynn, MA  02930

Beachten Sie, daß wir in dem xsl:for-each-Element ein Prädikat in unserem XPath-Ausdruck verwendet haben, so daß nur Adressen ausgewählt wurden, die <state>-Elemente enthalten, deren Inhalt mit M beginnt. Dieses Beispiel bietet einen guten Übergang zum nächsten Thema, dem Gruppieren von Knoten. Wir könnten hier noch eine Menge anderer Dinge tun:

Wir werden im nächsten Abschnitt auf diese Themen eingehen.

Gruppieren von Knoten

Wenn wir Knoten gruppieren, sortieren wir Dinge, um sie in eine bestimmte Reihenfolge zu bringen, und anschließend gruppieren wir alle Einträge, die für den oder die Sortierschlüssel den gleichen Wert aufweisen. Wir werden xsl:sort für diese Gruppierung verwenden und dann Variablen oder Funktionen wie key() oder generate-id() einsetzen, um die Aufgabe zu beenden.

Unser erster Versuch

Für unser erstes Beispiel werden wir die Postadressen nehmen und gruppieren. Wir suchen nach den einzelnen Werten des Elements <zip> und geben die Adressen aus, die auf die jeweiligen Werte zutreffen. Wir werden die Liste also anhand der Postleitzahlen sortieren. Entspricht ein bestimmter Eintrag nicht der vorangegangenen Postleitzahl, geben wir eine Zwischenüberschrift aus; entspricht er dagegen dem vorangegangenen Wert, geben wir nur die Adresse aus. Hier ist unser erster Versuch:

<?xml version="1.0"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
  <xsl:output method="text" indent="no"/>
  <xsl:variable name="newline">
<xsl:text>
</xsl:text>
  </xsl:variable>

  <xsl:template match="/">
    <xsl:text>Anhand der Postleitzahlen sortierte Adressen</xsl:text>
    <xsl:value-of select="$newline"/>
    <xsl:for-each select="addressbook/address">
      <xsl:sort select="zip"/>
      <xsl:if test="zip!=preceding-sibling::address[1]/zip">
        <xsl:value-of select="$newline"/>
        <xsl:text>Postleitzahl </xsl:text>
        <xsl:value-of select="zip"/>
        <xsl:text> (</xsl:text>
        <xsl:value-of select="city"/>
        <xsl:text>, </xsl:text>
        <xsl:value-of select="state"/>
        <xsl:text>): </xsl:text>
        <xsl:value-of select="$newline"/>
      </xsl:if>
      <xsl:if test="name/title">
        <xsl:value-of select="name/title"/>
        <xsl:text> </xsl:text>
      </xsl:if>
      <xsl:value-of select="name/first-name"/>
      <xsl:text> </xsl:text>
      <xsl:value-of select="name/last-name"/>
      <xsl:value-of select="$newline"/>
      <xsl:value-of select="street"/>
      <xsl:value-of select="$newline"/>
      <xsl:value-of select="$newline"/>

    </xsl:for-each>
  </xsl:template>
</xsl:stylesheet>

In diesem Stylesheet gehen in wir in zwei Schritten vor:

  1. Wir sortieren die Adressen anhand der Postleitzahl.

    <xsl:sort select="zip"/>
  2. Wir geben für jede Postleitzahl, die nicht der vorangegangenen Postleitzahl entspricht, eine Überschrift und anschließend die dazugehörenden Adressen aus.

    <xsl:if test="zip!=preceding-sibling::address[1]/zip">
      <xsl:value-of select="$newline"/>
      <xsl:text>Postleitzahl </xsl:text>
      ...

    (Erinnern Sie sich daran, daß preceding-sibling ein NodeSet zurückliefert, das heißt, preceding-sibling::address[1] repräsentiert den ersten vorhergehenden Geschwisterknoten.)

Das klingt vernünftig, nicht wahr? Schauen wir uns die Ergebnisse an:

Anhand der Postleitzahlen sortierte Adressen

Postleitzahl 00218 (Winter Harbor, ME):
Ms. Natalie Attired
707 Breitling Way


Postleitzahl 02718 (Skunk Haven, MA):
Mary Backstayge
283 First Avenue

Harry Backstayge
283 First Avenue


Postleitzahl 02930 (Lynn, MA):
Ms. Amanda Reckonwith
930-A Chestnut Street


Postleitzahl 27318 (Boylston, VA):
Mary McGoon
103 Bryant Street

Mr. Chester Hasbrouck Frisby
1234 Main Street

Ja, das scheint sicher ein guter Ansatz zu sein, es gibt aber ein kleines Problem: Er funktioniert nicht.

Wenn wir unsere Ergebnisse betrachten, scheint es nur ein Problem zu geben: Eine der Adressen (Mr. Chester Hasbrouck Frisby) wurde der Überschrift für Boylston, Virginia, zugeordnet, dabei lebt er tatsächlich in Sheboygan, Wisconsin, Postleitzahl 48392. Das Problem hierbei ist, daß die Achsen mit der Dokumentenreihenfolge arbeiten, nicht mit der sortierten Reihenfolge, die wir innerhalb des xsl:for-each-Elements erzeugt haben.

So einfach uns unser Ansatz auch erscheint, wir müssen eine andere Möglichkeit finden.

Ein Brute-Force-Ansatz (»Hauruck-Verfahren«)

Wir könnten die Transformation beispielsweise in zwei Durchläufen durchführen; wir könnten ein Zwischen-Stylesheet schreiben, das die Namen sortiert und ein neues XML-Dokument erzeugt. Anschließend benutzen wir das Stylesheet, das wir bereits geschrieben haben, da Dokumentenreihenfolge und sortierte Reihenfolge dann gleich sind. Dieses Zwischen-Stylesheet sieht dann so aus:

<?xml version="1.0"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">

  <xsl:output method="xml" indent="no"/>
  <xsl:strip-space elements="*"/>

  <xsl:template match="/">
    <addressbook>
      <xsl:for-each select="addressbook/address">
        <xsl:sort select="name/last-name"/>
        <xsl:sort select="name/first-name"/>
        <xsl:copy-of select="."/>
      </xsl:for-each>
    </addressbook>
  </xsl:template>
</xsl:stylesheet>

Dieses Stylesheet erzeugt ein neues <addressbook>-Dokument, in dem alle <address>-Elemente korrekt sortiert sind. Wir können dann unser ursprüngliches Stylesheet auf das sortierte Dokument anwenden und die gewünschten Ergebnisse erzielen. Das funktioniert, ist aber nicht besonders elegant. Was sogar noch schlimmer ist, es ist ziemlich langsam, weil wir in der Mitte anhalten, eine Datei auf die Festplatte schreiben und dann die Daten erneut einlesen müssen. Wir sollten eine Methode suchen, um die Elemente in einem einzigen Stylesheet gruppieren zu können. Dazu brauchen wir aber eine andere Technik.

Gruppieren mit < xsl:variable>

Wir haben bereits früher erwähnt, daß das Element <xsl:variable> manchmal ganz gut zum Gruppieren geeignet ist, wir wollen daher diesen Ansatz einmal ausprobieren. Wir werden den Wert des Elements <zip> jedesmal durch das Element <xsl:for-each> sichern und preceding-sibling auf eine etwas andere Weise benutzen. Versuch Nummer drei sieht so aus:

<?xml version="1.0"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">

  <xsl:output method="text" indent="no"/>

  <xsl:variable name="newline">
<xsl:text>
</xsl:text>
  </xsl:variable>

  <xsl:template match="/">
    <xsl:text>Anhand der Postleitzahlen sortierte Adressen</xsl:text>
    <xsl:value-of select="$newline"/>
    <xsl:for-each select="addressbook/address">
      <xsl:sort select="zip"/>
      <xsl:sort select="name/last-name"/>
      <xsl:sort select="name/first-name"/>
      <xsl:variable name="lastZip" select="zip"/>
      <xsl:if test="not(preceding-sibling::address[zip=$lastZip])">
        <xsl:text>Postleitzahl </xsl:text>
        <xsl:value-of select="zip"/>
        <xsl:text>: </xsl:text>
        <xsl:value-of select="$newline"/>
        <xsl:for-each select="/addressbook/address[zip=$lastZip]">
          <xsl:sort select="name/last-name"/>
          <xsl:sort select="name/first-name"/>
          <xsl:if test="name/title">
            <xsl:value-of select="name/title"/>
            <xsl:text> </xsl:text>
          </xsl:if>
          <xsl:value-of select="name/first-name"/>
          <xsl:text> </xsl:text>
          <xsl:value-of select="name/last-name"/>
          <xsl:value-of select="$newline"/>
          <xsl:value-of select="street"/>
          <xsl:value-of select="$newline"/>
          <xsl:value-of select="$newline"/>
        </xsl:for-each>
      </xsl:if>
    </xsl:for-each>
  </xsl:template>
</xsl:stylesheet>

Dieses Stylesheet erzeugt die gewünschte Ausgabe:

Anhand der Postleitzahlen sortierte Adressen
Postleitzahl 00218:
Ms. Natalie Attired
707 Breitling Way

Postleitzahl 02718:
Harry Backstayge
283 First Avenue

Mary Backstayge
283 First Avenue

Postleitzahl 02930:
Ms. Amanda Reckonwith
930-A Chestnut Street

Postleitzahl 27318:
Mary McGoon
103 Bryant Street

Postleitzahl 48392:
Mr. Chester Hasbrouck Frisby
1234 Main Street

Weshalb funktioniert nun dieser Ansatz, während dies bei unserem ersten Versuch nicht der Fall war? Die Antwort: Wir verlassen uns nicht auf die sortierte Reihenfolge der Elemente, um unsere Ausgabe zu generieren. Nachteilig ist bei diesem Ansatz, daß wir mehrere Schritte durchlaufen müssen, um die gewünschten Ergebnisse zu erzielen:

  1. Wir sortieren alle Adressen nach ihrer Postleitzahl:

    <xsl:sort select="zip"/>
  2. Wir speichern den Wert des aktuellen <zip>-Elements in der Variablen lastZip:

    <xsl:variable name="lastZip" select="zip"/>
  3. Wir schauen uns bei jedem <zip>-Element die vorhergehenden Geschwister an um festzustellen, ob dieser bestimmte Wert (der in lastZip gespeichert ist) das erste Mal aufgetreten ist. Ist dies der Fall, gibt es unter den vorhergehenden Geschwistern keinen passenden Wert.

    <xsl:if test="not(preceding-sibling::address[zip=$lastZip])">
  4. Wenn dies das erste Mal ist, daß wir im Element <zip> auf diesen Wert gestoßen sind, gehen wir zurück und wählen alle <address>-Elemente mit <zip>-Kindelementen aus, die diesem Wert entsprechen. Haben wir diese Gruppe ermittelt, sortieren wir sie innerhalb der Nachnamen nach dem Vornamen und geben die einzelnen Adressen aus.

    <xsl:for-each select="/addressbook/address[zip=$lastZip]">
      <xsl:sort select="name/last-name"/>
      <xsl:sort select="name/first-name"/>

Wir haben jetzt also eine Methode ermittelt, mit deren Hilfe wir die gewünschten Ergebnisse erhalten. Diese Methode ist aber recht ineffizient. Wir sortieren die Daten, anschließend schauen wir uns die Postleitzahlen in der sortierten Reihenfolge an, stellen dann fest, ob dieser Wert in der Dokumentenreihenfolge bereits aufgetreten ist, wählen erneut alle Einträge aus, die der aktuellen Postleitzahl entsprechen und sortieren sie erneut, bevor wir sie ausgeben. Puh! Es muß einen besseren Weg geben, nicht wahr? Nun, da wir noch nicht am Ende dieses Kapitels angekommen sind, besteht noch eine Chance, daß wir im nächsten Abschnitt eine bessere Möglichkeit finden. Lesen Sie einfach weiter....

Der < xsl:key>-Ansatz

In diesem Abschnitt werden wir uns anschauen, wie man <xsl:key> verwenden kann, um Einträge in einem XML-Dokument zu gruppieren. Dieser Ansatz wird nach dem Oracle-XML-Guru (und O'Reilly-Autor) Steve Muench, der diese Technik als erster vorschlug, häufig als »Muench-Methode« bezeichnet. Die Muench-Methode besteht aus drei Schritten:

  1. Definieren eines key (Schlüssel) für die Eigenschaft, die wir zum Gruppieren einsetzen wollen.

  2. Auswählen aller Knoten, die wir gruppieren wollen. Wir werden einige Tricks mit den Funktionen key() und generate-id() vollführen, um die eindeutigen Gruppierungswerte zu ermitteln.

  3. Benutzung der Funktion key() für jeden eindeutigen Gruppierungswert, um alle zutreffenden Knoten zu ermitteln. Da die key()-Funktion eine Knotenmenge zurückliefert, können wir mit der Menge der Knoten, die auf einen bestimmten Gruppierungswert zutreffen, weitere Sortierungen durchführen.

Nun, so funktioniert diese Technik – erstellen wir nun ein Stylesheet, das unsere Vorstellungen umsetzt. Der erste Schritt, das Erzeugen einer Schlüsselfunktion, ist einfach. Die Funktion sieht folgendermaßen aus:

<xsl:key name="zipcodes" match="address" use="zip"/>

Dieses <xsl:key>-Element definiert einen neuen Index namens zipcodes. Er indexiert <address>-Elemente auf der Grundlage des <zip>-Elements, das er enthält.

Nachdem wir unseren key definiert haben, können wir zum komplizierteren Teil übergehen. Wir verwenden die Funktionen key() und generate-id() zusammen. Es kommt folgende Syntax zum Einsatz, die wir gleich ausführlich vorstellen:

<xsl:for-each select="//address[generate-id(.)=
  generate-id(key('zipcodes', zip)[1])]">

OK, atmen wir alle noch einmal tief durch und stürzen wir uns dann auf diese Syntax. Wir wählen hier alle <address>-Elemente aus, in denen die automatisch erzeugte id der automatisch erzeugten id des ersten Knotens entspricht, der durch die key()-Funktion zurückgegeben wird, wenn wir nach allen <address>-Elementen fragen, die dem aktuellen <zip>-Element entsprechen.

Das ist sonnenklar, nicht wahr?! Ich werde versuchen, dies aus einer etwas anderen Perspektive heraus zu erklären.

Wir benutzen für jedes <address>-Element die Funktion key(), um alle <address>-Elemente zu erhalten, die die gleiche Postleitzahl (<zip>) haben. Anschließend nehmen wir den ersten Knoten aus dieser Knotenmenge. Zum Schluß benutzen wir die Funktion generate-id(), um für beide Knoten eine id zu generieren. Die beiden Knoten sind gleich, wenn die beiden erzeugten ids identisch sind.

Hu. Lassen Sie mich kurz verschnaufen.

Wenn diese <address> auf den ersten Knoten paßt, der durch die Funktion key() zurückgegeben wird, wissen wir, daß wir das erste <address>-Element gefunden haben, das diesem Gruppierungswert entspricht. Die Auswahl aller ersten Werte (erinnern Sie sich, unser vorheriges Prädikat endet mit [1]) liefert uns eine Knotenmenge mit einer bestimmten Anzahl von <address>-Elementen, von denen jedes einen der eindeutigen, von uns benötigten Gruppierungswerte enthält.

So funktioniert also diese Technik. Wir haben jetzt eine Methode ermittelt, mit der wir eine Knotenmenge generieren können, die alle eindeutigen Gruppierungswerte beinhaltet. Nun müssen wir diese Knoten verarbeiten. An dieser Stelle werden wir verschiedene Dinge erledigen, die alle vergleichsweise einfach sind:

Hier ist unser komplettes Stylesheet:

<?xml version="1.0"?>

<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">

  <xsl:output method="html" indent="no"/>

  <xsl:key name="zipcodes" match="address" use="zip"/>

  <xsl:template match="/">
    <table border="1">
      <xsl:for-each select="//address[generate-id(.)=
        generate-id(key('zipcodes', zip)[1])]">
        <xsl:sort select="zip"/>
        <xsl:for-each select="key('zipcodes', zip)">
          <xsl:sort select="name/last-name"/>
          <xsl:sort select="name/first-name"/>
          <tr>
            <xsl:if test="position() = 1">
              <td valign="center" bgcolor="#999999">
                <xsl:attribute name="rowspan">
                  <xsl:value-of select="count(key('zipcodes', zip))"/>
                </xsl:attribute>
                <b>
                  <xsl:text>Postleitzahl </xsl:text><xsl:value-of select="zip"/>
                </b>
              </td>
            </xsl:if>
            <td align="right">
              <xsl:value-of select="name/first-name"/>
              <xsl:text> </xsl:text>
              <b><xsl:value-of select="name/last-name"/></b>
            </td>
            <td>
              <xsl:value-of select="street"/>
              <xsl:text>, </xsl:text>
              <xsl:value-of select="city"/>
              <xsl:text>, </xsl:text>
              <xsl:value-of select="state"/>
              <xsl:text> </xsl:text>
              <xsl:value-of select="zip"/>
            </td>
          </tr>
        </xsl:for-each>
      </xsl:for-each>
    </table>
  </xsl:template>

</xsl:stylesheet>

Wenn wir das entstandene HTML-Dokument in einem Browser betrachten, sollte es so aussehen wie in Abbildung 6-1.

Abbildung 6-1

Abbildung 6-1: HTML-Dokument mit gruppierten Einträgen

Beachten Sie die Zusammenarbeit der beiden <xsl:for-each>- und der verschiedenen <xsl:sort>-Elemente. Das äußere <xsl:for-each>-Element wählt die einzelnen Werte des <zip>-Elements aus und sortiert sie. Das innere <xsl:for-each>-Element wählt alle <address>-Elemente, die auf das aktuelle <zip>-Element zutreffen, und sortiert sie dann nach <last-name> und <first-name>.

Zusammenfassung

In diesem Kapitel haben wir uns mit den gebräuchlichen Techniken zum Sortieren und Gruppieren von Elementen befaßt. Ungeachtet der Arten von Stylesheets, die Sie in Ihren XML-Projekten schreiben müssen, werden Sie diese Techniken vermutlich bei allem einsetzen, was Sie tun. Nachdem wir wissen, wie Elemente sortiert und gruppiert werden, sprechen wir als nächstes darüber, wie mehrere Eingabedokumente kombiniert werden. Dieses Thema baut auf den hier behandelten Themen auf.

zurück zu XSLT


O'Reilly Home | O'Reilly-Partnerbuchhandlungen | Bestellinformationen
Kontaktieren Sie uns | Über O'Reilly | Datenschutz

© 2002, O'Reilly Verlag GmbH & Co. KG