1

Here's what I've got.

My data: data.xml

<?xml version="1.0" encoding="UTF-8"?>
<?xml-stylesheet type="text/xsl" href="myxslt2.xslt"?>
<data>
    <foo>
        <innerfoo1>inner-foo-1-text</innerfoo1>
        <innerfoo2>inner-foo-2-text</innerfoo2>
    </foo>
    <bar>Hello World</bar>
    <foobar>This is a test</foobar>
</data>

My metadata - this is tell the xslt which of the data nodes are to be displayed.

metadata.xml

<Metadata>
    <Data>
        <Detail>foobar</Detail>
        <Detail>bar</Detail>
        <Detail>foo/innerfoo1</Detail>  
    </Data>

</Metadata>

We want to display everything except innerfoo2.

My xslt: myxslt.xsl

<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:msxsl="urn:schemas-microsoft-com:xslt" exclude-result-prefixes="msxsl" version="1.0">
    <xsl:output method="html" doctype-system="http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd" doctype-public="-//W3C//DTD XHTML 1.0 Strict//EN" indent="yes"/>
    <xsl:variable name="main" select="/data"/>
    <xsl:template name="myTemplate">
        <xsl:param name="myparam"/>
        <xsl:param name="node"/>

        Node: <xsl:value-of select="$node"/><br/>
        Inner:<xsl:value-of select="msxsl:node-set($myparam)/data/*[local-name() = $node][1]"/>
    </xsl:template>
    <xsl:template match="/data">
        <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
        HTML STARTS
            <br/>
            <xsl:variable name="data" select="."/>
            Outer1:<xsl:value-of select="$data"/>
            <br/>
            Outer2:<xsl:value-of select="$data/foobar"/>
            <br/>
            <xsl:variable name="defaultMetadata" select="document('metadata.xml')"/>
            <xsl:for-each select="msxsl:node-set($defaultMetadata)/Metadata/Data/Detail">
                <br/>----<br/>
                <xsl:call-template name="myTemplate">
                    <xsl:with-param name="node">
                        <xsl:value-of select="."></xsl:value-of>
                    </xsl:with-param>

                    <xsl:with-param name="myparam">
                        <xsl:copy-of select="$data"/>
                    </xsl:with-param>
                </xsl:call-template>

            </xsl:for-each>
        </html>
    </xsl:template>
</xsl:stylesheet>

(pastebin for better readability - http://pastebin.com/Uw7bFYWM )

Output:

HTML STARTS 
Outer1: inner-foo-1-text inner-foo-2-text Hello World This is a test 
Outer2:This is a test

----
Node: foobar
Inner:This is a test
----
Node: bar
Inner:Hello World
----
Node: foo/innerfoo1
Inner: 

So what I'm doing is looping through each detail element of the metadata, and calling the template passing in the data, the name of of the node to be displayed.

The template then resolves that node and displays it.

So you can see here that it resolves single level elements fine, but I can't use that local-name() = $node comparison when it's more than one element deep.

What I'd like to do is something like:

Inner:<xsl:value-of select="msxsl:node-set($myparam)/data/$node"/>

But this doesn't work.

How can achieve this?

dwjohnston
  • 11,163
  • 32
  • 99
  • 194
  • What you are looking for is 'dynamic' evaluation. Take a look at this question, which is similar to what you are asking, and see if that meets your needs to start with: http://stackoverflow.com/questions/4630023/dynamic-xpath-in-xslt – Tim C Oct 21 '13 at 12:42
  • 2
    NB. If metaData.Xml only ever contains simple expressions (i.e. the expressions are just a list of nodes without any conditions), it should be possible to write a recursive template to split the expression, and check each node in turn. – Tim C Oct 21 '13 at 12:45
  • At this stage I can get away with simple expressions. Could post how you'd implement a recursive template? In the meantime I'll give it a go. – dwjohnston Oct 21 '13 at 21:20

3 Answers3

1

As per the suggestion in the question comments, here's my recursive function to dynamically evaluate simple expressions.

<xsl:template name="recursiveTemplate">
    <xsl:param name="data"/>
    <xsl:param name="node"/>

    <xsl:for-each select="msxsl:node-set($data)/*/*">
        <xsl:choose>

            <xsl:when test="contains($node,'/')">
                <!--not the final node so recursion required-->
                <xsl:if test="substring-before($node, '/') = local-name() ">
                    <xsl:call-template name="recursiveTemplate">
                        <xsl:with-param name="data">
                            <xsl:copy-of select="."/>
                        </xsl:with-param>
                        <xsl:with-param name="node">
                            <xsl:value-of select="substring-after($node,'/')"/>
                        </xsl:with-param>
                    </xsl:call-template>
                </xsl:if>
            </xsl:when>

            <xsl:otherwise>
                <!--final node, so find the one that matches -->
                <xsl:if test="local-name()= $node">
                    <xsl:value-of select="."/>
                </xsl:if>
            </xsl:otherwise>

        </xsl:choose>

    </xsl:for-each>

</xsl:template>

called with

    <xsl:call-template name="recursiveTemplate">
        <xsl:with-param name="data">
            <xsl:copy-of select="msxsl:node-set($myparam)/data"/>
        </xsl:with-param>
        <xsl:with-param name="node">
            <xsl:value-of select="$node"/>
        </xsl:with-param>
    </xsl:call-template>

This seems incredibly verbose and complicated, but it works. Any suggestions about how this could be otherwise implemented?

dwjohnston
  • 11,163
  • 32
  • 99
  • 194
1

If you are stuck with XSLT 1.0 and are unable to use any extension functions that use dynamic evaluation, then with certain restictions, it is possible to do this in pure XSLT. The restriction in this particular answer is your xpath expressions in metadata.xml are nothing that a list of nodes without any conditions.

Before I show this particular answer, it is worth noting you do not need to use the node-set extension functions here, not even in your current XSLT. You would use node-set when you want to convert an "result tree fragment" into nodes that can then be matched by XSLT. If you are referencing input documents directly, you don't actually need it. The reason you may think you need it is actually because of this line...

 <xsl:with-param name="myparam">
     <xsl:copy-of select="$data"/>
 </xsl:with-param>

You should be doing this..

<xsl:with-param name="myparam" select="$data"/>

Using xsl:copy-of means you are actually creating a Result Tree Fragment, but the latter call is not creating any copy, but referencing the original node.

Anyway, to solve this, myTemplate can be made into a recursive template. The idea being you check whether the current parameter contains a slash. If so, you find the node with the name of the element before the slash, and recursively call myTemplate as the new parameter, also passing in the remaining expressin after the slash.

<xsl:template name="myTemplate">
    <xsl:param name="myparam"/>
    <xsl:param name="node"/>

<xsl:choose>
    <xsl:when test="contains($node, '/')">
                <xsl:call-template name="myTemplate">
                     <xsl:with-param name="node" select="substring-after($node, '/')" />
                     <xsl:with-param name="myparam" select="$myparam/*[local-name() = substring-before($node, '/')][1]"/>
                </xsl:call-template>
    </xsl:when>
    <xsl:otherwise>
            Node: <xsl:value-of select="$node"/><br/>
            Inner:<xsl:value-of select="$myparam/*[local-name() = $node][1]"/>
    </xsl:otherwise>
</xsl:choose>
</xsl:template>

The output is therefore only generated when you get an expression with no slash in.

Try this XSLT

<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">
    <xsl:output method="html" doctype-system="http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd" doctype-public="-//W3C//DTD XHTML 1.0 Strict//EN" 

indent="yes"/>
    <xsl:variable name="main" select="/data"/>
    <xsl:template name="myTemplate">
        <xsl:param name="myparam"/>
        <xsl:param name="node"/>

    <xsl:choose>
        <xsl:when test="contains($node, '/')">
                    <xsl:call-template name="myTemplate">
                         <xsl:with-param name="node" select="substring-after($node, '/')" />
                         <xsl:with-param name="myparam" select="$myparam/*[local-name() = substring-before($node, '/')][1]"/>
                    </xsl:call-template>
        </xsl:when>
        <xsl:otherwise>
                Node: <xsl:value-of select="$node"/><br/>
                Inner:<xsl:value-of select="$myparam/*[local-name() = $node][1]"/>
        </xsl:otherwise>
    </xsl:choose>
    </xsl:template>

    <xsl:template match="/data">
        <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
        HTML STARTS
            <br/>
            <xsl:variable name="data" select="."/>
            Outer1:<xsl:value-of select="$data"/>
            <br/>
            Outer2:<xsl:value-of select="$data/foobar"/>
            <br/>
            <xsl:variable name="defaultMetadata" select="document('metadata.xml')"/>
            <xsl:for-each select="$defaultMetadata/Metadata/Data/Detail">
                <br/>----<br/>
                <xsl:call-template name="myTemplate">
                    <xsl:with-param name="node" select="." />
                    <xsl:with-param name="myparam" select="$data"/>
                </xsl:call-template>
            </xsl:for-each>
        </html>
    </xsl:template>
</xsl:stylesheet>
Tim C
  • 70,053
  • 14
  • 74
  • 93
  • Re: `xsl:copy-of` - thanks - I think I didn't read your answer in the [other question](http://stackoverflow.com/questions/19284037/resolving-subelements-of-a-variable-that-has-been-passed-into-a-template) properly. – dwjohnston Oct 21 '13 at 22:57
-1

Just replace your

<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:msxsl="urn:schemas-microsoft-com:xslt" exclude-result-prefixes="msxsl" version="1.0">

with

<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
  xmlns:msxsl="urn:schemas-microsoft-com:xslt"
  xmlns:ext="http://exslt.org/common"
  exclude-result-prefixes="ext msxsl" version="1.0">

and msxsl:node-set with ext:node-set to make it work.

complete xslt is as follows:

<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
  xmlns:msxsl="urn:schemas-microsoft-com:xslt" xmlns:ext="http://exslt.org/common"
  exclude-result-prefixes="ext msxsl" version="1.0">

  <xsl:output method="html" doctype-system="http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"
    doctype-public="-//W3C//DTD XHTML 1.0 Strict//EN" indent="yes"/>
  <xsl:variable name="main" select="/data"/>
  <xsl:template name="myTemplate">
    <xsl:param name="myparam"/>
    <xsl:param name="node"/> Node: <xsl:value-of select="$node"/><br/> Inner:<xsl:choose>
      <xsl:when test="contains($node,'foo/')"><xsl:value-of
          select="ext:node-set($myparam)/data/foo/*[local-name() = substring-after($node,'/')][1]"
        /></xsl:when>
      <xsl:otherwise><xsl:value-of select="ext:node-set($myparam)/data/*[local-name() = $node][1]"
        /></xsl:otherwise>
    </xsl:choose>
  </xsl:template>

  <xsl:template match="/data">
    <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en"> HTML STARTS <br/>
      <xsl:variable name="data" select="."/> Outer1:<xsl:value-of select="$data"/>
      <br/> Outer2:<xsl:value-of select="$data/foobar"/>
      <br/>
      <xsl:variable name="defaultMetadata"
        select="document('metadata.xml')"/>
      <xsl:for-each select="ext:node-set($defaultMetadata)/Metadata/Data/Detail">
        <br/>----<br/>
        <xsl:call-template name="myTemplate">
          <xsl:with-param name="node"><xsl:value-of select="."/></xsl:with-param>
          <xsl:with-param name="myparam"><xsl:copy-of select="$data"/></xsl:with-param>
        </xsl:call-template>
      </xsl:for-each>
    </html>
  </xsl:template>
</xsl:stylesheet>
Navin Rawat
  • 3,208
  • 1
  • 19
  • 31
  • 1
    if/then/else in XPath requires XSLT 2.0, the OP's question (and the presence of `msxsl:node-set`) strongly suggests a requirement for XSLT 1.0. – Ian Roberts Oct 21 '13 at 13:40
  • User can use xsl:choose, and I don't think there is any problem to convert this if condition with xsl:choose – Navin Rawat Oct 22 '13 at 05:26