dynamic xpath in xslt?

Dynamic XPath evaluation is not possible in pure XSLT 1.0 or 2.0.

There are at least three ways to do this in a "hybrid" solution:

I. Use the EXSLT function dyn:evaluate()

Unfortunately, very few XSLT 1.0 processors implement dyn:evaluate().

II. Process the XML document with XSLT and generate a new XSLT file that contains the XPath expressions -- then execute the newly-generated transformation.

Very few people do this and, in my opinion, this is more complex than the next solution.

III. The way the XPath Visualizer works

The idea is:

  1. Have a global variable in the XSLT stylesheet defined like this:

      <xsl:variable name="vExpression" select="dummy"/>
    
  2. Then, load the stylesheet as an XML document using DOM, and replace the select attribute of the vExpression variable with the actual XPath expression that is contained in the source XML document.

  3. Finally, initiate the transformation using the loaded into memory and dynamically updated xslt stylesheet.


IV. With XSLT 3.0

Use the <xsl:evaluate> instruction


You can't do it in XSLT 2.0, but you will be able to do it in the latest version of XSLT:

http://www.w3.org/TR/xslt-21/#element-evaluate


Yes we can ... at least rudimentarily. Here is a workaround which I use with Saxon CE (XSLT 2.0) until the "evaluate" function is available. Maybe this does not work for all kinds of complex XML documents but probably you can adjust the "filter" as you need (query on attributes, etc.).

In my special situation I have xPath expressions describing to "full" path to elements including their names and the trick is to use a wildcard in combination with only the last element of the dynamic xPath expression, e.g. use "third" instead of "first/second/third":

<xsl:variable name="value" select="//*[name() = 'third']" />

In order to limit the result (would select all elements with name "third") you will have to query on the ancestors "first" and "second", too. Maybe anyone has an idea to simplify the following code, in particular the calling of the ancestors:

<!-- global variable which holds a XML document with root node "data" -->
<xsl:variable name="record" select="document('record.xml')/data"/>

<!-- select elements from the global "record" variable using dynamic xpath expressions -->
<xsl:function name="utils:evaluateXPath">

    <xsl:param name="xpath" as="xs:string"/>

    <xsl:choose>

        <xsl:when test="function-available('evaluate')">

            <!-- modify the code if the function has been implemented :-) -->
            <xsl:value-of select="'function evaluate() can be used ...'"/>

        </xsl:when>

        <xsl:otherwise>

            <!-- get a list of elements defined in the xpath expression -->
            <xsl:variable name="sequence" select="tokenize($xpath, '/')" />

            <!-- get the number of ancestors for the last element -->
            <xsl:variable name="iAncestors" select="count($sequence)-1" as="xs:integer" />

            <!-- get the last element from the xpath expression -->
            <xsl:variable name="lastElement" select="if ($iAncestors > 0) then $sequence[last()] else $xpath" />

            <!-- try to find the desired element as defined in xpath expression -->
            <!-- use parenthesis to grab only the first occurrence -->
            <xsl:value-of select="
                if ($iAncestors = 0) then
                    ($record//*[name() = $lastElement and not(*)])[1]
                else if ($iAncestors = 1) then
                    ($record//*[name() = $lastElement and not(*) and (name(..) = $sequence[1])])[1]
                else if ($iAncestors = 2) then
                    ($record//*[name() = $lastElement and not(*) and (name(..) = $sequence[2]) and (name(../..) = $sequence[1])])[1]
                else if ($iAncestors = 3) then
                    ($record//*[name() = $lastElement and not(*) and (name(..) = $sequence[3]) and (name(../..) = $sequence[2]) and (name(../../..) = $sequence[1])])[1]
                else if ($iAncestors = 4) then
                    ($record//*[name() = $lastElement and not(*) and (name(..) = $sequence[4]) and (name(../..) = $sequence[3]) and (name(../../..) = $sequence[2]) and (name(../../../..) = $sequence[1])])[1]
                else if ($iAncestors = 5) then
                    ($record//*[name() = $lastElement and not(*) and (name(..) = $sequence[5]) and (name(../..) = $sequence[4]) and (name(../../..) = $sequence[3]) and (name(../../../..) = $sequence[2]) and (name(../../../../..) = $sequence[1])])[1]
                else 'failure: too much elements for evaluating dyn. xpath ... add another level!'"
            />

        </xsl:otherwise>

    </xsl:choose>

</xsl:function>

As for my purpose only the first matching element which has no child nodes is returned. Probably you have to adjust this for your specific needs.