dynamic xpath in xslt?
Asked Answered
T

4

4

I have follwing 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 below line in Transform.xsl

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

into

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

then I am getting my output but I want to keep the XPath epression for both SourceFile.xml and Lookup.xml into ParamerterSettings.xml so that I can write a more generic script. Can this be done in any other way then the dynamic xpath? Any idea or hint to impelement the same will be highly appreciated.

Telephone answered 7/1, 2011 at 20:31 Comment(4)
It is a good progress that you have simplified your initial almost monstrous question, but this question is still too-complex and not well-defined. Try to rephraze it and simplify it further -- I am sure that you don't need all of the details. Especially, having to work with more than two files makes everyone give up on even trying to understand the question. Too complex: I would never design an XSLT app this way, and believe me, I have XSLT apps of really challenging complexity that 99% of the devs don't believe is possible to do with XSLT.Clockwork
Hi Dimitre, what i want is to execute the xpath value from the external file. Reason being i have multiple exeternal files from which i want to take data and insert that data back into main source file. I can do it by hard coding multiple templates but i want to avoid that and make one template which reads from multiple files based on different joins or xpath values define as a settings in external file.Telephone
@Nick-Jones 's answer is correct: this cannot be done in XSLT/XPath 2.0 and it might be provided by a next version. However, I highly doubt the necessity of dynamic XPath evaluation -- if you describe your problem well there might be a solution that doesn;t need this. Why dont you just ask the question in its simplest form: "How do I evaluate this expression, contained in this XML document?". While a pure XSLT solution isn't possible there are at least three different "hybrid" solutions that I know to this problem.Clockwork
See my answer for three different solutions to your problem, so that you'd not have to wait until XSLT 3.0 arrives. :)Clockwork
C
13

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

Clockwork answered 10/1, 2011 at 14:30 Comment(7)
Hi Dimitre, I guess thrid option can be used but this it self is a bit complticated. I will try to work with multiple templates for now. Thanks for the input though.Telephone
@atif: It isn't too complicated -- you may be interested to look at the source code of the XPath Visualizer -- this is just a few lines of code.Clockwork
Here is an example for the approach #1: #230911Cinch
dyn:evaluate ingenious.. xsltproc implements it and that was all I needed. Thanks for the great hint.Montague
@Jagger, In XSLT 3.0 there is a new instruction: <xsl:evaluate> -- read about it here: w3.org/TR/2014/WD-xslt-30-20141002/#dynamic-xpathClockwork
yes, but doing so requires my xslt to be recompiled every time, no?Obsecrate
@neu-rah, No. The string containing the XPath expression is typically read from another source (file) -- or generally obtained during run-time.Clockwork
A
3

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

Alleras answered 10/1, 2011 at 14:1 Comment(0)
L
2

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.

Liris answered 9/8, 2013 at 12:40 Comment(1)
The use of <xsl:choose> is a good idea, at least if the selection is relatively short, it solved my dynamism problem in a few lines.Bedcover
D
0

One possible trick is to call an parametrized xsl:template which carries out the crucial select comparison part and evaluate it's output as string. See this answer

Denary answered 3/6, 2020 at 8:52 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.