Reputation: 170
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
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
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