Best practice for inserting and deleting elements via XSLT
xml, xslt
Solution
Here is a generic solution that works for any number of differently named children of `outerElement` and any prefered order between them:
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:ext="http://exslt.org/common" exclude-result-prefixes="ext">
<xsl:output omit-xml-declaration="yes" indent="yes"/>
<xsl:strip-space elements="*"/>
<xsl:param name="pUncertainElName" select="'second'"/>
<xsl:param name="pOrderedNames" select="'|first|second|third|'"/>
<xsl:template match="node()|@*">
<xsl:copy>
<xsl:apply-templates select="node()|@*"/>
</xsl:copy>
</xsl:template>
<xsl:template match="outerElement">
<xsl:variable name="vrtfFirstPass">
<xsl:copy>
<xsl:apply-templates select="node()|@*"/>
<xsl:apply-templates select=
"self::*[not(*[name() = $pUncertainElName])
or
*[name()=$pUncertainElName and @missing-cause]]"
mode="missing"/>
</xsl:copy>
</xsl:variable>
<xsl:apply-templates select="ext:node-set($vrtfFirstPass)/*" mode="pass2"/>
</xsl:template>
<xsl:template match="*[@missing-cause]"/>
<xsl:template match="*" mode="missing">
<xsl:element name="{$pUncertainElName}">
<textElement>Some Text</textElement>
</xsl:element>
</xsl:template>
<xsl:template match="outerElement" mode="pass2">
<xsl:copy>
<xsl:apply-templates>
<xsl:sort data-type="number" select=
"string-length(substring-before($pOrderedNames,
concat('|', name(), '|')
)
)"/>
</xsl:apply-templates>
</xsl:copy>
</xsl:template>
</xsl:stylesheet>
When this transformation is applied on the following XML document:
<doc>
<outerElement>
<first>
<textElement>Some Text</textElement>
</first>
<second missing-cause="">
<textElement>Some Text</textElement>
</second>
<third>
<textElement>Some Text</textElement>
</third>
</outerElement>
</doc>
the wanted, correct result is produced:
<doc>
<outerElement>
<first>
<textElement>Some Text</textElement>
</first>
<second>
<textElement>Some Text</textElement>
</second>
<third>
<textElement>Some Text</textElement>
</third>
</outerElement>
</doc>
When it is applied on this XML document:
<doc>
<outerElement>
<first>
<textElement>Some Text</textElement>
</first>
<third>
<textElement>Some Text</textElement>
</third>
</outerElement>
</doc>
again the wanted, correct result is produced:
<doc>
<outerElement>
<first>
<textElement>Some Text</textElement>
</first>
<second>
<textElement>Some Text</textElement>
</second>
<third>
<textElement>Some Text</textElement>
</third>
</outerElement>
</doc>
Finally, when the same transformation is applied on this XML document:
<doc>
<outerElement>
<first>
<textElement>Some Text</textElement>
</first>
<second>
<textElement>Some Text</textElement>
</second>
<third>
<textElement>Some Text</textElement>
</third>
</outerElement>
</doc>
again the same wanted, correct result is produced:
<doc>
<outerElement>
<first>
<textElement>Some Text</textElement>
</first>
<second>
<textElement>Some Text</textElement>
</second>
<third>
<textElement>Some Text</textElement>
</third>
</outerElement>
</doc>
Do Note:
There can be any, unlimited number of differently named children of `outerElement` (not just three), and their order may not be known in advance.
For example:
Given this XML document:
<doc>
<outerElement>
<first>
<textElement>Some Text</textElement>
</first>
<second missing-cause="">
<textElement>Some Text</textElement>
</second>
<third>
<textElement>Some Text</textElement>
</third>
<fourth>
<textElement>Some Text</textElement>
</fourth>
</outerElement>
</doc>
and this order: forth, second, third, first
We only have to replace:
<xsl:param name="pOrderedNames" select="'|first|second|third|'"/>
with:
<xsl:param name="pOrderedNames" select="'|fourth|second|third|first|'"/>
And now the new wanted result is produced:
<doc>
<outerElement>
<fourth>
<textElement>Some Text</textElement>
</fourth>
<second>
<textElement>Some Text</textElement>
</second>
<third>
<textElement>Some Text</textElement>
</third>
<first>
<textElement>Some Text</textElement>
</first>
</outerElement>
</doc>
Explanation:
This is a two-pass transformation.
The name of the element that may or maynot be present is specified in the external/global parameter `$pUncertainElName`. For convenience in this explanation we refer to this element as `second`.
On the first pass, all children of `outerElement` with the exception of `second` that has the `missing-cause` attribute, are copied "as-is". If the `second` element was absent or had the `missing-cause` attribute, we output a new child of `outerElement` -- the exactly wanted `second` element.
In the second pass we sort the children of the `outerElement` produced in the first pass, according to their priority, as specified in another external/global parameter, named `$pOrderedNames` (a name that is left of another name in this string, has higher priority)
Problem
I am new to XSLT and now i have a rather complicated problem... My input file looks like ``` <?xml version="1.0" encoding="UTF-8" standalone="yes"?> <doc> <outerElement> <first> <textElement>Some Text</textElement> </first> <second> <textElement>Some Text</textElement> </second> <third> <textElement>Some Text</textElement> </third> </outerElement> </doc> ``` The problem occurs with the "second" Element. In the input file it can have one of 3 forms: ``` MISSING <second> <textElement>Some Text</textElement> </second> <second missingCause="" /> ``` In the output file it should be inserted like the second form. If it was missing before the textElement should indicate it was inserted by the Template and whats important here is it has to be at the second position because i want to validate it against a xsd schema... Here is my current XSL: ``` <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"> <xsl:output indent="yes"/> <xsl:strip-space elements="*"/> <!-- COPY ALL ELEMENTS --> <xsl:template match="node()|@*"> <xsl:copy> <xsl:apply-templates select="node()|@*"/> </xsl:copy> </xsl:template> <!-- remove Elements with attribute deleteme --> <xsl:template match="outerElement/second[@missingCause='*']" /> <!-- look if second is there. If not insert --> <xsl:template match="outerElement"> <xsl:copy> <xsl:apply-templates select="node()|@*"/> <xsl:if test="not(second)"> <second> </second> </xsl:if> </xsl:copy> </xsl:template> <!-- Insert Element second --> <xsl:template match="outerElement/second"> <xsl:apply-templates select="node()|@*"/> <xsl:copy> <xsl:if test="not(textElement)"> <textElement>Inserted by Template</textElement> </xsl:if> </xsl:copy> </xsl:template> </xsl:stylesheet> ``` If "second" is missing my output file just gets an Element "" but its empty and the last transformation does not get applied. Everything else looks fine while when i have in the Document i get a warning... Can someone please help me moving the Element to where it has to be so it is validates to a Schema and make it work in all three cases? I think my way seems not very good and will end up chaotic because i got several similar Elements to insert / delete like this. Thanks for reading ;)