how to transform xpath expression to xsl key? - xslt

I'm stuck with what should be a simple problem with XSLT keys.
If it is relevant, I'm forced into an XSLT 1.0 parser.
Sample XML:
<updates>
<update>
<id>first</id>
<pkglist>
<collection arch='i686'>
<package name='bin' version='1.0'/>
</collection>
<collection arch='x86_64'>
<package name='bin' version='1.0'/>
</collection>
</pkglist>
</update>
<update>
<id>second</id>
<pkglist>
<collection arch='i686'>
<package name='bin' version='1.1'/>
<package name='conf' version='1.1'/>
</collection>
<collection arch='x86_64'>
<package name='bin' version='1.2'/>
<package name='conf' version='1.1'/>
</collection>
</pkglist>
</update>
<update>
<id>third</id>
<pkglist>
<collection arch='i686'>
<package name='bin' version='1.3'/>
<package name='conf' version='1.1'/>
<package name='src' version='1.3'/>
</collection>
<collection arch='x86_64'>
<package name='bin' version='1.3'/>
<package name='conf' version='1.2'/>
<package name='src' version='1.3'/>
</collection>
</pkglist>
</update>
</updates>
This XPATH selects what I'm looking for
/updates//update[pkglist/collection/package/#name = 'bin']/id/text()
I'm looking for all 'id' values in the whole document that have packages with an attribute of 'name'. But in the real world I have way more packages than would be sensible to list out by hand.
So I figured a key would be the way to go
<xsl:key name="idByPackage" match="/updates//update/pkglist/collection/package/#name" use="../../../../id" />
But that doesn't give me back anything useful
<xsl:key name="idByPackage" match="/updates//update/pkglist/collection/package/#name" use="../../../../id" />
<xsl:template match="/">
<xsl:apply-templates select="updates/update" />
</xsl:template>
<xsl:template match="update">
Updates:
<xsl:value-of select="id" />
Related updates
<xsl:apply-templates select="pkglist" />
</xsl:template>
<xsl:template match="pkglist">
<xsl:for-each select="key('idByPackage', collection/package/#name)">
<xsl:value-of select="." />
<xsl:value-of select="collection/package/#name" />
</xsl:for-each>
</xsl:template>
I know I'm in the right area as when I change the key to this:
<xsl:key name="idByPackage" match="/updates//update/pkglist/collection/package/#name" use="../../../../pkglist/collection/package/#name" />
The same xsl template spits pack my package names.
When I run the template with the 'looks valid to me but does not work' key I get this:
Updates:
first
Related updates
Updates:
second
Related updates
Updates: third
Related updates
When I expect to get something like
Updates:
first
Related updates
second
third
Updates:
second
Related updates
first
third
Updates: third
Related updates
first
second
What am I doing wrong?

This is the key you need:
<xsl:key name="idByPackage"
match="update/id" use="../pkglist/collection/package/#name" />
When this XSLT is run on your sample input:
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="text" indent="yes" omit-xml-declaration="yes"/>
<xsl:key name="idByPackage"
match="update/id" use="../pkglist/collection/package/#name" />
<xsl:variable name="nl" select="'
'" />
<xsl:template match="/">
<xsl:apply-templates select="updates/update" />
</xsl:template>
<xsl:template match="update">
<xsl:value-of select="concat('Updates:', $nl,
id, $nl,
'Related updates:', $nl)" />
<xsl:variable name="name" select="pkglist/collection/package/#name" />
<xsl:apply-templates select="key('idByPackage', $name)[. != current()/id]" />
</xsl:template>
<xsl:template match="id">
<xsl:value-of select="concat(., $nl)" />
</xsl:template>
</xsl:stylesheet>
The result is:
Updates:
first
Related updates:
second
third
Updates:
second
Related updates:
first
third
Updates:
third
Related updates:
first
second

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.

How to add Closing tags in XSLT

I need your assistance with the logic to add the end tags. The structure i am looking at is . I tried with for-each or xsl:if or xsl:choose. The input XML is as below
<SuperShipNotice>
<Package packageType="P" packageLevel="1">
<PackageNumber>PWN34332</PackageNumber>
<ShipmentNumber>105909390</ShipmentNumber>
<ShipmentLineNumber>1</ShipmentLineNumber>
<PartNumber>1CH162-510</PartNumber>
<Quantity>1000</Quantity>
<SSCCNumber>00176364909402100165</SSCCNumber>
</Package>
<Package packageType="C" packageLevel="2">
<PackageNumber>CWX612432660</PackageNumber>
<ParentPackageNumber>PWN34332</ParentPackageNumber>
<ShipmentNumber>105909390</ShipmentNumber>
<ShipmentLineNumber>1</ShipmentLineNumber>
<PartNumber>1CH162-510</PartNumber>
<Quantity>25</Quantity>
<SSCCNumber>00176364909402100165</SSCCNumber>
</Package>
<Package packageType="S" packageLevel="3">
<PackageNumber>W1D2WNGL</PackageNumber>
<ParentPackageNumber>CWX612432660</ParentPackageNumber>
<ShipmentNumber>105909390</ShipmentNumber>
<ShipmentLineNumber>1</ShipmentLineNumber>
<PartNumber>1CH162-510</PartNumber>
<Quantity>1</Quantity>
<DateOfMfg>20131209</DateOfMfg>
<COO>CN</COO>
<SSCCNumber>00176364909402100165</SSCCNumber>
</Package>
<Package packageType="S" packageLevel="3">
<PackageNumber>W1D2WNGL</PackageNumber>
<ParentPackageNumber>CWX612432660</ParentPackageNumber>
<ShipmentNumber>105909390</ShipmentNumber>
<ShipmentLineNumber>1</ShipmentLineNumber>
<PartNumber>1CH162-510</PartNumber>
<Quantity>1</Quantity>
<DateOfMfg>20131209</DateOfMfg>
<COO>CN</COO>
<SSCCNumber>00176364909402100165</SSCCNumber>
</Package>
</SuperShipNotice>
Not sure if this can be of any use for you - following XSLT
<?xml version="1.0" encoding="UTF-8"?>
<xsl:transform xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">
<xsl:output method="xml" omit-xml-declaration="yes" encoding="UTF-8" indent="yes" />
<xsl:strip-space elements="*" />
<xsl:template match="SuperShipNotice">
<xsl:apply-templates select="//PackageNumber[parent::Package[#packageLevel='1']]" />
</xsl:template>
<xsl:template match="PackageNumber[parent::Package[#packageLevel='1']]">
<xsl:variable name="packageNumber" select="." />
<PkgLevel1>
<xsl:copy>
<xsl:apply-templates />
</xsl:copy>
<xsl:apply-templates select="//PackageNumber[parent::Package[#packageLevel='2'] and parent::Package/ParentPackageNumber = $packageNumber]" />
</PkgLevel1>
</xsl:template>
<xsl:template match="PackageNumber[parent::Package[#packageLevel='2']]">
<xsl:variable name="packageNumber" select="." />
<PkgLevel2>
<xsl:copy>
<xsl:apply-templates />
</xsl:copy>
<xsl:apply-templates select="//PackageNumber[parent::Package[#packageLevel='3'] and parent::Package/ParentPackageNumber = $packageNumber
and not(parent::Package/ParentPackageNumber = preceding::Package[#packageLevel='3']/ParentPackageNumber)]" />
</PkgLevel2>
</xsl:template>
<xsl:template match="PackageNumber[parent::Package[#packageLevel='3']]">
<PkgLevel3>
<xsl:copy>
<xsl:apply-templates />
</xsl:copy>
</PkgLevel3>
</xsl:template>
</xsl:transform>
when applied to your input XML produces the output
<PkgLevel1>
<PackageNumber>PWN34332</PackageNumber>
<PkgLevel2>
<PackageNumber>CWX612432660</PackageNumber>
<PkgLevel3>
<PackageNumber>W1D2WNGL</PackageNumber>
</PkgLevel3>
</PkgLevel2>
</PkgLevel1>
The first template matching SuperShipNotice applies templates to PackageNumbers of Packages with the packageLevel value 1.
In the template matching those Packagenumbers templates are applied to all PackageNumbers with the packageLevel value 2 and the ParentPackageNumber of the current PackageNumber.
As there is a double entry for Packages with the packageLevel value 3, the second Package with the same ParentPackageNumber is omitted:
select="//PackageNumber[parent::Package[#packageLevel='3']
and parent::Package/ParentPackageNumber = $packageNumber
and not(parent::Package/ParentPackageNumber = preceding::Package[#packageLevel='3']/ParentPackageNumber)]"
In case you can adjust this to fit further requirements, you can use the saved Demo

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

XPath - Selecting nodes with one similar and one different attribute

So I'm having trouble understanding why one XPath expression gets the nodes I want, while the other doesn't.
First, the xml:
<doc>
<source id="225" clientID="567" matterID="225" level="2" />
<source id="226" clientID="993" matterID="226" level="2" />
<dest id="185" level="7" />
<dest id="226" level="7" />
</doc>
The keys in my xsl template are defined as follows:
<xsl:key name="sourceId" match="//source" use="#id" />
<xsl:key name="destId" match="//dest" use="#id" />
<xsl:key name="destLevel" match="//dest" use="#level" />
What I'm looking for are the source nodes, that match dest nodes on id, but have a different level attribute. The apply template that I figured would work in my head is the following:
<xsl:apply-templates select="source[key('destId', #id) and not(key('destLevel', #level))]" mode="update" />
But that doesn't seem to work. A colleague suggested putting a not around an expression that matches the nodes I don't want, and after a lot of trial and error, I thought this might work, to no effect:
<xsl:apply-templates select="source[not(not(key('destId', #id)) or not(key('destLevel', #level)))]" mode="update" />
Can anyone please walk me through what I need in order to solve this?
Edit: I previously thought I'd solved this with the second query, but it seems I was mistaken.
====Solution====
Dimitre Novatchev has a detailed breakdown of different ways to solve this, but my ultimate solution was actually slightly different than his.
In essence, I created a virtual key with the concat() function that combined the two attributes. That way, I could find nodes that matched the id, but not the id-level combo.
Extra key:
<xsl:key name="destByIdAndLevel" match="//dest" use="concat(#id,'+',#level)" />
Changed apply-template call:
<xsl:apply-templates select="source[key('destId', #id) and not(key('destByIdAndLevel',concat(#id,'+',#level)))]" mode="update" />
I. This transformation:
<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="source">
<xsl:copy-of select=
"self::*[../dest[#id = current()/#id and not(#level=current()/#level)]]"/>
</xsl:template>
</xsl:stylesheet>
when applied on this XML document (an additional source element added to the provided XML document -- to verify more cases):
<doc>
<source id="185" clientID="567" matterID="225" level="7" />
<source id="225" clientID="567" matterID="225" level="2" />
<source id="226" clientID="993" matterID="226" level="2" />
<dest id="185" level="7" />
<dest id="226" level="7" />
</doc>
produces the wanted, correct result:
<source id="226" clientID="993" matterID="226" level="2"/>
II. Solution using one key:
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output omit-xml-declaration="yes" indent="yes"/>
<xsl:key name="kDestById" match="dest" use="#id"/>
<xsl:template match="/*">
<xsl:copy-of select=
"source[key('kDestById',#id)
and
not(#level=key('kDestById',#id)/#level)]"/>
</xsl:template>
</xsl:stylesheet>
When this transformation is applied on the same XML document (above), again the same wanted, correct result is produced:
<source id="226" clientID="993" matterID="226" level="2"/>
III. Solution with two keys:
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output omit-xml-declaration="yes" indent="yes"/>
<xsl:key name="kDestById" match="dest" use="#id"/>
<xsl:key name="kDestByLevel" match="dest" use="#level"/>
<xsl:template match="source">
<xsl:copy-of select=
"self::*
[key('kDestById',#id)
and
key('kDestById',#id)
[not(count(.|key('kDestByLevel',current()/#level))
=
count(key('kDestByLevel',current()/#level))
)
]
]"/>
</xsl:template>
</xsl:stylesheet>
This again produces the wanted, correct result:
<source id="226" clientID="993" matterID="226" level="2"/>
source[key('destId', #id) and not(key('destLevel', #level))]
This gives source nodes which have an id the same as some dest nodes, and also have a level which is not the same as any dest nodes. Note that it doesn't require a particular dest node to have the same id but different level.
I think you are looking for something like this:
source[set:difference(key('destId', #id), key('destLevel', #level))]
set:difference() is from EXSLT.