Search code examples
xmlxsltsetfilteringmultimap

Map XML content with XSLT


I am trying to list all the nodes which contains a child node which of a certain id.

Take this example xml:

<foo>
  <bar name="hello">
    <baz id="1" />
  </bar>
  <bar name="there">
    <baz id="1" />
    <baz id="2" />
  </bar>
  <bar name="world">
    <baz id="3" />
  </bar>
</foo>

I've come up with the following XSLT template which contains two nested for-each loops

<xsl:for-each select="/foo/bar/baz">
  <xsl:variable name="id" select="@id" />
    <xsl:value-of select="$id" />
    <ul>
      <xsl:for-each select="/foo/bar/baz">
        <xsl:variable name="local_id" select="@id" />
        <xsl:variable name="bar_name" select="../@name" />

        <xsl:if test="$id = $local_id">
          <li><xsl:value-of select="$bar_name" /></li>
        </xsl:if>

      </xsl:for-each>
    </ul>
</xsl:for-each>

Which gives the following result

1
- hello
- there
1
- hello
- there
2
- there
3
- world

The problem is that the first key/values pair is duplicated.


Solution

  • To keep the solution as it is you can change the first for-each that it only consider the first occurrence of an id.

    <xsl:for-each select="/foo/bar/baz[not (preceding::baz/@id = @id)] ">
    

    This is by far not the best solution for this kind of "problem". To improve this have a look for " Grouping Using the Muenchian Method" (E.g.. And also it is better practice to use apply-templates instead of for-each.

    Here a key based solution:

    <?xml version="1.0" encoding="UTF-8"?>
    <xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">
      <xsl:output method="xml" indent="yes"/>
        <xsl:key name="kBazId" match="bar/baz" use="@id"/>
    
        <xsl:template match="/" >
            <xsl:for-each select="/foo/bar/baz[count( . | key('kBazId', @id)[1])=1]" >
                <xsl:value-of select="@id" />
                <ul>
                    <xsl:apply-templates select="key('kBazId', @id)/.." />
                </ul>
            </xsl:for-each>
        </xsl:template>
    
        <xsl:template match="bar">
            <li>
                <xsl:value-of select="@name"/>
            </li>
        </xsl:template>
    </xsl:stylesheet>