How to use variable as XPath in XSLT test - xslt

I have a function that accepts a parameter $path. It should contain an XPath expression, and my goal is to test whether or not the node at the end of the expression is valid.
However, when I try to do
<xsl:function name="testPath">
<xsl:param name="path">
<xsl:if test="$path">
it tests $path as a string, and not as an XPath expression (meaning that it returns true if $path is not empty). If I hardcode a XPath expression in, then it does the check properly.
I am using XPath 2.0
How do I use a variable as an XPath expression?

Dynamic XPath evaluation is an optional feature supported in XSLT 3.0. Here is an example using Saxon 9.5 PE:
<xsl:stylesheet
version="3.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:xs="http://www.w3.org/2001/XMLSchema"
xmlns:mf="http://example.org/mf"
exclude-result-prefixes="xs mf">
<xsl:output method="text"/>
<xsl:function name="mf:testPath" as="xs:boolean">
<xsl:param name="context-node" as="node()"/>
<xsl:param name="path" as="xs:string"/>
<xsl:variable name="seq" as="item()*">
<xsl:evaluate xpath="$path" context-item="$context-node"/>
</xsl:variable>
<xsl:sequence select="exists($seq)"/>
</xsl:function>
<xsl:template match="/">
<xsl:value-of select="('a/b[#id = "b2"]', 'a/c') ! mf:testPath(current(), .)" separator="
"/>
</xsl:template>
</xsl:stylesheet>
Evaluated against the input sample
<a>
<b id="b1">foo</b>
<b id="b2">bar</b>
</a>
it outputs
true
false

Related

XSLT 3.0 Dynamic select with variable in apply-templates?

I want to apply templates to a set of nodes where part of the select path is a variable. I'm using Saxon-HE 9.8 (awesome lib!)
I'm trying to achieve the following
<variable name="x" select="string('baz')"/>
<xsl:apply-templates select="foo/bar/$x"/>
This doesn't seem to work. Is there a syntax that will allow me to dynamically construct the select XPath for this apply-templates instruction? Or, is there another technique for dynamically achieving this effect? I even tried pushing this down to my <xsl:template match=foo/bar/$x> but no luck.
My motivation here is in my application the variable value is coming from a separate configuration file. Based on the configuration I need to run templates matching specific path segments driven by config strings...
If your variables are always going to be a simple string value expressing the name of an element, then one option would be to match a little more generically on an element and then use the string variable in a predicate to filter for a match of the element name:
<xsl:apply-templates select="foo/bar/*[local-name() = $x]"/>
With Saxon-PE or Saxon-EE, you could leverage xsl:evaluate and do something like this:
<xsl:variable name="var" as="node()*">
<xsl:evaluate xpath="concat('foo/bar/',$x)" context-item="."/>
</xsl:variable>
<xsl:apply-templates select="$var"/>
If you declare a static parameter <xsl:param name="x" static="yes" as="xs:string" select="'baz'"/> for the value and then use a shadow attribute in the form of _select="foo/bar/{$x}" you can even construct the path dynamically, but only when compiling the XSLT.
In a static parameter you can of course pull in a configuration file and use values from it:
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:xs="http://www.w3.org/2001/XMLSchema"
version="3.0">
<xsl:param name="config-uri" static="yes" as="xs:string" select="'https://martin-honnen.github.io/xslt/2018/config-example1.xml'"/>
<xsl:param name="config-doc" static="yes" as="document-node()" select="doc($config-uri)"/>
<xsl:mode on-no-match="shallow-copy"/>
<xsl:template match="item[#type = 'foo']">
<xsl:copy>
<xsl:value-of _select="{$config-doc/map/from[#key = 'foo']}"/>
</xsl:copy>
</xsl:template>
<xsl:template match="item[#type = 'bar']">
<xsl:copy>
<xsl:value-of _select="{$config-doc/map/from[#key = 'bar']}"/>
</xsl:copy>
</xsl:template>
</xsl:stylesheet>
https://xsltfiddle.liberty-development.net/6qVRKvX/1
Another option I didn't mention in my first answer but that is also a viable way with Saxon 9.8 or any other XSLT 3 processor is the use of XSLT to create XSLT and then to use the transform function (https://www.w3.org/TR/xpath-functions/#func-transform) to run that generated XSLT. That approach has the advantage that it works with Saxon 9.8 HE where xsl:evaluate is not supported:
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:xs="http://www.w3.org/2001/XMLSchema"
xmlns:axsl="http://www.w3.org/1999/XSL/Transform-alias"
exclude-result-prefixes="axsl"
version="3.0">
<xsl:param name="config-uri" as="xs:string" select="'https://martin-honnen.github.io/xslt/2018/config-example1.xml'"/>
<xsl:param name="config-doc" as="document-node()" select="doc($config-uri)"/>
<xsl:namespace-alias stylesheet-prefix="axsl" result-prefix="xsl"/>
<xsl:variable name="generated-xslt">
<axsl:stylesheet version="3.0">
<axsl:mode on-no-match="shallow-copy"/>
<xsl:for-each select="$config-doc/map/from">
<axsl:template match="item[#type = '{#key}']">
<axsl:copy>
<axsl:value-of select="{.}"/>
</axsl:copy>
</axsl:template>
</xsl:for-each>
</axsl:stylesheet>
</xsl:variable>
<xsl:mode on-no-match="shallow-copy"/>
<xsl:template match="/">
<xsl:sequence
select="transform(map {
'source-node' : .,
'stylesheet-node' : $generated-xslt
})?output"/>
</xsl:template>
</xsl:stylesheet>
https://xsltfiddle.liberty-development.net/6qVRKvX/2

Otherwise-statement in XSLT-choose is not applied/working

I have an XML-document with a type-node whose value is either "1" or "2":
<MyDoc>
<foo>
<bar>
<type>2</type>
</bar>
</foo>
</MyDoc>
I want to set a variable typeBool depending on the value of the type-node, if it is "1" it should be set to false, if it's "2" to true.
With the XSLT-choose-Element it should be possible to test for the current value and set typeBool according to the outcome.
I'm trying to do this with the following construct in XSLT 2.0, but I'm puzzled that the "otherwise"-path is not applied and I get an error that typeBool is not created:
<xsl:transform xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="2.0">
<xsl:variable name="type" select="/MyDoc/foo/bar/type/text()"/>
<xsl:choose>
<xsl:when test="$type = '2'">
<xsl:variable name="typeBool">true</xsl:variable>
</xsl:when>
<xsl:otherwise>
<xsl:variable name="typeBool">false</xsl:variable>
</xsl:otherwise>
</xsl:choose>
<h1><b><xsl:value-of select="$typeBool"/></b></h1>
</xsl:transform>
This is the transformation error I get:
error during xslt transformation:
Source location: line 0, col 0 Description:
No variable with name typeBool exists
As you currently present your problem, an xsl:choose is not needed and it unnecessarily complicates your XSLT code. Your actual problem might be more intricate though.
You can write a template that matches the element you are interested in (for instance, type elements) and then simply select the value of a comparison that will evaluate to either true or false.
XSLT Stylesheet
<?xml version="1.0" encoding="UTF-8" ?>
<xsl:transform xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="2.0">
<xsl:output method="html" omit-xml-declaration="yes" encoding="UTF-8" indent="yes" />
<xsl:strip-space elements="*"/>
<xsl:template match="type">
<h1>
<b>
<xsl:value-of select=". = '2'"/>
</b>
</h1>
</xsl:template>
<xsl:template match="text()"/>
</xsl:transform>
HTML Output
<h1><b>true</b></h1>
Try it online here.
With choose-element
The choose-clause has to be defined inside of the variable-declaration:
<xsl:variable name="type">
<xsl:value-of select="/MyDoc/foo/bar/type/text()"/>
</xsl:variable>
<xsl:variable name="typeBool">
<xsl:choose>
<xsl:when test="$type = '2'">true</xsl:when>
<xsl:otherwise>false</xsl:otherwise>
</xsl:choose>
</xsl:variable>
The conditional also looks cleaner this way.
With XSLT 2.0
#MichaelKay pointed out that in XSLT 2.0 a xpath-conditional can be used, which is even simpler:
<xsl:variable name="type">
<xsl:value-of select="/MyDoc/foo/bar/type/text()"/>
</xsl:variable>
<h1>
<b>
<xsl:value-of select="select="if($type=2) then 'true' else 'false'"/>
</b>
</h1>

How to create template to match based upon an XSLT parameter

I'm trying to create a standard-use XSLT that will perform a given task based upon a user-provided XPATH expression as an XSLT parameter.
That is, I need something like this:
<xsl:template match="$paramContainingXPATH">
<!-- perform the task on the node(s) in the given xpath -->
</xsl:template>
For example, suppose I have some XML:
<xml>
<nodeA>whatever</nodeA>
<nodeB>whatever</nodeB>
<nodeC>whatever</nodeC>
<nodeD>whatever</nodeD>
<nodeE>whatever</nodeE>
</xml>
The XSLT needs to transform just a node or nodes matching a provided XPATH expression. So, if the xslt parameter is "/xml/nodeC", it processes nodeC. If the xslt parameter is "*[local-name() = 'nodeC' or local-name() = 'nodeE']", it processes nodeC and nodeE.
This should work for absolutely any XML message. That is, the XSLT cannot have any direct knowledge of the content of the XML. So, it could be a raw XML, or a SOAP Envelope.
I was guessing I might need to grab all the nodes matching the xpath, and then looping over them calling a named template, and using the standard identity template for all other nodes.
All advice is appreciated.
If you really need that feature with XSLT 1.0 or 2.0 then I think you should consider writing one stylesheet that takes that string parameter with the XPath expression and then simply generates the code of a second stylesheet where the XPath expression is used as a match pattern and the other needed templates like the identity template are included statically. Dynamic XPath evaluation is only available in XSLT 3.0 or in earlier versions as a proprietary extension mechanism.
You cannot match a template using a parameter - but you can traverse the tree and compare the path of each node with the given path. Here's a simple example:
XSLT 1.0
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="xml" version="1.0" encoding="UTF-8" indent="yes"/>
<xsl:param name="path" select="'/world/America/USA/California'"/>
<xsl:template match="/">
<root>
<xsl:apply-templates select="*"/>
</root>
</xsl:template>
<xsl:template match="*">
<xsl:variable name="path-to-me">
<xsl:for-each select="ancestor-or-self::node()">
<xsl:value-of select="name()" />
<xsl:if test="position()!=last()">
<xsl:text>/</xsl:text>
</xsl:if>
</xsl:for-each>
</xsl:variable>
<xsl:if test="$path=$path-to-me">
<xsl:call-template name="action"/>
</xsl:if>
<xsl:apply-templates select="*"/>
</xsl:template>
<xsl:template name="action">
<return>
<xsl:value-of select="." />
</return>
</xsl:template>
</xsl:stylesheet>
Applied to a slightly more ambitious test input of:
<world>
<Europe>
<Germany>1</Germany>
<France>2</France>
<Italy>3</Italy>
</Europe>
<America>
<USA>
<NewYork>4</NewYork>
<California>5</California>
</USA>
<Canada>6</Canada>
</America>
</world>
the result will be:
<?xml version="1.0" encoding="UTF-8"?>
<root>
<return>5</return>
</root>
This could be made more efficient by passing the accumulated path as a parameter of the recursive template, so that each node needs only to add its own name to the chain.
Note:
The given path must be absolute;
Predicates (including positional predicates) and attributes are not implemented in this. They probably could be, with a bit more effort;
Namespaces are ignored (I don't see how you could pass an XPath as a parameter and include namespaces anyway).
If your processor supports an evaluate() extension function, you could forgo the calculated text path and test for intersection instead.
Edit:
Here's an example using EXSLT dyn:evaluate() and set:intersection():
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:dyn="http://exslt.org/dynamic"
xmlns:set="http://exslt.org/sets"
extension-element-prefixes="dyn set">
<xsl:output method="xml" version="1.0" encoding="UTF-8" indent="yes"/>
<xsl:param name="path" select="'/world/America/USA/California'"/>
<xsl:variable name="path-set" select="dyn:evaluate($path)" />
<xsl:template match="/">
<root>
<xsl:apply-templates select="*"/>
</root>
</xsl:template>
<xsl:template match="*">
<xsl:if test="set:intersection(. , $path-set)">
<xsl:call-template name="action"/>
</xsl:if>
<xsl:apply-templates select="*"/>
</xsl:template>
<xsl:template name="action">
<return>
<xsl:value-of select="." />
</return>
</xsl:template>
</xsl:stylesheet>
Note that this will also work with with paths like:
/world/America/USA/*[2]
//California
and many others that the text comparison method could not accommodate.
I'm sending the element name as a param to the XSLT
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:xs="http://www.w3.org/2001/XMLSchema" version="2.0">
<xsl:output method="xml"/>
<xsl:param name="user"/>
<xsl:template match="/">
<xsl:call-template name="generic" />
</xsl:template>
<xsl:template name="generic">
<count><xsl:value-of select="count(.//*[local-name()=$user])"/></count>
</xsl:template>
</xsl:stylesheet>
I hope this could help!

XPATH for first element whose name is among the names of some other elements

<choices>
<sic />
<corr />
<reg />
<orig />
</choices>
<choice>
<corr>Red</corr>
<sic>Blue</sic>
<choice>
I want to select the first element in <choice> whose name matches the name of any element in <choices>.
If name(node-set) returned a list of names instead of only the name of the first node, I could use
select="choice/*[name() = name(choices/*)][1]"
But it doesn't (at least not in 1.0), so instead I join the names together in a string and use contains():
<xsl:variable name="choices.str">
<xsl:for-each select="choices/*">
<xsl:text> </xsl:text><xsl:value-of select="concat(name(),' ')"/>
</xsl:for-each>
</xsl:variable>
<xsl:apply-templates select="choice/*[contains($choices.str,name())][1]"/>
and get what I want:
Red, the value of <corr>
Is there a more straightforward way?
I. Use this XPath 2.0 one-liner:
/*/choice/*[name() = /*/choices/*/name()][1]
When this XPath expression is evaluated against the following XML document (the provided one, but corrected to become a well-formed XML document):
<t>
<choices>
<sic />
<corr />
<reg />
<orig />
</choices>
<choice>
<corr>Red</corr>
<sic>Blue</sic>
</choice>
</t>
the correct element is selected:
<corr>Red</corr>
II. XSLT 1.0 (no keys!):
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output omit-xml-declaration="yes" indent="yes"/>
<xsl:strip-space elements="*"/>
<xsl:variable name="vNames">
<xsl:for-each select="/*/choices/*">
<xsl:value-of select="concat(' ', name(), ' ')"/>
</xsl:for-each>
</xsl:variable>
<xsl:template match="/">
<xsl:copy-of select=
"/*/choice/*
[contains($vNames, concat(' ', name(), ' '))]
[1]"/>
</xsl:template>
</xsl:stylesheet>
When this transformation is applied on the same XML document (above), again the correct element is selected (and copied to the output):
<corr>Red</corr>
III. Using keys:
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output omit-xml-declaration="yes" indent="yes"/>
<xsl:strip-space elements="*"/>
<xsl:key name="kChoiceByName" match="choice/*"
use="boolean(/*/choices/*[name()=name(current())])"/>
<xsl:template match="/">
<xsl:copy-of select="/*/choice/*[key('kChoiceByName', true())][1]"/>
</xsl:template>
</xsl:stylesheet>
When this transformation is applied against the same XML document (above), the same correct result is produced:
<corr>Red</corr>
It is recommended to the reader to try to understand how this all "works" :)
You can use the key() function like this...
When this input document...
<t>
<choices>
<sic />
<corr />
<reg />
<orig />
</choices>
<choice>
<corr>Red</corr>
<sic>Blue</sic>
</choice>
</t>
...is supplied as input to this XSLT 1.0 style-sheet...
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="text"/>
<xsl:key name="kChoices" match="choices/*" use="name()" />
<xsl:template match="/">
<xsl:variable name="first-choice" select="(*/choice/*[key('kChoices',name())])[1]" />
<xsl:value-of select="$first-choice" />
<xsl:text>, the value of <</xsl:text>
<xsl:value-of select="name( $first-choice)" />
<xsl:text>></xsl:text>
</xsl:template>
</xsl:stylesheet>
...this output text is produced...
Red, the value of <corr>
XSLT 2.0 Aside
In XSLT 2.0, you would be able to use the following alternatives for the computation of the $first-choice variable...
Option 1:
(*/choice/*[for $c in . return ../../choices/*[name()=name($c)]])[1]
Option 2:
(*/choice/*[some $c in ../../choices/* satisfies name($c)=name()])[1]

XPath/XSLT nested predicates: how to get the context of outer predicate?

It seems that this question was not discussed on stackoverflow before, save for Working With Nested XPath Predicates ... Refined where the solution not involving nested predicates was offered.
So I tried to write the oversimplified sample of what I'd like to get:
Input:
<root>
<shortOfSupply>
<food animal="doggie"/>
<food animal="horse"/>
</shortOfSupply>
<animalsDictionary>
<cage name="A" animal="kittie"/>
<cage name="B" animal="dog"/>
<cage name="C" animal="cow"/>
<cage name="D" animal="zebra"/>
</animals>
</root>
Output:
<root>
<hungryAnimals>
<cage name="B"/>
<cage name="D"/>
</hungryAnimals>
</root>
or, alternatively, if there is no intersections,
<root>
<everythingIsFine/>
</root>
And i want to get it using a nested predicates:
<xsl:template match="cage">
<cage>
<xsl:attribute name="name">
<xsl:value-of select="#name"/>
</xsl:attribute>
</cage>
</xsl:template>
<xsl:template match="/root/animalsDictionary">
<xsl:choose>
<!-- in <food> in <cage> -->
<xsl:when test="cage[/root/shortOfSupply/food[ext:isEqualAnimals(./#animal, ?????/#animal)]]">
<hungryAnimals>
<xsl:apply-templates select="cage[/root/shortOfSupply/food[ext:isEqualAnimals(#animal, ?????/#animal)]]"/>
</hungryAnimals>
</xsl:when>
<xsl:otherwise>
<everythingIsFine/>
</xsl:otherwise>
</xsl:choose>
</xsl:template>
So what should i write in place of that ??????
I know i could rewrite the entire stylesheet using one more template and extensive usage of variables/params, but it makes even this stylesheet significantly more complex, let alone the real stylesheet i have for real problem.
It is written in XPath reference that the dot . sign means the current context node, but it doesn't tell whether there is any possibility to get the node of context before that; and i just can't believe XPath is missing this obvious feature.
XPath 2.0 one-liner:
for $a in /*/animalsDictionary/cage
return
if(/*/shortOfSupply/*[my:isA($a/#animal, #animal)])
then $a
else ()
When applied on the provided XML document selects:
<cage name="B"/>
<cage name="D"/>
One cannot use a single XPath 1.0 expression to find that a given cage contains a hungry animal.
Here is an XSLT solution (XSLT 2.0 is used only to avoid using an extension function for the comparison -- in an XSLT 1.0 solution one will use an extension function for the comparison and the xxx:node-set() extension to test if the RTF produced by applying templates in the body of the variable contains any child element):
<xsl:stylesheet version="2.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:xs="http://www.w3.org/2001/XMLSchema"
xmlns:my="my:my" exclude-result-prefixes="xs my">
<xsl:output omit-xml-declaration="yes" indent="yes"/>
<my:Dict>
<a genName="doggie">
<name>dog</name>
<name>bulldog</name>
<name>puppy</name>
</a>
<a genName="horse">
<name>horse</name>
<name>zebra</name>
<name>pony</name>
</a>
<a genName="cat">
<name>kittie</name>
<name>kitten</name>
</a>
</my:Dict>
<xsl:variable name="vDict" select=
"document('')/*/my:Dict/a"/>
<xsl:template match="/">
<root>
<xsl:variable name="vhungryCages">
<xsl:apply-templates select=
"/*/animalsDictionary/cage"/>
</xsl:variable>
<xsl:choose>
<xsl:when test="$vhungryCages/*">
<hungryAnimals>
<xsl:copy-of select="$vhungryCages"/>
</hungryAnimals>
</xsl:when>
<xsl:otherwise>
<everythingIsFine/>
</xsl:otherwise>
</xsl:choose>
</root>
</xsl:template>
<xsl:template match="cage">
<xsl:if test="
/*/shortOfSupply/*[my:isA(current()/#animal,#animal)]">
<cage name="{#name}"/>
</xsl:if>
</xsl:template>
<xsl:function name="my:isA" as="xs:boolean">
<xsl:param name="pSpecName" as="xs:string"/>
<xsl:param name="pGenName" as="xs:string"/>
<xsl:sequence select=
"$pSpecName = $vDict[#genName = $pGenName]/name"/>
</xsl:function>
</xsl:stylesheet>
When this transformation is applied on the provided XML document (corrected to be well-formed):
<root>
<shortOfSupply>
<food animal="doggie"/>
<food animal="horse"/>
</shortOfSupply>
<animalsDictionary>
<cage name="A" animal="kittie"/>
<cage name="B" animal="dogs"/>
<cage name="C" animal="cow"/>
<cage name="D" animal="zebras"/>
</animalsDictionary>
</root>
the wanted, correct result is produced:
<root>
<hungryAnimals>
<cage name="B"/>
<cage name="D"/>
</hungryAnimals>
</root>
Explanation: Do note the use of the XSLT current() function.
XPath 1.0 is not "relationally complete" - it can't do arbitrary joins. If you're in XSLT, you can always get round the limitations by binding variables to intermediate nodesets, or (sometimes) by using the current() function.
XPath 2.0 introduces range variables, which makes it relationally complete, so this limitation has gone.
Doesn't <xsl:when test="cage[#animal = /root/shortOfSupply/food/#animal]"> suffice to express your test condition?
Notice The dot operator in XPath is related to the current context. In XSLT the current template context_ is given by the function current(), which most of the time (not always) coincides with the ..
You can perform the test (and the apply templates as well), using the parent axis abbreviation (../):
cage[#animal=../../shortOfSupply/food/#animal]
Moreover the match pattern in the the first template is wrong, it should be relative to the root:
/root/animalsDictionary
#Martin suggestion is also obviously correct.
Your final template slightly modified:
<xsl:stylesheet
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
version="1.0">
<xsl:output method="xml" indent="yes" omit-xml-declaration="yes"/>
<xsl:strip-space elements="*"/>
<xsl:template match="root/animalsDictionary">
<xsl:choose>
<xsl:when test="cage[#animal=../../shortOfSupply/food/#animal]">
<hungryAnimals>
<xsl:apply-templates select="cage[#animal
=../../shortOfSupply/food/#animal]"/>
</hungryAnimals>
</xsl:when>
<xsl:otherwise>
<everythingIsFine/>
</xsl:otherwise>
</xsl:choose>
</xsl:template>
<xsl:template match="cage">
<cage name="{#name}"/>
</xsl:template>
</xsl:stylesheet>