XSLT getting grandchild node attribute values in template - xslt

Given XML like this:
<?xml version="1.0" encoding="UTF-8"?>
<Products>
<Product someId="1EFAD9659EC">
<Identifiers>
<Identifier Id="234532423" Name="globalTradeItemNumber (GTIN)" Value="00671657621322" />
<Identifier Id="99845898" Name="Internal Supplier Part #" Value="DEL 20 10B015000" />
<Identifier Id="49348598" Name="MFG Model # (Series)" Value="DEL 20 10B015000" />
<Identifier Id="439854985" Name="MFG Part # (OEM)" Value="DEL 20 10B015000" />
<Identifier Id="2349832489" Name="UPC" Value="671657621322" />
</Identifiers>
</Product>
<Product someId="1EFAD9659EC">
<Identifiers>
<Identifier Id="234532423" Name="globalTradeItemNumber (GTIN)" Value="51651518" />
<Identifier Id="99845898" Name="Internal Supplier Part #" Value="TIM 20 10B015000" />
<Identifier Id="49348598" Name="MFG Model # (Series)" Value="TOM 20 10B015000" />
<Identifier Id="439854985" Name="MFG Part # (OEM)" Value="TAK 20 10B015000" />
<Identifier Id="2349832489" Name="UPC" Value="87468387468" />
</Identifiers>
</Product>
. . .
I want to end up with something like
...
<Product upc="671657621322"/>
<Product upc="87468387468"/>
...
But what I'm getting is
...
<Product upc="true"/>
<Product upc="true"/>
...
I keep getting the boolean answer to my select rather than the value of the attribute. What silly thing am I doing wrong here? This is the XSLT I'm trying:
...
<xsl:template match="/">
<Output>
<xsl:apply-templates />
</Output>
</xsl:template>
<xsl:template match="Product">
<xsl:variable name="productCode" select="./Identifiers/Identifier/#Name='UPC'"/>
<Product upc="{$productCode}">
</Product>
</xsl:template>
...
Thanks.

You are using the wrong xpath selection. Use:
select="Identifiers/Identifier[#Name='UPC']/#Value"

If you are interested only with those two node values match them with the template:
<xsl:template match="Product/Identifiers/Identifier[#Name='UPC']">
<xsl:variable name="productCode" select="#Value"/>
<Product upc="{$productCode}">
</Product>
</xsl:template>

Related

XSLT Template Matching from xml

I have the following xml code that I need to transform into a text file. I'm struggling massively with the namespaces in XSLT. I'm used to doing Export/Record and simple default namespace matches. I have the following XML code:
<?xml version="1.0" encoding="utf-8"?>
<ArrayOfPerson xmlns:i="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://schemas.datacontract.org/2004/07/MultiCountryIntegrationService.Core.Entities.Dto">
<Person>
<Addresses>
<Address>
<City>London</City>
<Country>GB</Country>
<County>London</County>
<CreatedDate xmlns:d5p1="http://schemas.datacontract.org/2004/07/System">
<d5p1:DateTime>2017-02-21T11:05:08.8387752Z</d5p1:DateTime>
<d5p1:OffsetMinutes>0</d5p1:OffsetMinutes>
</CreatedDate>
<DeletedDate xmlns:d5p1="http://schemas.datacontract.org/2004/07/System" i:nil="true" />
<EndDate xmlns:d5p1="http://schemas.datacontract.org/2004/07/System" i:nil="true" />
<Extension i:nil="true" />
<Id>8e5b30d0</Id>
<ModifiedDate xmlns:d5p1="http://schemas.datacontract.org/2004/07/System" i:nil="true" />
<Number>8</Number>
<StartDate xmlns:d5p1="http://schemas.datacontract.org/2004/07/System">
<d5p1:DateTime>2016-06-30T22:00:00Z</d5p1:DateTime>
<d5p1:OffsetMinutes>120</d5p1:OffsetMinutes>
</StartDate>
<Street>Somewhere</Street>
<Type>Primary</Type>
<ZipCode>L1 1LL</ZipCode>
</Address>
</Addresses>
<PersonLocalReferences xmlns:d3p1="http://schemas.microsoft.com/2003/10/Serialization/Arrays" />
</Person>
<Person>
<Addresses>
<Address>
<City>Birmingham</City>
<Country>ETC...</Country>
</Address>
</Addresses>
<PersonLocalReferences xmlns:d3p1="http://schemas.microsoft.com/2003/10/Serialization/Arrays" />
</Person>
</ArrayOfPerson>
My XSLT - As you can see I've tried several approaches, done countless hours on google and stackoverflow. If I remove the i: and xmlns from the XML I can get the stylesheet to work, but I'm not in a position to change the XML. I'm using Visual Studio 2016 to create and run the xslt. If I use "#* | node()" I get everything, and I only want to output certain bits of information into a text file.
If I run the xslt below, I just get the header and EOF, so it looks like the <xsl:apply-templates select="ArrayOfPerson/Person"/> isn't selecting the right level of data.
<?xml version="1.0" encoding="utf-8"?>
<xsl:stylesheet version="2.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:i="http://www.w3.org/2001/XMLSchema-instance" exclude-result-prefixes="i">
<xsl:output omit-xml-declaration="yes" />
<xsl:template match="/">
<xsl:call-template name="header" />
<xsl:text>Person</xsl:text>
<xsl:apply-templates select="ArrayOfPerson/Person"/>
<xsl:value-of select="$newline" />
<xsl:text>EOF</xsl:text>
</xsl:template>
<xsl:template match="Person">
<xsl:value-of select="ArrayOfPerson/Person/Addresses/Address/City"/>
<xsl:value-of select="Person/Addresses/Address/City"/>
<xsl:value-of select="Addresses/Address/City"/>
<xsl:value-of select="."/>
<xsl:value-of select="$newline" />
<xsl:text>match</xsl:text>
<xsl:value-of select="$newline" />
</xsl:template>
</xsl:stylesheet>
As Michael wrote in his comment, add xpath-default-namespace="http://schemas.datacontract.org/2004/07/MultiCountryIntegrationService.Core.Entities.Dto"
to your xsl:stylesheet tag.
Note that you must include full namespace.

complex variables in xslt template v1, v2

I have source xml looking like this :
<Data>
<ActionPlaces>
<ActionPlace>
<ActionPlaceID>74</ActionPlaceID>
<PlaceName>Theatre Of Classic</PlaceName>
</ActionPlace>
</ActionPlaces>
<Actions>
<CommonAction Id="2075" Name="King">
<Action>
<ActionID>4706</ActionID>
<ActionPlaceID>74</ActionPlaceID>
</Action>
</CommonAction>
</Actions>
</Data>
Which is to transform to this:
<category name="King">
<name>King</name>
<parent name="Theatre Of Classic" />
</category>
I want to use variable :
<xsl:template match="ActionPlaces">
<xsl:variable name="id" select="/ActionPlace/ActionPlaceID"/>
<xsl:template match="CommonAction" >
<category name="<xsl:value-of select="#name"/> >
<name><xsl:value-of select="#name"/></name>
<parent <xsl:if test="/Action/ActionPlaceID = $id">
name=/Action/ActionPlaceID/> <- how to get name of theatre here?
</xsl:template>
Can variable store not only id but name also? And how to get it? What is the most common approach to handle this ?
Here's one option using XSL keys (as #michael-hor257k suggested):
Input
<Root>
<ActionPlaces>
<ActionPlace>
<ActionPlaceID>74</ActionPlaceID>
<PlaceName>Theatre Of Classic</PlaceName>
</ActionPlace>
</ActionPlaces>
<Actions>
<CommonAction Id="2075" Name="King">
<Action>
<ActionID>4706</ActionID>
<ActionPlaceID>74</ActionPlaceID>
</Action>
</CommonAction>
</Actions>
</Root>
Stylesheet
<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"/>
<!-- Collect all <ActionPlace> elements into an XSL key -->
<xsl:key name="ActionPlaceById" match="ActionPlace" use="ActionPlaceID"/>
<xsl:template match="/">
<xsl:apply-templates select="Root/Actions/CommonAction"/>
</xsl:template>
<xsl:template match="CommonAction">
<category name="{#Name}">
<name>
<xsl:value-of select="#Name"/>
</name>
<!--
Using the ActionPlaceById key we created earlier, fetch the <ActionPlace>
element that has an <ActionPlaceID> child that has the same value as the
<ActionPlaceID> descendant of the current <CommonAction> element.
-->
<parent name="{key('ActionPlaceById', Action/ActionPlaceID)/PlaceName}"/>
</category>
</xsl:template>
</xsl:stylesheet>
Output
<?xml version="1.0" encoding="utf-8"?>
<category name="King">
<name>King</name>
<parent name="Theatre Of Classic"/>
</category>

Removing duplicates from a bag returned by xsl:key based on attribute value

I have the following xml file.
<Bank>
<Person personId="1" type="1071" deleted="0">
</Person>
<Person personId="2" type="1071" deleted="0">
</Person>
<Person personId="3" type="1071" deleted="0">
</Person>
<Account>
<Role personId="1" type="1025" />
</Account>
<Account>
<Role personId="1" type="1025" />
</Account>
<Account>
<Role personId="1" type="1018" />
</Account>
<Account>
<Role personId="3" type="1025" />
<Role personId="1" type="1018" />
</Account>
<Account>
<Role personId="2" type="1025" />
</Account>
</Bank>
and the following xsl transformation.
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="text" encoding="ISO-8859-1" />
<xsl:strip-space elements="*" />
<xsl:key name="roleKey"
match="Role[(#type = '1025' or #type = '1018' or #type = '1022' or #type = '1023') and not(#validTo)]"
use="#personId" />
<xsl:template match="Person">
<xsl:value-of select="#personId" />
<xsl:variable name="roles" select="key('roleKey', #personId)" />
<xsl:for-each select="$roles">
<xsl:text>;</xsl:text><xsl:value-of select="#type" />
</xsl:for-each>
<xsl:text>
</xsl:text>
</xsl:template>
</xsl:stylesheet>
The actual result is following and I would like to remove the duplicated type values.
1;1025;1025;1018;1018
2;1025
3;1025
The expected results should be like follows...
1;1025;1018
2;1025
3;1025
I have tried the tips involving keyword following from this website and also the trick with the Muenchian method. They all do not work as they seem to be browsing through the whole document and matching the duplicates for each and every Person element, whereas I want to remove duplicates only in a Person context defined by personId attribute.
How do I remove those duplicates from the bag returned by key function? Or maybe there is a method that I can use in xsl:for-each to print only what I want to?
I can only use what there is available in XSLT 1.0. I do not have a possibility to use any XSLT 2.0 processor.
Ok, do not know if this is the best solution available but I achieved what I wanted by introducing such a key and using Muenchian method.
<xsl:key name="typeKey" match="Role" use="concat(#type, '|', #personId)" />
The whole transformation looks like that after the change...
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="text" encoding="ISO-8859-1" />
<xsl:strip-space elements="*" />
<xsl:key name="roleKey"
match="Role[(#type = '1025' or #type = '1018' or #type = '1022' or #type = '1023') and not(#validTo)]"
use="#personId" />
<xsl:key name="typeKey" match="Role" use="concat(#type, '|', #personId)" />
<xsl:template match="Person">
<xsl:value-of select="#personId" />
<xsl:variable name="roles" select="key('roleKey', #personId)" />
<xsl:for-each select="$roles[generate-id() = generate-id(key('typeKey', concat(#type, '|', #personId)))]">
<xsl:text>;</xsl:text><xsl:value-of select="#type" />
</xsl:for-each>
<xsl:text>
</xsl:text>
</xsl:template>
</xsl:stylesheet>
And the actual result is now...
1;1025;1018
2;1025
3;1025

Namespace issue in xslt

I am trying to convert following xml into other xml but I am not getting the values for xCoordinate and yCoordinate. I would like to convert the structure from source - XML to Target-XML where the goocodes will match and the result would be assigned to x and y.
Source - XML
<?xml version="1.0"?>
<AddressResponse xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" errorCode="0" errorDescription="">
<wrappedResultList xmlns="http://xlocate.xserver.ptvag.com">
<ResultAddress city="Amsterdam" city2="" country="NL" houseNumber="" postCode="1***" state="Noord-Holland" street="" adminRegion="Amsterdam" appendix="" classificationDescription="EXACT" countryCapital="Amsterdam" detailLevelDescription="CITY" totalScore="100">
<wrappedAdditionalFields />
<coordinates>
<kml xsi:nil="true" xmlns="http://common.xserver.ptvag.com" />
<point x="4.89327999999999" y="52.373090000000005" xmlns="http://common.xserver.ptvag.com" />
</coordinates>
</ResultAddress>
<ResultAddress city="Amsterdam-Zuidoost" city2="" country="NL" houseNumber="" postCode="110*" state="Noord-Holland" street="" adminRegion="Amsterdam" appendix="" classificationDescription="EXACT" countryCapital="Amsterdam" detailLevelDescription="CITY" totalScore="80">
<wrappedAdditionalFields />
<coordinates>
<kml xsi:nil="true" xmlns="http://common.xserver.ptvag.com" />
<point x="4.9513699999999838" y="52.316199999999988" xmlns="http://common.xserver.ptvag.com" />
</coordinates>
</ResultAddress>
<ResultAddress city="Nieuw-Amsterdam" city2="" country="NL" houseNumber="" postCode="7833" state="Drenthe" street="" adminRegion="Emmen" appendix="" classificationDescription="EXACT" countryCapital="Amsterdam" detailLevelDescription="CITY" totalScore="80">
<wrappedAdditionalFields />
<coordinates>
<kml xsi:nil="true" xmlns="http://common.xserver.ptvag.com" />
<point x="6.8528699999999994" y="52.716139999999982" xmlns="http://common.xserver.ptvag.com" />
</coordinates>
</ResultAddress>
</wrappedResultList>
</AddressResponse>
Target - XML
<GeoCodeResponse>
<geocordinate>
<xCordinate>4.89327999999999</xCordinate>
<yCordinate>52.716139999999982</yCordinate>
</geocordinate>
</GeoCodeResponse>
XSLT
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:xl="http://xlocate.xserver.ptvag.com" xmlns:xsi="http://www.w3.org/2001/XMLSchema" xmlns:xsd="http://www.w3.org/2001/XMLSchema-instance" xmlns:cm="http://common.xserver.ptvag.com" exclude-result-prefixes="xl xsi xsd cm" version="1.0">
<xsl:output method="xml" indent="yes"/>
<xsl:template match="/">
<GeoCodeResponse xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<geocordinate xmlns:i="http://www.w3.org/2001/XMLSchema-instance">
<xsl:for-each select="AddressResponse/xl:wrappedResultList/xl:ResultAddress">
<xsl:sort select="#xl:totalScore" order="descending" data-type="number"/>
<xsl:if test="position()= 1">
<xCordinate> <xsl:value-of select="/xl:coordinates/cm:point/cm:x" /></xCordinate>
<yCordinate> <xsl:value-of select="/xl:coordinates/cm:point/cm:y" /></yCordinate>
</xsl:if>
</xsl:for-each>
</geocordinate>
</GeoCodeResponse>
</xsl:template>
</xsl:stylesheet>
Please help what could be done in above xslt.
You were almost there. The coordinates are attributes, not nodes.
change it into this:
<xsl:if test="position()= 1">
<xCordinate>
<xsl:value-of select="xl:coordinates/cm:point/#x" />
</xCordinate>
<yCordinate>
<xsl:value-of select="xl:coordinates/cm:point/#y" />
</yCordinate>
</xsl:if>
You are also referencing the coordinates node from the root, but it should be relative. I changed /xl:coordinaties into xl:coordinates

XSLT Select Only Nodes With Text in a Loop

I would like to select only nodes containing text nodes. But when I test for text() parent nodes that contain child nodes with text get returned also. I tried to check by
string-length() but the parent nodes are returning a string length greater than 0.
In this scenario I would like to select only
SendDate
FirstName
LastName
Company
Street
City
State
Zip
Thanks
XML
<BusinessLetter>
<Head>
<SendDate>November 29, 2005</SendDate>
<Recipient>
<Name Title="Mr.">
<FirstName>Joshua</FirstName>
<LastName>Lockwood</LastName>
</Name>
<Company>Lockwood & Lockwood</Company>
<Address>
<Street>291 Broadway Ave.</Street>
<City>New York</City>
<State>NY</State>
<Zip>10007</Zip>
<Country>United States</Country>
</Address>
</Recipient>
</Head>
</BusinessLetter>
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="html" indent="yes"/>
<xsl:template match="/">
<xsl:for-each select="//*[text()]">
<xsl:if test="text()">
<Match>
<xsl:value-of select="name()"/>:
<xsl:value-of select="string-length(text())"/>
</Match>
<br />
</xsl:if>
</xsl:for-each>
</xsl:template>
</xsl:stylesheet>
OUTPUT
BusinessLetter: 2
Head: 3
SendDate: 17
Recipient: 4
Name: 5
FirstName:6
LastName:8
Company:19
Address:5
Street:17
City:8
State:2
Zip:5
Country:13
You are experience a problem with white space only text nodes...
Solution preserving white space only text nodes (better when processing (X)HTML ):
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:template match="*[text()[normalize-space()]]">
<Match>
<xsl:value-of select="concat(name(),
' : ',
string-length(normalize-space()))"/>
</Match>
<br />
</xsl:template>
</xsl:stylesheet>
Output:
<Match>SendDate : 17</Match>
<br />
<Match>FirstName : 6</Match>
<br />
<Match>LastName : 8</Match>
<br />
<Match>Company : 19</Match>
<br />
<Match>Street : 17</Match>
<br />
<Match>City : 8</Match>
<br />
<Match>State : 2</Match>
<br />
<Match>Zip : 5</Match>
<br />
<Match>Country : 13</Match>
<br />
Good question. I spent a few days looking for an answer.
<xsl:value-of select="string-length(normalize-space(text()[1]))" />
This problem is caused by the fact that the white-space-only text nodes are also considered.
Solution:
Include this global (best place is before any <xsl:template>) directive:
<xsl:strip-space elements="*"/>
This instructs the XSLT processor to strip-off the white-space-only text-node children of any (*) element in the XML document.
So, your fixed transformation now is:
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="html" indent="yes"/>
<xsl:strip-space elements="*"/>
<xsl:template match="/">
<xsl:for-each select="//*[text()]">
<xsl:if test="text()">
<Match>
<xsl:value-of select="name()"/>:
<xsl:value-of select="string-length(text())"/>
</Match>
<br />
</xsl:if>
</xsl:for-each>
</xsl:template>
</xsl:stylesheet>
and when applied on the provided XML document:
<BusinessLetter>
<Head>
<SendDate>November 29, 2005</SendDate>
<Recipient>
<Name Title="Mr.">
<FirstName>Joshua</FirstName>
<LastName>Lockwood</LastName>
</Name>
<Company>Lockwood & Lockwood</Company>
<Address>
<Street>291 Broadway Ave.</Street>
<City>New York</City>
<State>NY</State>
<Zip>10007</Zip>
<Country>United States</Country>
</Address>
</Recipient>
</Head>
</BusinessLetter>
the wanted, correct result is produced:
<Match>SendDate:
17
</Match><br><Match>FirstName:
6
</Match><br><Match>LastName:
8
</Match><br><Match>Company:
19
</Match><br><Match>Street:
17
</Match><br><Match>City:
8
</Match><br><Match>State:
2
</Match><br><Match>Zip:
5
</Match><br><Match>Country:
13
</Match><br>