XPath - Selecting nodes with one similar and one different attribute - xslt

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.

Related

XSLT - Lookup value from other part of xml inside foreach

I have the following XML.
<?xml version="1.0" ?>
<Root xmlns="http://1.local/1.xsd">
<Definitions>
<FileTypes>
<FileType ID="1" Name="FileType1"/>
<FileType ID="2" Name="FileType2"/>
<!--... - lots of file types-->
<FileTypes>
</Definitions>
<Files>
<File Name="File1" FileTypeID="1" />
<File Name="File2" FileTypeID="1" />
<File Name="File3" FileTypeID="2" />
<!--... - lots of files-->
<Files>
</Root>
For each file, I need to get its Name attribute and for its FileTypeID lookup corresponding file type name
So example output would be:
File name: File1
File type: FileType1
File name: File2
File type: FileType1
File name: File3
File type: FileType2
This is XSLT I have so far but I'm not sure how to lookup name of file type.
<?xml version="1.0"?>
<xsl:stylesheet
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:l="http://1.local/1.xsd"
exclude-result-prefixes="l"
version="1.0">
<xsl:output method="text" omit-xml-declaration="yes" media-type="text/plain" />
<xsl:template match="/">
<xsl:apply-templates select="l:Root/l:Files" />
</xsl:template>
<xsl:template match="l:Root/l:Files">
Why
<xsl:for-each select="l:File">
File name: <xsl:value-of select="#Name">
File type:
</xsl:for-each>
</xsl:template>
</xsl:stylesheet>
Use a xsl:key here to look up the FileTypes
<xsl:key name="FileTypes" match="l:FileType" use="#ID" />
Then, to get the relevant FileType name, you would do this
<xsl:value-of select="key('FileTypes', #FileTypeID)/#Name" />
Try this XSLT
<xsl:stylesheet
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:l="http://1.local/1.xsd"
exclude-result-prefixes="l"
version="1.0">
<xsl:output method="text" omit-xml-declaration="yes" media-type="text/plain" />
<xsl:key name="FileTypes" match="l:FileType" use="#ID" />
<xsl:template match="/">
<xsl:apply-templates select="l:Root/l:Files" />
</xsl:template>
<xsl:template match="l:Files">
<xsl:for-each select="l:File">
File name: <xsl:value-of select="#Name" />
File type: <xsl:value-of select="key('FileTypes', #FileTypeID)/#Name" />
</xsl:for-each>
</xsl:template>
</xsl:stylesheet>
(Note your XML is not well-formed, as you do not have correct closing tags for FileTypes and Files.)
If you want to work with XSLT you need to start with understanding its expression language XPath to navigate XML trees, you can select //l:FileType[#ID = current()/#FileTypeID]/#Name. Or in XSLT, as Tim has already posted, you can use a key to efficiently implement the lookup.
You can try the following XSL.
<xsl:stylesheet
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:l="http://1.local/1.xsd"
exclude-result-prefixes="l"
version="1.0">
<xsl:output method="text" omit-xml-declaration="yes" media-type="text/plain" />
<xsl:template match="/">
<xsl:apply-templates select="l:Root/l:Files" />
</xsl:template>
<xsl:template match="l:Root/l:Files">
Why
<xsl:for-each select="l:File">
File name: <xsl:value-of select="#Name"/>
File type: <xsl:value-of select="//l:FileTypes/l:FileType[#ID=current()/#FileTypeID]/#Name"/>
</xsl:for-each>
</xsl:template>
</xsl:stylesheet>
Also, you need to make sure you put the correct namespace in your xsl transformation to actually match the values in you XML, and you were missing a few close tags in your XML.
For completeness I've included the fixed XML that I used for the solution
<Root xmlns="http://1.local/1.xsd">
<Definitions>
<FileTypes>
<FileType ID="1" Name="FileType1"/>
<FileType ID="2" Name="FileType2"/>
<!--... - lots of file types-->
</FileTypes>
</Definitions>
<Files>
<File Name="File1" FileTypeID="1" />
<File Name="File2" FileTypeID="1" />
<File Name="File3" FileTypeID="2" />
<!--... - lots of files-->
</Files>
</Root>

XSLT - Match and replace every XML attribute value with specific attribute value

I wanted to replace wildcards in a control XML-file from a third-party software.
Unfortunately these wildcards also used as attribute values in this XML-file.
I will give you an example:
<control>
<some-tag id="$wildcard1$" version="3.14">
<another-tag id="second_level">stackoverflow rocks!</another-tag>
</some-tag>
<some-tag id="foo" version="$wildcard2$"/>
<some-tag id="bar" version="145.31.1"/>
</control>
I tried to write a generic transformation with parameters to replace the wildcards in the attribute values.
My biggest problem was, that i don't know the attribute name. So i need to match every attribute in the XML file. That is easy but how i match every attribute with a specific value (e.g. $wildcard$) ?
The answer to this question was quite easier than I thought it would be.
<xsl:template match="#*[. = $wildcard]">
<xsl:attribute name="{name(.)}">
<xsl:value-of select="$wildcard_value"/>
</xsl:attribute>
</xsl:template>
I hope it helps someone.
P.S: Here is my full XSL-Transformation to replace wildcards in attributes values:
<?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:param name="wildcard" required="yes" />
<xsl:param name="wildcard_value" required="yes" />
<xsl:output method="xml" version="1.0" encoding="UTF-8"
indent="yes" />
<xsl:template match="#*|node()">
<xsl:copy>
<xsl:apply-templates select="#*|node()" />
</xsl:copy>
</xsl:template>
<xsl:template match="#*[. = $wildcard]">
<xsl:attribute name="{name(.)}">
<xsl:value-of select="$wildcard_value" />
</xsl:attribute>
</xsl:template>
</xsl:stylesheet>

XPATH for first element whose name is among the names of some other elements

<choices>
<sic />
<corr />
<reg />
<orig />
</choices>
<choice>
<corr>Red</corr>
<sic>Blue</sic>
<choice>
I want to select the first element in <choice> whose name matches the name of any element in <choices>.
If name(node-set) returned a list of names instead of only the name of the first node, I could use
select="choice/*[name() = name(choices/*)][1]"
But it doesn't (at least not in 1.0), so instead I join the names together in a string and use contains():
<xsl:variable name="choices.str">
<xsl:for-each select="choices/*">
<xsl:text> </xsl:text><xsl:value-of select="concat(name(),' ')"/>
</xsl:for-each>
</xsl:variable>
<xsl:apply-templates select="choice/*[contains($choices.str,name())][1]"/>
and get what I want:
Red, the value of <corr>
Is there a more straightforward way?
I. Use this XPath 2.0 one-liner:
/*/choice/*[name() = /*/choices/*/name()][1]
When this XPath expression is evaluated against the following XML document (the provided one, but corrected to become a well-formed XML document):
<t>
<choices>
<sic />
<corr />
<reg />
<orig />
</choices>
<choice>
<corr>Red</corr>
<sic>Blue</sic>
</choice>
</t>
the correct element is selected:
<corr>Red</corr>
II. XSLT 1.0 (no keys!):
<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:variable name="vNames">
<xsl:for-each select="/*/choices/*">
<xsl:value-of select="concat(' ', name(), ' ')"/>
</xsl:for-each>
</xsl:variable>
<xsl:template match="/">
<xsl:copy-of select=
"/*/choice/*
[contains($vNames, concat(' ', name(), ' '))]
[1]"/>
</xsl:template>
</xsl:stylesheet>
When this transformation is applied on the same XML document (above), again the correct element is selected (and copied to the output):
<corr>Red</corr>
III. Using keys:
<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:key name="kChoiceByName" match="choice/*"
use="boolean(/*/choices/*[name()=name(current())])"/>
<xsl:template match="/">
<xsl:copy-of select="/*/choice/*[key('kChoiceByName', true())][1]"/>
</xsl:template>
</xsl:stylesheet>
When this transformation is applied against the same XML document (above), the same correct result is produced:
<corr>Red</corr>
It is recommended to the reader to try to understand how this all "works" :)
You can use the key() function like this...
When this input document...
<t>
<choices>
<sic />
<corr />
<reg />
<orig />
</choices>
<choice>
<corr>Red</corr>
<sic>Blue</sic>
</choice>
</t>
...is supplied as input to this XSLT 1.0 style-sheet...
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="text"/>
<xsl:key name="kChoices" match="choices/*" use="name()" />
<xsl:template match="/">
<xsl:variable name="first-choice" select="(*/choice/*[key('kChoices',name())])[1]" />
<xsl:value-of select="$first-choice" />
<xsl:text>, the value of <</xsl:text>
<xsl:value-of select="name( $first-choice)" />
<xsl:text>></xsl:text>
</xsl:template>
</xsl:stylesheet>
...this output text is produced...
Red, the value of <corr>
XSLT 2.0 Aside
In XSLT 2.0, you would be able to use the following alternatives for the computation of the $first-choice variable...
Option 1:
(*/choice/*[for $c in . return ../../choices/*[name()=name($c)]])[1]
Option 2:
(*/choice/*[some $c in ../../choices/* satisfies name($c)=name()])[1]

can we use dynamic variable name in the select statement in xslt?

I wanted to use a dynamic variable name in the select statement in xslt.
<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:template match="/">
<xsl:variable name="input" select="input/message" />
<xsl:variable name="Name" select="'MyName'" />
<xsl:variable name="Address" select="MyAddress" />
<xsl:variable name="output" select="concat('$','$input')" /> <!-- This is not working -->
<output>
<xsl:value-of select="$output" />
</output>
</xsl:template>
The possible values for the variable "input" is 'Name' or 'Address'.
The select statement of the output variable should have a dynamic variable name based on the value of input variable. I don't want to use xsl:choose. I wanted to select the value dynamically.
Please provide me a solution.
Thanks,
dhinu
XSLT 1.0 and XSLT 2.0 don't have dynamic evaluation.
Solution for your problem:
This transformation:
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:my="my:my">
<xsl:output method="text"/>
<my:values>
<name>MyName</name>
<address>MyAdress</address>
</my:values>
<xsl:template match="/">
<xsl:variable name="vSelector"
select="input/message"/>
<xsl:value-of select=
"document('')/*/my:values/*[name()=$vSelector]"/>
</xsl:template>
</xsl:stylesheet>
when applied on the following XML document:
<input>
<message>address</message>
</input>
produces the wanted, correct result:
MyAdress
when the same transformation is applied on this XML document:
<input>
<message>name</message>
</input>
again the wanted, correct result is produced:
MyName
Finally: If you do not wish to use the document() function, but would go for using the xxx:node-set() extension function, then this solution (looking very similar) is what you want, where you may consult your XSLTprocessor documentation for the exact namespace of the extension:
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:ext="http://exslt.org/common" >
<xsl:output method="text"/>
<xsl:variable name="vValues">
<name>MyName</name>
<address>MyAdress</address>
</xsl:variable>
<xsl:template match="/">
<xsl:variable name="vSelector"
select="input/message"/>
<xsl:value-of select=
"ext:node-set($vValues)/*[name()=$vSelector]"/>
</xsl:template>
</xsl:stylesheet>
Beside #Dimitre's good answer, for this particular case (output string value) you could also use:
<xsl:variable name="output"
select="concat(substring($Name, 1 div ($input = 'Name')),
substring($Address, 1 div ($input = 'Address')))"/>

attribute value using msxml from xml

I have the following XML
<?xml version="1.0" encoding="ISO-8859-1" ?>
- <DEVICEMESSAGES>
<VERSION xml="1" checksum="" revision="0" envision="33050000" device="" />
<HEADER id1="0001" id2="0001" content="Nasher[<messageid>]: <!payload>" />
<MESSAGE level="7" parse="1" parsedefvalue="1" tableid="15" id1="24682" id2="24682" eventcategory="1003010000" content="Access to <webpage> was blocked due to its category (<info> by <hostname>)" />
</DEVICEMESSAGES>
I am using the following xslt
<xsl:stylesheet version="2.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="text"/>
<xsl:strip-space elements="*"/>
<xsl:template match="DEVICEMESSAGES">
<xsl:value-of select="#id2"/>,<xsl:text/>
<xsl:value-of select="#content"/>,<xsl:text/>
<xsl:text>
</xsl:text>
</xsl:template>
</xsl:stylesheet>
when i use MSXML i just get ,, whereas i want to have something like
id2, content
0001 , Nasher[<messageid>]: <!payload>"
The DEVICEMESSAGES element doesn't have attributes at all.
Change:
<xsl:template match="DEVICEMESSAGES">
to:
<xsl:template match="DEVICEMESSAGES/HEADER">