I have this XML:
<table tableName="Groups">
<item table="Groups">
<column columnName="GroupID">GROUP1</column>
<column columnName="GroupID">GROUP2</column>
</item>
</table>
<table tableName="Products">
<item table="Products">
<column columnName="ProductID">1</column>
<column columnName="ProductName">Product A</column>
<column columnName="Groups">"GROUP1","GROUP2"</column>
</item>
<item table="EcomProducts">
<column columnName="ProductID">2</column>
<column columnName="ProductName">Product B</column>
<column columnName="Groups">"GROUP3","GROUP4"</column>
</item>
<item table="EcomProducts">
<column columnName="ProductID">3</column>
<column columnName="ProductName">Product C</column>
<column columnName="Groups">"GROUP1","GROUP3"</column>
</item>
</table>
I'm trying to filter out products that has any groups that are present in the Groups-table.
I would like to end up with this.
<products>
<product>
<name>Product A</name>
<groups>"GROUP1","GROUP2"</groups>
</product>
<product>
<name>Product C</name>
<groups>"GROUP1","GROUP3"</groups>
</product>
</products>
So far I have this XSLT, but as I'm pretty new to XSLT, I have no idea how to make that test-statement more dynamic:
<products>
<xsl:for-each select="table[#tableName='Products']/item">
<xsl:if test="contains(column[#columnName = 'Groups'] = 'GROUP1' and contains(column[#columnName = 'Groups'] = 'GROUP2'">
<product>
<name><xsl:value-of select="column[#columnName = 'ProductName']"/></name>
<groups><xsl:value-of select="column[#columnName = 'Groups']"/></groups>
</product>
</xsl:if>
</xsl:for-each>
</products>
Try it this way:
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="/root">
<xsl:variable name="group-ids" select="table[#tableName='Groups']/item/column[#columnName='GroupID']" />
<products>
<xsl:for-each select="table[#tableName='Products']/item">
<xsl:if test="$group-ids[contains(current()/column[#columnName='Groups'], concat('"', ., '"'))]">
<product>
<name>
<xsl:value-of select="column[#columnName='ProductName']"/>
</name>
<groups>
<xsl:value-of select="column[#columnName='Groups']"/>
</groups>
</product>
</xsl:if>
</xsl:for-each>
</products>
</xsl:template>
</xsl:stylesheet>
Demo: https://xsltfiddle.liberty-development.net/jxNakB6
However, if your processor happens to support the EXSLT str:tokenize() extension function, you could do something a bit simpler:
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:str="http://exslt.org/strings"
extension-element-prefixes="exsl str">
<xsl:output method="xml" version="1.0" encoding="UTF-8" indent="yes"/>
<xsl:key name="grp-id" match="table[#tableName='Groups']/item/column[#columnName='GroupID']" use="concat('"', ., '"')" />
<xsl:template match="/root">
<products>
<xsl:for-each select="table[#tableName='Products']/item[key('grp-id', str:tokenize(column[#columnName='Groups'], ','))]">
<product>
<name>
<xsl:value-of select="column[#columnName='ProductName']"/>
</name>
<groups>
<xsl:value-of select="column[#columnName='Groups']"/>
</groups>
</product>
</xsl:for-each>
</products>
</xsl:template>
</xsl:stylesheet>
XSLT 1.0 solution, using keys:
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output omit-xml-declaration="yes" indent="yes"/>
<xsl:strip-space elements="*"/>
<xsl:key name="kContainsGroup" match="item[not(#table='Groups')]"
use="boolean(
/*/table[#tableName='Groups']/item/column
[contains(current()/column[#columnName='Groups'],
concat('"', ., '"'))]
)"/>
<xsl:template match="/">
<products>
<xsl:apply-templates select="key('kContainsGroup', 'true')"/>
</products>
</xsl:template>
<xsl:template match="item">
<product>
<name><xsl:value-of select="column[#columnName='ProductName']"/></name>
<groups><xsl:value-of select="column[#columnName='Groups']"/></groups>
</product>
</xsl:template>
</xsl:stylesheet>
When this transformation is applied on the provided XML (fragment, made a well-formed XML document by enclosing it within a single top element <tables>):
<tables>
<table tableName="Groups">
<item table="Groups">
<column columnName="GroupID">GROUP1</column>
<column columnName="GroupID">GROUP2</column>
</item>
</table>
<table tableName="Products">
<item table="Products">
<column columnName="ProductID">1</column>
<column columnName="ProductName">Product A</column>
<column columnName="Groups">"GROUP1","GROUP2"</column>
</item>
<item table="EcomProducts">
<column columnName="ProductID">2</column>
<column columnName="ProductName">Product B</column>
<column columnName="Groups">"GROUP3","GROUP4"</column>
</item>
<item table="EcomProducts">
<column columnName="ProductID">3</column>
<column columnName="ProductName">Product C</column>
<column columnName="Groups">"GROUP1","GROUP3"</column>
</item>
</table>
</tables>
the wanted, correct result is produced:
<products>
<product>
<name>Product A</name>
<groups>"GROUP1","GROUP2"</groups>
</product>
<product>
<name>Product C</name>
<groups>"GROUP1","GROUP3"</groups>
</product>
</products>
Do note:
No XSLT conditional instruction (such as <xsl:if>) has been used.
No <xsl:for-each> instruction has been used
The solution is implemented in almost pure "push-style"
Related
We have a requirement to read the xml file and capture the EmployeeName and EmailId values from tags to create the output as xml file.
The first tag always represents EmployeeName and 5th tag always represents EmailId.
Need to capture the values present in the row/value....
The input xml file as follows:
<?xml version="1.0" encoding="utf-8"?>
<dataset xmlns="http://developer.net.com/schemas/xmldata/1/" xmlns:xs="http://www.w3.org/2001/XMLSchema-instance">
<!--
<dataset
xmlns="http://developer.net.com/schemas/xmldata/1/"
xmlns:xs="http://www.w3.org/2001/XMLSchema-instance"
xs:schemaLocation="http://developer.net.com/schemas/xmldata/1/ xmldata.xsd"
>
-->
<metadata>
<item length="20" type="xs:string" name="EmployeeName"/>
<item length="4" type="xs:string" name="Full/Part Time Code"/>
<item type="xs:dateTime" name="Hire Date"/>
<item type="xs:dateTime" name="Termination Date"/>
<item length="30" type="xs:string" name="EmailID"/>
<item length="30" type="xs:string" name="State"/>
</metadata>
<data>
<row>
<value>JOSEPH</value>
<value>F</value>
<value>1979-04-19T00:00:00</value>
<value>2007-08-27T00:00:00</value>
<value>joseph.Tim#gmail.com</value>
<value>TX</value>
</row>
<row>
<value>NANDY</value>
<value>F</value>
<value>1979-04-19T00:00:00</value>
<value>2007-08-27T00:00:00</value>
<value>Nandy123#gmailcom</value>
<value>PA</value>
</row>
</data>
</dataset>
The Expected Ouput as below:
<?xml version="1.0" encoding="UTF-8"?>
<ns0:EMPLOYEEDETAILS xmlns:ns0="http://net.com/EmployeeDetails">
<Records>
<EmployeeName>JOSEPH</EmployeeName>
<EmailId>joseph.Tim#gmail.com</EmailId>
</Records>
<Records>
<EmployeeName>NANDY</EmployeeName>
<EmailId>Nandy123#gmailcom</EmailId>
</Records>
</ns0:EMPLOYEEDETAILS>
Thanks,
Ravi
Please try the XSLT below. You need to make additional changes for matching namespaces according to the input XML and adding the root node <EMPLOYEEDETAILS> in the output.
EDIT: XSLT solution updated to handle the namespace issue. Root node <ns0:EMPLOYEEDETAILS> included in the solution.
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:ns0="http://developer.net.com/schemas/xmldata/1/">
<xsl:output method="xml" indent="yes" />
<xsl:strip-space elements="*"/>
<xsl:param name="param-name" select="1" />
<xsl:param name="param-email" select="5" />
<xsl:template match="/">
<ns0:EMPLOYEEDETAILS>
<xsl:for-each select="//ns0:data/ns0:row">
<Records>
<xsl:for-each select="ns0:value">
<xsl:choose>
<xsl:when test="position() = $param-name">
<EmployeeName>
<xsl:value-of select="." />
</EmployeeName>
</xsl:when>
<xsl:when test="position() = $param-email">
<EmailId>
<xsl:value-of select="." />
</EmailId>
</xsl:when>
</xsl:choose>
</xsl:for-each>
</Records>
</xsl:for-each>
</ns0:EMPLOYEEDETAILS>
</xsl:template>
</xsl:stylesheet>
Output
<ns0:EMPLOYEEDETAILS xmlns:ns0="http://developer.net.com/schemas/xmldata/1/">
<Records>
<EmployeeName>JOSEPH</EmployeeName>
<EmailId>joseph.Tim#gmail.com</EmailId>
</Records>
<Records>
<EmployeeName>NANDY</EmployeeName>
<EmailId>Nandy123#gmailcom</EmailId>
</Records>
</ns0:EMPLOYEEDETAILS>
How can i select the position of a node with a specific attribute in XSL.
XML:
<document type="async" page="tabTabel">
<tabs>
<table>
<row type="header">
<column type="text">href</column>
<column type="number">Mapnr.</column>
<column type="mapposition">Nr.</column>
<column type="text">Description</column>
<column type="text">Document</column>
<column type="date">Date</column>
</row>
<row type="data">
<column><![CDATA[]]></column>
<column><![CDATA[10]]></column>
<column><![CDATA[17]]></column>
<column><![CDATA[Documentation may 2013 .pdf]]></column>
<column><![CDATA[Documentation may 2013 .pdf]]></column>
<column><![CDATA[03-04-2014]]></column>
</row>
</table>
</tabs>
</document>
Current not-working XSLT:
<xsl:template match="tabs//row[#type='data']>
<xsl:variable name="mapnumber">
<xsl:value-of select="../row[#type='header']/column[#type='mapposition'][position()]" />
</xsl:variable>
</xsl:template>
I want the index number/position of the column with type 'mapposition'. How can I do this?
Try:
<xsl:variable name="mapnumber" select="count(../row[#type='header']/column[#type='mapposition']/preceding-sibling::column) + 1" />
In view of your edit, you'd probably want to do something like:
<xsl:template match="table">
<xsl:variable name="mapnumber" select="count(row[#type='header']/column[#type='mapposition']/preceding-sibling::column) + 1" />
<xsl:value-of select="row[#type='data']/column[$mapnumber]" />
</xsl:template>
This example might help you:
If you've the following XML
<row type='header'>
<column type='text'>a</column>
<column type='mapposition'>b</column>
<column type='number'>c</column>
</row>
To get the position of column with #type = 'mapposition', you can use this stylesheet:
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="text"/>
<xsl:template match="row[#type='header']">
<xsl:apply-templates select="column"/>
</xsl:template>
<xsl:template match="column">
<xsl:if test="#type='mapposition'"><xsl:value-of select="position()"/></xsl:if>
</xsl:template>
</xsl:stylesheet>
I am new to XSLT and I need help in merging two different XML documents into one.
XML1.xml
<customers>
<customer>
<Person name="Ram" Id="101"/>
<address>flat 4</address>
</customer>
<customer>
<Person name="Raghav" Id="102"/>
<address>flat 9</address>
</customer>
</customers>
XML2.xml
<Products>
<Product>
<name>Onida Tv</name>
<consumer>Ram</consumer>
</Product>
<Product>
<name>washing machine</name>
<consumer>Ram</consumer>
</Product>
<Product>
<name>Water purifier</name>
<consumer>Raghav</consumer>
</Product>
<Product>
<name>iPhone</name>
<consumer>Raghav</consumer>
</Products>
</Products>
Desired XML output:
<customers>
<customer>
<Person name="Ram" Id="101"/>
<address>flat 4</address>
<products>
<name>washing machine</name>
<name>Onida TV</name>
</products>
</customer>
<customer>
<Person name="Raghav" Id="102"/>
<address>flat 9</address>
<products>
<name>iPhone</name>
<name>Water purifier</name>
</products>
</customer>
</customers>
The second XML is to be considered external in this context. I need to append to each customer the corresponding products. How can I do that?
Try it this 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">
<xsl:output method="xml" version="1.0" encoding="UTF-8" indent="yes"/>
<xsl:strip-space elements="*"/>
<xsl:variable name="lookup-document" select="document('XML2.xml')" />
<xsl:key name="product-by-consumer" match="Product" use="consumer" />
<!-- identity transform -->
<xsl:template match="#*|node()">
<xsl:copy>
<xsl:apply-templates select="#*|node()"/>
</xsl:copy>
</xsl:template>
<xsl:template match="customer">
<xsl:copy>
<xsl:apply-templates select="#*|node()"/>
<xsl:variable name="name" select="Person/#name" />
<products>
<!-- switch context to the other document in order to use key -->
<xsl:for-each select="$lookup-document">
<xsl:copy-of select="key('product-by-consumer', $name)/name"/>
</xsl:for-each>
</products>
</xsl:copy>
</xsl:template>
</xsl:stylesheet>
Note that this assumes that customer names are unique.
My XML looks like folloing:
<?xml version="1.0" encoding="utf-8" standalone="no"?>
<BATCHES>
<item>
<Material>1000000079</Material>
<Description>330 Bulk</Description>
<Tank>T123</Tank>
<Batch>2013225287</Batch>
<Quantity>510</Quantity>
</item>
<item>
<Material>1000000079</Material>
<Description>330 Bulk</Description>
<Tank>T123</Tank>
<Batch>2013225301</Batch>
<Quantity>520</Quantity>
</item>
<item>
<Material>1000000196</Material>
<Description>340R Bulk</Description>
<Tank>T700</Tank>
<Batch>1000188378</Batch>
<Quantity>510</Quantity>
</item>
<item>
<Material>1000002754</Material>
<Description>43 Bulk</Description>
<Tank>T515</Tank>
<Batch>2013180125</Batch>
<Quantity>300</Quantity>
</item>
<item>
<Material>1000002754</Material>
<Description>43 Bulk</Description>
<Tank>T515</Tank>
<Batch>2013203124</Batch>
<Quantity>200</Quantity>
</item>
<item>
<Material>1000002754</Material>
<Description>43 Bulk</Description>
<Tank>T515</Tank>
<Batch>2013214839</Batch>
<Quantity>700</Quantity>
</item>
<item>
<Material>1000002754</Material>
<Description>43 Bulk</Description>
<Tank>T517</Tank>
<Batch>2013214342</Batch>
<Quantity>890</Quantity>
</item>
</BATCHES>
My original XSLT look like this:
<?xml version="1.0" encoding="utf-8" standalone="no"?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">
<xsl:output encoding="UTF-8" indent="yes" method="xml" version="1.0"/>
<xsl:template match="/">
<Rowsets>
<Rowset>
<xsl:variable name="materials" select=".//item[Tank!='RECV' and Tank!='PARK'] "/>
<xsl:for-each select="$materials">
<xsl:if test="generate-id(.)= generate-id($materials[Material=current()/Material])">
<Row>
<Material>
<xsl:value-of select="Material"/>
</Material>
<Description>
<xsl:value-of select="Description"/>
</Description>
<Value>
<xsl:for-each select="$materials[Material=current()/Material]/Tank">
<xsl:if test="node()">
<xsl:value-of select="concat(.,'||')"/>
</xsl:if>
</xsl:for-each>
</Value>
</Row>
</xsl:if>
</xsl:for-each>
</Rowset>
</Rowsets>
</xsl:template>
</xsl:stylesheet>
The result of this XSLT looks like this:
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<Rowsets>
<Rowset>
<Row>
<Material>1000000079</Material>
<Description>330 Bulk</Description>
<Value>T123||T123||</Value>
</Row>
<Row>
<Material>1000000196</Material>
<Description>340R Bulk</Description>
<Value>T700||</Value>
</Row>
<Row>
<Material>1000002754</Material>
<Description>43 Bulk</Description>
<Value>T515||T517||</Value>
</Row>
</Rowset>
</Rowsets>
I wanted to remove duplicate tanks while concatinating it in Value field. So I changed my XSLT to follwoing:
<?xml version="1.0" encoding="utf-8" standalone="no"?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">
<xsl:output encoding="UTF-8" indent="yes" method="xml" version="1.0"/>
<xsl:template match="/">
<Rowsets>
<Rowset>
<xsl:variable name="materials" select=".//item[Tank!='RECV' and Tank!='PARK' and Quantity > 500]"/>
<xsl:for-each select="$materials">
<xsl:if test="generate-id(.)= generate-id($materials[Material=current()/Material])">
<Row>
<Material>
<xsl:value-of select="Material"/>
</Material>
<Description>
<xsl:value-of select="Description"/>
</Description>
<Value>
<xsl:for-each select="$materials[Material=current()/Material]/Tank[not(.=preceding::Tank)]">
<xsl:if test="node()">
<xsl:value-of select="concat(.,'||')"/>
</xsl:if>
</xsl:for-each>
</Value>
</Row>
</xsl:if>
</xsl:for-each>
</Rowset>
</Rowsets>
</xsl:template>
</xsl:stylesheet>
My result now looks like this:
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<Rowsets>
<Rowset>
<Row>
<Material>1000000079</Material>
<Description>330 Bulk</Description>
<Value>T123||</Value>
</Row>
<Row>
<Material>1000000196</Material>
<Description>340R Bulk</Description>
<Value>T700||</Value>
</Row>
<Row>
<Material>1000002754</Material>
<Description>43 Bulk</Description>
<Value>T517||</Value>
</Row>
</Rowset>
</Rowsets>
It removed the duplicate tank T123 for material 1000000079 but for material 1000002754, it even removed T515which should appear in Value field as its quantity is greater than 500 for folloing:
<item>
<Material>1000002754</Material>
<Description>43 Bulk</Description>
<Tank>T515</Tank>
<Batch>2013214839</Batch>
<Quantity>700</Quantity>
</item>
what am I doing wrong here?
Ok I see your problem.
For this a key based solution Using Keys to Group: The Muenchian Method:
Try this:
<?xml version="1.0" encoding="utf-8" standalone="no"?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">
<xsl:output encoding="UTF-8" indent="yes" method="xml" version="1.0"/>
<xsl:key name="kMaterial" match="item[Tank!='RECV' and Tank!='PARK' and Quantity > 500]"
use="Material"/>
<xsl:key name="kMaterialTank" match="item[Tank!='RECV' and Tank!='PARK' and Quantity > 500]"
use="concat(Material,'|', Tank)"/>
<xsl:template match="/">
<Rowsets>
<Rowset>
<xsl:variable name="materials" select=".//item[Tank!='RECV' and Tank!='PARK'] "/>
<xsl:for-each select="//item[ generate-id(.) = generate-id( key('kMaterial', Material)[1]) ]" >
<xsl:variable name="m" select="Material" />
<Row>
<Material>
<xsl:value-of select="Material"/>,<xsl:value-of select="$m"/>
</Material>
<Description>
<xsl:value-of select="Description"/>
</Description>
<Value>
<xsl:for-each select="//item[ generate-id(.) =
generate-id( key('kMaterialTank', concat( $m,'|', Tank))[1])]" mode="tank" >
<xsl:apply-templates select="." mode="tank" />
</xsl:for-each>
</Value>
</Row>
</xsl:for-each>
</Rowset>
</Rowsets>
</xsl:template>
<xsl:template match="item" mode="tank" >
<xsl:value-of select="Tank"/>
<xsl:text>||</xsl:text>
</xsl:template>
</xsl:stylesheet>
Which will generate the following output:
<?xml version="1.0" encoding="UTF-8"?>
<Rowsets>
<Rowset>
<Row>
<Material>1000000079,1000000079</Material>
<Description>330 Bulk</Description>
<Value>T123||</Value>
</Row>
<Row>
<Material>1000000196,1000000196</Material>
<Description>340R Bulk</Description>
<Value>T700||</Value>
</Row>
<Row>
<Material>1000002754,1000002754</Material>
<Description>43 Bulk</Description>
<Value>T515||T517||</Value>
</Row>
</Rowset>
</Rowsets>
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.