XSLT 1 Exclude parent element if not in a list - xslt

I want to perform an xslt transformation where I split products up by type. As an example:
Source XML:
<Products>
<Product>
<Name>Cheese</Name>
<Value>30</Value>
</Product>
<Product>
<Name>Bread</Name>
<Value>10</Value>
</Product>
<Product>
<Name>Bacon</Name>
<Value>100</Value>
</Product>
</Products>
Required Output XML:
<Products>
<AnimalProducts>
<Product>
<Name>Cheese</Name>
<Value>30</Value>
</Product>
<Product>
<Name>Bacon</Name>
<Value>100</Value>
</Product>
</AnimalProducts>
<VeganProducts>
<Product>
<Name>Bread</Name>
<Value>10</Value>
</Product>
</VeganProducts>
</Products>
If there are no animal products, or no vegan products then the parent elements should not be included. I have it half working with:
<xsl:variable name="veganProducts" select="'Bread:Lettuce'" />
<xsl:if test="Products/Product[count(*) > 0]">
<AnimalProducts>
<xsl:for-each select="Products/Product">
<xsl:if test="not(contains(concat(':', $veganProducts, ':'), concat(':', Name, ':')))">
<Product>
<Name>
<xsl:value-of select="Name" />
</Name>
<Value>
<xsl:value-of select="Value" />
</Value>
</Product>
</xsl:if>
</xsl:for-each>
</AnimalProducts>
<VeganProducts>
<xsl:for-each select="Products/Product">
<xsl:if test="contains(concat(':', $veganProducts, ':'), concat(':', Name, ':'))">
<Product>
<Name>
<xsl:value-of select="Name" />
</Name>
<Value>
<xsl:value-of select="Value" />
</Value>
</Product>
</xsl:if>
</xsl:for-each>
</VeganProducts>
</xsl:if>
The problem is I am getting empty parent elements if there are no vegan or animal products in certain cases. I am unsure how I can test for this.

This is a good time to make use of variables:
<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:variable name="veganProductNames" select="'Bread:Lettuce'" />
<xsl:variable name="veganProductNamesPadded"
select="concat(':', $veganProductNames, ':')" />
<xsl:template match="#* | node()">
<xsl:copy>
<xsl:apply-templates select="#* | node()"/>
</xsl:copy>
</xsl:template>
<xsl:template match="/*">
<xsl:copy>
<xsl:variable name="animalProducts"
select="Product[not(contains($veganProductNamesPadded,
concat(':', Name, ':')))]" />
<xsl:variable name="veganProducts"
select="Product[contains($veganProductNamesPadded,
concat(':', Name, ':'))]" />
<xsl:if test="$animalProducts">
<AnimalProducts>
<xsl:apply-templates select="$animalProducts" />
</AnimalProducts>
</xsl:if>
<xsl:if test="$veganProducts">
<VeganProducts>
<xsl:apply-templates select="$veganProducts" />
</VeganProducts>
</xsl:if>
</xsl:copy>
</xsl:template>
</xsl:stylesheet>
When run on your sample input, the result is:
<Products>
<AnimalProducts>
<Product>
<Name>Cheese</Name>
<Value>30</Value>
</Product>
<Product>
<Name>Bacon</Name>
<Value>100</Value>
</Product>
</AnimalProducts>
<VeganProducts>
<Product>
<Name>Bread</Name>
<Value>10</Value>
</Product>
</VeganProducts>
</Products>

I would prefer to solve this the XML way:
XSLT 1.0
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:my="http://www.example.com/my"
exclude-result-prefixes="my">
<xsl:output method="xml" version="1.0" encoding="UTF-8" indent="yes"/>
<my:vegan-items>
<item>Bread</item>
<item>Lettuce</item>
</my:vegan-items>
<xsl:variable name="vegan-items" select="document('')/xsl:stylesheet/my:vegan-items/item" />
<xsl:template match="/Products">
<xsl:variable name="vegan-products" select="Product[Name=$vegan-items]" />
<xsl:variable name="animal-products" select="Product[not(Name=$vegan-items)]" />
<xsl:copy>
<xsl:if test="$animal-products">
<AnimalProducts>
<xsl:copy-of select="$animal-products"/>
</AnimalProducts>
</xsl:if>
<xsl:if test="$vegan-products">
<VeganProducts>
<xsl:copy-of select="$vegan-products"/>
</VeganProducts>
</xsl:if>
</xsl:copy>
</xsl:template>
</xsl:stylesheet>
Here we are assuming that anything not "vegan" is "animal" (since no list of "animal" items has been provided).
Keeping an external XML document listing the items in each category would probably be even better.

Related

Generate new group based on node value in XSLT

I am new to XSLT, and I am trying to create new group based on node value eventType so if eventType is alert, create new group event.
I am checking for last sibling
Input XML
<?xml version="1.0" encoding="UTF-8"?><Rowsets >
<Row>
<eventId>2</eventId>
<plantId>1020</plantId>
<workCenter>WC1</workCenter>
<eventType>alert</eventType>
<eventText>Downtime</eventText>
<eventDesc>WorkcenterDown</eventDesc>
</Row>
<Row>
<eventId>3</eventId>
<plantId>1021</plantId>
<workCenter>WC1</workCenter>
<eventType>alert</eventType>
<eventText>Downtime</eventText>
<eventDesc>WorkcenterDown</eventDesc>
</Row>
<Row>
<eventId>4</eventId>
<plantId>1020</plantId>
<workCenter>WC2</workCenter>
<eventType>incident</eventType>
<eventText>eventtext</eventText>
<eventDesc>failed</eventDesc>
</Row>
<Row>
<plantId>1020</plantId>
<workCenter>WC2</workCenter>
<eventType>incident</eventType>
<eventText>Text</eventText>
<eventDesc>failed</eventDesc>
</Row>
</Rowsets>
Expected output:
<?xml version="1.0" encoding="UTF-8"?>
<Rowsets>
<Alert>
<element>
<Title>Downtime:DIA01</Title>
<eventDesc>WorkcenterDown</eventDesc>
</element>
<element>
<Title>Downtime:DIA01</Title>
<eventDesc>WorkcenterDown</eventDesc>
</element>
</Alert>
<Incident>
<element>
<Title>YAT < 60%:DIA01</Title>
<eventDesc>7 Parts in 60 minutes have failed</eventDesc>
</element>
<element>
<Title>YAT < 60%:DIA01</Title>
<eventDesc>7 Parts in 60 minutes have failed</eventDesc>
</element>
</Incident>
</Rowsets>
Based on eventType, I want to generate group.
I am using this XSLT:
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="xml" version="1.0" encoding="UTF-8"
indent="yes" />
<xsl:template match="/">
<Rowsets>
<Rowset>
<xsl:variable name="Type" select="'alert'"/>
<xsl:for-each select="/Rowsets/Rowset/Row">
<xsl:choose>
<xsl:when test="$Type = eventType">
<element>
<xsl:variable name="text" select="eventText"/>
<xsl:variable name="WC" select="workCenter"/>
<Title><xsl:value-of select="concat($text,':',$WC)" /></Title>
<xsl:copy-of select="eventDesc"/>
</element>
</xsl:when>
<xsl:otherwise>
<element>
<xsl:variable name="text" select="eventText"/>
<xsl:variable name="WC" select="workCenter"/>
<Title><xsl:value-of select="concat($text,':',$WC)" /></Title>
<xsl:copy-of select="eventDesc"/>
</element>
</xsl:otherwise>
</xsl:choose>
</xsl:for-each>
</Rowset>
</Rowsets>
</xsl:template>
</xsl:stylesheet>
Need help in generating id and key based on eventType
This is a grouping problem - and a rather trivial one at that. In XSLT 2.0 you could do simply:
XSLT 2.0
<xsl:stylesheet version="2.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="xml" version="1.0" encoding="UTF-8" indent="yes"/>
<xsl:template match="Rowsets">
<Rowsets>
<xsl:for-each-group select="Row" group-by="eventType">
<xsl:element name="{current-grouping-key()}">
<xsl:for-each select="current-group()">
<element>
<Title>
<xsl:value-of select="eventText, workCenter" separator=":"/>
</Title>
<xsl:copy-of select="eventDesc"/>
</element>
</xsl:for-each>
</xsl:element>
</xsl:for-each-group>
</Rowsets>
</xsl:template>
</xsl:stylesheet>
Alternatively, with only two possible types, you could do:
XSLT 1.0
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="xml" version="1.0" encoding="UTF-8" indent="yes"/>
<xsl:template match="Rowsets">
<Rowsets>
<Alert>
<xsl:apply-templates select="Row[eventType='alert']"/>
</Alert>
<Incident>
<xsl:apply-templates select="Row[eventType='incident']"/>
</Incident>
</Rowsets>
</xsl:template>
<xsl:template match="Row">
<element>
<Title>
<xsl:value-of select="eventText"/>
<xsl:text>:</xsl:text>
<xsl:value-of select="workCenter"/>
</Title>
<xsl:copy-of select="eventDesc"/>
</element>
</xsl:template>
</xsl:stylesheet>
This is assuming you don't mind creating a group even if it is empty. Otherwise you would do:
<xsl:template match="Rowsets">
<Rowsets>
<xsl:variable name="alerts" select="Row[eventType='alert']"/>
<xsl:variable name="incidents" select="Row[eventType='incident']"/>
<xsl:if test="$alerts">
<Alert>
<xsl:apply-templates select="Row[eventType='alert']"/>
</Alert>
</xsl:if>
<xsl:if test="$incidents">
<Incident>
<xsl:apply-templates select="Row[eventType='incident']"/>
</Incident>
</xsl:if>
</Rowsets>
</xsl:template>
P.S. Note that XML is case-sensitive: <Alert> is not the same as <alert>.

XSLT for-each namespace

I'm using a for-each in my XSLT template.
This is my example input XML:
<products>
<data>
<label_1>some_label1</label_1>
<label_2>some_label2</label_2>
<values>
<a>a</a>
<b>b</b>
</values>
</data>
<data>
<label_1>some_label1</label_1>
<label_2>some_label2</label_2>
<values>
<c>c</c>
<d>d</d>
</values>
</data>
</products>
Now based on my template:
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:ns="http:/example.com/ns">
<xsl:template match="/">
<xsl:apply-templates/>
</xsl:template>
<xsl:template match="#*|node()">
<xsl:copy>
<xsl:apply-templates select="#*|node()" />
</xsl:copy>
</xsl:template>
<xsl:template match="data">
<data>
<xsl:variable name="values" select="values" />
<xsl:for-each select="$values">
<xsl:apply-templates select="#*|node()" />
</xsl:for-each>
</data>
</xsl:template>
</xsl:stylesheet>
I get only <values></values> and that is ok for me.
That's my output:
<products>
<data>
<a>a</a>
<b>b</b>
</data>
<data>
<c>c</c>
<d>d</d>
</data>
</products>
What i need in my output is namespace like this:
<products>
<data>
<ns:a>a</ns:a>
<ns:b>b</ns:b>
</data>
<data>
<ns:c>c</ns:c>
<ns:d>d</ns:d>
</data>
</products>
So what i understand is "each element of values is applied by template". How can I add namespace ?
You can get output similar to what you show (albeit well-formed) by using:
XSLT 1.0
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="xml" version="1.0" encoding="UTF-8" indent="yes"/>
<xsl:strip-space elements="*"/>
<!-- identity transform -->
<xsl:template match="#*|node()">
<xsl:copy>
<xsl:apply-templates select="#*|node()"/>
</xsl:copy>
</xsl:template>
<xsl:template match="data">
<xsl:copy>
<xsl:apply-templates select="values/*"/>
</xsl:copy>
</xsl:template>
<xsl:template match="values/*">
<xsl:element name="ns:{local-name()}" namespace="http:/example.com/ns">
<xsl:value-of select="."/>
</xsl:element>
</xsl:template>
</xsl:stylesheet>
Or, if you prefer:
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:ns="http:/example.com/ns">
<xsl:output method="xml" version="1.0" encoding="UTF-8" indent="yes"/>
<xsl:template match="/products">
<products>
<xsl:for-each select="data">
<xsl:copy>
<xsl:for-each select="values/*">
<xsl:element name="ns:{local-name()}">
<xsl:value-of select="."/>
</xsl:element>
</xsl:for-each>
</xsl:copy>
</xsl:for-each>
</products>
</xsl:template>
</xsl:stylesheet>
Replace http:/example.com/ns with your own namespace URI.
Credits
This answer follows the technique used in this SO answer to a similar problem.
Solution
Add namespace information to all descendants of specific elements. Augment the stylesheet by a template matching this set of nodes:
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet
version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:ns="http://my.ns.uri"
>
<xsl:template match="/">
<xsl:apply-templates/>
</xsl:template>
<xsl:template match="#*|node()">
<xsl:copy>
<xsl:apply-templates select="#*|node()" />
</xsl:copy>
</xsl:template>
<xsl:template match="data">
<data>
<xsl:variable name="values" select="values" />
<xsl:for-each select="$values">
<xsl:apply-templates select="#*|node()" />
</xsl:for-each>
</data>
</xsl:template>
<!--
Added template.
-->
<xsl:template match="data//*">
<xsl:element name="ns:{name()}" namespace="http://my.ns.uri">
<xsl:for-each select=".">
<xsl:apply-templates select="#*|node()" />
</xsl:for-each>
</xsl:element>
</xsl:template>
</xsl:stylesheet>

XSLT performance issue with large data

having performance issues with my xslt code:
this is my input file:
<?xml version="1.0" encoding="UTF-8"?>
<Products>
<Product ID="111111" Type="Item" ParentID="7402">
<Name>ABC</Name>
<Values>
<Value AttributeID="11">8.00</Value>
<Value AttributeID="12">8.00</Value>
<Value AttributeID="13">0.18</Value>
</Values>
<Product ID="B582B65D" Type="UID" ParentID="111111">
<Values>
<Value AttributeID="11">8.00</Value>
<Value AttributeID="12">8.00</Value>
<Value AttributeID="13">0.18</Value>
<Value AttributeID="14">0.18</Value>
</Values>
</Product>
</Product>
<Product ID="222222" Type="Item" ParentID="7402">
<Name>XYZ</Name>
<Values>
<Value AttributeID="12">8.00</Value>
<Value AttributeID="13">8.00</Value>
<Value AttributeID="15">0.18</Value>
</Values>
<Product ID="B582B65D" Type="UID" ParentID="111111">
<Values>
<Value AttributeID="11">8.00</Value>
<Value AttributeID="12">8.00</Value>
<Value AttributeID="16">0.18</Value>
<Value AttributeID="18">0.18</Value>
</Values>
</Product>
</Product>
</Products>
and this is my transformation code:
<?xml version="1.0" encoding="ISO-8859-1"?>
<xsl:stylesheet version="2.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:math="http://exslt.org/math"
extension-element-prefixes="math">
<xsl:output method="xml" indent="yes" />
<xsl:param name="file2" select="document('Mapping.xml')" />
<xsl:template match="/Products">
<Products>
<xsl:for-each select="Product">
<xsl:call-template name="item" />
</xsl:for-each>
</Products>
</xsl:template>
<xsl:template name="item">
<Product type="{./#Type}" ID="{./#ID}">
<xsl:for-each select="./Values/Value">
<xsl:variable name="Idval" select="#AttributeID" />
<xsl:element name="{$file2//Groups/AttributeID[#ID=$Idval]/#group}">
<xsl:element name="{$file2//Groups/AttributeID[#ID=$Idval]}">
<xsl:attribute name="ID"><xsl:value-of select="$Idval"/></xsl:attribute>
<xsl:value-of select="." />
</xsl:element>
</xsl:element>
</xsl:for-each>
<xsl:call-template name="uid" />
</Product>
</xsl:template>
<xsl:template name="uid">
<Product type="{./Product/#Type}" ParentId="{./Product/#ParentID}">
<xsl:for-each select="./Product/Values/Value">
<xsl:variable name="Idval" select="#AttributeID" />
<xsl:element name="{$file2//Groups/AttributeID[#ID=$Idval]/#group}">
<xsl:element name="{$file2//Groups/AttributeID[#ID=$Idval]}">
<xsl:attribute name="ID"><xsl:value-of select="$Idval"/></xsl:attribute>
<xsl:value-of select="." />
</xsl:element>
</xsl:element>
</xsl:for-each>
</Product>
</xsl:template>
</xsl:stylesheet>
above xslt is using below xml file for mapping attribute id to corresponding name and group
Mapping.xml
<?xml version="1.0" encoding="UTF-8"?>
<Groups>
<AttributeID ID="11" group="Pack1">Height</AttributeID>
<AttributeID ID="12" group="Pack2">Width</AttributeID>
<AttributeID ID="13" group="Pack1">Depth</AttributeID>
<AttributeID ID="14" group="Pack3">Length</AttributeID>
<AttributeID ID="15" group="Pack3">Lbs</AttributeID>
<AttributeID ID="16" group="Pack4">Litre</AttributeID>
</Groups>
Replace the use of expressions like
select="$file2//Groups/AttributeID[#ID=$Idval]"
with a key:
<xsl:key name="ID" match="Groups/AttributeID" use="#ID"/>
and then
select="key('ID', $IDval, $file)"/>
Alternatively, Saxon-EE will do this optimization for you automatically.
The key() function with 3 arguments is XSLT 2.0 syntax. If you have the misfortune to be using XSLT 1.0, you have to write a dummy xsl:for-each that makes $file the context item, because key() will only select within the document containing the context item.
Define a key for the cross document lookup: <xsl:key name="by-id" match="Groups/AttributeID" use="#ID"/>, then (assuming an XSLT 2.0 processor) you can simplify expressions like <xsl:element name="{$file2//Groups/AttributeID[#ID=$Idval]/#group}"> to <xsl:element name="{key('by-id', #AttributeID, $file2)/#group">. Make the same change for the other cross references you have, i.e. all those $file2//Groups/AttributeID[#ID=$Idval] expressions should use the key lookup.
Making the simplified assumption that your second file isn't too big, you want to fold the values there into your template. It would work with XSLT 1.0 too. Something like this:
<?xml version="1.0" encoding="ISO-8859-1"?>
<xsl:stylesheet version="2.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:math="http://exslt.org/math"
extension-element-prefixes="math">
<xsl:output method="xml" indent="yes" />
<xsl:template match="/">
<Products>
<xsl:apply-templates select="/Products/Product" />
</Products>
</xsl:template>
<xsl:template match="Product">
<xsl:element name="Product">
<xsl:apply-templates select="#*" />
<xsl:apply-templates />
</xsl:element>
</xsl:template>
<xsl:template match="Name">
<Name>
<xsl:value-of select="." />
</Name>
</xsl:template>
<xsl:template match="#*">
<xsl:attribute name="{name()}">
<xsl:value-of select="." />
</xsl:attribute>
</xsl:template>
<xsl:template match="Values">
<Values>
<xsl:apply-templates />
</Values>
</xsl:template>
<!-- Templates for individual AttributeIDs, only when there are few -->
<xsl:template match="Value[#AttributeID='11']">
<Pack1>
<xsl:element name="Height">
<xsl:attribute name="ID">
<xsl:value-of select="#AttributeID" />
</xsl:attribute>
<xsl:value-of select="." />
</xsl:element>
</Pack1>
</xsl:template>
<!-- Repeat for the other AttributeID values -->
</xsl:stylesheet>
(Typed off my head, will contain typos)
Of course if it is big Michael's advice is the best course of action.

XSLT: y/n on new field if price difference > 1

I am creating a new productfeed and need the following field: <diff>.
if difference between price and old_price is more than 1: y (from Yes) in field: <diff>
if difference between price and old_price is 1 or less than 1: n (from No) in field: <diff>
File: Data.xml
<?xml version="1.0"?>
<products>
<product id="0001">
<price>120.00</price>
<old_price>125.00</old_price>
</product>
<product id="0002">
<price>5.00</price>
<old_price>5.50</old_price>
</product>
</products>
Wished output:
<?xml version="1.0"?>
<products>
<product id="0001">
<diff>y</diff>
</product>
<product id="0002">
<diff>n</diff>
</product>
</products>
I haven't tested that but it should be like that:
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="xml" version="1.0" encoding="utf-8" indent="yes" />
<xsl:template match="/">
<products>
<xsl:for-each select="//product">
<diff>
<xsl:choose>
<xsl:when test="price - old_price > 1">
y
</xsl:when>
<xsl:otherwise>
n
</xsl:otherwise>
</xsl:choose>
</diff>
<xsl:copy-of select="*" />
</xsl:for-each>
</products>
</xsl:template>
</xsl:stylesheet>
Will test and put update
Give this a whirl:
<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="#* | node()">
<xsl:copy>
<xsl:apply-templates select="#* | node()"/>
</xsl:copy>
</xsl:template>
<xsl:template match="price[translate(. - ../old_price, '-', '') > 1]">
<diff>y</diff>
</xsl:template>
<xsl:template match="price">
<diff>n</diff>
</xsl:template>
<xsl:template match="old_price" />
</xsl:stylesheet>
When run on your sample input, this produces:
<products>
<product id="0001">
<diff>y</diff>
</product>
<product id="0002">
<diff>n</diff>
</product>
</products>

XSLT merging/concatenating values of siblings nodes of same name into single node

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.
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>
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.
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() > 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.