I have got a source XML
<Records>
<Data>
<RecordType>New</RecordType>
<Number>4734122946</Number>
<DateOfBirth>20160506</DateOfBirth>
<Title>Mr</Title>
<ChangeTimeStamp>20160101010001</ChangeTimeStamp>
<SerialChangeNumber>01</SerialChangeNumber>
</Data>
<Data>
<RecordType>New</RecordType>
<Number>4734122946</Number>
<LastName>Potter</LastName>
<DateOfBirth>20160506</DateOfBirth>
<ChangeTimeStamp>20160101010002</ChangeTimeStamp>
<SerialChangeNumber>01</SerialChangeNumber>
</Data>
</Records>
I want to use XSLT 1.0 to produce the below output
<Contact>
<Number>4734122946</Number>
<Title>Mr</Title>
<LastName>Potter</LastName>
<BirthDate>20160506</BirthDate>
<ChangeTimeStamp>20160101010002</ChangeTimeStamp>
</Contact>
The XSLT has to group and merge the child nodes of Data records into one based on the Number field. Also if there are same elements present, then it should use the ChangeTimeStamp element to figure out the latest change and use that element.
I tried the below XSLT. But I am nowhere close to the output.
<?xml version="1.0" encoding="utf-8"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:msxsl="urn:schemas-microsoft-com:xslt" exclude-result-prefixes="msxsl">
<xsl:output method="xml" indent="yes" />
<xsl:key name="groups" match="Data" use="Number"/>
<xsl:key name="sortGroup" match="Data" use ="ChangeTimeStamp"/>
<xsl:template match="#* | node()">
<xsl:copy>
<xsl:apply-templates select="#* | node()"/>
</xsl:copy>
</xsl:template>
<xsl:template match="Records">
<xsl:for-each select="Data[generate-id() = generate-id(key('groups',Number))]">
<Contact>
<Number>
<xsl:value-of select="Number"/>
</Number>
<xsl:for-each select="key('groups',Number)">
<xsl:for-each select="key('sortGroup',ChangeTimeStamp)">
<xsl:sort select="sortGroup" order="ascending"/>
<xsl:if test="Title">
<Title>
<xsl:value-of select ="Title"/>
</Title>
</xsl:if>
<xsl:if test="LastName">
<LastName>
<xsl:value-of select="LastName"/>
</LastName>
</xsl:if>
<BirthDate>
<xsl:value-of select="DateOfBirth"/>
</BirthDate>
</xsl:for-each>
</xsl:for-each>
</Contact>
</xsl:for-each>
</xsl:template>
</xsl:stylesheet>
Appreciate your help.
The XSLT has to group and merge the child nodes of Data records into
one based on the Number field. Also if there are same elements
present, then it should use the ChangeTimeStamp element to figure out
the latest change and use that element.
For that, I believe you would want to do something like:
<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="group" match="Data" use="Number"/>
<xsl:key name="item" match="Data/*" use="concat(../Number, '|', name())"/>
<xsl:template match="/Records">
<root>
<xsl:for-each select="Data[generate-id() = generate-id(key('group', Number)[1])]">
<Contact>
<xsl:for-each select="key('group', Number)/*[generate-id() = generate-id(key('item', concat(../Number, '|', name()))[1])]">
<xsl:for-each select="key('item', concat(../Number, '|', name()))">
<xsl:sort select="../ChangeTimeStamp" data-type="number" order="descending"/>
<xsl:if test="position()=1">
<xsl:copy-of select="."/>
</xsl:if>
</xsl:for-each>
</xsl:for-each>
</Contact>
</xsl:for-each>
</root>
</xsl:template>
</xsl:stylesheet>
You will have to make some adjustments if you want to include only some data items and/or if you want them to appear in particular order. If you have a list of all possible data item names (e.g. Title, LastName, DateOfBirth, etc.) then this could be simpler.
Related
When i use XSLT 2.0 key and tokenize function, it's return items order getting changed based on key value. in our output we required retain the same order of tokenize sequence.
Input File
<?xml version="1.0" encoding="UTF-8"?> <a> <bd id="a">a</bd> <bd id="b">b</bd> <bd id="e">e</bd> <bd id="d">d</bd> </a>
XSLT
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="2.0">
<xsl:key name="idcollect" match="*[#id]" use="#id" />
<xsl:variable name="name" select="'d,b,e,a'"/>
<xsl:template match="/">
<xsl:for-each select="key('idcollect',tokenize($name,','))" >
<xsl:copy-of select="."/>
</xsl:for-each>
</xsl:template>
</xsl:stylesheet>
current Output
<?xml version="1.0" encoding="UTF-8"?><bd id="a">a</bd><bd id="b">b</bd><bd id="e">e</bd><bd id="d">d</bd>
Expected output
<?xml version="1.0" encoding="UTF-8"?><bd id="d">d</bd><bd id="b">b</bd><bd id="e">e</bd><bd id="a">a</bd>
I think you want e.g.
<xsl:variable name="main-doc" select="/"/>
<xsl:for-each select="for $token in tokenize($name,',') return key('idcollect', $token, $main-doc)">
<xsl:copy-of select="."/>
</xsl:for-each>
or in XSLT 3
<xsl:variable name="main-doc" select="/"/>
<xsl:for-each select="tokenize($name,',') ! key('idcollect', ., $main-doc)">
<xsl:copy-of select="."/>
</xsl:for-each>
Of course in both cases the for-each/copy-of nesting is not needed and e.g.
<xsl:copy-of select="let $main-doc := / return tokenize($name,',') ! key('idcollect', ., $main-doc)"/>
or
<xsl:variable name="main-doc" select="/"/>
<xsl:copy-of select="for $token in tokenize($name,',') return key('idcollect', $token, $main-doc)"/>
would suffice.
Try:
XSLT 2.0
<xsl:stylesheet version="2.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:key name="idcollect" match="*[#id]" use="#id" />
<xsl:variable name="name" select="'d,b,e,a'"/>
<xsl:template match="/">
<xsl:variable name="ids" select="tokenize($name,',')"/>
<xsl:for-each select="key('idcollect', $ids)" >
<xsl:sort select="index-of($ids, .)"/>
<xsl:copy-of select="."/>
</xsl:for-each>
</xsl:template>
</xsl:stylesheet>
Or, if you prefer:
<xsl:stylesheet version="2.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:key name="idcollect" match="*[#id]" use="#id" />
<xsl:variable name="name" select="'d,b,e,a'"/>
<xsl:template match="/">
<xsl:variable name="xml" select="/"/>
<xsl:for-each select="tokenize($name, ',')" >
<xsl:copy-of select="key('idcollect', ., $xml)"/>
</xsl:for-each>
</xsl:template>
</xsl:stylesheet>
I have a file called ori.xml:
<?xml version="1.0" encoding="UTF-8"?>
<root>
<container>
<elA>
<el1>value1</el1>
<el2>value2</el2>
</elA>
<elB>
<el3>value3</el3>
<el4>value4</el4>
<el5>value5</el5>
</elB>
<elC>
<el6>value5</el6>
</elC>
</container>
</root>
and another one called modifs.xml:
<?xml version="1.0" encoding="UTF-8"?>
<els>
<el2>newvalue2</el2>
<el5>newvalue5</el5>
</els>
and I would like to obtain result.xml:
<?xml version="1.0" encoding="UTF-8"?>
<root>
<container>
<elA>
<el1>value1</el1>
<el2>newvalue2</el2>
</elA>
<elB>
<el3>value3</el3>
<el4>value4</el4>
<el5>newvalue5</el5>
</elB>
<elC>
<el6>value5</el6>
</elC>
</container>
</root>
I'm a beginner in XSLT.
So I started to write a stylesheet with which I'm able to change value2 into newvalue2:
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="xml" indent="yes"/>
<xsl:param name="fileName" select="'modifs.xml'" />
<xsl:param name="modifs" select="document($fileName)" />
<xsl:param name="updateEl" >
<xsl:value-of select="$modifs/els/el2" />
</xsl:param>
<xsl:template match="#* | node()">
<xsl:copy>
<xsl:apply-templates select="#* | node()"/>
</xsl:copy>
</xsl:template>
<xsl:template match="//elA/el2">
<xsl:copy>
<xsl:apply-templates select="$updateEl" />
</xsl:copy>
</xsl:template>
</xsl:stylesheet>
But now I have to modify this stylesheet to be able to know which elements are in modifs.xml and find them in ori.xml. I don't know how to do that. Could you help please ?
I would use a key:
<xsl:key name="ref-change" match="els/*" use="local-name()"/>
<xsl:template match="*[key('ref-change', local-name(), $modifs)]">
<xsl:copy-of select="key('ref-change', local-name(), $modifs)"/>
</xsl:template>
However, using the third argument for the key function is only supported in XSLT 2 and later thus if you use an XSLT 1 processor you need to move the logic into the template, that requires using for-each to "switch" the context document
<xsl:template match="*">
<xsl:variable name="this" select="."/>
<xsl:for-each select="$modifs">
<xsl:choose>
<xsl:when test="key('ref-change', local-name($this))">
<xsl:copy-of select="key('ref-change', local-name($this))"/>
</xsl:when>
<xsl:otherwise>
<xsl:for-each select="$this">
<xsl:call-template name="identity"/>
</xsl:for-each>
</xsl:otherwise>
</xsl:choose>
</xsl:for-each>
</xsl:template>
Put name="identity" on your identity transformation template.
I am trying to transform XML to CSV where each entry does not contain all values. The column order must be preserved.
Initial file:
<?xml version='1.0' encoding='UTF-8'?>
<data>
<entry>
<a>FR</a>
<b>Dupont</b>
<c>123456</c>
<d>zzz</d>
<f>New York</f>
</entry>
<entry>
<a>FR</a>
<b>Martin</b>
<c>234561</c>
<d>xxx</d>
<e>2019-01-01</e>
<f>Paris</f>
</entry>
<entry>
<a>FR</a>
<b>Chris</b>
<c>345612</c>
<d>yyy</d>
<e>2019-01-01</e>
</entry>
</data>
Expected output:
a;b;c;d;e;f
FR;Dupont;123456;zzz;;New York
FR;Martin;234561;xxx;2019-01-01;Paris
FR;Chris;345612;yyy;2019-01-01;
I am struggling with getting the header values in the correct order. I tried distinct-values() and for-each-group() but I am not able to preserve the order.
One example of what I tried:
<xsl:stylesheet version="2.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
exclude-result-prefixes="#all">
<xsl:output method="text"/>
<xsl:template match="/">
<xsl:for-each select="distinct-values(//entry/*/local-name())">
<xsl:value-of select="."/>
</xsl:for-each>
</xsl:template>
</xsl:stylesheet>
Output:
abcdfe
Any ideas? Thanks.
I would use the following approach:
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:xs="http://www.w3.org/2001/XMLSchema"
exclude-result-prefixes="#all"
version="3.0">
<xsl:output method="text"/>
<xsl:strip-space elements="*"/>
<xsl:variable name="cols"
as="xs:string*"
select="let $max-cols := max(data/entry/count(*))
return distinct-values(data/entry[count(*) = $max-cols]/*/local-name())"/>
<xsl:template match="data">
<xsl:value-of select="$cols" separator=";"/>
<xsl:text>
</xsl:text>
<xsl:apply-templates/>
</xsl:template>
<xsl:template match="entry">
<xsl:value-of
select="for $col in $cols
return string(*[local-name() = $col])"
separator=";"/>
<xsl:text>
</xsl:text>
</xsl:template>
</xsl:stylesheet>
https://xsltfiddle.liberty-development.net/bFWR5Em/1
Try this (updated):
<xsl:stylesheet version="2.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" exclude-result-prefixes="#all">
<xsl:output method="text"/>
<xsl:template match="/">
<xsl:variable name="cols" as="element()*">
<xsl:for-each select="distinct-values(//entry/*/local-name())">
<xsl:sort select="."/>
<Item><xsl:value-of select="."/></Item>
</xsl:for-each>
</xsl:variable>
<xsl:for-each select="$cols">
<xsl:value-of select="concat(.,';')"/>
</xsl:for-each>
<xsl:text>
</xsl:text>
<xsl:for-each select="//entry">
<xsl:variable name="entry" select="."/>
<xsl:for-each select="$cols">
<xsl:value-of select="concat($entry/*[local-name() = current()], ';')"/>
</xsl:for-each>
<xsl:text>
</xsl:text>
</xsl:for-each>
</xsl:template>
</xsl:stylesheet>
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>
I have to add one attribute Publisher="Penguin" to the nodes from a NodeList : The input xml looks like:
<Rack RackNo="1">
<Rows>
<Row RowNo="1" NoOfBooks="10"/>
<Row RowNo="2" NoOfBooks="15"/>
<Rows>
</Rack>
The output xml lookslike:
<Rack RackNo="1">
<Rows>
<Row RowNo="1" NoOfBooks="10" Publisher="Penguin"/>
<Row RowNo="2" NoOfBooks="15" Publisher="Penguin"/>
<Rows>
</Rack>
The xsl i wrote is :
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="xml" indent="yes" />
<xsl:template match="/">
<Order>
<xsl:copy-of select = "Rack/#*"/>
<xsl:for-each select="Rows/Row">
<OrderLine>
<xsl:copy-of select = "Row/#*"/>
<xsl:attribute name="Publisher"></xsl:attribute>
<xsl:copy-of select = "Row/*"/>
</OrderLine>
</xsl:for-each>
<xsl:copy-of select = "Rack/*"/>
</Order>
</xsl:template>
</xsl:stylesheet>
This doesnt return the desired output.
Any help will be much appreciated.
Thanks in advance guys.
This is a job for the XSLT identity transform. On its own it simple creates a copy of all the nodes in your input XML
<xsl:template match="#*|node()">
<xsl:copy>
<xsl:apply-templates select="#*|node()"/>
</xsl:copy>
</xsl:template>
All you need to do is add an extra template to match Row element, and add a Publisher attribute to it. It might be good to first parameterise the publisher you wish to add
<xsl:param name="publisher" select="'Penguin'" />
You then create the matching template as follows:
<xsl:template match="Row">
<OrderLine Publisher="{$publisher}">
<xsl:apply-templates select="#*|node()"/>
</OrderLine>
</xsl:template>
Note the use of "Attribute Value Templates" to create the Publisher attribute. The curly braces indicate it is an expression to be evaluated. Also note in your XSLT it looks like you are renaming the elements too, so I have done this in my XSLT as well. (If this is not the case, simply replace OrderLine back with Row.
Here is the full XSLT
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="xml" indent="yes"/>
<xsl:param name="publisher" select="'Penguin'" />
<xsl:template match="Rack">
<Order>
<xsl:apply-templates />
</Order>
</xsl:template>
<xsl:template match="Rows">
<xsl:apply-templates />
</xsl:template>
<xsl:template match="Row">
<OrderLine Publisher="{$publisher}">
<xsl:apply-templates select="#*|node()"/>
</OrderLine>
</xsl:template>
<xsl:template match="#*|node()">
<xsl:copy>
<xsl:apply-templates select="#*|node()"/>
</xsl:copy>
</xsl:template>
</xsl:stylesheet>
When applied to your XML, the following is output
<Order>
<OrderLine Publisher="Penguin" RowNo="1" NoOfBooks="10"></OrderLine>
<OrderLine Publisher="Penguin" RowNo="2" NoOfBooks="15"></OrderLine>
</Order>