Mengenoperationen auf Knotenmengen mit Hilfe von Wertsemantiken ausführen

(Auszug aus "XSLT Kochbuch" von Sal Mangano)

Problem

Sie müssen die Vereinigung, den Durchschnitt, die Mengendifferenz und die symmetrische Mengendifferenz von Elementen zweier Knotenmengen ermitteln; allerdings ist in Ihrem Problem die Gleichheit nicht alsIdentität der Knotenmengen definiert. Mit anderen Worten, Gleichheit ist eine Funktion des Wertes eines Knotens.

Lösung

XSLT 1.0

Die Notwendigkeit dieser Lösung wird deutlich, wenn Sie mit mehreren Dokumenten arbeiten. Stellen Sie sich zwei Dokumente mit der gleichen DTD vor, die aber keine doppelten Elementwerte enthalten müssen. XSLT-Elemente, die aus verschiedenen Dokumenten kommen, sind verschieden, selbst wenn sie Elemente mit den gleichen Namensraum-, Attribut- und Textwerten enthalten (siehe die vier folgenden Beispiele).

Beispiel: people1.xslt

<people>
  <person name="Brad York" age="38" sex="m" smoker="ja"/>
  <person name="Charles Xavier" age="32" sex="m" smoker="nein"/>
  <person name="David Williams" age="33" sex="m" smoker="nein"/>
</people>

Beispiel: people2.xslt

<people>
  <person name="Al Zehtooney" age="33" sex="m" smoker="nein"/>
  <person name="Brad York" age="38" sex="m" smoker="ja"/>
  <person name="Charles Xavier" age="32" sex="m" smoker="nein"/>
</people>

Beispiel: Fehlgeschlagener Versuch, XSLT-Vereinigung einzusetzen, um eindeutige Personen auszuwählen.

<xsl:template match="/">
  <people>
    <xsl:copy-of select="//person | document('people2.xml')//person"/>
  </people>
</xsl:template>

Beispiel: Ausgabe bei Ausführung mit people1.xml als Eingabe.

<people>
  <person name="Brad York" age="38" sex="m" smoker="ja"/>
  <person name="Charles Xavier" age="32" sex="m" smoker="nein"/>
  <person name="David Williams" age="33" sex="m" smoker="nein"/>
  <person name="Al Zehtooney" age="33" sex="m" smoker="nein"/>
  <person name="Brad York" age="38" sex="m" smoker="ja"/>
  <person name="Charles Xavier" age="32" sex="m" smoker="nein"/>
</people>

Auch bei nur einem Dokument kann es zu Problemen kommen, wenn man sich auf die Identität von Knoten verlässt, das heißt, wenn man möchte, dass die Identität der Knoten eine Funktion ihrer Text- und Attributwerte ist.

Das folgende Stylesheet liefert eine wiederverwendbare Implementierung von Vereinigung, Durchschnitt und Mengendifferenz auf der Grundlage von Wertsemantiken. Der Grundgedanke besteht darin, dass ein Stylesheet, das dieses Stylesheet importiert, das Template außer Kraft setzt, dessen mode="vset:element-equality" gesetzt ist. Dies erlaubt es dem importierenden Stylesheet, die für die entsprechende Eingabe sinnvolle Gleichheitssemantik zu definieren:

<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:vset="http:/www.ora.com/XSLTCookbook/namespaces/vset">
  <xsl:output method="xml" version="1.0" encoding="UTF-8" indent="yes"/>
  <!-- Die vorgegebene Implementierung der Elementgleichheit. Wenn notwendig, im importierenden Stylesheet außer Kraft setzen. -->
  <xsl:template match="node( ) | @*" mode="vset:element-equality">
    <xsl:param name="other"/>
    <xsl:if test=". = $other">
      <xsl:value-of select="true( )"/>
    </xsl:if>
  </xsl:template>
  <!-- Der vorgegebene Mengenmitgliedschaftstest verwendet Elementgleichheit. Sie müssen dies im importierenden Stylesheet selten außer Kraft setzen. -->
  <xsl:template match="node( ) | @*" mode="vset:member-of">
    <xsl:param name="elem"/>
    <xsl:variable name="member-of">
      <xsl:for-each select=".">
        <xsl:apply-templates select="." mode="vset:element-equality">
          <xsl:with-param name="other" select="$elem"/>
        </xsl:apply-templates>
      </xsl:for-each>
    </xsl:variable>
    <xsl:value-of select="string($member-of)"/>
  </xsl:template>
  <!-- Berechnen der Vereinigung zweier Mengen mittels Gleichheit "nach Wert". -->
  <xsl:template name="vset:union">
    <xsl:param name="nodes1" select="/.." />
    <xsl:param name="nodes2" select="/.." />
    <!-- für die interne Benutzung -->
    <xsl:param name="nodes" select="$nodes1 | $nodes2" />
    <xsl:param name="union" select="/.." />
    <xsl:choose>
      <xsl:when test="$nodes">
        <xsl:variable name="test">
          <xsl:apply-templates select="$union" mode="vset:member-of">
            <xsl:with-param name="elem" select="$nodes[1]" />
          </xsl:apply-templates>
        </xsl:variable>
        <xsl:call-template name="vset:union">
          <xsl:with-param name="nodes" select="$nodes[position( ) &gt; 1]" />
          <xsl:with-param name="union" select="$union | $nodes[1][not(string($test))]" />
        </xsl:call-template>
      </xsl:when>
      <xsl:otherwise>
        <xsl:apply-templates select="$union" mode="vset:union" />
      </xsl:otherwise>
    </xsl:choose>
  </xsl:template>
  <!-- Standardmäßiges Zurückliefern einer Kopie der Vereinigung. Im importierenden Stylesheet überschreiben, um Ergebnisse als "Callback" zu empfangen-->
  <xsl:template match="/ | node( ) | @*" mode="vset:union">
   <xsl:copy-of select="."/>
  </xsl:template>
  <!-- Berechnen des Durchschnitts zweier Mengen mittels Gleichheit "nach Wert". -->
  <xsl:template name="vset:intersection">
    <xsl:param name="nodes1" select="/.."/>
    <xsl:param name="nodes2" select="/.."/>
    <!-- für die interne Benutzung -->
    <xsl:param name="intersect" select="/.."/>
    <xsl:choose>
      <xsl:when test="not($nodes1)">
        <xsl:apply-templates select="$intersect" mode="vset:intersection"/>
      </xsl:when>
      <xsl:when test="not($nodes2)">
        <xsl:apply-templates select="$intersect" mode="vset:intersection"/>
      </xsl:when>
      <xsl:otherwise>
        <xsl:variable name="test1">
          <xsl:apply-templates select="$nodes2" mode="vset:member-of">
            <xsl:with-param name="elem" select="$nodes1[1]"/>
          </xsl:apply-templates>
        </xsl:variable>
        <xsl:variable name="test2">
          <xsl:apply-templates select="$intersect" mode="vset:member-of">
            <xsl:with-param name="elem" select="$nodes1[1]"/>
          </xsl:apply-templates>
        </xsl:variable>
        <xsl:choose>
          <xsl:when test="string($test1) and not(string($test2))">
            <xsl:call-template name="vset:intersection">
              <xsl:with-param name="nodes1" select="$nodes1[position( ) &gt; 1]"/>
              <xsl:with-param name="nodes2" select="$nodes2"/>
              <xsl:with-param name="intersect" select="$intersect | $nodes1[1]"/>
            </xsl:call-template>
          </xsl:when>
          <xsl:otherwise>
            <xsl:call-template name="vset:intersection">
              <xsl:with-param name="nodes1" select="$nodes1[position( ) &gt; 1]"/>
              <xsl:with-param name="nodes2" select="$nodes2"/>
              <xsl:with-param name="intersect" select="$intersect"/>
            </xsl:call-template>
          </xsl:otherwise>
        </xsl:choose>
      </xsl:otherwise>
    </xsl:choose>
  </xsl:template>
  <!-- Standardmäßiges Zurückliefern einer Kopie des Durchschnitts. Im importierenden Stylesheet überschreiben, um Ergebnisse als "Callback" zu empfangen-->
  <xsl:template match="/ | node( ) | @*" mode="vset:intersection">
    <xsl:copy-of select="."/>
  </xsl:template>
  <!-- Berechnen der Differenz zweier Mengen (nodes1 - nodes2) mittels Gleichheit "nach Wert". -->
  <xsl:template name="vset:difference">
    <xsl:param name="nodes1" select="/.."/>
    <xsl:param name="nodes2" select="/.."/>
    <!-- für die interne Benutzung -->
    <xsl:param name="difference" select="/.."/>
    <xsl:choose>
      <xsl:when test="not($nodes1)">
        <xsl:apply-templates select="$difference" mode="vset:difference"/>
      </xsl:when>
      <xsl:when test="not($nodes2)">
        <xsl:apply-templates select="$nodes1" mode="vset:difference"/>
      </xsl:when>
      <xsl:otherwise>
        <xsl:variable name="test1">
          <xsl:apply-templates select="$nodes2" mode="vset:member-of">
            <xsl:with-param name="elem" select="$nodes1[1]"/>
          </xsl:apply-templates>
        </xsl:variable>
        <xsl:variable name="test2">
          <xsl:apply-templates select="$difference" mode="vset:member-of">
            <xsl:with-param name="elem" select="$nodes1[1]"/>
          </xsl:apply-templates>
        </xsl:variable>
        <xsl:choose>
          <xsl:when test="string($test1) or string($test2)">
            <xsl:call-template name="vset:difference">
              <xsl:with-param name="nodes1" select="$nodes1[position( ) &gt; 1]"/>
              <xsl:with-param name="nodes2" select="$nodes2"/>
              <xsl:with-param name="difference" select="$difference"/>
            </xsl:call-template>
          </xsl:when>
          <xsl:otherwise>
            <xsl:call-template name="vset:difference">
              <xsl:with-param name="nodes1" select="$nodes1[position( ) &gt; 1]"/>
              <xsl:with-param name="nodes2" select="$nodes2"/>
              <xsl:with-param name="difference" select="$difference | $nodes1[1]"/>
            </xsl:call-template>
          </xsl:otherwise>
        </xsl:choose>
      </xsl:otherwise>
    </xsl:choose>
  </xsl:template>
  <!-- Standardmäßiges Zurückliefern einer Kopie der Differenz. Im importierenden Stylesheet überschreiben, um die Ergebnisse als "Callback" zu empfangen-->
  <xsl:template match="/ | node( ) | @*" mode="vset:difference">
    <xsl:copy-of select="."/>
  </xsl:template>
</xsl:stylesheet>

Diese rekursiven Templates werden im Sinne der folgenden Definitionen implementiert:

Vereinigung(knoten1,knoten2)

Die Vereinigung enthält alles in knoten2 sowie alles in knoten1, was nicht bereits Mitglied von knoten2 ist.

Durchschnitt(knoten1,knoten2)

Der Durchschnitt enthält alles in knoten1, was auch Mitglied von knoten2 ist.

Differenz(knoten1,knoten2)

Die Differenz enthält alles in knoten1, was kein Mitglied von knoten2 ist.

In allen Fällen ist die Mitgliedschaft standardmäßig die Gleichheit der Stringwerte, allerdings kann das importierende Stylesheet diese Vorgabe außer Kraft setzen.

Angesichts dieser wertorientierten Mengenoperationen können Sie die gewünschte Wirkung bei people1.xml und people2.xml mit Hilfe des folgenden Stylesheets erzielen:

<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:vset="http:/www.ora.com/XSLTCookbook/namespaces/vset">
  <xsl:import href="set.ops.xslt"/>
  <xsl:output method="xml" version="1.0" encoding="UTF-8" indent="yes"/>
  <xsl:template match="/">
    <people>
      <xsl:call-template name="vset:union">
        <xsl:with-param name="nodes1" select="//person"/>
        <xsl:with-param name="nodes2" select="document('people2.xml')//person"/>
      </xsl:call-template>
    </people>
  </xsl:template>
  <!--Personengleichheit wird so definiert, dass der gleiche Name vorliegen muss -->
  <xsl:template match="person" mode="vset:element-equality">
    <xsl:param name="other"/>
    <xsl:if test="@name = $other/@name">
      <xsl:value-of select="true( )"/>
    </xsl:if>
  </xsl:template>
</xsl:stylesheet>

XSLT 2.0

Die wichtigste Verbesserung von XSLT 2.0 besteht darin, dass die Verfügbarkeit von erstklassigen Funktionen und Sequenzen ausgenutzt wird. Dadurch sind Rekursion und der Callback-Trick nicht mehr erforderlich, außerdem führt es zu saubereren Definitionen und klarerer Verwendung. Die Funktionen vset:element-equality und vset:member-of können in importierenden Stylesheets weiterhin außer Kraft gesetzt werden, um das Verhalten anzupassen.

<xsl:stylesheet version="2.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:vset="http:/www.ora.com/XSLTCookbook/namespaces/vset">
  <!-- Die vorgegebene Implementierung der Elementgleichheit. Wenn notwendig, im importierenden Stylesheet außer Kraft setzen. -->
  <xsl:function name="vset:element-equality" as="xs:boolean">
    <xsl:param name="item1" as="item( )?"/>
    <xsl:param name="item2" as="item( )?"/>
    <xsl:sequence select="$item1 = $item2"/>
  </xsl:function>
  <!-- Der vorgegebene Mengenmitgliedschaftstest verwendet Elementgleichheit. Dies müssen Sie im importierenden Stylesheet selten überschreiben. -->
  <xsl:function name="vset:member-of" as="xs:boolean">
    <xsl:param name="set" as="item( )*"/>
    <xsl:param name="elem" as="item( )"/>
    <xsl:variable name="member-of" as="xs:boolean*" select="for $test in $set return if (vset:element-equality($test, $elem)) then true( ) else ( )"/>
    <xsl:sequence select="not(empty($member-of))"/>
  </xsl:function>
  <!-- Berechnen der Vereinigung zweier Mengen mittels Gleichheit "nach Wert". -->
  <xsl:function name="vset:union" as="item( )*">
    <xsl:param name="nodes1" as="item( )*"  />
    <xsl:param name="nodes2" as="item( )*" />
    <xsl:sequence select="$nodes1, for $test in $nodes2 return if (vset:member-of($nodes1,$test)) then ( ) else $test"/>
  </xsl:function>
  <!-- Berechnen des Durchschnitts zweier Mengen mittels Gleichheit "nach Wert". -->
  <xsl:function name="vset:intersection" as="item( )*">
    <xsl:param name="nodes1" as="item( )*" />
    <xsl:param name="nodes2" as="item( )*" />
    <xsl:sequence select="for $test in $nodes1 return if (vset:member-of($nodes2,$test)) then $test else ( )"/>
  </xsl:function>
  <!-- Berechnen der Differenz zweier Mengen (nodes1 - nodes2) mittels Gleichheit "nach Wert". -->
  <xsl:function name="vset:difference" as="item( )*">
    <xsl:param name="nodes1" as="item( )*" />
    <xsl:param name="nodes2" as="item( )*" />
    <xsl:sequence select="for $test in $nodes1 return if (vset:member-of($nodes2,$test)) then ( ) else $test"/>
  </xsl:function>
</xsl:stylesheet>

Diskussion

Sie glauben vielleicht, dass Gleichheit ein triviales Problem ist – zwei Dinge sind entweder gleich oder sie sind es nicht. Bei der Programmierung liegt Gleichheit jedoch (wie in der Politik) im Auge des Betrachters. In einem typischen Dokument wird ein Element mit einem eindeutig identifizierbaren Objekt verknüpft. Beispielsweise ist ein Absatzelement, <p>...</p>, von einem anderen Absatzelement irgendwo anders im Dokument verschieden, auch wenn beide den gleichen Inhalt aufweisen. Daher sind Mengenoperationen, die auf der eindeutigen Identität von Elementen basieren, die Norm. Wenn Sie jedoch XSLT-Operationen betrachten, die mehrere Dokumente einbeziehen oder auf Elementen stattfinden, die aus der Anwendung von xsl:copy resultieren, müssen Sie sorgfältig festlegen, welcher Art die Gleichheit sein soll.

Hier sind einige Abfragebeispiele, in denen Wertmengensemantiken erforderlich sind:

  1. Sie haben zwei Dokumente aus unterschiedlichen Namensräumen. Die vier folgenden Beispiele helfen Ihnen, alle (lokalen) Elementnamen zu ermitteln, die diese Dokumente gemeinsam haben und die für den jeweiligen Namensraum eindeutig sind.

Beispiel: doc1.xml

<doc xmlns:doc1="doc1" xmlns="doc1">
  <chapter label="1">
    <section label="1">
      <p>Es war einmal...</p>
    </section>
  </chapter>
  <chapter label="2">
    <note to="editor">Ich warte immer noch auf meinen Vorschuss von $100000.</note>
    <section label="1">
      <p>... und sie lebten glücklich bis an ihr Ende.</p>
    </section>
  </chapter>
</doc>

Beispiel: doc2.xml

<doc xmlns:doc1="doc2" xmlns="doc2">
  <chapter label="1">
    <section label="1">
      <sub>
        <p>Es war einmal... <ref type="footnote" label="1"/></p>
      </sub>
      <fig>Figure1</fig>
    </section>
    <footnote label="1">Hey schubidubi.</footnote>
  </chapter>
  <chapter label="2">
    <section label="1">
      <p>... und sie lebten glücklich bis an ihr Ende.</p>
    </section>
  </chapter>
</doc>

Beispiel: unique-element-names.xslt

<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:doc1="doc1" xmlns:doc2="doc2" xmlns:vset="http:/www.ora.com/XSLTCookbook/namespaces/vset" extension-element-prefixes="vset">
  <xsl:import href="set.ops.xslt"/>
  <xsl:output method="text" />
  <xsl:template match="/">
    <xsl:text>&#xa;Gemeinsame Elemente sind: </xsl:text>
    <xsl:call-template name="vset:intersection">
      <xsl:with-param name="nodes1" select="//*"/>
      <xsl:with-param name="nodes2" select="document('doc2.xml')//*"/>
    </xsl:call-template>
    <xsl:text>&#xa;Elemente nur in doc1 sind: </xsl:text>
    <xsl:call-template name="vset:difference">
      <xsl:with-param name="nodes1" select="//*"/>
      <xsl:with-param name="nodes2" select="document('doc2.xml')//*"/>
    </xsl:call-template>
    <xsl:text>&#xa;Elemente nur in doc2 sind: </xsl:text>
    <xsl:call-template name="vset:difference">
      <xsl:with-param name="nodes1" select="document('doc2.xml')//*"/>
      <xsl:with-param name="nodes2" select="//*"/>
    </xsl:call-template>
    <xsl:text>&#xa;</xsl:text>
  </xsl:template>
  <xsl:template match="*" mode="vset:intersection">
    <xsl:value-of select="local-name(.)"/>
    <xsl:if test="position() != last()">
      <xsl:text>, </xsl:text>
    </xsl:if>
  </xsl:template>
  <xsl:template match="*" mode="vset:difference">
    <xsl:value-of select="local-name(.)"/>
    <xsl:if test="position() != last()">
      <xsl:text>, </xsl:text>
    </xsl:if>
  </xsl:template>
  <xsl:template match="doc1:* | doc2:*" mode="vset:element-equality">
    <xsl:param name="other"/>
    <xsl:if test="local-name(.) = local-name($other)">
      <xsl:value-of select="true()"/>
    </xsl:if>
  </xsl:template>
</xsl:stylesheet>

Beispiel: Ausgabe.

Gemeinsame Elemente sind: doc, chapter, section, p
Elemente nur in doc1 sind: note
Elemente nur in doc2 sind: sub, ref, fig, footnote

  1. Ein Visio-XML-Dokument besteht aus Master-Shapes, Master-Shape-Instanzen und benutzerdefinierten Shapes ohne dazugehörenden Master. Sie wollen die Daten für alle eindeutigen Shapes extrahieren. Für den Zweck dieser Abfrage sind zwei Shapes gleich, wenn eine der folgenden Bedingungen wahr ist:
    • Beide haben Master-Attribute, @Master, und diese Attributwerte sind gleich.
    • Wenigstens einem fehlt ein Master-Attribut, aber ihre Geometrie-Elemente, Geom, sind gleich. Geometrie-Elemente sind gleich, wenn alle Attribute aller Abkömmlinge von Geom gleich sind.

    Ansonsten sind sie nicht gleich.

    Diese Abfrage kann implementiert werden, indem der Durchschnitt der Menge aller Shapes mit sich selbst gebildet wird, wobei die bereits genannten Regeln für Gleichheit gelten.

    Anmerkung:
    Ein Mathematiker wird Ihnen erklären, dass der Durchschnitt einer Menge mit sich selbst immer die gleiche Menge ergibt. Dies gilt in der Tat für richtige Mengen (ohne Duplikate). Hier verwenden Sie jedoch einen anwendungsspezifischen Begriff von Gleichheit, und die Knotenmengen werden unter diesem Gleichheitstest typischerweise keine echten Mengen sein. Allerdings erzeugen die Wertmengenoperationen immer echte Mengen, sodass diese Technik eine Methode darstellt, um Duplikate zu entfernen.

    Sie können auch das Template vset:union mit dem nodes-Parameter benutzen:

<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:vxd="urn:schemas-microsoft-com:office:visio" xmlns:vset="http:/www.ora.com/XSLTCookbook/namespaces/vset" extension-element-prefixes="vset">
  <xsl:import href="set.ops.xslt"/>
  <xsl:output method="xml" version="1.0" encoding="UTF-8" indent="yes"/>
  <xsl:template match="/">
    <UniqueShapes>
      <xsl:call-template name="vset:intersection">
        <xsl:with-param name="nodes1" select="//vxd:Pages/*/*/vxd:Shape"/>
        <xsl:with-param name="nodes2" select="//vxd:Pages/*/*/vxd:Shape"/>
      </xsl:call-template>
    </UniqueShapes>
  </xsl:template>
  <xsl:template match="vxd:Shape" mode="vset:intersection">
    <xsl:copy-of select="." />
  </xsl:template>
  <xsl:template match="vxd:Shape" mode="vset:element-equality">
    <xsl:param name="other"/>
    <xsl:choose>
      <xsl:when test="@Master and $other/@Master and @Master = $other/@Master">
        <xsl:value-of select="true( )"/>
      </xsl:when>
      <xsl:when test="not(@Master) or not($other/@Master)">
        <xsl:variable name="geom1">
          <xsl:for-each select="vxd:Geom//*/@*">
            <xsl:sort select="name( )"/>
            <xsl:value-of select="."/>
          </xsl:for-each>
        </xsl:variable>
        <xsl:variable name="geom2">
          <xsl:for-each select="$other/vxd:Geom//*/@*">
            <xsl:sort select="name( )"/>
            <xsl:value-of select="."/>
          </xsl:for-each>
        </xsl:variable>
        <xsl:if test="$geom1 = $geom2">
          <xsl:value-of select="true( )"/>
        </xsl:if>
      </xsl:when>
    </xsl:choose>
  </xsl:template>
</xsl:stylesheet>

  

<< zurück vor >>

 

 

 

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