Generic XSLT transform to manipulate similar XML data - xslt

We have a tool which is producing an unwanted XML element and we are using XSLT to translate it to the required format.
We are writing a different XSLT for every XML generated by the file. e.g. one for the Customers XML, one for the Orders XML and so on.
Below are a couple of XML data files produced by the tool and the actual output expected
Customers
Tool-generated XML
<Message>
<Data>
<CustomerArray>
<Customer>
<X>
<Name>John</Name>
<Id>100</Id>
<Roles>
<Role>
<X>Manager</X>
<X>Architect</X>
</Role>
</Roles>
</X>
<X>
<Name>Doe</Name>
<Id>102</Id>
<Roles>
<Role>
<X>Supervisor</X>
<X>Admin</X>
</Role>
</Roles>
</X>
</Customer>
</CustomerArray>
</Data>
</Message>
Required XML data
<Message>
<Data>
<CustomerArray>
<Customer>
<Name>John</Name>
<Id>100</Id>
<Roles>
<Role>Manager</Role>
<Role>Architect</Role>
</Roles>
</Customer>
<Customer>
<Name>Doe</Name>
<Id>102</Id>
<Roles>
<Role>Supervisor</Role>
<Role>Admin</Role>
</Roles>
</Customer>
</CustomerArray>
</Data>
</Message>
Orders
Tool-generated XML
<Message>
<Orders>
<Order>
<X>
<OrderNumber>O123</OrderNumber>
<CustomerID>C100</CustomerID>
<Quantity>100</Quantity>
<UnitPrice>10.0</UnitPrice>
</X>
<X>
<OrderNumber>O456</OrderNumber>
<CustomerID>C107</CustomerID>
<Quantity>100</Quantity>
<UnitPrice>5.0</UnitPrice>
</X>
</Order>
</Orders>
</Message>
Required XML data
<Message>
<Orders>
<Order>
<OrderNumber>O123</OrderNumber>
<CustomerID>C100</CustomerID>
<Quantity>100</Quantity>
<UnitPrice>10.0</UnitPrice>
</Order>
<Order>
<OrderNumber>O456</OrderNumber>
<CustomerID>C107</CustomerID>
<Quantity>100</Quantity>
<UnitPrice>5.0</UnitPrice>
</Order>
</Orders>
</Message>
The unwanted element X can come at any level.
Is it possible to write a generic XSLT transform to achieve this result across all XML input? For instance, where an X is found, replace it with the parent tag and then delete the parent tag.

You need to write an identity transform with an explicit template for all nodes that have any X children so that they can be replicated.
This transform does what you asked. It uses the variable name to save the name of the element that is parent to the X to avoid writing the more obscure name(current()) when it comes to generating each output element.
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">
<xsl:strip-space elements="*"/>
<xsl:output method="xml" indent="yes" encoding="UTF-8" omit-xml-declaration="yes"/>
<xsl:template match="node()|#*">
<xsl:copy>
<xsl:apply-templates/>
</xsl:copy>
</xsl:template>
<xsl:template match="*[X]">
<xsl:variable name="name" select="name()"/>
<xsl:for-each select="X">
<xsl:element name="{$name}">
<xsl:apply-templates/>
</xsl:element>
</xsl:for-each>
</xsl:template>
</xsl:stylesheet>
output
Customers
<Message>
<Data>
<CustomerArray>
<Customer>
<Name>John</Name>
<Id>100</Id>
<Roles>
<Role>Manager</Role>
<Role>Architect</Role>
</Roles>
</Customer>
<Customer>
<Name>Doe</Name>
<Id>102</Id>
<Roles>
<Role>Supervisor</Role>
<Role>Admin</Role>
</Roles>
</Customer>
</CustomerArray>
</Data>
</Message>
Orders
<Message>
<Orders>
<Order>
<OrderNumber>O123</OrderNumber>
<CustomerID>C100</CustomerID>
<Quantity>100</Quantity>
<UnitPrice>10.0</UnitPrice>
</Order>
<Order>
<OrderNumber>O456</OrderNumber>
<CustomerID>C107</CustomerID>
<Quantity>100</Quantity>
<UnitPrice>5.0</UnitPrice>
</Order>
</Orders>
</Message>

Here is a slightly simpler/shorter solution, which also handles correctly the case when a X can have non X sibling elements:
<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="node()|#*">
<xsl:copy>
<xsl:apply-templates select="node()|#*"/>
</xsl:copy>
</xsl:template>
<xsl:template match="*[X]"><xsl:apply-templates/></xsl:template>
<xsl:template match="X">
<xsl:element name="{name(..)}">
<xsl:apply-templates select="node()|#*"/>
</xsl:element>
</xsl:template>
</xsl:stylesheet>
When this transformation is applied to the following XML document (the first of the provided ones, with one non-X sibling added to the X elements:
<Message>
<Data>
<CustomerArray>
<Customer>
<X>
<Name>John</Name>
<Id>100</Id>
<Roles>
<Role>
<X>Manager</X>
<X>Architect</X>
</Role>
</Roles>
</X>
<somethingElse/>
<X>
<Name>Doe</Name>
<Id>102</Id>
<Roles>
<Role>
<X>Supervisor</X>
<X>Admin</X>
</Role>
</Roles>
</X>
</Customer>
</CustomerArray>
</Data>
</Message>
the wanted, correct result is produced:
<Message>
<Data>
<CustomerArray>
<Customer>
<Name>John</Name>
<Id>100</Id>
<Roles>
<Role>Manager</Role>
<Role>Architect</Role>
</Roles>
</Customer>
<somethingElse/>
<Customer>
<Name>Doe</Name>
<Id>102</Id>
<Roles>
<Role>Supervisor</Role>
<Role>Admin</Role>
</Roles>
</Customer>
</CustomerArray>
</Data>
</Message>
Do note that the solution by Borodin loses the somethingElse element.

Related

Separate duplicates from unique

I tried with many XSLT methods like Key, for-each-group and match method options, but it is not working for me.
Input XML:
<?xml version='1.0' encoding='UTF-8'?>
<CustomerRecord>
<Customer>
<chargeto>ABC</chargeto>
<chargename>GARY</chargename>
</Customer>
<Customer>
<chargeto>XYZ</chargeto>
<chargename>DAVIS</chargename>
</Customer>
<Customer>
<chargeto>CDE</chargeto>
<chargename>GARY DAVIS</chargename>
</Customer>
<Customer>
<chargeto>ABC</chargeto>
<chargename>DAV</chargename>
</Customer>
</CustomerRecord>
Expected output XML:
<?xml version='1.0' encoding='UTF-8'?>
<Root>
<Customer_PO>
<Customer>
<chargeto>ABC</chargeto>
<chargename>GARY</chargename>
</Customer>
<Customer>
<chargeto>ABC</chargeto>
<chargename>DAV</chargename>
</Customer>
</Customer_PO>
<Customer_Falty>
<Customer>
<chargeto>XYZ</chargeto>
<chargename>DAVIS</chargename>
</Customer>
<Customer>
<chargeto>CDE</chargeto>
<chargename>GARY DAVIS</chargename>
</Customer>
</Customer_Falty>
</Root>
Below is the XSLT which I wrote Initially to at least get the some details in the Output, but data is getting missed when the target is generated:
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:msxsl="urn:schemas-microsoft-com:xslt" xmlns:var="http://schemas.microsoft.com/BizTalk/2003/var" exclude-result-prefixes="msxsl var" version="1.0" xmlns:ns0="http://Namespace">
<xsl:output omit-xml-declaration="yes" method="xml" version="1.0" />
<xsl:key use="chargeto" match="/CustomerRecord/Customer" name ="groups"/>
<xsl:template match="/">
<xsl:apply-templates select="/CustomerRecord" />
</xsl:template>
<xsl:template match="/CustomerRecord">
<Root>
<xsl:for-each select="/CustomerRecord/Customer[generate-id(.)=generate-id(key('groups',chargeto))]">
<Customer_PO>
<xsl:for-each select="key('groups',chargeto)">
<Customer>
<chargeto>
<xsl:value-of select="CustomerRecord/chargeto/text()" />
</chargeto>
<chargename>
<xsl:value-of select="CustomerRecord/chargename/text()" />
</chargename>
</Customer>
</xsl:for-each>
</Customer_PO>
</xsl:for-each>
</Root>
</xsl:template>
</xsl:stylesheet>
Please find the explanation about the Expected Output
<Customer_PO>. Under this tag we will have all the <Customer> segments where all the <chargeto> fields are having duplicate value
<Customer_Falty> Under this tag we will have all the <Customer> segment where all the <chargeto> field values are unique
If I understand correctly your explanation, you want to do something like:
XSL 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:key name="cust-by-charge" match="Customer" use="chargeto" />
<xsl:template match="/CustomerRecord">
<Root>
<Customer_PO>
<xsl:copy-of select="Customer[count(key('cust-by-charge', chargeto)) > 1]"/>
</Customer_PO>
<Customer_Falty>
<xsl:copy-of select="Customer[count(key('cust-by-charge', chargeto)) = 1]"/>
</Customer_Falty>
</Root>
</xsl:template>
</xsl:stylesheet>
Possibly there's a more elegant approach that would count the size of each group only once.

Filter Common Field Value in One each <Record> Target Field Tag and others in individual <Record> Field Tags using XSLT

Need Help on the below XSLT scenario
```
Input Payload:
===============
<?xml version='1.0' encoding='UTF-8'?>
<CustomerRecord>
<Customer>
<chargeto>ABC</chargeto>
<chargename>GARY</chargename>
<customer_number>EP10025</customer_number>
</Customer>
<Customer>
<chargeto>XYZ</chargeto>
<chargename>DAV</chargename>
<customer_number>EP10023</customer_number>
</Customer>
<Customer>
<chargeto>CDE</chargeto>
<chargename>GARY DAVIS</chargename>
<customer_number>EP10023</customer_number>
</Customer>
<Customer>
<chargeto>ABC</chargeto>
<chargename>DAV</chargename>
<customer_number>EP10024</customer_number>
</Customer>
<Customer>
<chargeto>CDE</chargeto>
<chargename>GARY DAVIS</chargename>
<customer_number>EP10025</customer_number>
</Customer>
<Customer>
<chargeto>ABC</chargeto>
<chargename>DAV</chargename>
<customer_number>EP10022</customer_number>
</Customer>
</CustomerRecord>```
Below is the Expected Output
```Expected Output Payload:
===========================
<?xml version="1.0" encoding="UTF-8"?>
<Root>
<Customer_PO>
<Customer>
<chargeto>ABC</chargeto>
<chargename>GARY</chargename>
<customer_number>EP10025</customer_number>
</Customer>
<Customer>
<chargeto>CDE</chargeto>
<chargename>GARY DAVIS</chargename>
<customer_number>EP10025</customer_number>
</Customer>
</Customer_PO>
<Customer_PO>
<Customer>
<chargeto>XYZ</chargeto>
<chargename>DAV</chargename>
<customer_number>EP10023</customer_number>
</Customer>
<Customer>
<chargeto>CDE</chargeto>
<chargename>GARY DAVIS</chargename>
<customer_number>EP10023</customer_number>
</Customer>
</Customer_PO>
<Customer_Faulty>
<Customer>
<chargeto>ABC</chargeto>
<chargename>DAV</chargename>
<customer_number>EP10024</customer_number>
</Customer>
<Customer>
<chargeto>ABC</chargeto>
<chargename>DAV</chargename>
<customer_number>EP10022</customer_number>
</Customer>
</Customer_Faulty>
</Root>```
I have below XSLT which is partially working fine
``` XSLT Code:
===============
<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:key name="cust-by-charge" match="Customer" use="customer_number" />
<xsl:template match="/CustomerRecord">
<Root>
<Customer_PO>
<xsl:copy-of select="Customer[count(key('cust-by-charge', customer_number)) > 1]"/>
</Customer_PO>
<Customer_Faulty>
<xsl:copy-of select="Customer[count(key('cust-by-charge', customer_number)) = 1]"/>
</Customer_Faulty>
</Root>
</xsl:template>
</xsl:stylesheet>
```
**I am partially getting output correctly, I need to create multiple <Customer_PO> segments for the common <customer_number> field values.
In our current case all common values are getting added into same <Customer_PO>. --> This part is not working fine
And Non Common <customer_number> field values will be set under <Customer_Faulty> -- This part is working Fine**
I am again struggling to understand your explanation. I think this does what you want:
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="/CustomerRecord">
<xsl:variable name="groups">
<xsl:for-each-group select="Customer" group-by="customer_number">
<group size="{count(current-group())}">
<xsl:copy-of select="current-group()"/>
</group>
</xsl:for-each-group>
</xsl:variable>
<Root>
<xsl:for-each select="$groups/group[#size > 1]">
<Customer_PO>
<xsl:copy-of select="Customer"/>
</Customer_PO>
</xsl:for-each>
<Customer_Faulty>
<xsl:copy-of select="$groups/group[#size = 1]/Customer"/>
</Customer_Faulty>
</Root>
</xsl:template>
</xsl:stylesheet>

Verticalize XML using XSLT

I am trying to implement an at first looking simple transformation but whatever I have tried has been failed.
The XML is generated from a fixed length record and have the below format.
<?xml version="1.0" encoding="UTF-8"?>
<record>
<no_of_records>30</no_of_records>
<cust_lastname_1>Smith</cust_lastname_1>
<cust_name_1>John</cust_name_1>
<cust_id_1>X45</cust_id_1>
<cust_lastname_2>George</cust_lastname_2>
<cust_name_2>Michael</cust_name_2>
<cust_id_2>X76</cust_id_2>
<cust_lastname_3>Ria</cust_lastname_3>
<cust_name_3>Chris</cust_name_3>
<cust_id_3>C87</cust_id_3>
...
</record>
The no_of_records indicates how many _X suffixed elements contains each record and because of its fix length origin has a defined maximum.
I want to transform it to a “verticalized” form resempling the below.
<record>
<customer num="1">
<lastname>Smith</lastname>
<name>John</name>
<id>X45</id>
</customer>
<customer num="2">
<lastname>George</lastname>
<name>Michael</name>
<id>X76</id>
</customer>
<customer num="3">
<lastname>Ria</lastname>
<name>Chris</name>
<id>C87</id>
...
</customer>
</record>
Any help would greatly appreciated.
In XSLT 2.0, you want something like
<xsl:for-each-group select="*" group-starting-with="*[starts-with(local-name(), 'cust_lastname']">
<customer num="{position()}">
<xsl:apply-templates select="current-group()"/>
</customer>
</xsl:for-each-group>
....
<xsl:template match="*[starts-with(local-name(), 'cust')]">
<xsl:element name="{replace(local-name(), 'cust_(.*?)_[0-9]+', '$1')}">
<xsl:value-of select="."/>
</xsl:element>
</xsl:template>
The solution from #Michael Kay works fine. Thank you !
XML
<?xml version="1.0" encoding="UTF-8"?>
<record>
<no_of_records>3</no_of_records>
<cust_lastname_1>Smith</cust_lastname_1>
<cust_name_1>John</cust_name_1>
<cust_id_1>X45</cust_id_1>
<cust_lastname_2>George</cust_lastname_2>
<cust_name_2>Michael</cust_name_2>
<cust_id_2>X76</cust_id_2>
<cust_lastname_3>Ria</cust_lastname_3>
<cust_name_3>Chris</cust_name_3>
<cust_id_3>C87</cust_id_3>
</record>
XSLT
<xsl:stylesheet version="2.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output indent="yes"/>
<xsl:strip-space elements="*"/>
<xsl:template match="record">
<records>
<xsl:for-each-group select="*[starts-with(local-name(), 'cust_')]"
group-starting-with="*[starts-with(local-name(), 'cust_lastname')]">
<customer num="{position()}">
<xsl:apply-templates select="current-group()"/>
</customer>
</xsl:for-each-group>
</records>
</xsl:template>
<xsl:template match="*[starts-with(local-name(), 'cust')]">
<xsl:element name="{replace(local-name(), 'cust_(.*?)_[0-9]+', '$1')}">
<xsl:value-of select="."/>
</xsl:element>
</xsl:template>
</xsl:stylesheet>
Result
<?xml version="1.0" encoding="UTF-8"?>
<records>
<customer num="1">
<lastname>Smith</lastname>
<name>John</name>
<id>X45</id>
</customer>
<customer num="2">
<lastname>George</lastname>
<name>Michael</name>
<id>X76</id>
</customer>
<customer num="3">
<lastname>Ria</lastname>
<name>Chris</name>
<id>C87</id>
</customer>
</records>

Merging two diff XMLS using XSLT(1.0)

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.

XSLT transform to multiple complex types within for loop

I have a requirment to transform a XML with the below structure
<CustomerStatements>
<CustomerStatement>
<Name>ABC</Name>
<ID>1</ID>
<Amt>10</Amt>
</CustomerStatement>
<CustomerStatement>
<Name>ABC</Name>
<ID>1</ID>
<Amt>20</Amt>
</CustomerStatement>
<CustomerStatement>
<Name>XYZ</Name>
<ID>2</ID>
<Amt>30</Amt>
</CustomerStatement>
<CustomerStatement>
<Name>XYZ</Name>
<ID>2</ID>
<Amt>40</Amt>
</CustomerStatement>
</CustomerStatements>
To
<Customers>
<Customer>
<Name>ABC</Name>
<Id>1</Id>
<Amounts>
<Amount>10</Amount>
<Amount>20</Amount>
</Amounts>
</Customer>
<Customer>
<Name>XYZ</Name>
<Id>2</Id>
<Amount>30</Amount>
<Amount>40</Amount>
</Customer>
</Customers>
I tried using a for loop and taking the name into a variable to compare the name in the next record, but this doesn't work. Can you any one help me with a sample XSLT psudo code.
Thanks
I. When this XSLT 1.0 solution:
<?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:key
name="kCustByNameId"
match="CustomerStatement"
use="concat(Name, '+', ID)" />
<xsl:template match="/*">
<Customers>
<xsl:apply-templates
select="CustomerStatement[
generate-id() =
generate-id(key('kCustByNameId', concat(Name, '+', ID))[1])]" />
</Customers>
</xsl:template>
<xsl:template match="CustomerStatement">
<Customer>
<xsl:copy-of select="Name|ID" />
<Amounts>
<xsl:for-each select="key('kCustByNameId', concat(Name, '+', ID))/Amt">
<Amount>
<xsl:apply-templates />
</Amount>
</xsl:for-each>
</Amounts>
</Customer>
</xsl:template>
</xsl:stylesheet>
...is applied to the OP's original XML:
<CustomerStatements>
<CustomerStatement>
<Name>ABC</Name>
<ID>1</ID>
<Amt>10</Amt>
</CustomerStatement>
<CustomerStatement>
<Name>ABC</Name>
<ID>1</ID>
<Amt>20</Amt>
</CustomerStatement>
<CustomerStatement>
<Name>XYZ</Name>
<ID>2</ID>
<Amt>30</Amt>
</CustomerStatement>
<CustomerStatement>
<Name>XYZ</Name>
<ID>2</ID>
<Amt>40</Amt>
</CustomerStatement>
</CustomerStatements>
...the wanted result is produced:
<?xml version="1.0" encoding="UTF-8"?><Customers>
<Customer>
<Name>ABC</Name>
<ID>1</ID>
<Amounts>
<Amount>10</Amount>
<Amount>20</Amount>
</Amounts>
</Customer>
<Customer>
<Name>XYZ</Name>
<ID>2</ID>
<Amounts>
<Amount>30</Amount>
<Amount>40</Amount>
</Amounts>
</Customer>
</Customers>
The primary thing to look at here is Muenchian Grouping, which is the generally accepted method for grouping problems in XSLT 1.0.
II. Here's a more compact XSLT 2.0 solution:
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="2.0">
<xsl:output omit-xml-declaration="no" indent="yes"/>
<xsl:strip-space elements="*"/>
<xsl:template match="/*">
<Customers>
<xsl:for-each-group select="CustomerStatement" group-by="ID">
<Customer>
<xsl:copy-of select="current-group()[1]/Name|current-group()[1]/ID" />
<Amounts>
<xsl:for-each select="current-group()/Amt">
<Amount>
<xsl:apply-templates />
</Amount>
</xsl:for-each>
</Amounts>
</Customer>
</xsl:for-each-group>
</Customers>
</xsl:template>
</xsl:stylesheet>
In this case, notice XSLT 2.0's use of the for-each-group element, which eliminates the need for the sometimes-verbose and potentially confusing Muenchian Grouping method.