1

I have Source XML in this format. The relevant part of the source is under the values tag, nested about four levels down, and represents one or more tables of data where the elements are of the format: name = TableName-Row#.FieldName and value = fieldValue.

<root>
    <someData>1</someData>
    <otherData>free</otherData>
    <parent1>
        <parent2>
            <parent3>
                <values>
                    <data>
                        <name>ComputerInfo-1.CPU</name>
                        <value>4</value>
                    </data>
                    <data>
                        <name>ComputerInfo-1.Memory</name>
                        <value>32</value>
                    </data>
                    <data>
                        <name>ComputerInfo-1.Storage</name>
                        <value>1024</value>
                    </data>
                    <data>
                        <name>ComputerInfo-2.CPU</name>
                        <value>2</value>
                    </data>
                    <data>
                        <name>ComputerInfo-2.Memory</name>
                        <value>64</value>
                    </data>
                    <data>
                        <name>ComputerInfo-2.Storage</name>
                        <value>2048</value>
                    </data>
                    <data>
                        <name>ComputerInfo-3.CPU</name>
                        <value>4</value>
                    </data>
                    <data>
                        <name>ComputerInfo-3.Memory</name>
                        <value>16</value>
                    </data>
                    <data>
                        <name>ComputerInfo-3.Storage</name>
                        <value>512</value>
                    </data>
                    <data>
                        <name>UserInfo-1.firstName</name>
                        <value>Mary</value>
                    </data>
                    <data>
                        <name>UserInfo-1.lastName</name>
                        <value>Jones</value>
                    </data>
                    <data>
                        <name>UserInfo-1.login</name>
                        <value>mjones</value>
                    </data>
                    <data>
                        <name>UserInfo-2.firstName</name>
                        <value>Doctor</value>
                    </data>
                    <data>
                        <name>UserInfo-2.lastName</name>
                        <value>Who</value>
                    </data>
                    <data>
                        <name>UserInfo-2.login</name>
                        <value>dwho</value>
                    </data>
                    <data>
                        <name>UserInfo-3.firstName</name>
                        <value>John</value>
                    </data>
                    <data>
                        <name>UserInfo-3.lastName</name>
                        <value>Mellencamp</value>
                    </data>
                    <data>
                        <name>UserInfo-3.login</name>
                        <value>cougar69</value>
                    </data>
                </values>
            </parent3>
        </parent2>
    </parent1>
</root>

The desired result is a CSV file with the header row on top, followed by the corresponding rows:

CPU,Memory,Storage
4,32,1024
2,64,2048
4,16,512

I also wanted to use a variable to hold the particular table I wish to process, e.g.,

<xsl:variable name="table" select="/root/tableName"/>

This is because I have the ability via the application to include the tableName in the source XML if I choose to. However, xsl:key doesn't allow me to use variables in my match and I'm also forced to use XSLT 1.0.

My issue is I can do this in steps, but I really need to do this in a single transform.
Here's what I have so far.

I can pass the Source XML to this transform:

<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">

    <xsl:output method="xml" indent="yes" omit-xml-declaration="yes"/>

    <xsl:template match="/root/parent1/parent2/parent3/values/data">
        <xsl:element name="{name}">
            <xsl:value-of select="value" />
        </xsl:element>
    </xsl:template>

    <xsl:template match="@* | node()">
        <xsl:copy>
            <xsl:apply-templates select="@*|node()"/>
        </xsl:copy>
    </xsl:template>

    <xsl:template match="/">
        <values>
            <xsl:apply-templates select="//values/node()"/>
        </values>
    </xsl:template>

</xsl:stylesheet>

and it will produce this result:

<values>
    <ComputerInfo-1.CPU>4</ComputerInfo-1.CPU>
    <ComputerInfo-1.Memory>32</ComputerInfo-1.Memory>
    <ComputerInfo-1.Storage>1024</ComputerInfo-1.Storage>
    <ComputerInfo-2.CPU>2</ComputerInfo-2.CPU>
    <ComputerInfo-2.Memory>64</ComputerInfo-2.Memory>
    <ComputerInfo-2.Storage>2048</ComputerInfo-2.Storage>
    <ComputerInfo-3.CPU>4</ComputerInfo-3.CPU>
    <ComputerInfo-3.Memory>16</ComputerInfo-3.Memory>
    <ComputerInfo-3.Storage>512</ComputerInfo-3.Storage>
    <UserInfo-1.firstName>Mary</UserInfo-1.firstName>
    <UserInfo-1.lastName>Jones</UserInfo-1.lastName>
    <UserInfo-1.login>mjones</UserInfo-1.login>
    <UserInfo-2.firstName>Doctor</UserInfo-2.firstName>
    <UserInfo-2.lastName>Who</UserInfo-2.lastName>
    <UserInfo-2.login>dwho</UserInfo-2.login>
    <UserInfo-3.firstName>John</UserInfo-3.firstName>
    <UserInfo-3.lastName>Mellencamp</UserInfo-3.lastName>
    <UserInfo-3.login>cougar69</UserInfo-3.login>
</values>

I can then take this result and apply the following transform:

<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">

    <xsl:output method="xml" indent="yes" omit-xml-declaration="yes"/>

    <xsl:key name="elementByRow" match="/*/*[contains(name(), 'ComputerInfo')]" use="substring-before(name(), '.')"/>

    <xsl:template match="/*">
        <values>
            <xsl:apply-templates select="*[generate-id() = generate-id(key('elementByRow', substring-before(name(), '.'))[1])]" />
        </values>
    </xsl:template>

    <xsl:template match="*">
        <Row>
            <xsl:for-each select="key('elementByRow', substring-before(name(), '.'))">
                <xsl:element name="{substring-after(name(), '.')}">
                    <xsl:value-of select="." />
                </xsl:element>
            </xsl:for-each>
        </Row>
    </xsl:template>

</xsl:stylesheet>

which will produce this result:

<values>
    <Row>
        <CPU>4</CPU>
        <Memory>32</Memory>
        <Storage>1024</Storage>
    </Row>
    <Row>
        <CPU>2</CPU>
        <Memory>64</Memory>
        <Storage>2048</Storage>
    </Row>
    <Row>
        <CPU>4</CPU>
        <Memory>16</Memory>
        <Storage>512</Storage>
    </Row>
</values>

I specifically aimed to get it to this previous format, because I found another posting here ( convert xml document to comma delimited (CSV) file using xslt stylesheet ), which allows me to take this result and apply the transform from the aforementioned post to get the desired result. Here is the transform from that post for reference:

<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
    <xsl:key name="field" match="/*/*/*" use="name()" />
    <xsl:output method="text"/>

    <xsl:template match="/">
        <xsl:for-each select="*/*/*[generate-id() = generate-id(key('field',name())[1])]">
            <xsl:value-of select="name()" />
            <xsl:if test="position() != last()">,</xsl:if>
        </xsl:for-each>
        <xsl:text>&#10;</xsl:text>
        <xsl:apply-templates select="*/*" mode="row"/>
    </xsl:template>

    <xsl:template match="*" mode="row">
        <xsl:variable name="row" select="*" />
        <xsl:for-each select="/*/*/*[generate-id() = generate-id(key('field',name())[1])]">
            <xsl:variable name="name" select="name()" />
            <xsl:apply-templates select="$row[name()=$name]" mode="data" />
            <xsl:if test="position() != last()">,</xsl:if>
        </xsl:for-each>
        <xsl:text>&#10;</xsl:text>
    </xsl:template>

    <xsl:template match="*" mode="data">
        <xsl:choose>
            <xsl:when test="contains(text(),',')">
                <xsl:text>&quot;</xsl:text>
                <xsl:call-template name="doublequotes">
                    <xsl:with-param name="text" select="text()" />
                </xsl:call-template>
                <xsl:text>&quot;</xsl:text>
            </xsl:when>
            <xsl:otherwise>
                <xsl:value-of select="." />
            </xsl:otherwise>
        </xsl:choose>
        <xsl:if test="position() != last()">,</xsl:if>
    </xsl:template>

    <xsl:template name="doublequotes">
        <xsl:param name="text" />
        <xsl:choose>
            <xsl:when test="contains($text,'&quot;')">
                <xsl:value-of select="concat(substring-before($text,'&quot;'),'&quot;&quot;')" />
                <xsl:call-template name="doublequotes">
                    <xsl:with-param name="text" select="substring-after($text,'&quot;')" />
                </xsl:call-template>
            </xsl:when>
            <xsl:otherwise>
                <xsl:value-of select="$text" />
            </xsl:otherwise>
        </xsl:choose>
    </xsl:template>
</xsl:stylesheet>
Community
  • 1
  • 1
Jasonovich
  • 621
  • 5
  • 13
  • It sounds like you have a working solution. I presume you want a more direct one? To process an XSLT 1.0 Result Tree Fragment as an XML tree, you'll need to convert it to a Nodeset. There's an EXSLT function which does so, and many XSLT processors offer some flavor of nodeset() as a built-in custom operation as well. – keshlam Dec 31 '13 at 23:54
  • I found a way to get there. My problem is it's only a working solution for me if I can do it one transformation. There are programmatic reasons why. The particular app I'm working with has to take the Source XML and output the desired result in one step. I just need help combining these steps into one transform instead of three. – Jasonovich Jan 01 '14 at 00:13

1 Answers1

2

Couldn't you do this simply as:

<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"> 
<xsl:output method="text" encoding="utf-8"/>

<xsl:template match="/">
    <xsl:text>CPU,Memory,Storage&#10;</xsl:text>
    <xsl:for-each select="root/parent1/parent2/parent3/values/data[starts-with(./name, 'ComputerInfo')]">
        <xsl:value-of select="value" />
        <xsl:choose>
            <xsl:when test="position() mod 3">
                <xsl:value-of select="','" />
            </xsl:when>
            <xsl:otherwise>
                <xsl:value-of select="'&#10;'" />
            </xsl:otherwise>
        </xsl:choose>
    </xsl:for-each>
</xsl:template>
</xsl:stylesheet>  

assuming the structure of the data is constant.


ADDED:

Now, if you wanted to generalize the above, you could do something like:

<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"> 
<xsl:output method="text" version="1.0" encoding="utf-8" indent="yes"/>

<xsl:template match="/">
<xsl:variable name="tableName" select="root/tableName"/>
<xsl:variable name="tableData" select="root/parent1/parent2/parent3/values/data[substring-before(./name, '-')=$tableName]" />
<xsl:variable name="firstRow" select="$tableData[substring-before(substring-after(./name, '-'), '.')='1']" />
<xsl:variable name="countCols" select="count($firstRow)" />

    <xsl:for-each select="$firstRow">
    <xsl:value-of select="substring-after(./name, '-1.')" />
            <xsl:choose>
                <xsl:when test="position() mod $countCols">
                    <xsl:value-of select="','" />
                </xsl:when>
                <xsl:otherwise>
                    <xsl:value-of select="'&#10;'" />
                </xsl:otherwise>
            </xsl:choose>
    </xsl:for-each> 

    <xsl:for-each select="$tableData">
        <xsl:value-of select="value" />
        <xsl:choose>
            <xsl:when test="position() mod $countCols">
                <xsl:value-of select="','" />
            </xsl:when>
            <xsl:otherwise>
                <xsl:value-of select="'&#10;'" />
            </xsl:otherwise>
        </xsl:choose>
    </xsl:for-each>

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

Given an input of:

<root>
    <tableName>ComputerInfo</tableName>
    <someData>1</someData>
    <otherData>free</otherData>
    <parent1>
        <parent2>
            <parent3>
                <values>
                    <data>
                        <name>ComputerInfo-1.CPU</name>
                        <value>4</value>
                    </data>
                    <data>
                        <name>ComputerInfo-1.Memory</name>
                        <value>32</value>
                    </data>
                    <data>
                        <name>ComputerInfo-1.Storage</name>
                        <value>1024</value>
                    </data>
                    <data>
                        <name>ComputerInfo-1.Age</name>
                        <value>11</value>
                    </data>
                    <data>
                        <name>ComputerInfo-2.CPU</name>
                        <value>2</value>
                    </data>
                    <data>
                        <name>ComputerInfo-2.Memory</name>
                        <value>64</value>
                    </data>
                    <data>
                        <name>ComputerInfo-2.Storage</name>
                        <value>2048</value>
                    </data>
                    <data>
                        <name>ComputerInfo-2.Age</name>
                        <value>22</value>
                    </data>
                    <data>
                        <name>ComputerInfo-3.CPU</name>
                        <value>4</value>
                    </data>
                    <data>
                        <name>ComputerInfo-3.Memory</name>
                        <value>16</value>
                    </data>
                    <data>
                        <name>ComputerInfo-3.Storage</name>
                        <value>512</value>
                    </data>
                    <data>
                        <name>ComputerInfo-3.Age</name>
                        <value>33</value>
                    </data>
                    <data>
                        <name>UserInfo-1.firstName</name>
                        <value>Mary</value>
                    </data>
                    <data>
                        <name>UserInfo-1.lastName</name>
                        <value>Jones</value>
                    </data>
                    <data>
                        <name>UserInfo-1.login</name>
                        <value>mjones</value>
                    </data>
                    <data>
                        <name>UserInfo-2.firstName</name>
                        <value>Doctor</value>
                    </data>
                    <data>
                        <name>UserInfo-2.lastName</name>
                        <value>Who</value>
                    </data>
                    <data>
                        <name>UserInfo-2.login</name>
                        <value>dwho</value>
                    </data>
                    <data>
                        <name>UserInfo-3.firstName</name>
                        <value>John</value>
                    </data>
                    <data>
                        <name>UserInfo-3.lastName</name>
                        <value>Mellencamp</value>
                    </data>
                    <data>
                        <name>UserInfo-3.login</name>
                        <value>cougar69</value>
                    </data>
                </values>
            </parent3>
        </parent2>
    </parent1>
</root>

it would return:

CPU,Memory,Storage,Age
4,32,1024,11
2,64,2048,22
4,16,512,33

but if you change the <tableName> to "UserInfo", the same stylesheet will now produce:

firstName,lastName,login
Mary,Jones,mjones
Doctor,Who,dwho
John,Mellencamp,cougar69
michael.hor257k
  • 113,275
  • 6
  • 33
  • 51