tags:

views:

261

answers:

3

From this XML source :

<?xml version="1.0" encoding="utf-8" ?>
<ROOT>
  <STRUCT>
    <COL order="1" nodeName="FOO/BAR" colName="Foo Bar" />
    <COL order="2" nodeName="FIZZ" colName="Fizz" />
  </STRUCT>

  <DATASET>
    <DATA>
      <FIZZ>testFizz</FIZZ>
      <FOO>
        <BAR>testBar</BAR>
        <LIB>testLib</LIB>
      </FOO>
    </DATA>
    <DATA>
      <FIZZ>testFizz2</FIZZ>
      <FOO>
        <BAR>testBar2</BAR>
        <LIB>testLib2</LIB>
      </FOO>
    </DATA>
  </DATASET>
</ROOT>

I want to generate this HTML :

<html>
  <head>
    <title>Test</title>
  </head>
  <body>
    <table border="1">
      <tr>
        <td>Foo Bar</td>
        <td>Fizz</td>
      </tr>
      <tr>
        <td>testBar</td>
        <td>testFizz</td>
      </tr>
      <tr>
        <td>testBar2</td>
        <td>testFizz2</td>
      </tr>
    </table>
  </body>
</html>

Here is the XSLT I currently have :

<?xml version="1.0" encoding="utf-8"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:msxsl="urn:schemas-microsoft-com:xslt" exclude-result-prefixes="msxsl">
  <xsl:output method="html" indent="yes"/>

  <xsl:template match="/ROOT">
    <html>
      <head>
        <title>Test</title>
      </head>
      <body>
        <table border="1">
          <tr>
            <!--Generate the table header-->
            <xsl:apply-templates select="STRUCT/COL">
              <xsl:sort data-type="number" select="@order"/>
            </xsl:apply-templates>
          </tr>
          <xsl:apply-templates select="DATASET/DATA" />
        </table>
      </body>
    </html>
  </xsl:template>

  <xsl:template match="COL">
    <!--Template for generating the table header-->
    <td>
      <xsl:value-of select="@colName"/>
    </td>
  </xsl:template>

  <xsl:template match="DATA">
    <xsl:variable name="pos" select="position()" />
    <tr>
      <xsl:for-each select="/ROOT/STRUCT/COL">
        <xsl:sort data-type="number" select="@order"/>
        <xsl:variable name="elementName" select="@nodeName" />
        <td>
          <xsl:value-of select="/ROOT/DATASET/DATA[$pos]/*[name() = $elementName]" />
        </td>
      </xsl:for-each>
    </tr>
  </xsl:template>

</xsl:stylesheet>

It almost works, the problem I have is to retrieve the correct DATA node from the path specified in the "nodeName" attribute value of the STRUCT block.

+1  A: 

The problem with what you've got:

<xsl:variable name="elementName" select="@nodeName" />
...
<xsl:value-of select="/ROOT/DATASET/DATA[$pos]/*[name() = $elementName]" />

is that it assumes elementName is only a single element's name. If it's an arbitrary XPath expression, the test will fail.

The second problem you'll run into (or probably already have) is that attribute value templates are not allowed in select clauses, so you can't do something simple like this:

<xsl:value-of select="/ROOT/DATASET/DATA[$pos]/{$elementName}" />

What you need is something that will dynamically create the XPath expression to the element you're looking for, and then dynamically evaluate that expression.

For a solution, I turned to EXSLT's evaluate() function, in the dynamic library. I had to use it twice: once to build up the entire XPath expression representing the query, and once to evaluate that query. The advantage of this approach is that you get access to evaluate's full XPath parsing and execution capabilities.

<xsl:variable name="elementLocation" select="@nodeName" />
<xsl:variable name="query" select="concat('/ROOT/DATASET/DATA[$pos]/',
                                          dyn:evaluate('$elementLocation'))"/>
...
<xsl:value-of select="dyn:evaluate($query)"/>

where the dyn namespace is declared up top as http://exslt.org/dynamic. Figuring out where to quote here is tricky and took me several tries to get right.

Using these instead of your elementName and value-of expressions, I get:

<html xmlns:msxsl="urn:schemas-microsoft-com:xslt"
      xmlns:dyn="http://exslt.org/dynamic"&gt;
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>Test</title>
</head>
<body><table border="1">
<tr>
<td>Foo Bar</td>
<td>Fizz</td>
</tr>
<tr>
<td>testBar</td>
<td>testFizz</td>
</tr>
<tr>
<td>testBar2</td>
<td>testFizz2</td>
</tr>
</table></body>
</html>

which is what I think you're looking for.

Unfortunately, I'm not versed in MSXML, so I can't tell you whether your specific XSLT processor supports this extension or something similar.

Owen S.
Nice answer, thanks for this explanation (+1)
Olivier PAYEN
+5  A: 

Here is a pure XSLT 1.0 solution that doesn't use any extensions:

<xsl:stylesheet version="1.0"
 xmlns:xsl="http://www.w3.org/1999/XSL/Transform"&gt;
 <xsl:output omit-xml-declaration="yes" indent="yes"/>

 <xsl:template match="/">
  <html>
    <head>
      <title>Test</title>
    </head>
    <body>
      <table border="1">
        <xsl:apply-templates select="*/STRUCT"/>
        <xsl:apply-templates select="*/DATASET/DATA"/>
      </table>
    </body>
  </html>
 </xsl:template>

 <xsl:template match="STRUCT">
  <tr>
    <xsl:apply-templates select="COL"/>
  </tr>
 </xsl:template>

 <xsl:template match="COL">
  <td><xsl:value-of select="@colName"/></td>
 </xsl:template>

 <xsl:template match="DATA">
      <tr>
        <xsl:apply-templates select="/*/STRUCT/*/@nodeName">
         <xsl:with-param name="pCurrentNode" select="."/>
        </xsl:apply-templates>
      </tr>
 </xsl:template>

 <xsl:template match="@nodeName" name="getNodeValue">
   <xsl:param name="pExpression" select="string(.)"/>
   <xsl:param name="pCurrentNode"/>

   <xsl:choose>
    <xsl:when test="not(contains($pExpression, '/'))">
      <td><xsl:value-of select="$pCurrentNode/*[name()=$pExpression]"/></td>
    </xsl:when>
    <xsl:otherwise>
      <xsl:call-template name="getNodeValue">
        <xsl:with-param name="pExpression"
          select="substring-after($pExpression, '/')"/>
        <xsl:with-param name="pCurrentNode" select=
        "$pCurrentNode/*[name()=substring-before($pExpression, '/')]"/>
      </xsl:call-template>
    </xsl:otherwise>
   </xsl:choose>

 </xsl:template>
</xsl:stylesheet>

when this transformation is applied on the provided XML document:

<ROOT>
  <STRUCT>
    <COL order="1" nodeName="FOO/BAR" colName="Foo Bar" />
    <COL order="2" nodeName="FIZZ" colName="Fizz" />
  </STRUCT>

  <DATASET>
    <DATA>
      <FIZZ>testFizz</FIZZ>
      <FOO>
        <BAR>testBar</BAR>
        <LIB>testLib</LIB>
      </FOO>
    </DATA>
    <DATA>
      <FIZZ>testFizz2</FIZZ>
      <FOO>
        <BAR>testBar2</BAR>
        <LIB>testLib2</LIB>
      </FOO>
    </DATA>
  </DATASET>
</ROOT>

the wanted, correct result is produced:

<html>
    <head>
        <title>Test</title>
    </head>
    <body>
        <table border="1">
            <tr>
                <td>Foo Bar</td>
                <td>Fizz</td>
            </tr>
            <tr>
                <td>testBar</td>
                <td>testFizz</td>
            </tr>
            <tr>
                <td>testBar2</td>
                <td>testFizz2</td>
            </tr>
        </table>
    </body>
</html>
Dimitre Novatchev
I.e. parse out the hierarchical structure and do recursive calls to the templates. Nice, but only works for element hierarchies. If you need to scale up to arbitrary expressions in COL/@nodeName, I'd definitely start looking at extensions instead.
Owen S.
@Owen-s: I have my own XPath 2.0 parser implemented completely with pure XSLT 2.0 -- see http://dnovatchev.spaces.live.com/blog/cns!44B0A32C2CCF7488!367.entry?sa=34732502 :)
Dimitre Novatchev
@Dimitre: Very good answer. That's what I call a "walking" function, although I have rarely had to use it. I know your parser developed with XSLT2. Have you ever thought of developing a parser with XSLT1 following the pattern of functional parser? Something like "Higher-Order Functions for Parsing" [Hutton, 1992]
Alejandro
@Alejandro: I will be glad to have enough free time to do more on my XSLT LR-Parsing Framework. This item is in the high-priority group, although there are some even more attractive tasks. Besides parsing, do you know something challenging and broadly useful that I could consider for my future "fun-projects"?
Dimitre Novatchev
@Dimitre: Inspired by your demonstration of functional programming in XSLT, I'm also looking to follow the path of YAHT (Yet Another Haskell Tutorial) ranging from functional parsers (actually as an introduction to monoids) to regular expressions. Having a XSLT library (today, truly widely cross browsers) for dynamic evaluation of XPath and regular expressions would greatly increase the adoption of XSLT for transform and manipulating XML (including XHTML) on the client, giving another step in the development of the semantic web. Or that's what I bealive...
Alejandro
It works perfectly. Thanks a lot !!
Olivier PAYEN
A: 

Owen,

your solution making use of dyn:evaluate>() is fine, but does not work in browsers, see here:

http://www.biglist.com/lists/lists.mulberrytech.com/xsl-list/archives/201008/msg00126.html

The problem with what you've got: ... is that it assumes elementName is only a single element's name. If it's an arbitrary XPath expression, the test will fail.

Dimitrie's solution was not for general XPath parsing, and the handling of more than one node can simply be added to his solution by adding <xsl:for-each ...>s, see the diff below:

$ diff -u x.xsl y.xsl
--- x.xsl       2010-08-13 14:53:42.000000000 +0200
+++ y.xsl       2010-08-14 11:59:42.000000000 +0200
@@ -40,15 +40,19 @@

    <xsl:choose>
     <xsl:when test="not(contains($pExpression, '/'))">
-      <td><xsl:value-of select="$pCurrentNode/*[name()=$pExpression]"/></td>
+     <xsl:for-each select="$pCurrentNode/*[name()=$pExpression]">
+      <td><xsl:value-of select="."/></td>
+     </xsl:for-each>
     </xsl:when>
     <xsl:otherwise>
+     <xsl:for-each select="$pCurrentNode/*[name()=substring-before($pExpression, '/')]">
       <xsl:call-template name="getNodeValue">
         <xsl:with-param name="pExpression"
           select="substring-after($pExpression, '/')"/>
         <xsl:with-param name="pCurrentNode" select=
-        "$pCurrentNode/*[name()=substring-before($pExpression, '/')]"/>
+        "."/>
       </xsl:call-template>
+     </xsl:for-each>
     </xsl:otherwise>
    </xsl:choose>

$ 

Hermann Stamm-Wilbrandt