Reputation: 393
Can a call to an XSLT template be setup such that it is called with the parent of the current context?
My XML looks like the following with Job nodes that have 1+ child location nodes:
<Job>
<JobId>12345</JobId>
<JobTitle>Programmer</JobTitle>
<Location>
<LocationCode>US</LocationCode>
<!-- there is a variable number of comma-deliminated strings within the sublocations node -->
<SubLocations>US1,US2,US3</SubLocations>
</Location>
<Location>
<LocationCode>CAN</LocationCode>
</Location>
</Job>
I would like the output to be a single row per Job per Location OR SubLocation:
<Id>12345</Id><Title>Programmer</Title><Location>US1</Location>
<Id>12345</Id><Title>Programmer</Title><Location>US2</Location>
<Id>12345</Id><Title>Programmer</Title><Location>US3</Location>
<Id>12345</Id><Title>Programmer</Title><Location>CAN</Location>
The core logic of my XSLT looks like:
<xsl:template match="Job/Location">
<xsl:choose>
<!-- Test for presence of sublocation -->
<xsl:when test="SubLocation != null">
<xsl:for-each select="distinct-values(SubLocation/tokenize(.,','))">
<xsl:call-template name="JobRecord">
<xsl:with-param name="Location">
<xsl:value-of select="."/>
</xsl:with-param>
</xsl:call-template>
</xsl:for-each>
</xsl:when>
<xsl:otherwise>
<!-- No Sublocation present -->
<xsl:call-template name="JobRecord">
<xsl:with-param name="Location">
<xsl:value-of select="/LocationCode"/>
</xsl:with-param>
</xsl:call-template>
</xsl:otherwise>
</xsl:choose>
</xsl:template>
<xsl:template name="JobRecord">
<xsl:param name="Location"/>
<Id><xsl:value-of select="../JobId"/></Id>
<Name><xsl:value-of select="../JobTitle"/></Name>
<Location><xsl:value-of select="$Location"/></Location>
</xsl:template>
The JobRecord template needs to be called per location OR Sub location (if applicable), even though the contents of the output is at the Job node level. How can the sub locations be broken up or iterated through without loosing context of the parent?
A workaround would be to pass all the Job level information as parameters but I'm looking for a more natural XSLT approach.
Upvotes: 1
Views: 1193
Reputation: 167401
A common idiom in XSLT/XPath, to process only the first of two possible elements is to construct a sequence and select the first item in that sequence, i.e. for your case to select ((SubLocations, LocationCode)[1])
, as that way you get the elements SubLocations
if it exists or otherwise the elements LocationCode
. Then you can tokenize and send the result on to another template, instead of using call-template
I would simply suggest to push the element Job
to another template matching it with a named mode:
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
expand-text="yes"
version="3.0">
<xsl:output omit-xml-declaration="yes"/>
<xsl:template match="Job">
<xsl:variable name="job" select="."/>
<xsl:for-each select="Location/tokenize((SubLocations, LocationCode)[1], ',')">
<xsl:apply-templates select="$job" mode="row">
<xsl:with-param name="loc" select="current()"/>
</xsl:apply-templates>
</xsl:for-each>
</xsl:template>
<xsl:template match="Job" mode="row">
<xsl:param name="loc"/>
<Id>{JobId}</Id>
<Title>{JobTitle}</Title>
<Location>{$loc}</Location>
<xsl:text> </xsl:text>
</xsl:template>
</xsl:stylesheet>
That is an XSLT 3 example working with Saxon 9.8 all editions, online at http://xsltfiddle.liberty-development.net/bFukv8i, but of course it can be adapted to XSLT 2 if needed by changing the last template to
<xsl:template match="Job" mode="row">
<xsl:param name="loc"/>
<Id>
<xsl:value-of select="JobId"/>
</Id>
<Title>
<xsl:value-of select="JobTitle"/>
</Title>
<Location>
<xsl:value-of select="$loc"/>
</Location>
<xsl:text> </xsl:text>
</xsl:template>
and removing the expand-text
attribute on xsl:stylesheet
, http://xsltransform.hikmatu.com/eiQZDbi
Upvotes: 1
Reputation: 7173
You can do that by storing the values in a variable, as:
<xsl:variable name="JID" select="preceding-sibling::JobId"/>
<xsl:variable name="JTITLE" select="preceding-sibling::JobTitle"/>
and eventually passing them as a parameter in your named template. The whole stylesheet is below.
<?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"
exclude-result-prefixes="xs"
version="2.0">
<xsl:strip-space elements="*"/>
<xsl:output indent="yes" omit-xml-declaration="yes"/>
<xsl:template match="Job/Location">
<xsl:variable name="JID" select="preceding-sibling::JobId"/>
<xsl:variable name="JTITLE" select="preceding-sibling::JobTitle"/>
<xsl:choose>
<xsl:when test="SubLocations != ''">
<xsl:for-each select="distinct-values(SubLocations/tokenize(.,','))">
<xsl:call-template name="JobRecord">
<xsl:with-param name="Location" select="."/>
<xsl:with-param name="JobID" select="$JID"/>
<xsl:with-param name="JobTITLE" select="$JTITLE"/>
</xsl:call-template>
</xsl:for-each>
</xsl:when>
<xsl:otherwise>
<xsl:call-template name="JobRecord">
<xsl:with-param name="Location" select="LocationCode"/>
<xsl:with-param name="JobID" select="$JID"/>
<xsl:with-param name="JobTITLE" select="$JTITLE"/>
</xsl:call-template>
</xsl:otherwise>
</xsl:choose>
</xsl:template>
<xsl:template name="JobRecord">
<xsl:param name="JobID"/>
<xsl:param name="JobTITLE"/>
<xsl:param name="Location"/>
<Id><xsl:value-of select="$JobID"/></Id>
<Name><xsl:value-of select="$JobTITLE"/></Name>
<Location><xsl:value-of select="$Location"/></Location>
</xsl:template>
<xsl:template match="JobId|JobTitle"/>
</xsl:stylesheet>
Upvotes: 1