paulzip
paulzip

Reputation: 86

Flatten XML using XSLT but based on nesting level

I'm new to XSLT and I'm trying to write some XSLT that will flatten any given XML such that a new line occurs whenever the nesting level changes. My input can be any XML document, with any number of nested levels so the structure isn't known to the XSLT. Due to the tools available to me, my solution has to use XSLT version 1.0.

For example.

<?xml version="1.0"?>
<ROWSET>
  <ROW>
    <CUSTOMER_ID>0</CUSTOMER_ID>
    <NAME>Default Company</NAME>
    <BONUSES>
      <BONUSES_ROW>
        <BONUS_ID>21</BONUS_ID>
        <DESCRIPTION>Performance Bonus</DESCRIPTION>
      </BONUSES_ROW>
      <BONUSES_ROW>
        <BONUS_ID>26</BONUS_ID>
        <DESCRIPTION>Special Bonus</DESCRIPTION>
      </BONUSES_ROW>
    </BONUSES>
  </ROW>
  <ROW>
    <CUSTOMER_ID>1</CUSTOMER_ID>
    <NAME>Dealer 1</NAME>
    <BONUSES>
      <BONUSES_ROW>
        <BONUS_ID>27</BONUS_ID>
        <DESCRIPTION>June Bonus</DESCRIPTION>
        <BONUS_VALUES>
          <BONUS_VALUES_ROW>
            <VALUE>10</VALUE>
            <PERCENT>N</PERCENT>
          </BONUS_VALUES_ROW>
          <BONUS_VALUES_ROW>
            <VALUE>11</VALUE>
            <PERCENT>Y</PERCENT>
          </BONUS_VALUES_ROW>
        </BONUS_VALUES>
      </BONUSES_ROW>
    </BONUSES>
  </ROW>
</ROWSET>

needs to becomes....

0, Default Company
21, Performance Bonus
26, Special Bonus
1, Dealer 1
27, June Bonus
10, N
11, Y

The XSLT I've written so far is...

<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
  <xsl:output method="text" encoding="iso-8859-1"/>
  <xsl:strip-space elements="*" />
  <xsl:template match="/*/child::*">
    <xsl:apply-templates select="*"/> 
  </xsl:template>
   <xsl:template match="*">
     <xsl:value-of select="text()" />
     <xsl:if test="position()!= last()"><xsl:text>,</xsl:text></xsl:if>
     <xsl:if test="position()= last()"><xsl:text>&#xD;</xsl:text></xsl:if>     
     <xsl:apply-templates select="./child::*"/>          
   </xsl:template>
</xsl:stylesheet> 

but my output just isn't correct, with gaps and unnecessary data.

0,Default Company,
,21,Performance Bonus

26,Special Bonus
1,Dealer 1,

27,June Bonus,
,10,N

11,Y

It seems there needs to be a check as whether or not a node can contain text, but I'm stuck and could do with an XSLT expert's help.

Upvotes: 4

Views: 1338

Answers (2)

Daniel Haley
Daniel Haley

Reputation: 52888

You can test to see if an element has text by doing: *[text()]

Example...

XML Input

<ROWSET>
    <ROW>
        <CUSTOMER_ID>0</CUSTOMER_ID>
        <NAME>Default Company</NAME>
        <BONUSES>
            <BONUSES_ROW>
                <BONUS_ID>21</BONUS_ID>
                <DESCRIPTION>Performance Bonus</DESCRIPTION>
            </BONUSES_ROW>
            <BONUSES_ROW>
                <BONUS_ID>26</BONUS_ID>
                <DESCRIPTION>Special Bonus</DESCRIPTION>
            </BONUSES_ROW>
        </BONUSES>
    </ROW>
    <ROW>
        <CUSTOMER_ID>1</CUSTOMER_ID>
        <NAME>Dealer 1</NAME>
        <BONUSES>
            <BONUSES_ROW>
                <BONUS_ID>27</BONUS_ID>
                <DESCRIPTION>June Bonus</DESCRIPTION>
                <BONUS_VALUES>
                    <BONUS_VALUES_ROW>
                        <VALUE>10</VALUE>
                        <PERCENT>N</PERCENT>
                    </BONUS_VALUES_ROW>
                    <BONUS_VALUES_ROW>
                        <VALUE>11</VALUE>
                        <PERCENT>Y</PERCENT>
                    </BONUS_VALUES_ROW>
                </BONUS_VALUES>
            </BONUSES_ROW>
        </BONUSES>
    </ROW>
</ROWSET>

XSLT 1.0

<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
    <xsl:output method="text"/>
    <xsl:strip-space elements="*"/>

    <xsl:template match="*[text()]">
        <xsl:if test="position() > 1">
            <xsl:text>, </xsl:text>
        </xsl:if>
        <xsl:value-of select="."/>
        <xsl:if test="not(following-sibling::*[text()])">
            <xsl:text>&#xA;</xsl:text>          
        </xsl:if>
    </xsl:template>

</xsl:stylesheet>

Text Output

0, Default Company
21, Performance Bonus
26, Special Bonus
1, Dealer 1
27, June Bonus
10, N
11, Y

Also, take a look at Built-in Template Rules to see what makes this stylesheet work with only one template.

EDIT

This stylesheet will also output a comma for empty elements:

<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
    <xsl:output method="text"/>
    <xsl:strip-space elements="*"/>

    <xsl:template match="*[text() or not(*)]">
        <xsl:if test="position() > 1">
            <xsl:text>, </xsl:text>
        </xsl:if>
        <xsl:value-of select="."/>
        <xsl:if test="not(following-sibling::*[text() or not(*)])">
            <xsl:text>&#xA;</xsl:text>          
        </xsl:if>
    </xsl:template>

</xsl:stylesheet>

Upvotes: 2

Manoj
Manoj

Reputation: 329

Check or validate your XML.

In your XML, Start Node <ROWSET> does not end with </ROWSET>

If  <ROWSET> node ends with </ROWSET> then, following XSL code will work for your expectation output

<xsl:template match="ROWSET">
    <xsl:for-each select="ROW">
        <xsl:value-of select="CUSTOMER_ID"/>, <xsl:value-of select="NAME"/><xsl:text>&#10;</xsl:text>

        <xsl:for-each select="BONUSES/BONUSES_ROW">
            <xsl:value-of select="BONUS_ID"/>, <xsl:value-of select="DESCRIPTION"/><xsl:text>&#10;</xsl:text>
            <xsl:variable name="cnt" select="count(BONUS_VALUES/BONUS_VALUES_ROW)"/>
            <xsl:if test="$cnt &gt; 0">
                <xsl:for-each select="BONUS_VALUES/BONUS_VALUES_ROW">
                    <xsl:value-of select="VALUE"/>, <xsl:value-of select="PERCENT"/><xsl:text>&#10;</xsl:text>
                </xsl:for-each>
            </xsl:if>
        </xsl:for-each>
    </xsl:for-each>
</xsl:template>

Upvotes: 0

Related Questions