5
votes

Input xml

<catalog>
    <product id="1">
        <name>abc</name>
        <category>aaa</category>
        <category>bbb</category>
        <category>ccc</category>
    </product>
    <product id="2">
        <name>cde</name>
        <category>aaa</category>
        <category>bbb</category>
    </product>
</catalog>

Expected Output xml

<products>
    <product>
        <id>1</id>
        <name>abc</name>
        <category>aaa,bbb,ccc</category>
    </product>
    <product>
        <id>2</id>
        <name>cde</name>
        <category>aaa,bbb</category>
    </product>
</products>

XSLT for transformation

<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
    <xsl:output method="xml" indent="yes"/>
    <xsl:template match="/catalog">
        <products>
            <xsl:for-each select="product">
                <product>
                    <id><xsl:value-of select="@id"/></id>
                    <name><xsl:value-of select="name"/></name>
                    <category><xsl:value-of select="category" /></category>
                </product>
            </xsl:for-each>
        </products>
    </xsl:template>
</xsl:stylesheet>

Actual Output xml :(

<products>
    <product>
        <id>1</id>
        <name>abc</name>
        <category>aaa</category>
    </product>
    <product>
        <id>2</id>
        <name>cde</name>
        <category>aaa</category>
    </product>
</products>

Code needed in looping through all sibling node by the name 'category' under every 'product' and merging/concatenating into single node separated by a comma. Number of 'category' varies for every product and hence the count is unknown.

3

3 Answers

8
votes

Using this handy join call-template defined here, this becomes as simple as:

<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
    <xsl:output method="xml" indent="yes"/>
    <xsl:template match="/catalog">
        <products>
            <xsl:for-each select="product">
                <product>
                    <id>
                        <xsl:value-of select="@id"/>
                    </id>
                    <name>
                        <xsl:value-of select="name"/>
                    </name>
                    <category>
                        <xsl:call-template name="join">
                            <xsl:with-param name="list" select="category" />
                            <xsl:with-param name="separator" select="','" />
                        </xsl:call-template>
                    </category>
                </product>
            </xsl:for-each>
        </products>
    </xsl:template>

    <xsl:template name="join">
        <xsl:param name="list" />
        <xsl:param name="separator"/>

        <xsl:for-each select="$list">
            <xsl:value-of select="." />
            <xsl:if test="position() != last()">
                <xsl:value-of select="$separator" />
            </xsl:if>
        </xsl:for-each>
    </xsl:template>

</xsl:stylesheet>

Output:

<products>
  <product>
    <id>1</id>
    <name>abc</name>
    <category>aaa,bbb,ccc</category>
  </product>
  <product>
    <id>2</id>
    <name>cde</name>
    <category>aaa,bbb</category>
  </product>
</products>
6
votes

In XSLT 2.0 you only need to make one small change to your code:

<category><xsl:value-of select="category" separator=","/></category>

Note that if you require an XSLT 1.0 solution it's a good idea to say so. Some people in some environments are stuck on 1.0, but a lot of people aren't.

3
votes

Here's one other XSLT 1.0 solution.

When this XSLT:

<?xml version="1.0" encoding="utf-8"?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
version="1.0">
  <xsl:output omit-xml-declaration="no" indent="yes" />
  <xsl:strip-space elements="*" />

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

  <xsl:template match="product">
    <xsl:copy>
      <xsl:apply-templates select="*[not(self::category)]" />
      <category>
        <xsl:apply-templates select="category/text()" />
      </category>
    </xsl:copy>
  </xsl:template>

  <xsl:template match="category/text()">
    <xsl:if test="position() &gt; 1">,</xsl:if>
    <xsl:value-of select="."/>
  </xsl:template>
</xsl:stylesheet>

...is applied to the OP's original XML:

<catalog>
  <product id="1">
    <name>abc</name>
    <category>aaa</category>
    <category>bbb</category>
    <category>ccc</category>
  </product>
  <product id="2">
    <name>cde</name>
    <category>aaa</category>
    <category>bbb</category>
  </product>
</catalog>

...the desired result is produced:

<?xml version="1.0"?>
<catalog>
  <product>
    <name>abc</name>
    <category>aaa,bbb,ccc</category>
  </product>
  <product>
    <name>cde</name>
    <category>aaa,bbb</category>
  </product>
</catalog>

Explanation:

  • The first template -- the Identity Template -- matches all nodes and attributes and copies them to the result document as-is.
  • The second template overrides the Identity Template by creating a new <category> element and processing the text children of each <category> element in the current location of the document.
  • The final template outputs the text values and commas as necessary.