tkET
tkET

Reputation: 65

Issue using current() in xsl:for-each to retrieve nodes which are out of for-each's scope

Hi I am a beginner in XSLT2.0 but as XML config is required in a recent project I would give it a try. The XML structure will look like this:

<bookstore>
<books>
    <book author="a" title="a1"/>
    <book author="b" title="b1"/>
    <book author="c" title="c1"/>
    <book author="d" title="d1"/>
</books>
<topAuthorList>
    <thisMonth>c,a,d,b</thisMonth>
</topAuthorList>

And the XSLT looks like this:

    <xsl:template match="/bookstore">
    <result>
        <xsl:variable name="varList">
            <xsl:value-of select="topAuthorList/thisMonth"></xsl:value-of>
        </xsl:variable> 

        <test>
            <xsl:value-of select="books/book[@author='a']/@title"></xsl:value-of>
        </test>

        <books>
            <xsl:for-each select="tokenize($varList, ',')">
                <xsl:value-of select="books/book[@author=current()]/@title"></xsl:value-of>
            </xsl:for-each>
        </books>
    </result>
</xsl:template>

But in XMLSpy (ver 2011 rev3) it give me the error msg:

XSLT 2.0 Debugging Error: Error in XPath 2.0 expression (Type error XPTY0004: Expected a node - current item is 'c' of type xs:string)

I have search google and this site many times and couldn't find the answer. I have even tried using call-template, i.e. pass the current() as param and let the second template to handle the node selection, but still the same error.

Upvotes: 3

Views: 1812

Answers (3)

Michael Kay
Michael Kay

Reputation: 163352

When you write <xsl:value-of select="books/book[@author=current()]/@title">, the "books" is short for "child::books", that is, books elements that are children of the context item. But what is the context item? The containing xsl:for-each sets the context item to each item selected in its select expression. But that's a call on tokenize, so the context item is a string, and strings don't have children.

The concept of context item is useful when you are drilling down into a hierarchy. It's not useful when you are doing a join. For joins, you need variables. Sometimes you can get by using "." as one of the variables and "current()" as the other, but when that runs out of steam you need real named variables.

Incidentally, please don't use xsl:value-of as a child of xsl:variable unless you really want to create a new document tree. You can almost certainly rewrite the declaration of varlist as <xsl:variable name="varList" select="topAuthorList/thisMonth"> which is not only less code to write, it is also far more efficient, because it avoids the need to construct a new tree.

Upvotes: 1

Jim Garrison
Jim Garrison

Reputation: 86774

This works:

  <xsl:template match="/bookstore">
    <result>
      <xsl:variable name="varList">
        <xsl:value-of select="topAuthorList/thisMonth"></xsl:value-of>
      </xsl:variable> 

      <test>
        <xsl:value-of select="books/book[@author='a']/@title"></xsl:value-of>
      </test>

      <books>
        <xsl:variable name="books" select="/bookstore/books"/>
        <xsl:for-each select="tokenize($varList, ',')">
          <xsl:value-of select="$books/book[@author=current()]/@title"></xsl:value-of>
        </xsl:for-each>
      </books>
    </result>
  </xsl:template>

All I did was put the /bookstore/books in a variable and then do the lookup from the variable. I have an intuitive understanding of why this is needed, but don't have the exact formal reason for this without some research. Maybe one of the other XML experts here can chime in.

EDIT: I found the relevant information in Michael Kay's excellent "XSLT 2.0 and XPath 2.0" magnum opus, page 625:

A rooted path represents a path starting at the root node of the tree that contains the context node

Since the context node in your case is a naked string, it does not contain the nodes you're looking for. Using the variable provides the proper context for the XPath expression.

Upvotes: 3

Emiliano Poggi
Emiliano Poggi

Reputation: 24826

I think you want:

        <books>
            <xsl:variable name="tokens" select="tokenize($varList, ',')"/>
            <xsl:value-of select="
                for $a in $tokens 
                return books/book[@author=$a]/@title"/>
        </books>

Upvotes: 0

Related Questions