Dynamic xpath in xslt?

I have a set of files:

SourceFile.xml:

<?xml version="1.0" encoding="utf-8" ?> <Employees> <Employee id="1"> <firstname relationship="headnote">Atif</firstname> <lastname relationship="lname">Bashir</lastname> <age relationship="age">32</age> </Employee> </Employees> 

ParamerterSettings.xml

  <?xml version="1.0" encoding="utf-8"?> <Settings> <Employee id="1"> <sourceFile>Lookup1.xml</sourceFile> <sourceXpathfield>Employees/Employee[@id</sourceXpathfield> <lookupXpathfield>Employees/Employee[@id='1']</lookupXpathfield> <elementstoinsert>xyz</elementstoinsert> </Employee> </Settings> 

Lookup.xml

 <?xml version="1.0" encoding="utf-8"?> <Employees> <Employee id="1"> <department code="102">HR</department> </Employee> </Employees> 

transform.xsl

  <?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" exclude-result-prefixes="xs" version="2.0"> <xsl:include href="identity.xsl"/> <xsl:param name="EmployeeId" select="'1,2'" /> <xsl:variable name="FileSettings" select="document('test3.xml')" /> <xsl:variable name="SuppressSetting" select="$FileSettings/Settings/Employee[@id = tokenize($EmployeeId, ',')]" /> <xsl:template match="Employee"> <xsl:copy> <xsl:apply-templates select="@*"/> <xsl:apply-templates select="publisher" /> <xsl:apply-templates select="node() except publisher"/> <xsl:variable name="outerfile" select="document($SuppressSetting/sourceFile)"></xsl:variable> <xsl:variable name="outerfiledetails" select="$outerfile/$SuppressSetting/lookupXpathfield"></xsl:variable> <xsl:value-of select="$outerfiledetails"></xsl:value-of> </xsl:copy> </xsl:template> </xsl:stylesheet> 

The output should be:

  <?xml version="1.0" encoding="utf-8" ?> <Employees> <Employee id="1"> <firstname relationship="headnote">Atif</firstname> <lastname relationship="lname">Bashir</lastname> <age relationship="age">32</age> HR </Employee> </Employees> 

I changed the bottom line in Transform.xsl

 <xsl:variable name="outerfiledetails" select="$outerfile/$SuppressSetting/lookupXpathfield"></xsl:variable> 

in

 <xsl:variable name="outerfiledetails" select="$outerfile/Employees/Employee[@id='1']"></xsl:variable> 

then I get my output, but I want to save the XPath epression for SourceFile.xml and Lookup.xml in ParamerterSettings.xml so that I can write a more general script. Can this be done in any other way and then with dynamic xpath? Any idea or hint to imitate the same would be greatly appreciated.

+2
source share
3 answers

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 dyn:evaluate() function dyn:evaluate()

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

II. Process the XML document using XSLT and generate a new XSLT file containing XPath expressions, and then perform the newly generated conversion .

Very few people do this, and in my opinion, it is more difficult than the following solution.

III. XPath Visualizer Method Works

The idea is this:

  • You have a global variable in an XSLT stylesheet like this :

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

  • Finally, initiate the conversion using an in-memory and dynamically updated xslt stylesheet.

+11
source share

You cannot do this in XSLT 2.0, but you can do it in the latest version of XSLT:

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

+3
source share

Yes, we can ... at least rudimentarily. Here is a workaround that I use with Saxon CE (XSLT 2.0) until the evaluate function is available. This may not work for all kinds of complex XML documents, but you can probably tweak the β€œfilter” as needed (request for attributes, etc.).

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

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

To limit the result (we select all elements with the name "third"), you will also have to query the ancestors "first" and "second". Maybe someone has an idea to simplify the following code, in particular the ancestors call:

 <!-- 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> 

For my purpose, only the first matching element that has no child nodes is returned. You probably need to customize this for your specific needs.

+2
source share

All Articles