tags:

views:

49

answers:

5

I have a very specific problem where I want to be able to extract the default attribute value of an element as illustrated in the example below.

Every item in the input XML contains multiple child name elements, one to represent the primary name, which is the default attribute value (type='main') and another secondary name (type='short'). The primary name does not have the attribute value 'main' specified. Here is a sample input XML with the first name element deliberately commented out to illustrate the issue further down:

<?xml version="1.0"?>
<food_list>
  <food_item>
    <!--name>Apple</name-->
    <name type="short">APL</name>
  </food_item>
  <food_item>
    <name>Asparagus</name>
    <name type="short">ASP</name>
  </food_item>
  <food_item>
    <name>Cheese</name>
    <name type="short">CHS</name>
  </food_item>
</food_list>

The XSD for the NameType looks like this:

<complexType name="NameType">
    <simpleContent>
        <extension base="TextualBaseType">
            <attribute name="type" use="optional" default="main">
                <simpleType>
                    <restriction base="NMTOKEN">
                        <enumeration value="main"/>
                        <enumeration value="short"/>
                        <enumeration value="alternative"/>
                    </restriction>
                </simpleType>
            </attribute>
        </extension>
    </simpleContent>
</complexType>

The XSLT to transform the input XML and extract the Primary name and the Short name is below:

<?xml version="1.0"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"&gt;

<xsl:template match="/">
  <xsl:apply-templates/>
</xsl:template>

<xsl:template match="food_list">
  <table>
    <tr style="background-color:#ccff00">
      <th>Food Name</th>
      <th>Food Short Name</th>
   </tr>
    <xsl:for-each select="food_item">
        <tr style="background-color:#00cc00">
          <td><xsl:value-of select="name"/></td>
          <td><xsl:value-of select="name[@type='short']"/></td>
       </tr>
   </xsl:for-each>
  </table>
</xsl:template>

</xsl:stylesheet>

When the input XML is transformed the Primary name for the first food item is wrongly picked up from the element with type='short'. Question: How do you restrict the first value-of statement in the xslt to only pick up name values when a default element is defined?

A: 

If I understand your question correctly, you could put a <xsl:if> around the <xsl:value-of> that checks if the name doesn't have the @type attribute. (probably something like <xsl:if test="not(name[@type])">)

CharlesLeaf
+1  A: 

XSLT has no knowledge of XSD schemas, so you need to add the information (which value is the default) in your stylesheet. A matching xsl:if would solve the problem. Of course, if you change the XSD, you will have to update the stylesheet. The only way to be more flexible by using the schema would be to read and analyse the schema from the stylesheet, but this would be very difficult and you would have to restrict the possible changes in your schema to simplify the problem.

Damien
+1  A: 

Question: How do you restrict the first value-of statement in the xslt to only pick up name values when a default element is defined?

XSLT 1.0 is not schema-awere, and may be not internal-DTD-aware.

XSLT 2.0 basic processor is not schema-awere.

Bottom line, you need an XSLT 2.0 schema-awere.

A workaround would be to test not existence of such attribute for treating such element as having the default attribute defined:

name[not(@type) or @type='main']
Alejandro
A: 

This transformation:

<xsl:stylesheet version="1.0"
 xmlns:xsl="http://www.w3.org/1999/XSL/Transform"&gt;
 <xsl:output omit-xml-declaration="yes" indent="yes"/>

 <xsl:template match="/">
  <xsl:apply-templates/>
 </xsl:template>

 <xsl:template match="food_list">
  <table>
    <tr style="background-color:#ccff00">
      <th>Food Name</th>
      <th>Food Short Name</th>
   </tr>
    <xsl:apply-templates/>
  </table>
 </xsl:template>

 <xsl:template match="food_item">
  <tr style="background-color:#00cc00">
   <td><xsl:value-of select=
        "concat(name[not(@type)],
                name[@type='main'],
                substring('some hardcoded default',
                          1
                         div
                           (not(name[not(@type)])
                             and
                            not(name[@type='main'])
                           )
                          )
                )
         "/></td>
   <td><xsl:value-of select="name[@type='short']"/></td>
  </tr>
 </xsl:template>
</xsl:stylesheet>

when applied to the following XML document (based on the provided, but slightly enlarged to represent more possible cases):

<food_list>
  <food_item>
    <!--name>Apple</name-->
    <name type="short">APL</name>
    <name type="alternative">Gala APL</name>
  </food_item>
  <food_item>
    <name>Asparagus</name>
    <name type="short">ASP</name>
  </food_item>
  <food_item>
    <name>Cheese</name>
    <name type="short">CHS</name>
  </food_item>
  <food_item>
    <name type="main">Grapes</name>
    <name type="short">GPS</name>
  </food_item>
</food_list>

produces the wanted, correct result:

<table>
    <tr style="background-color:#ccff00">
        <th>Food Name</th>
        <th>Food Short Name</th>
    </tr>
    <tr style="background-color:#00cc00">
        <td>some hardcoded default</td>
        <td>APL</td>
    </tr>
    <tr style="background-color:#00cc00">
        <td>Asparagus</td>
        <td>ASP</td>
    </tr>
    <tr style="background-color:#00cc00">
        <td>Cheese</td>
        <td>CHS</td>
    </tr>
    <tr style="background-color:#00cc00">
        <td>Grapes</td>
        <td>GPS</td>
    </tr>
</table>
Dimitre Novatchev
@Dimitre, I'm fairly sure the OP doesn't want the word 'main' in the first `<td>` on each row. Also, your solution will fail if an element `<name type="alternative">Something</name>` were to exist, which based on the schema is quite possible; it would return the alternative name in the first `<td>` element as well as the default food name.
Flynn1179
@Flynn1179: Thanks for this remark. I have updated my answer after further understanding the question -- it isn't defined very well and allows different interpretations.
Dimitre Novatchev
+1  A: 

The problem you've got is that <xsl:value-of select="name" /> will select any name element, regardless of whether or not it has an attribute, which is including those with the 'short' type; you need to restrict it to just those without a type attribute, or those where the type attribute has the value 'main'.

You can do this with: <xsl:value-of select="name[not(@type) or @type='main']" />.

This of course doesn't explicitly reference the schema, but as the schema is itself an XML document, you could potentially replace 'main' with a reference to the schema using the document() function; if you choose to do this, I'd recommend storing the value in a variable in the root of the xslt, so it only fetches this once. For example:

<xsl:variable name="defaultType" select="document('<schema url>')//complexType[@name='NameType']/simpleContent/extension/attribute/@default" />

Then you can just replace the reference to 'main' with $defaulttype.

Flynn1179