XSLT 3.0: Create element only if input element has data

Viewed 66

We are using XSLT internally to map a single input schema to a large number of distinct output schemas. Most of the servers using these schemas return errors on empty elements, so empty elements cannot appear in the output. In many cases, a piece of data in the input will simply map to a piece of data in the output, possibly with a minor transformation, e.g.:


<!-- Input -->
<ourns:DateCreated>2021-12-09</ourns:DateCreated>

<!-- Output -->
<otherns:CreatedDt>2021-12-09<otherns:CreatedDt>

The XSLT for this is straightforward, even with the "no empty elements" requirement:

<xsl:if test="ourns:DateCreated != ''">
  <otherns:CreatedDt>
    <xsl:value-of select="ourns:DateCreated/text()"/>
  </otherns:CreatedDt>
</xsl:if>

However, when you're mapping thousands of elements across hundreds of schemas, this business of wrapping everything in <xsl:if/> gets tiresome. You could add a function, say:

<xsl:function name="ourfn:createElementIfData">
  <xsl:param name="tag" as="xs:string"/>
  <xsl:param name="data" as="xs:string"/>
  <xsl:if test="$data != ''">
    <xsl:element name="{$tag}"><xsl:value-of select="$data"/></xsl:element>
  </xsl:if>
</xsl:function>
...
<xsl:sequence select="ourfn:createElementIfData('otherns:CreatedDt', ourns:DateCreated)"/>

But this function will only work if it lives in a stylesheet where both namespaces are declared. If you wanted to share it (as you probably would such a general-purpose function), you would end up needing to either

  1. Declare every possible otherns in the shared stylesheet, or
  2. Pass in the fully qualified namespace on every invocation,

both of which feel wrong.

This seems like such a common use case that I feel like there must be a simple way to do it. What am I missing?

2 Answers

You could define your basic rules like this:

<xsl:template match="ourns:DateCreated" 
              mode="copySimpleElement">
  <otherns:CreatedDt>{.}</otherns:CreatedDt>
</xsl:template>

and then override it for empty elements:

<xsl:template match="*[. = '']" 
              mode="copySimpleElement" 
              priority="20"/>

and then you just have to apply-templates to the relevant elements in the appropriate mode.

You haven't shown any context but perhaps <xsl:template match="ourns:*[not(has-children())]"/> suffices to prevent any processing of the elements without content and adding <xsl:template match="ourns:DateCreated[has-children()]" expand-text="yes"><otherns:CreatedDt>{.}</otherns:CreatedDt></xsl:template> suffices to map the non-empty element to the wanted output element.

Of course <xsl:template match="ourns:*[not(has-children())]"/> could be set up as <xsl:template match="*[not(has-children())]"/> if the rule can be applied to input elements from any namespace or can take a sequence of patterns with <xsl:template match="ourns:*[not(has-children())] | ourns2:*[not(has-children())]"/>.

All the above assumes you are processing those nodes through other templates e.g. the identity transformation <xsl:mode on-no-match="shallow-copy"/>.

If you want to take the function approach I would check if you can pass in an xs:QName:

<xsl:function name="ourfn:createElementIfData">
  <xsl:param name="node-name" as="xs:QName"/>
  <xsl:param name="data" as="xs:string"/>
  <xsl:if test="$data != ''">
    <xsl:element name="{$node-name}" namespace="{namespace-uri-from-QName($node-name)}"><xsl:value-of select="$data"/></xsl:element>
  </xsl:if>
</xsl:function>

and use e.g. <xsl:sequence select="ourfn:createElementIfData(QName('http://yourothernamespace/', 'otherns:CreatedDt'), ourns:DateCreated)"/>.

Related