Finding node with duplicate id and changing the ID - xslt

I need to find duplicate nodes (identified by ID) and if such nodes exist, then I need to update the id of one of them. Will appreciate if someone can let me know how to do it based on xpath or xsl.
example xml:
<music>
<title id="1"/>
<title id="2"/>
<title id="1"/>
</music>
The first and third node have the same ID. So, the id of the third is changed to '3'. I need to change it to the following:
<music>
<title id="1"/>
<title id="2"/>
<title id="3"/>
</music>

Please try the following template:
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
version="1.0">
<xsl:template match="music">
<xsl:copy>
<xsl:for-each select="*">
<xsl:element name="{name()}">
<xsl:attribute name="id">
<xsl:choose>
<xsl:when test="preceding::*/#id=current()/#id">
<xsl:value-of select="generate-id()"/>
</xsl:when>
<xsl:otherwise>
<xsl:value-of select="#id"/>
</xsl:otherwise>
</xsl:choose>
</xsl:attribute>
<xsl:apply-templates/>
</xsl:element>
</xsl:for-each>
</xsl:copy>
</xsl:template>
</xsl:stylesheet>

Usually, the purpose of an ID is to uniquely identify elements. If so, it is insignificant what the actual ID string is - as long as there are no duplicates.
Therefore, the easiest approach to your problem is to number all title elements consistently, as remarked already by #michael.hor257k. This can be done using either position() or xsl:number.
<?xml version="1.0" encoding="utf-8"?>
<xsl:stylesheet version="2.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="xml" indent="yes"/>
<xsl:template match="/music">
<xsl:copy>
<xsl:apply-templates/>
</xsl:copy>
</xsl:template>
<xsl:template match="title">
<xsl:copy>
<xsl:attribute name="id">
<xsl:number/>
</xsl:attribute>
</xsl:copy>
</xsl:template>
</xsl:stylesheet>
Output
<?xml version="1.0" encoding="UTF-8"?>
<music>
<title id="1"/>
<title id="2"/>
<title id="3"/>
</music>

Related

Add mandatory nodes with XSLT

I am facing an xslt/xpath problem and hope someone could help, in a few words here is what I try to achieve.
I have to transform an XML document where some nodes may be missing, these missing nodes are mandatory in the final result. I have the set of mandatory node names available in an xsl:param.
The base document is:
<?xml version="1.0"?>
<?xml-stylesheet type="text/xsl" href="TRANSFORM.xslt"?>
<BEGIN>
<CLIENT>
<NUMBER>0021732561</NUMBER>
<NAME1>John</NAME1>
<NAME2>Connor</NAME2>
</CLIENT>
<PRODUCTS>
<PRODUCT_ID>12</PRODUCT_ID>
<DESCRIPTION>blah blah</DESCRIPTION>
</PRODUCTS>
<PRODUCTS>
<PRODUCT_ID>13</PRODUCT_ID>
<DESCRIPTION>description ...</DESCRIPTION>
</PRODUCTS>
<OPTIONS>
<OPTION_ID>1</OPTION_ID>
<DESCRIPTION>blah blah blah ...</DESCRIPTION>
</OPTIONS>
<PROMOTIONS>
<PROMOTION_ID>1</PROMOTION_ID>
<DESCRIPTION>blah blah blah ...</DESCRIPTION>
</PROMOTIONS>
</BEGIN>
Here is the stylesheet so far:
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="2.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:fn="http://www.w3.org/2005/xpath-functions">
<xsl:output method="xml" encoding="UTF-8" indent="yes"/>
<xsl:param name="mandatoryNodes" as="xs:string*" select=" 'PRODUCTS', 'OPTIONS', 'PROMOTIONS' "/>
<xsl:template match="/">
<xsl:apply-templates select="child::node()"/>
</xsl:template>
<xsl:template match="node()">
<xsl:copy>
<xsl:apply-templates/>
</xsl:copy>
</xsl:template>
<xsl:template match="BEGIN">
<xsl:element name="BEGIN">
<xsl:for-each select="$mandatoryNodes">
<!-- If there is no node with this name -->
<xsl:if test="count(*[name() = 'current()']) = 0">
<xsl:element name="{current()}" />
</xsl:if>
</xsl:for-each>
<xsl:apply-templates select="child::node()"/>
</xsl:element>
</xsl:template>
</xsl:stylesheet>
I tried the transformation in XML Spy, the xsl:iftest failed saying that 'current item is PRODUCTS of type xs:string.
I've tried the same xsl:if outside of a for-each and it seems to work ... what am I missing ?
Inside of <xsl:for-each select="$mandatoryNodes"> the context item is a string but you want to access the primary input document and its nodes so you need to store that document or the template's context node in a variable and use that e.g.
<xsl:template match="BEGIN">
<xsl:variable name="this" select="."/>
<xsl:element name="BEGIN">
<xsl:for-each select="$mandatoryNodes">
<!-- If there is no child node of `BEGIN` with this name -->
<xsl:if test="count($this/*[name() = current()]) = 0">
<xsl:element name="{current()}" />
</xsl:if>
</xsl:for-each>
<xsl:apply-templates select="child::node()"/>
</xsl:element>
</xsl:template>

How to insert an element into a previously created element in xslt?

I have several records from the DB for a corresponding record in a file.
Example
Record no. XML
<XML_FILE_HEADER file_name="sample.txt" />
<XML_RECORD record_number="1" name="John Doe" Age="21"/>
<XML_RECORD record_number="2" name""Jessica Sanchez" Age="23"/>
<XML_FILE_FOOTER total_records="2"/>
Now for each record I have an xslt template that would create the output file in xml.
For record no 1:
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:xalan="http://xml.apache.org/xslt">
<xsl:output method="xml"/>
<xsl:template match="XML_FILE_HEADER">
<xsl:element name="File">
<xsl:attribute name="FileName"><xsl:value-of select="#file_name"/></xsl:attribute>
</xsl:element>
</xsl:element>
</xsl:template>
</xsl:stylesheet>
For records 2 and 3:
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:xalan="http://xml.apache.org/xslt">
<xsl:output omit-xml-declaration="yes"/>
<xsl:template match="XML_RECORD">
<xsl:element name="Record">
<xsl:attribute name="Name"><xsl:value-of select="#name"/></xsl:attribute>
<xsl:element name="Details">
<xsl:attribute name="Age"><xsl:value-of select="#Age"/></xsl:attribute>
</xsl:element>
</xsl:element>
</xsl:template>
</xsl:stylesheet>
For record 4:
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:xalan="http://xml.apache.org/xslt">
<xsl:output omit-xml-declaration="yes"/>
<xsl:template match="XML_FILE_FOOTER">
<xsl:element name="Totals">
<xsl:attribute name="Total Records"><xsl:value-of select="#total_records"/></xsl:attribute>
</xsl:element>
</xsl:template>
</xsl:stylesheet>
The problem with is is I would have an output of this after appending each record using the templates above:
<?xml version="1.0" encoding="UTF-8"?>
<File FileName="sample.txt"></File>
<Record Name="John Doe" Age="21"></Record>
<Record Name="Jessica Sanchez" Age="22"></Record>
<Totals Total Records="2"></Totals>
How would I be able to insert the Record and Totals elements under File? so that it would have an output like this:
<?xml version="1.0" encoding="UTF-8"?>
<File FileName="sample.txt">
<Record Name="John Doe" Age="21"></Record>
<Record Name="Jessica Sanchez" Age="22"></Record>
<Totals Total Records="2"></Totals>
</File>
Any help would be very much appreciated. Thanks.
As short and easy as this:
<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:template match="/*">
<xsl:apply-templates select="XML_FILE_HEADER"/>
</xsl:template>
<xsl:template match="XML_FILE_HEADER">
<File FileName="{#file_name}">
<xsl:apply-templates select="../*[not(self::XML_FILE_HEADER)]"/>
</File>
</xsl:template>
<xsl:template match="XML_RECORD">
<Record name="{#name}" Age="{#Age}"/>
</xsl:template>
<xsl:template match="XML_FILE_FOOTER">
<Totals TotalRecords="{#total_records}"/>
</xsl:template>
</xsl:stylesheet>
When this transformation is applied on the provided XML (corrected to be well-formed) document:
<t>
<XML_FILE_HEADER file_name="sample.txt" />
<XML_RECORD record_number="1" name="John Doe" Age="21"/>
<XML_RECORD record_number="2" name="Jessica Sanchez" Age="23"/>
<XML_FILE_FOOTER total_records="2"/>
</t>
the wanted, correct result is produced:
<File FileName="sample.txt">
<Record name="John Doe" Age="21"/>
<Record name="Jessica Sanchez" Age="23"/>
<Totals TotalRecords="2"/>
</File>
Explanation:
Proper use of templates.
Proper use of xsl:apply-templates for ordering the results.
Proper use of AVT (Attribute Value Templates).
Avoided the use of xsl:element
No use of xsl:call-template.
Implemented in "push style" almost completely.
What you want is the <xsl:call-template name="templatename" /> element. This allows you to call a template from inside another template.
Something like
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:xalan="http://xml.apache.org/xslt">
    <xsl:output method="xml"/>
<xsl:template match="/XML_FILE/XML_FILE_HEADER">
<xsl:element name="File">
<xsl:attribute name="FileName">
<xsl:value-of select="#file_name"/>
</xsl:attribute>
<xsl:for-each select="/XML_FILE/XML_RECORD">
<xsl:call-template name="RecordTemplate" />
</xsl:for-each>
<xsl:call-template name="TotalTemplate" />
</xsl:element>
</xsl:template>
<xsl:template name="RecordTemplate">
<xsl:element name="Record">
<xsl:attribute name="Name"><xsl:value-of select="#name"/></xsl:attribute>
<xsl:attribute name="Age"><xsl:value-of select="#Age"/></xsl:attribute>
</xsl:element>
</xsl:template>
<xsl:template match="/XML_FILE/XML_FILE_FOOTER" name="TotalTemplate">
<xsl:element name="Totals">
<xsl:attribute name="Total Records"><xsl:value-of select="#total_records"/>
</xsl:element>
</xsl:template>
</xsl:stylesheet>
of course your input you have to be XML valid (i.e. have a single root node) like so
<XML_FILE>
<XML_FILE_HEADER file_name="sample.txt" />
<XML_RECORD record_number="1" name="John Doe" Age="21"/>
<XML_RECORD record_number="2" name""Jessica Sanchez" Age="23"/>
<XML_FILE_FOOTER total_records="2"/>
</XML_FILE>

Multi-document xslt, value-of with addition misbehaving

I am trying to write something for another individual and im stuck on the final part of the stylesheet.
We have two XML Documents:
TestXML.xml:
<rootNode>
<header>
<title agg="sum">1</title>
<records agg="sum">10</records>
<number agg="min">5</number>
</header>
</rootNode>
and TestXMLTwo.xml:
<rootNode>
<header>
<title agg="sum">2</title>
<records agg="sum">20</records>
<number agg="min">15</number>
</header>
</rootNode>
Where if the node has its agg attribute equal to 'sum' we combine the values of the two documents nodes. I am doing this using:
<xsl:param name="InputFileOne">[EditedOut]\TestXML.xml</xsl:param>
<xsl:param name="InputFileTwo">[EditedOut]\TestXMLTwo.xml</xsl:param>
<xsl:template match="node()|#*">
<xsl:call-template name="ConcatFiles"/>
</xsl:template>
<xsl:template name="ConcatFiles">
<xsl:variable name="tempStoreDocOne" select ="document($InputFileOne)/rootNode/header" />
<xsl:variable name="tempStoreDocTwo" select ="document($InputFileTwo)/rootNode/header" />
<xsl:element name="rootNode">
<xsl:element name="header">
<xsl:for-each select="$tempStoreDocOne/node()">
<xsl:choose>
<xsl:when test="./#agg = 'sum'">
<xsl:variable name="tempElementDocTwo" select ="$tempStoreDocTwo/."/>
<xsl:element name="{name(.)}">
<xsl:value-of select=". + $tempElementDocTwo"/>
</xsl:element>
</xsl:when>
<xsl:otherwise>
<xsl:element name="{name(.)}">
<xsl:value-of select="."/>
</xsl:element>
</xsl:otherwise>
</xsl:choose>
</xsl:for-each>
</xsl:element>
</xsl:element>
</xsl:template>
However on the line <xsl:value-of select=". + $tempElementDocTwo"/> I just get a value of '22016' for the <title> and 22025 for the <records>. Can someone enlighten me as to where I'm going wrong?
Change <xsl:for-each select="$tempStoreDocOne/node()"> to <xsl:for-each select="$tempStoreDocOne/*">, then add a variable storing the position i.e.
<xsl:variable name="pos" select="position()"/>
inside of the for-each, then change
<xsl:variable name="tempElementDocTwo" select ="$tempStoreDocTwo/."/>
to
<xsl:variable name="tempElementDocTwo" select ="$tempStoreDocTwo/*[$pos]"/>
Currently you are accessing the string value of the complete header element in the second document which is the concatenation of its descendant nodes while you want to access the child element with the same position as the one in the first document.
Here is a much simpler and shorter solution (no explicit conditionals):
<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:param name="pDoc2">
<rootNode>
<header>
<title agg="sum">2</title>
<records agg="sum">20</records>
<number agg="min">15</number>
</header>
</rootNode>
</xsl:param>
<xsl:variable name="vDoc2" select="document('')/*/xsl:param"/>
<xsl:template match="node()|#*">
<xsl:copy>
<xsl:apply-templates select="node()|#*"/>
</xsl:copy>
</xsl:template>
<xsl:template match="header/*[#agg='sum']">
<title>
<xsl:value-of select=
". + $vDoc2/*/header/*[name()=name(current()) and #agg='sum']"/>
</title>
</xsl:template>
</xsl:stylesheet>
When this transformation is applied on the first XML document (the second is inlined in the transformation just for convenience):
<rootNode>
<header>
<title agg="sum">1</title>
<records agg="sum">10</records>
<number agg="min">5</number>
</header>
</rootNode>
the wanted, correct result is produced:
<rootNode>
<header>
<title>3</title>
<title>30</title>
<number agg="min">5</number>
</header>
</rootNode>

How to parse nested tags using XSLT in sequence?

I have below scenario for my XML.
<content>
<para>text-1 <emphasis type="bold">text-2</emphasis> text-3</para>
</content>
I want to parse it like below
<content>
<p>text-1 <b>text-2</b> text-3</p>
</content>
I have created my XSLT as below
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="2.0">
<xsl:output method="xml" encoding="ISO-8859-1" indent="no"/>
<xsl:template name="para">
<p>
<xsl:value-of select="text()" disable-output-escaping="yes"/>
<xsl:for-each select="child::*">
<xsl:if test="name()='emphasis'">
<xsl:call-template name="emphasis"/>
</xsl:if>
</xsl:for-each>
</p>
</xsl:template>
<xsl:template name="emphasis">
<xsl:if test="attribute::type = 'bold'">
<b>
<xsl:value-of select="text()" disable-output-escaping="yes"/>
</b>
</xsl:if>
</xsl:template>
<xsl:template match="/">
<content>
<xsl:for-each select="content/child::*">
<xsl:if test="name()='para'">
<xsl:call-template name="para"/>
</xsl:if>
</xsl:for-each>
</content>
</xsl:template>
</xsl:stylesheet>
XSLT provided above is generating output like below
<content>
<p>text-1 text-3<b>text-2 </b></p>
</content>
Please guide me with your suggestions, how can I get my desire output?
To do this, you just need to extend the standard Identity Transform with special cases for matching your para and emphasis elements. For example, for para elements you would the following to replace para with p and then continue matching all the child nodes
<xsl:template match="para">
<p>
<xsl:apply-templates select="#*|node()"/>
</p>
</xsl:template>
So, given the following XSLT
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="xml" indent="yes" />
<!-- This is the Identity Transform -->
<xsl:template match="#*|node()">
<xsl:copy>
<xsl:apply-templates select="#*|node()"/>
</xsl:copy>
</xsl:template>
<!-- Replace para with p -->
<xsl:template match="para">
<p>
<xsl:apply-templates select="#*|node()"/>
</p>
</xsl:template>
<!-- Replace emphasis with b -->
<xsl:template match="emphasis[#type='bold']">
<b>
<xsl:apply-templates select="node()"/>
</b>
</xsl:template>
</xsl:stylesheet>
When applied to the following input XML
<content>
<para>text-1 <emphasis type="bold">text-2</emphasis> text-3</para>
</content>
The following is output
<content>
<p>text-1 <b>text-2</b> text-3</p>
</content>
You should be able to see how easy it is to extend to other cases should you input XML have more tags to transform.
do it like this ;)
<?xml version="1.0" encoding="utf-8" ?>
<xsl:stylesheet
version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
>
<xsl:template match="content">
<content><xsl:apply-templates select="para" /></content>
</xsl:template>
<xsl:template match="emphasis [#type='bold']">
<b><xsl:value-of select="." /></b>
</xsl:template>
</xsl:stylesheet>
when you do it like this the default template will catch text-1 and text-3

XSLT for replace attribute in XML element if it exists in another XML?

I have 2 files
catalog.xml
<?xml version="1.0" encoding="UTF-8"?>
<catalog>
<CD Title="Still got the blues" vinyl="unknown"/>
<CD Title="When a man loves a woman" vinyl="unknown"/>
</catalog>
vinyl.xml
<?xml version="1.0" encoding="UTF-8"?>
<Vinyl>
<Album>
<Title>When a man loves a woman</Title>
<Vinyl>Yes</Vinyl>
</Album>
</Vinyl>
How to produce such output.xml with xslt?
<?xml version="1.0" encoding="UTF-8"?>
<catalog>
<CD Title="Still got the blues" vinyl="unknown"/>
<CD Title="When a man loves a woman" vinyl="yes"/>***
</catalog>
***mark this line becouse it changed in output.xml
The following transform, uses catalog.xml as input and loads vinyl.xml using document(). It performs the merge just by making a simple test.
[XSLT 1.0]
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output indent="yes"/>
<xsl:variable name="vinyl" select="document('test_i2.xml')"/>
<xsl:template match="node()|#*">
<xsl:copy>
<xsl:apply-templates select="node()|#*"/>
</xsl:copy>
</xsl:template>
<xsl:template match="#vinyl">
<xsl:attribute name="vinyl">
<xsl:variable name="test" select="
$vinyl/Vinyl/Album[Title=current()/../#Title]/Vinyl"/>
<xsl:choose>
<xsl:when test="$test">
<xsl:value-of select="$test"/>
</xsl:when>
<xsl:otherwise>
<xsl:value-of select="."/>
</xsl:otherwise>
</xsl:choose>
</xsl:attribute>
</xsl:template>
</xsl:stylesheet>
This template for the attribute is less immediate, but it exploits pure XPath:
<xsl:template match="#vinyl">
<xsl:attribute name="vinyl">
<xsl:value-of select="
$vinyl/Vinyl/Album[Title=current()/../#Title]/Vinyl
|
self::node()[count($vinyl/Vinyl/Album[Title=current()/../#Title])=0]"/>
</xsl:attribute>
</xsl:template>