Reputation: 1461
I am having some trouble using XSLT to check if nodes exist, and if not add them to the document. Here is my situation:
Input
<Message>
<a>123</a>
<c>456</c>
<d>789</d>
</Message>
Desired output
<MsgHead>
<Document>
<Message>
<a>123</a>
<b>-1</b>
<c>456</c>
<d>789</d>
</Message>
<Document>
</MsgHead>
I am also given the following static file with "default values"
Defaults
<DefaultNodes>
<a>-1</a>
<b>-1</b>
<c>-1</c>
<d>-1</d>
</DefaultNodes>
The input files have varying numbers of nodes and I need to "complete" them with the default nodes that are missing. The node names are obviously not a, b, c, etc, but some 700 different nodes with different default values.
Here is my attempt so far My XSLT
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:template match="/">
<MsgHead>
<Document>
<Message>
<xsl:apply-templates></xsl:apply-templates>
</Message>
</Document>
</MsgHead>
</xsl:template>
<xsl:template match="Message">
<xsl:copy-of select="node()"/>
<xsl:for-each select="document('default-nodes.xml')/DefaultNodes/*">
<xsl:choose>
<xsl:when test="//*[local-name(current())]"> <!-- This is the line giving me trouble -->
<!--Node already present, do nothing-->
</xsl:when>
<xsl:otherwise>
<!--Node not in input, add from the defaults file -->
<xsl:copy-of select="self::node()"/>
</xsl:otherwise>
</xsl:choose>
</xsl:for-each>
</xsl:template>
</xsl:stylesheet>
This is almost working, but it seems to not be able to find if the nodes exist or not. The current test I am using (//*[local-name(current())]) seems to return true no matter what. Anyone have any suggestion on how I could fix this?
Thanks!
Upvotes: 1
Views: 4403
Reputation: 12729
This XSLT 1.0 stylesheet ...
<xsl:stylesheet
version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:esl="urn:schemas-microsoft-com:xslt"
exclude-result-prefixes="xsl esl" >
<xsl:output indent="yes" omit-xml-declaration="yes" />
<xsl:strip-space elements="*" />
<xsl:key name="kByName" match="*" use="local-name()" />
<xsl:variable name="DefaultNodes">
<DefaultNodes>
<a>-1</a>
<b>-1</b>
<c>-1</c>
<d>-1</d>
</DefaultNodes>
</xsl:variable>
<xsl:template match="Message">
<MsgHead>
<Document>
<xsl:copy>
<xsl:variable name="union">
<xsl:apply-templates select="* | esl:node-set($DefaultNodes)/DefaultNodes/*[local-name()]"/>
</xsl:variable>
<xsl:copy-of select="esl:node-set($union)/*[generate-id() = generate-id(key('kByName',local-name())[1])]" />
</xsl:copy>
</Document>
</MsgHead>
</xsl:template>
<xsl:template match="@*|node()">
<xsl:copy>
<xsl:apply-templates select="@*|node()" />
</xsl:copy>
</xsl:template>
</xsl:stylesheet>
... when applied to this input document ...
<Message>
<a>123</a>
<c>456</c>
<d>789</d>
</Message>
... yields ...
<MsgHead>
<Document>
<Message>
<a>123</a>
<c>456</c>
<d>789</d>
<b>-1</b>
</Message>
</Document>
</MsgHead>
Note:
Change the esl namespace to http://exslt.org/common
as required, depending on your XSLT processor.
Upvotes: 0
Reputation: 122364
The current test I am using (//*[local-name(current())]) seems to return true no matter what
Yes, because (in the context where you're testing it) that test means "select all the elements in the entire default-nodes.xml
document if the local-name of the current node for this template is non-empty, otherwise select nothing at all".
I assume that the default-nodes.xml
defines the order that you want the result elements to appear in, and that all the elements under DefaultNodes
have distinct names. In that case, how about:
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<!-- store a reference to the root of the main input tree -->
<xsl:variable name="root" select="/" />
<xsl:template match="/">
<MsgHead>
<Document>
<Message>
<xsl:apply-templates select="document('default-nodes.xml')/DefaultNodes/*"/>
</Message>
</Document>
</MsgHead>
</xsl:template>
<xsl:template match="DefaultNodes/*">
<!-- look for a matching element in the main input tree -->
<xsl:variable name="sourceNode" select="$root/Message/*[name() = name(current())]" />
<xsl:choose>
<!-- if such a node exists, copy it -->
<xsl:when test="$sourceNode">
<xsl:copy-of select="$sourceNode" />
</xsl:when>
<!-- else copy the default one -->
<xsl:otherwise>
<xsl:copy-of select="." />
</xsl:otherwise>
</xsl:choose>
</xsl:template>
</xsl:stylesheet>
Here I'm applying templates to the default elements, not the input ones.
Note the use of a variable $root
to hold a reference to the main input tree. Within the DefaultNodes/*
template, the current node is from default-nodes.xml
, so in that context /
means the root of that file, not the root of the main input tree.
Upvotes: 3
Reputation: 22617
Might be that this is not the most concise solution, but it works.
By the way, local-name
returns a name without its namespace (i.e. the part after a colon if there is one). Just in case you were not aware of this.
Stylesheet
<?xml version="1.0" encoding="utf-8"?>
<xsl:stylesheet version="2.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="xml" indent="yes"/>
<xsl:template match="/">
<MsgHead>
<Document>
<Message>
<xsl:apply-templates/>
</Message>
</Document>
</MsgHead>
</xsl:template>
<xsl:template match="Message">
<xsl:variable name="msg" select="."/>
<xsl:for-each select="document('default-nodes.xml')/DefaultNodes/*">
<xsl:choose>
<xsl:when test="$msg/*/name()=current()/name()">
<xsl:copy-of select="$msg/*[name()=current()/name()]"/>
</xsl:when>
<xsl:otherwise>
<xsl:copy-of select="."/>
</xsl:otherwise>
</xsl:choose>
</xsl:for-each>
</xsl:template>
</xsl:stylesheet>
Output
<?xml version="1.0" encoding="UTF-8"?>
<MsgHead>
<Document>
<Message>
<a>123</a>
<b>-1</b>
<c>456</c>
<d>789</d>
</Message>
</Document>
</MsgHead>
Upvotes: 0