Search code examples
xmlxslt-1.0xml-namespacessaxonexslt

node-set() does not select some elements in my XSL Stylesheet


We have an XML file which we use to produce another XML file through XSL 1.0 (using the exslt.org/common extension).

The input XML file uses namespaces, and the result is not what we expected. The input XML file is:

<dataTypeSet xsi:schemaLocation="http://www.aviation-ia.com/aeec/SupportFiles/schema-6/widgetDefn.xsd widgetDefn.xsd" xmlns="http://www.aviation-ia.com/aeec/SupportFiles/schema-6/widgetDefn.xsd" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
   <dataTypeList xsi:schemaLocation="http://www.aviation-ia.com/aeec/SupportFiles/schema-6/widgetDefn.xsd widgetDefn.xsd">
      <baseType name="uchar" typeRef="uchar" />
      <baseType name="ushort" typeRef="ushort"  />
      <baseEnumType name="ubyteEnum" typeRef="uchar"  />
      <baseEnumType name="ushortEnum" typeRef="ushort"  />
      <ubyteEnum name="A661_Bool" xsi:schemaLocation="http://www.aviation-ia.com/aeec/SupportFiles/schema-6/widgetDefn.xsd widgetDefn.xsd" />
      <ushortEnum name="A661_Picture" xsi:schemaLocation="http://www.aviation-ia.com/aeec/SupportFiles/schema-6/widgetDefn.xsd widgetDefn.xsd" />
   </dataTypeList>
   <dataTypeList>
      <struct xsi:schemaLocation="http://www.aviation-ia.com/aeec/SupportFiles/schema-6/widgetDefn.xsd widgetDefn.xsd" name="A661_EntryPopUpStruct">
         <field name="Enable" typeRef="A661_Bool" />
         <field name="Picture" typeRef="A661_Picture" />
      </struct>               
   </dataTypeList>
</dataTypeSet>

The schema this XML file uses is:

<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema" 
           xmlns:a661wd="http://www.aviation-ia.com/aeec/SupportFiles/schema-6/widgetDefn.xsd" 
           targetNamespace="http://www.aviation-ia.com/aeec/SupportFiles/schema-6/widgetDefn.xsd" 
           elementFormDefault="qualified" attributeFormDefault="unqualified">
   <xs:import namespace="http://www.w3.org/XML/1998/namespace" schemaLocation="http://www.w3.org/2001/03/xml.xsd"/>
      
   <xs:simpleType name="baseTypeDefinitionsSimple">
      <xs:restriction base="xs:token">
         <xs:enumeration value="uchar"/>
         <xs:enumeration value="ushort"/>
      </xs:restriction>
   </xs:simpleType>   
   
   <xs:complexType name="ubyteEnumType">
      <xs:attribute name="name" type="xs:string" use="required" />
   </xs:complexType>   
   
   <xs:complexType name="ushortEnumType">
      <xs:attribute name="name" type="xs:string" use="required" />
   </xs:complexType>      
   
   <xs:element name="dataTypeList" type="a661wd:dataTypesType"/>
   <xs:complexType name="struct">
      <xs:sequence minOccurs="0" maxOccurs="unbounded">
         <xs:element name="field" type="a661wd:fieldType"/>
      </xs:sequence>
      <xs:attribute name="name" type="xs:string" use="required" />
   </xs:complexType>
   <xs:complexType name="fieldType">
      <xs:attribute name="name" type="xs:string" use="required"/>
      <xs:attribute name="typeRef" type="xs:string" use="required"/>
   </xs:complexType>
   <xs:complexType name="baseBaseType">
      <xs:attribute name="name" type="xs:string" use="required" />
      <xs:attribute name="typeRef" type="xs:string" use="required"/>
   </xs:complexType>
   <xs:complexType name="dataTypesType">
      <xs:sequence minOccurs="0" maxOccurs="unbounded">
         <xs:choice>
            <xs:element name="struct" type="a661wd:struct"/>
            <xs:element name="baseType" type="a661wd:baseBaseType"/>
            <xs:element name="baseEnumType" type="a661wd:baseBaseType"/>
            <xs:element name="ubyteEnum" type="a661wd:ubyteEnumType"/>
            <xs:element name="ushortEnum" type="a661wd:ushortEnumType"/>
         </xs:choice>
      </xs:sequence>
      <xs:attribute ref="xml:base"/>
   </xs:complexType>
   <xs:element name="dataTypeSet">
      <xs:complexType>
         <xs:sequence>
            <xs:sequence minOccurs="0" maxOccurs="unbounded">
               <xs:element ref="a661wd:dataTypeList"/>
            </xs:sequence>
         </xs:sequence>
         <xs:attribute ref="xml:base"/>
      </xs:complexType>
   </xs:element>
</xs:schema>

The XSL transform file is:

<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
                xmlns:exsl="http://exslt.org/common"
                xmlns:wd="http://www.aviation-ia.com/aeec/SupportFiles/schema-6/widgetDefn.xsd"
                xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">

   <xsl:output method="xml" version="1.0" encoding="UTF-8" indent="yes"/>

   <!-- VARIABLE: g_baseEnumTypes nodeset - the base enum type types e.g. ubyteEnum, ushortEnum  -->
   <xsl:variable name="g_baseEnumTypes"
                 select="wd:dataTypeSet/wd:dataTypeList/wd:baseEnumType" />

   <xsl:variable name="g_baseEnumTypeNodeNames">
      <xsl:for-each select="$g_baseEnumTypes">
         <xsl:text>local-name()='</xsl:text>
         <xsl:value-of select="@name" disable-output-escaping="yes"/>
         <xsl:text>'</xsl:text>
         <xsl:if test="position() != last()">
            <xsl:text> or </xsl:text>
         </xsl:if>
      </xsl:for-each>
   </xsl:variable>

   <xsl:variable name="g_baseEnumTypeNodeNamesString" select="substring(exsl:node-set($g_baseEnumTypeNodeNames)[1],1,string-length(exsl:node-set($g_baseEnumTypeNodeNames)[1]))"/>

   <xsl:template match="/">

      <xsl:element name="docDetails">        
         <xsl:element name="docGenDataTypes">
            <xsl:element name="enumTypes">
               <xsl:for-each select="$g_baseEnumTypes">
                  <xsl:variable name="baseEnumNode" select="."/>
                  <xsl:for-each select="//wd:dataTypeSet/wd:dataTypeList/wd:*[local-name()=current()/@name]">
                     <xsl:call-template name="writeEnumDefinition">
                        <xsl:with-param name="baseEnumType" select="$baseEnumNode"/>
                     </xsl:call-template>
                  </xsl:for-each>
               </xsl:for-each>
            </xsl:element>
            <xsl:element name="structTypes">
               <xsl:for-each select="//wd:dataTypeSet/wd:dataTypeList/wd:struct">
                  <xsl:call-template name="writeStructDefinition"/>
               </xsl:for-each>
            </xsl:element>
         </xsl:element>
   
      </xsl:element>
   </xsl:template>
  
   <!-- Override default xslt template match rule - prevent copying over of text nodes-->
   <xsl:template match="text(  )"/>

   <!-- Template: locate alias type with matching name - if found recurse, else found actual root type  -->
   <xsl:template name="recurseDataType">
      <xsl:param name="searchForDataType"/>
      
      <xsl:call-template name="underlyingTypeDetails">
         <xsl:with-param name="searchForDataType" select="$searchForDataType"/>
      </xsl:call-template>

   </xsl:template>

   <!-- Template: locate underlying data type details with matching name. -->
   <!-- e.g. aliasType chain will eventually resolve to a concrete type with a defined size in bits. -->
   <xsl:template name="underlyingTypeDetails">
      <xsl:param name="searchForDataType"/>

      <!-- TODO performance enhancement - nest tests in order of proability of match-->
      <xsl:variable name="foundBaseTypeNode"
                    select="//wd:dataTypeSet/wd:dataTypeList/wd:baseType[@name=$searchForDataType]" />

      <xsl:message>template underlyingTypeDetails with <xsl:value-of select="$searchForDataType"/></xsl:message>
      <xsl:choose>
         <!-- When got base type def -->
         <xsl:when test="$foundBaseTypeNode">
            <!-- Copy found node, attributes and children -->
            <xsl:message>template underlyingTypeDetails with <xsl:value-of select="$searchForDataType"/></xsl:message>
            <xsl:apply-templates select="$foundBaseTypeNode" mode="copyAndChangeNS"/>
         </xsl:when>
         <xsl:otherwise>
            <xsl:variable name="foundEnumTypeNode"
                          select="//wd:dataTypeSet/wd:dataTypeList/wd:*[$g_baseEnumTypeNodeNamesString][@name=$searchForDataType]"/>
            <xsl:choose>
               <!-- When got base type def -->
               <xsl:when test="$foundEnumTypeNode">
                  <xsl:message>found foundEnumTypeNode</xsl:message>
                  <!-- context is the BASE enum definition -->
                  <xsl:call-template name="writeEnumBaseTypeDetails">
                     <xsl:with-param name="enumTypeDefnNode" select="$foundEnumTypeNode"/>
                  </xsl:call-template>
               </xsl:when>
            </xsl:choose>
         </xsl:otherwise>
      </xsl:choose>
   </xsl:template>

   <!-- Template: For a BASE Enum Type Definition scope - Create enumerated type details node. -->
   <xsl:template name="writeEnumBaseTypeDetails">
      <xsl:param name ="enumTypeDefnNode"/>
      <xsl:variable name ="enumTypeName" select ="local-name(exsl:node-set($enumTypeDefnNode)[1])"/>

      <xsl:variable name="enumBaseType" select="exsl:node-set($g_baseEnumTypes)[@name=$enumTypeName]"/>
      <xsl:element name="enumType">
         <xsl:attribute name="name">
            <xsl:value-of select="exsl:node-set($enumTypeDefnNode)[1]//@name"/>
         </xsl:attribute>
         <xsl:attribute name="typeRef">
            <xsl:value-of select="exsl:node-set($enumBaseType)[1]//@typeRef"/>
            <xsl:message>template writeEnumBaseTypeDetails typeRef with <xsl:value-of select="exsl:node-set($enumBaseType)[1]//@typeRef"/></xsl:message>
         </xsl:attribute>
      </xsl:element>

   </xsl:template>

   <!-- Template: Write out enumerate type definition details - required to record possible (range-of) values for some enumerated widget parameters -->
   <!-- Called for all enumerates - whether needed by widget parameter documentation or not - as this is simplest to implement. -->
   <xsl:template name="writeEnumDefinition">
      <xsl:param name="baseEnumType"/>
      <xsl:element name="enumTypeDefn">
         <xsl:attribute name="name">
            <xsl:value-of select="@name"/>
         </xsl:attribute>
         <xsl:attribute name="enumBase">
            <xsl:value-of select="local-name()"/>
         </xsl:attribute>
      </xsl:element>
   </xsl:template>

   <!-- Template: Write out structure type definition details - required for complex parameter type details. -->
   <!-- recurse structure field types - currently only supports simple types (not structures of structures)  -->
   <xsl:template name="writeStructDefinition">
      <xsl:element name="structTypeDefn">
         <xsl:attribute name="name">
            <xsl:value-of select="@name"/>
         </xsl:attribute>

         <xsl:for-each select="wd:*">
            <xsl:choose>
               <xsl:when test="local-name()='field'">
                  <xsl:element name="field">
                     <!-- Copy found node, attributes and children -->
                     <xsl:attribute name="name">
                        <xsl:value-of select="@name"/>
                        <xsl:message>Look for <xsl:value-of select="@name"/></xsl:message>
                     </xsl:attribute>
                     <xsl:attribute name="typeRef">
                        <xsl:value-of select="@typeRef"/>
                     </xsl:attribute>
                     <xsl:call-template name="recurseDataType">
                        <xsl:with-param name="searchForDataType" select="@typeRef"/>
                     </xsl:call-template>
                  </xsl:element>

               </xsl:when>
               <xsl:otherwise>
                  <xsl:apply-templates select="." mode="copyAndChangeNS"/>
               </xsl:otherwise>
            </xsl:choose>
         </xsl:for-each>
      </xsl:element>
   </xsl:template>


   <!-- Template: Copy node (and atributes and recurse through children) stripping out namespace details. -->
   <xsl:template match="*" mode="copyAndChangeNS">
      <xsl:element name="{local-name()}">
         <!-- copy all the attributes from the matched element, stripping schema xsi:schemaLocation and supporting namespace nodes -->
         <xsl:copy-of select="@*[not(namespace-uri() = 'http://www.w3.org/2001/XMLSchema-instance')]"/>
         <xsl:apply-templates select="node()" mode="copyAndChangeNS"/>
      </xsl:element>
   </xsl:template>

</xsl:stylesheet>

I am using SAXON (the last HE version), but I have the same result with Xalan. The Java code which I use to trigger the transform is:

public class XSLTTest3 {
   private File samples3 = null;
   private final File inputFile;
   private final File outputFile;
   private final File xslFile;

   public XSLTTest3() {
      File dir = new File(System.getProperty("user.dir"));
      samples3 = new File(dir, "samples3");

      inputFile = new File(samples3, "inputFile.xml");
      outputFile = new File(samples3, "outputFile.xml");
      xslFile = new File(samples3, "xslTest.xsl");
   }

   public void applyXSLT() {
      try {
         TransformerFactory factory = TransformerFactory.newInstance("net.sf.saxon.jaxp.SaxonTransformerFactory", Thread.currentThread().getContextClassLoader());
         Transformer transformer = factory.newTransformer(new StreamSource(xslFile));
         transformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "no");
         transformer.setOutputProperty(OutputKeys.METHOD, "xml");
         transformer.setOutputProperty(OutputKeys.INDENT, "yes");
         transformer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "2");
         transformer.setOutputProperty(OutputKeys.ENCODING, "UTF-8");

         DocumentBuilderFactory dFactory = DocumentBuilderFactory.newInstance();
         DocumentBuilder dBuilder = dFactory.newDocumentBuilder();
         InputSource xslInputSource = new InputSource(new FileInputStream(inputFile));
         xslInputSource.setSystemId(inputFile.toURI().toString());
         Document xslDoc = dBuilder.parse(xslInputSource);
         DOMSource xslDomSource = new DOMSource(xslDoc);

         transformer.transform(xslDomSource, new StreamResult(new FileOutputStream(outputFile)));
      } catch (Exception ex) {
         ex.printStackTrace();
      }
   }

   public static void main(String[] args) {
      XSLTTest3 xlstTest = new XSLTTest3();
      xlstTest.applyXSLT();
   }
}

I expected to have this result:

<docDetails>
   <docGenDataTypes>
      <enumTypes>
         <enumTypeDefn name="A661_Bool" enumBase="ubyteEnum"/>
         <enumTypeDefn name="A661_Picture" enumBase="ushortEnum"/>
      </enumTypes>
      <structTypes>
         <structTypeDefn name="A661_EntryPopUpStruct">
            <field name="Enable" typeRef="A661_Bool">
               <enumType name="A661_Bool" typeRef="uchar"/>
            </field>
            <field name="Picture" typeRef="A661_Picture">
               <enumType name="A661_Picture" typeRef="ushort"/>
            </field>
         </structTypeDefn>
      </structTypes>
   </docGenDataTypes>
</docDetails>

But I have:

<docDetails>
   <docGenDataTypes>
      <enumTypes>
         <enumTypeDefn name="A661_Bool" enumBase="ubyteEnum"/>
         <enumTypeDefn name="A661_Picture" enumBase="ushortEnum"/>
      </enumTypes>
      <structTypes>
         <structTypeDefn name="A661_EntryPopUpStruct">
            <field name="Enable" typeRef="A661_Bool">
               <enumType name="A661_Bool" typeRef="uchar"/>
            </field>
            <field name="Picture" typeRef="A661_Picture">
               <enumType name="A661_Picture" typeRef=""/>
            </field>
         </structTypeDefn>
      </structTypes>
   </docGenDataTypes>
</docDetails>

The way this XSL works is looking for a type in the XML file which has the same name as the type referenced in the Field, and emit its base type and size.

The problem is with the exsl:node-set($enumBaseType)[1]//@typeRef expression in line 197 of the XSL script. Strangely it works without any problem for the first Enable Field (type A661_Bool), but the script using this same function is not able to find the next Picture Field (type A661_Picture), even if the type above is defined exactly in the same way.

If I remove all the namespace stuff, everything works fine (but I can't do it, because the real use-case uses more than one Schema and relies on these schemas to validate the input content).

We used MSXML before, and it worked correctly with the same XSL script, so I suspect: either the way I use the script in Java is wrong or there is a difference in the way MSXML and SAXON / Xalan works.

What is wrong in the exsl:node-set($enumBaseType)[1]//@typeRef expression (it looks like the problem here)?


Solution

  • I don't see the need to use exsl:node-set in some of those expressions (as no result tree fragments are constructed) and both XslCompiledTransform as well as Saxon 10 and 11 with the simplification of

       <!-- Template: For a BASE Enum Type Definition scope - Create enumerated type details node. -->
       <xsl:template name="writeEnumBaseTypeDetails">
          <xsl:param name ="enumTypeDefnNode"/>
          <xsl:variable name ="enumTypeName" select ="local-name(exsl:node-set($enumTypeDefnNode)[1])"/>
    
          <xsl:variable name="enumBaseType" select="$g_baseEnumTypes[@name=$enumTypeName]"/>
          <xsl:element name="enumType">
             <xsl:attribute name="name">
                <xsl:value-of select="exsl:node-set($enumTypeDefnNode)[1]//@name"/>
             </xsl:attribute>
             <xsl:attribute name="typeRef">
                <xsl:value-of select="$enumBaseType/@typeRef"/>
             </xsl:attribute>
          </xsl:element>
    
       </xsl:template>
    

    give the result

    <docDetails>
       <docGenDataTypes>
          <enumTypes>
             <enumTypeDefn name="A661_Bool" enumBase="ubyteEnum"/>
             <enumTypeDefn name="A661_Picture" enumBase="ushortEnum"/>
          </enumTypes>
          <structTypes>
             <structTypeDefn name="A661_EntryPopUpStruct">
                <field name="Enable" typeRef="A661_Bool">
                   <enumType name="A661_Bool" typeRef="uchar"/>
                </field>
                <field name="Picture" typeRef="A661_Picture">
                   <enumType name="A661_Picture" typeRef="ushort"/>
                </field>
             </structTypeDefn>
          </structTypes>
       </docGenDataTypes>
    </docDetails>