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>
Related
I would like to get the distinct values inside of a for loop, or within some group. Since the xsl:key can only be declared at the top level, how would I be able to make a xsl:key for each group? In the example below, the group would be the most outer fruit tags. Note that there's also a xsl:sort. If there is a way to accomplish this by just xpaths (preceding-sibling), I would love to know this solution as well. I'm not sure if I would need to use the Muenchian method to accomplish this, but this is what I have:
Input.xml
<root>
<fruits>
<fruit>
<fruit id="2">
<banana><taste>Yummy</taste></banana>
<banana><taste>Disgusting</taste></banana>
</fruit>
<fruit id="1">
<banana><taste>Eh</taste></banana>
<banana><taste>Disgusting</taste></banana>
</fruit>
</fruit>
<fruit>
<fruit id="2">
<banana><taste>Yummy</taste></banana>
<banana><taste>Disgusting</taste></banana>
</fruit>
<fruit id="1">
<banana><taste>Amazing</taste></banana>
<banana><taste>Disgusting</taste></banana>
</fruit>
</fruit>
</fruits>
</root>
Transform.xsl
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:fo="http://www.w3.org/1999/XSL/Format">
<xsl:key name="taste" use="." match="taste" />
<xsl:template match="root">
<xsl:apply-templates select="fruits" />
</xsl:template>
<xsl:template match="fruits">
<xsl:element name="newFruits">
<xsl:call-template name="test" />
</xsl:element>
</xsl:template>
<xsl:template name="test">
<xsl:for-each select="fruit">
<xsl:sort select="fruit/#id" />
<xsl:element name="newFruit">
<!-- xsl:for-each select="fruit/banana/taste[not(.=preceding::taste)]/.." /> -->
<xsl:for-each select="fruit/banana/taste[generate-id() = generate-id(key('taste',.)[1])]/..">
<xsl:element name="fruit">
<xsl:value-of select="."/>
</xsl:element>
</xsl:for-each>
</xsl:element>
</xsl:for-each>
</xsl:template>
</xsl:stylesheet>
Output (comments in the output is the desired tags that should appear)
<?xml version="1.0" encoding="UTF-8"?>
<newFruits>
<newFruit>
<fruit>Yummy</fruit>
<fruit>Disgusting</fruit>
<fruit>Eh</fruit>
</newFruit>
<newFruit>
<!-- <fruit>Yummy</fruit> -->
<!-- <fruit>Disgusting</fruit> -->
<fruit>Amazing</fruit>
</newFruit>
</newFruits>
The issue is that you want your taste elements to be distinct per each top-level fruit element. Your current grouping is getting the distinct elements for the whole document.
If you can't update to XSLT 2.0 then shed a tear, as you have to then use a concatenated key in XSLT 1.0, to include a unique identifier for the relevant fruit element, which can be achieved by using generate-id()
<xsl:key name="taste" use="concat(generate-id(../../..), '|', .)" match="taste" />
Then, in your "test" template, define a variable to hold the id for the relevant fruit...
<xsl:variable name="id" select="generate-id()" />
And your expression to get the distinct tastes becomes this...
<xsl:for-each select="fruit/banana/taste[generate-id() = generate-id(key('taste', concat($id, '|', .))[1])]">
Try this XSLT
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:fo="http://www.w3.org/1999/XSL/Format">
<xsl:output method="xml" indent="yes" />
<xsl:key name="taste" use="concat(generate-id(../../..), '|', .)" match="taste" />
<xsl:template match="root">
<xsl:apply-templates select="fruits" />
</xsl:template>
<xsl:template match="fruits">
<newFruits>
<xsl:call-template name="test" />
</newFruits>
</xsl:template>
<xsl:template name="test">
<xsl:for-each select="fruit">
<xsl:variable name="id" select="generate-id()" />
<newFruit>
<!-- xsl:for-each select="fruit/banana/taste[not(.=preceding::taste)]/.." /> -->
<xsl:for-each select="fruit/banana/taste[generate-id() = generate-id(key('taste', concat($id, '|', .))[1])]">
<fruit>
<xsl:value-of select="."/>
</fruit>
</xsl:for-each>
</newFruit>
</xsl:for-each>
</xsl:template>
</xsl:stylesheet>
Note, you don't really need the first template, and I can't see the point of a named template, so you can simplify the above XSLT to this...
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:fo="http://www.w3.org/1999/XSL/Format">
<xsl:output method="xml" indent="yes" />
<xsl:key name="taste" use="concat(generate-id(../../..), '|', .)" match="taste" />
<xsl:template match="fruits">
<newFruits>
<xsl:apply-templates select="fruit" />
</newFruits>
</xsl:template>
<xsl:template match="fruit">
<xsl:variable name="id" select="generate-id()" />
<newFruit>
<!-- xsl:for-each select="fruit/banana/taste[not(.=preceding::taste)]/.." /> -->
<xsl:for-each select="fruit/banana/taste[generate-id() = generate-id(key('taste', concat($id, '|', .))[1])]">
<fruit>
<xsl:value-of select="."/>
</fruit>
</xsl:for-each>
</newFruit>
</xsl:template>
</xsl:stylesheet>
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.
I am new to xslt programming and xlm. I have created the code below this works fine, except that instead variable names for each column, it just shows "colno" How do I get the column names into the output?
<?xml version="1.0" encoding="utf-8"?>
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:fmp="http://www.filemaker.com/fmpxmlresult"
exclude-result-prefixes="fmp"
>
<xsl:output method="xml" version="1.0" encoding="utf-8" indent="yes"/>
<xsl:variable name="kMetaData" select="fmp:METADATA/fmp:FIELD"/>
<xsl:variable name="colno"
select="count($kMetaData[following-sibling::fmp:FIELD/#NAME]) + 1" />
<xsl:template match="/fmp:FMPXMLRESULT">
<PERSON>
<xsl:apply-templates select="fmp:RESULTSET/fmp:ROW" />
</PERSON>
</xsl:template>
<xsl:template match="fmp:ROW">
<ELEMENTS>
<xsl:apply-templates select="fmp:COL" />
</ELEMENTS>
</xsl:template>
<xsl:template match="fmp:COL">
<xsl:element name="colno">
<xsl:value-of select="fmp:DATA" />
</xsl:element>
</xsl:template>
</xsl:stylesheet>
It is hard to make some suggestion without input xml. But at first sight this <xsl:element name="colno"> says "output an element <colno>". I think you should use something like <xsl:element name="{xpath/to/columnName}">
edit:
According to your input xml your template for "COL" element should look like
<xsl:template match="COL">
<xsl:variable name="colPosition" select="position()" />
<!-- Prevent spaces in NAME attribute of FIELD element -->
<xsl:variable name="colName" select="translate($kMetaData[$colPosition]/#NAME, ' ', '_')" />
<xsl:element name="{$colName}">
<xsl:value-of select="DATA"/>
</xsl:element>
</xsl:template>
Then the output looks like
<?xml version="1.0" encoding="utf-8"?>
<PERSON>
<ELEMENTS>
<FIRSTNAME>Richard</FIRSTNAME>
<LASTNAME>Katz</LASTNAME>
<MIDDLENAME>David</MIDDLENAME>
<REQUESTDT>1/1/2001</REQUESTDT>
<salutation>Mr</salutation>
<Bargaining_Unit>CSEA (02,03,04)</Bargaining_Unit>
<Field_134>b</Field_134>
</ELEMENTS>
</PERSON>
I have the following template:
<xsl:template match="footnote">
<xsl:variable name = "string">
<xsl:value-of select="."/>
</xsl:variable>
<xsl:variable name = "bool">
<xsl:if test="$string = preceding-sibling::node()/$string">
<xsl:text>false</xsl:test>
</xsl:if>
</xsl:variable>
<xsl:if test="$bool != 'false'">
<!-- DO STUFF -->
</xsl:if>
</xsl:template>
I'm trying to check the $string variable of the current node and check it against all previous footnote nodes to see if they have the same $string variable. If it doesn't match up with any of the preceding siblings then it should do stuff, otherwise it should do nothing.
With the code I have, the test "$string = preceding-sibling::node()/$string" always evaluates to true, even when no footnote nodes have been created yet.
Can anyone help me out? I'm having a hard time creating the expression to compare against the variable in all of the previous siblings.
EDIT: Sample XML:
<xml>
<footnote>Footnote 1</footnote>
<footnote>Footnote 2</footnote>
<footnote>Footnote 1</footnote>
<footnote>Footnote 1</footnote>
<footnore>Footnote 3</footenote>
</xml>
I'm trying to transform that into:
<xml>
<footnote>Footnote 1</footnote>
<footnote>Footnote 2</footnote>
<footnote>Footnore 3</footnore>
</xml
Well. posting Your output XML made it more clear! there you go with solution:
Sample XML:
<?xml version="1.0" encoding="utf-8"?>
<root>
<footnote>Matching</footnote>
<footnote>Example1</footnote>
<footnote>Matching</footnote>
<footnote>Example2</footnote>
<footnote>Example3</footnote>
</root>
And XSL:
<?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" indent="yes"/>
<xsl:template match="#* | node()">
<xsl:copy>
<xsl:apply-templates select="#* | node()"/>
</xsl:copy>
</xsl:template>
<xsl:template match="footnote[.=preceding-sibling::footnote/.]"/>
</xsl:stylesheet>
Result:
<?xml version="1.0" encoding="utf-8"?>
<xml>
<footnote>Matching</footnote>
<footnote>Example1</footnote>
<footnote>Example2</footnote>
<footnote>Example3</footnote>
</xml>
Assuming you want to check the value of the current footnote text() against previous footnote text()'s for uniqueness, then you can just check the preceding-sibling::node()'s text():
<xsl:if test="$string = preceding-sibling::node()/text()">
e.g.
<?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="yes" method="xml" version="1.0" indent="yes" />
<xsl:template match="/xml">
<xml>
<xsl:apply-templates select="footnote" />
</xml>
</xsl:template>
<xsl:template match="footnote">
<xsl:variable name = "string">
<xsl:value-of select="./text()"/>
</xsl:variable>
<xsl:variable name = "bool">
<xsl:if test="$string = preceding-sibling::node()/text()">
<xsl:text>false</xsl:text>
</xsl:if>
</xsl:variable>
<xsl:if test="$bool != 'false'">
<xsl:copy-of select="."></xsl:copy-of>
</xsl:if>
</xsl:template>
</xsl:stylesheet>
Will turn the input:
<xml>
<footnote>
someVal
</footnote>
<footnote>
anotherVal
</footnote>
<footnote>
anotherVal
</footnote>
<footnote>
newVal
</footnote>
</xml>
Into:
<xml>
<footnote>
someVal
</footnote>
<footnote>
anotherVal
</footnote>
<footnote>
newVal
</footnote>
</xml>
You should also look at Muenchian grouping if your intention is to identify groups within elements.
You could use
<xsl:template match="footnote[not(. = preceding-sibling::footnote)]">
to create a template that only fires for the first instance of a given footnote string. The . = preceding-sibling::footnote is true if the string value of this node is the same as that of any of its preceding siblings, so not(....) will be true if it is different from all of them.
But for large documents this will be rather inefficient (quadratic in the number of footnote elements). Better would be to use <xsl:for-each-group> (XSLT 2.0) or Muenchian Grouping (XSLT 1.0) to group the footnote elements by their string value and extract the first element from each group.
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:key name="fn" match="footnote" use="." />
<!-- Ignore all but the first footnote for a given value -->
<xsl:template match="footnote[generate-id() != generate-id(key('fn', .)[1])]" />
<!-- copy everything else verbatim -->
<xsl:template match="#* | node()">
<xsl:copy>
<xsl:apply-templates select="#* | node()"/>
</xsl:copy>
</xsl:template>
</xsl:stylesheet>
<xsl:template match="footnote">
<xsl:variable name = "string">
<xsl:value-of select="./text()"/>
</xsl:variable>
<xsl:variable name = "bool">
<xsl:if test="$string != preceding-sibling::node()/text()">
<xsl:text>false</xsl:text>
</xsl:if>
</xsl:variable>
<xsl:if test="$bool != 'false'">
true
</xsl:if>
<xsl:if test="$bool = 'false'">
false
</xsl:if>
</xsl:template>
In XSLT 2.0 you have a distinct-values function:
<xsl:for-each select="distinct-values(footnote)">
<footnote><xsl:value-of select="."/></footnote>
</xsl:for-each>
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>