carlo
carlo

Reputation: 700

XSLT if attribute exists / else

I'm quite new to XSLT and therefore I want to know what is the best practice for checking the existance of an attribute. My XML looks something like this:

<root>
    <languages>
        <lang id="EN">English<lang>
        <lang id="FR">French<lang>
        <lang id="DE">German</lang>
    </languages>
    <items>
        <item lang="EN">test 1</item>
        <item>test 2</item>
        <item lang="FR">item 3</item>
    </items>
</root>

Note that the 'lang'-attribute for the 'item'-element is optional.

Now I want to loop through the items using a -loop, while checking if it has a "lang"-attribute. If it does, I want to fetch the entire string using the ID (eg. EN -> 'English'). If the attribute isn't set I want it to write "No language set" or something alike.

Now I use the following code but I'm questioning myself if it can't be done in a more efficient way.

<xsl:for-each select="//root/items/item">
    <xsl:variable name="cur_lang" select="@lang" /> <!-- first I store the attr lang in a variable -->
    <xsl:choose>
        <xsl:when test="@lang"> <!-- then i test if the attr exists -->
            <xsl:value-of select="//root/languages/lang[@id=$cur_lang]" /> <!-- if so, parse the element value -->
        </xsl:when>
        <xsl:otherwise>
            No language set <!-- else -->
        </xsl:otherwise>
    </xsl:choose>
</xsl:for-each>

Any suggestions / tips?

Upvotes: 7

Views: 21026

Answers (2)

Daniel Haley
Daniel Haley

Reputation: 52888

Another alternative, if you're able to use XSLT 3.0, is a map (another helpful link: map).

XML Input (fixed to be well-formed)

<root>
    <languages>
        <lang id="EN">English</lang>
        <lang id="FR">French</lang>
        <lang id="DE">German</lang>
    </languages>
    <items>
        <item lang="EN">test 1</item>
        <item>test 2</item>
        <item lang="FR">item 3</item>
    </items>
</root>

XSLT 3.0

<xsl:stylesheet version="3.0" 
    xmlns:xsl="http://www.w3.org/1999/XSL/Transform" 
    xmlns:map="http://www.w3.org/2005/xpath-functions/map"
    xmlns:xs="http://www.w3.org/2001/XMLSchema" extension-element-prefixes="xs map">
    <xsl:output indent="yes"/>
    <xsl:strip-space elements="*"/>

    <xsl:variable name="lang-map" as="map(xs:string, xs:string)" 
        select="map:new(
        for $lang in /*/languages/lang 
        return 
            map{$lang/@id := $lang/string()}
        )"/>

    <xsl:template match="@*|node()">
        <xsl:copy>
            <xsl:apply-templates select="@*|node()"/>
        </xsl:copy>
    </xsl:template>

    <xsl:template match="languages"/>

    <xsl:template match="item[@lang and map:contains($lang-map,@lang)]">
        <item><xsl:value-of select="$lang-map(current()/@lang)"/></item>
    </xsl:template>

    <xsl:template match="item">
        <item>No language found.</item>
    </xsl:template>

</xsl:stylesheet>

Output

<root>
   <items>
      <item>English</item>
      <item>No language found.</item>
      <item>French</item>
   </items>
</root>

Upvotes: 0

Ian Roberts
Ian Roberts

Reputation: 122414

It might be more efficient to use a key. You define a key using a top-level element outside your templates

<xsl:key name="langByCode" match="lang" use="@id" />

Then in the loop you can simply say

<xsl:when test="@lang"> <!-- then i test if the attr exists -->
   <xsl:value-of select="key('langByCode', @lang)" />
</xsl:when>

But generally speaking a more natural XSLT approach to the whole thing would be to use template matching instead of for-each and if:

<xsl:template match="item[@lang]">
  <xsl:value-of select="key('langByCode', @lang)" />
</xsl:template>

<xsl:template match="item">
  <xsl:text>No language set</xsl:text>
</xsl:template>

With these templates in place you can then do <xsl:apply-templates select="/root/items/item" /> and it will pick the appropriate template for each item automatically. The rule is that it will use the most specific template, so the item[@lang] one for those items that have a lang attribute and the plain item one for those that don't.

A third possibility is a little trick I learned on SO to put the whole if/else check into a single XPath expression

<xsl:value-of select="
  substring(
    concat('No language set', key('langByCode', @lang)),
    1 + (15 * boolean(@lang))
  )" />

The trick here is that boolean(@lang) when treated as a number is 1 if the lang attribute exists and 0 if it doesn't. If there is a lang="EN", say, then we construct a string "No language setEnglish" and then take the substring starting at the 16th character, which is "English". If there is no lang attribute we construct the string "No language set" (because the string value of an empty node set is the empty string) and take the substring starting at the first character (i.e. the whole string).

You could use the same trick with other attributes, e.g. suppose we had an optional color attribute and wanted to say "No color specified" if it is absent, you can do that with

<xsl:value-of select="substring(
   concat('No color specified', @color),
   1 + (18 * boolean(@color))
 )" />

Upvotes: 7

Related Questions