I have a xml file which have two elements 'employeeID' and 'managerID'. I like to check if managerID does not exist in any value in employeeID then replace the value of managerID with 'Not Found'.
I have to transform the below xml and replace the value of second element managerID with 'Not Found' if the value of managerID does not match with any value in employeeID.
<wd:Report_Data xmlns:wd="urn:com.workday.report/bsvc">
<wd:Report_Entry>
<wd:employeeID>EMPLOYEE1</wd:employeeID>
<wd:managerID>EMPLOYEE5</wd:managerID>
</wd:Report_Entry>
<wd:Report_Entry>
<wd:employeeID>EMPLOYEE2</wd:employeeID>
<wd:managerID>EMPLOYEE6</wd:managerID>
</wd:Report_Entry>
<wd:Report_Entry>
<wd:employeeID>EMPLOYEE6</wd:employeeID>
<wd:managerID>EMPLOYEE17</wd:managerID>
</wd:Report_Entry>
<wd:Report_Entry>
<wd:employeeID>EMPLOYEE17</wd:employeeID>
<wd:managerID>EMPLOYEE3</wd:managerID>
</wd:Report_Entry>
<wd:Report_Entry>
<wd:employeeID>EMPLOYEE3</wd:employeeID>
<wd:managerID>EMPLOYEE2</wd:managerID>
</wd:Report_Entry>
<wd:Report_Entry>
<wd:employeeID>EMPLOYEE4</wd:employeeID>
<wd:managerID>EMPLOYEE22</wd:managerID>
</wd:Report_Entry>
<wd:Report_Entry>
<wd:employeeID>EMPLOYEE8</wd:employeeID>
<wd:managerID>EMPLOYEE2</wd:managerID>
</wd:Report_Entry>
</wd:Report_Data>
My xslt is a below but this is returning 'Not found' for everyone. Expected out should be
EMPLOYEE1,Not Found
EMPLOYEE2,EMPLOYEE6
EMPLOYEE6,EMPLOYEE17
EMPLOYEE17,EMPLOYEE3
EMPLOYEE3,EMPLOYEE2
EMPLOYEE4,Not Found
EMPLOYEE8,EMPLOYEE2
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:wd="urn:com.workday.report/bsvc"
xmlns:this="urn:this-stylesheet" exclude-result-prefixes="xs" version="2.0">
<xsl:output method="text"/>
<xsl:variable name="Delimiter" select="';'"/>
<xsl:variable name="Newline" select="'
'"/>
<xsl:variable name="allemployeesID">
<xsl:for-each select="/wd:Report_Data/Report_Entry">
<xsl:value-of select="wd:employeeID"/>
</xsl:for-each>
</xsl:variable>
<xsl:template match="/">
<xsl:for-each select="wd:Report_Data/wd:Report_Entry">
<xsl:value-of select="wd:employeeID"/>
<xsl:value-of select="$Delimiter"/>
<xsl:choose>
<xsl:when test="contains($allemployeesID,wd:managerID)">
<xsl:value-of select="wd:managerID"/>
</xsl:when>
<xsl:otherwise>
<xsl:value-of select="'Not Found'" />
</xsl:otherwise>
</xsl:choose>
<xsl:value-of select="$Newline"/>
</xsl:for-each>
</xsl:template>
</xsl:stylesheet>
Looks like just a typo: you are missing a wd
namespace prefix in the XPath of the for-each
loop in your allemployeesID
definition.
<xsl:variable name="allemployeesID">
<xsl:for-each select="/wd:Report_Data/Report_Entry">
<xsl:value-of select="wd:employeeID"/>
</xsl:for-each>
</xsl:variable>
i.e. Report_Entry
should be wd:Report_Entry
Better: use a sequence variable instead of a string
But can I suggest a simplification? Rather than having your variable contain a concatenation of the employee identifiers, and use the contains
function to search that string, your variable could just be a sequence of the employee identifiers, which you could search using the =
operator:
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:wd="urn:com.workday.report/bsvc"
xmlns:this="urn:this-stylesheet" exclude-result-prefixes="xs" version="2.0">
<xsl:output method="text"/>
<xsl:variable name="Delimiter" select="';'"/>
<xsl:variable name="Newline" select="'
'"/>
<xsl:variable name="allemployeesID"
select="/wd:Report_Data/wd:Report_Entry/wd:employeeID"/>
<xsl:template match="/">
<xsl:for-each select="wd:Report_Data/wd:Report_Entry">
<xsl:value-of select="wd:employeeID"/>
<xsl:value-of select="$Delimiter"/>
<xsl:choose>
<xsl:when test="$allemployeesID = wd:managerID">
<xsl:value-of select="wd:managerID"/>
</xsl:when>
<xsl:otherwise>
<xsl:value-of select="'Not Found'" />
</xsl:otherwise>
</xsl:choose>
<xsl:value-of select="$Newline"/>
</xsl:for-each>
</xsl:template>
</xsl:stylesheet>
Better still, use a key
NB in the example above, the @test
expression $allemployeesID = wd:managerID
is going to search the $allemployeesID
sequence to find one which matches the wd:managerID
, and that search will be a linear search. A better approach is to use a key
, which will use an indexed search. If the input data file is large, and there are many employees, this may make a considerable difference to the runtime.
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:wd="urn:com.workday.report/bsvc"
xmlns:this="urn:this-stylesheet" exclude-result-prefixes="xs" version="2.0">
<xsl:output method="text"/>
<xsl:variable name="Delimiter" select="';'"/>
<xsl:variable name="Newline" select="'
'"/>
<xsl:key name="allemployeesID"
match="/wd:Report_Data/wd:Report_Entry/wd:employeeID"
use="."
/>
<xsl:template match="/">
<xsl:for-each select="wd:Report_Data/wd:Report_Entry">
<xsl:value-of select="wd:employeeID"/>
<xsl:value-of select="$Delimiter"/>
<xsl:choose>
<xsl:when test="key('allemployeesID', wd:managerID)">
<xsl:value-of select="wd:managerID"/>
</xsl:when>
<xsl:otherwise>
<xsl:value-of select="'Not Found'" />
</xsl:otherwise>
</xsl:choose>
<xsl:value-of select="$Newline"/>
</xsl:for-each>
</xsl:template>
</xsl:stylesheet>
The xsl:key
is used instead of the allemployeesID
variable, and creates a search index, which you can invoke using the key()
function.
<xsl:key name="allemployeesID"
match="/wd:Report_Data/wd:Report_Entry/wd:employeeID"
use="."
/>
name
attribute provides a name for the search index.match
attribute specifies which nodes will be indexed (all the wd:employeeID
elements, in this case)use
attribute specifies what you are looking them up by.
In this case .
means that you use the value of the wd:employeeID
itself to look it up. Normally you'd look up something by a different value, such as looking up an element by the value of one of its attributes, but here you are just looking it up to find out if it exists.To use the key, invoke the key()
function with parameters:
wd:employeeID
The result of calling the key()
function is the wd:employeeID
element itself (or an empty sequence, if there was no wd:employeeID
with that value).
Consider moving logic to XPath to reduce xsl
statements
One final thing which is more of a stylistic comment. I personally find that in a job like this, which is effectively a traditional record-processing job, such as you might write in SQL or COBOL, it can be simpler to use fewer xsl
elements but a more complex XPath expression. XPath's syntax is usually a lot more concise and readable, than the equivalent XSLT statements, so that can be worth doing when you aren't in need of XSLT's pattern-matching capabilities where you might need to use xsl:template
. For example:
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:wd="urn:com.workday.report/bsvc"
xmlns:this="urn:this-stylesheet" exclude-result-prefixes="xs" version="2.0">
<xsl:output method="text"/>
<xsl:variable name="Delimiter" select="';'"/>
<xsl:variable name="Newline" select="'
'"/>
<xsl:key name="allemployeesID"
match="/wd:Report_Data/wd:Report_Entry/wd:employeeID"
use="."
/>
<xsl:template match="/">
<xsl:value-of separator="{$Newline}" select="
for $entry in wd:Report_Data/wd:Report_Entry return
concat(
$entry/wd:employeeID,
$Delimiter,
if (key('allemployeesID', $entry/wd:managerID)) then
$entry/wd:managerID
else
'Not Found'
)
"/>
</xsl:template>
</xsl:stylesheet>