Janardan Kelkar
Janardan Kelkar

Reputation: 170

XSLT template matching using variable

In my source XML, i have an element that contains a list of Xpath expressions, pointing to different nodes within the same XML. Here is a sample, where the Xpaths are located in /root/Properties[@name='changed'] -

<root xmlns="http://www.example.org">
<main>
    <child1>123</child1>
    <child2>456</child2>
    <subChildren>
        <subChild1>321</subChild1>
        <subChild2>644</subChild2>
    </subChildren>
</main>
<Properties name="changed">/t:root/t:main/t:child2|/t:root/t:main/t:subChildren/t:subChild1</Properties>
</root>

I am writing an XSLT to add an attribute to all the nodes specified in the XPaths. The XLST below does the job, however notice that the xpaths are hardcoded in the second template's match value -

<?xml version="1.0" encoding="utf-8"?>
<xsl:stylesheet xmlns:xsl='http://www.w3.org/1999/XSL/Transform' xmlns:xsi='http://www.w3.org/2001/XMLSchema-instance'
xmlns:t="http://www.example.org" version='2.0'>

<xsl:variable name="xpaths" select="/t:root/t:Properties[@name='changed']" />

<xsl:template match="@*|node()">
    <xsl:copy>
        <xsl:apply-templates select="@*|node()" />
    </xsl:copy>
</xsl:template>

<xsl:template match='/t:root/t:main/t:child2|/t:root/t:main/t:subChildren/t:subChild1'>
    <xsl:call-template name="addChanged" />
</xsl:template>

<xsl:template name="addChanged">
    <xsl:copy>
        <xsl:attribute name="changed">true</xsl:attribute>
        <xsl:apply-templates select="@*|node()" />
    </xsl:copy>
</xsl:template>
</xsl:stylesheet>

I want to get the same stylesheet to work with a variable in the template's match attribute, like so -

<?xml version="1.0" encoding="utf-8"?>
<xsl:stylesheet xmlns:xsl='http://www.w3.org/1999/XSL/Transform' xmlns:xsi='http://www.w3.org/2001/XMLSchema-instance'
xmlns:t="http://www.example.org" version='2.0'>

<xsl:variable name="xpaths" select="/t:root/t:Properties[@name='changed']" />

<xsl:template match="@*|node()">
    <xsl:copy>
        <xsl:apply-templates select="@*|node()" />
    </xsl:copy>
</xsl:template>

<xsl:template match='$xpaths'>
    <xsl:call-template name="addChanged" />
</xsl:template>

<xsl:template name="addChanged">
    <xsl:copy>
        <xsl:attribute name="changed">true</xsl:attribute>
        <xsl:apply-templates select="@*|node()" />
    </xsl:copy>
</xsl:template>
</xsl:stylesheet>

How do i get this done ? The above stylesheet does not match the nodes correctly in the template. I am using Saxon HE 9.6.

Upvotes: 0

Views: 1949

Answers (2)

Michael Kay
Michael Kay

Reputation: 163262

Well, I hope it's obvious why your approach doesn't work: in XSLT 3.0 you can use a variable in a match pattern, but the variable's value must be a set of nodes, not an XPath expression for selecting those nodes.

To evaluate an XPath expression supplied as a string, you need xsl:evaluate

<!-- Beaware of quoting -->
<xsl:variable name="path-to-changed-nodes"
  select="'/t:root/t:Properties[@name=''changed'']'" as="xs:string"/>

<xsl:variable name="changed-nodes" as="node()*">
  <xsl:evaluate xpath="$path-to-changed-nodes"
                context-item="/"/>
</xsl:variable>

<xsl:template match="$changed-nodes">
  ...
</xsl:template>

In Saxon, the xsl:evaluate instruction requires Saxon-PE or higher. If you can't persuade someone to give you a Saxon-PE license as a Christmas present, the alternative approach is to implement your solution as a sequence of two transformations: first generate the stylesheet containing the required match patterns, then execute that stylesheet.

LATER

In fact with XSLT 3.0 there's an simpler alternative to generating the stylesheet as XML source; you can use static parameters and shadow variables (though I haven't tested it).

Start with a shadow variable for the match pattern:

<xsl:template _match="{$pattern}">...

Then define the static variable $pattern:

<xsl:variable name="pattern" static="yes" 
     select="string($doc//t:root/t:Properties[@name='changed'])"/>

Then declare the parameter $doc:

<xsl:param name="doc" static="yes" as="document-node()"/>

and supply a value for the static parameter doc when compiling the stylesheet.

I've no idea if this will work with Saxon-HE 9.6 - probably not, as that came out before XSLT 3.0 was finalized.

Upvotes: 1

michael.hor257k
michael.hor257k

Reputation: 116959

Here's another approach that's rather primitive, but gets it done in one pass:

XSLT 2.0

<xsl:stylesheet version="2.0" 
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xpath-default-namespace="http://www.example.org">
<xsl:output method="xml" version="1.0" encoding="UTF-8" indent="yes"/>
<xsl:strip-space elements="*"/>

<xsl:variable name="paths" select="tokenize(root/Properties[@name='changed'], '\|')" />

<xsl:template match="*">
    <xsl:variable name="path-to-me">
        <xsl:for-each select="ancestor-or-self::*">
            <xsl:value-of select="concat('/t:', name())"/>
        </xsl:for-each>    
    </xsl:variable> 
    <xsl:copy>
        <xsl:copy-of select="@*"/>
        <xsl:if test="$path-to-me = $paths">
            <xsl:attribute name="changed">true</xsl:attribute>
        </xsl:if>
        <xsl:apply-templates select="*"/>
    </xsl:copy>
</xsl:template>

</xsl:stylesheet>

Demo: http://xsltransform.hikmatu.com/6qVRKvL

Upvotes: 0

Related Questions