An XML Schema can be built by combining a number of schema files using <xsd:include>. This allows sharing of common type definitions between schemas. The Java EE deployment descriptor web.xml and its schema web-app_3.0.xsd is an example of this arrangement. If you want to use XSLT to convert the schema, you may need to look up the <xsd:complexType> defined in an included file.

Getting a list of included files

The first step is to make a list of all the included files and store the list in a variable. We use a recursive template find-includes which will add any files included from included files.

Template: find-includes
<xsl:variable name="allIncludes">
    <xsl:call-template name="find-includes"/>
</xsl:variable>

<xsl:template name="find-includes">
    <!-- We start with the root schema.  For included files, we are passed the
    included schema document. -->
    <xsl:param name="schema" select="/"/>

    <xsl:for-each select="$schema/xsd:schema/xsd:include">
        <!-- Output the schema location with trailing space. -->
        <xsl:value-of select="@schemaLocation"/><xsl:text> </xsl:text>

        <!-- Recurse using the included schema document. -->
        <xsl:call-template name="find-includes">
            <xsl:with-param name="schema" select="document(@schemaLocation)"/>
        </xsl:call-template>
    </xsl:for-each>
</xsl:template>

The allIncludes variable is then a space-separated list of included schema documents.

Consider the Java EE web application deployment descriptor: web.xml. The web.xml format is defined by the web-app_3.0.xsd schema which includes a number of other schemas to create the definition of a web descriptor. For the web-app_3.0.xsd schema, the value of allIncludes is:

web-common_3_0.xsd javaee_6.xsd javaee_web_services_client_1_3.xsd jsp_2_2.xsd javaee_6.xsd javaee_web_services_client_1_3.xsd

Note that this list has duplicates as the two first level includes (web-common_3.0.xsd and jsp_2_2.xsd) both include the same second level documents. If the schema is complete, this won’t be a problem because the duplicates appear after all the unique includes, hence would only be processed if any element does not exist in any include.

If duplicates are a problem, the simplest solution is to just set the allIncludes variable manually. Perhaps use the find-includes template to assemble the list (with duplicates) and print it out using <xsl:message> then paste the result back in sans duplicates.

Looking up a complexType

The root element of the web.xml file is <web-app> whose type is javaee:web-appType. The <web-app> element is defined in the base schema (web-app_3.0.xsd) but the type is defined by a complexType in an included schema (web-common_3.0.xsd). In order to combine the type definition with the element, the XSL template takes the form:

<xsl:template match="xsd:element">
    ...

    <xsl:call-template name="complexType-lookup">
        <xsl:with-param name="type" select="substring-after(@type, 'javaee:')"/>
    </xsl:call-template>
</xsl:template>

(Note that we strip the javaee: namespace before searching. This is the targetNamespace of all included schema documents hence doesn’t appear in the name attribute of the type definition but does appear in type attributes when referenced by an element.)

The lookup considers two cases: the type is defined in the base schema (web-app_3.0.xsd), or the type is defined in an included schema. If not found in the base schema, a search of the included schemas is performed using the complexType-search.

Template: complexType-lookup
<xsl:template name="complexType-lookup">
    <!-- The element type we are seeking. -->
    <xsl:param name="type"></xsl:param>

    <xsl:if test="string-length($type) > 0">
        <!-- Check for type in base schema. -->
        <xsl:variable name="found_type" select="/xsd:schema/xsd:complexType[@name=$type]"/>

        <xsl:choose>
            <xsl:when test="count($found_type) = 0">
                <!-- Not in the base schema: Search the included schemas. -->
                <xsl:call-template name="complexType-search">
                    <xsl:with-param name="type" select="$type"/>
                    <xsl:with-param name="includes" select="$allIncludes"/>
                </xsl:call-template>
            </xsl:when>
            <xsl:otherwise>
                <!-- In the base schema: Process as normal. -->
                <xsl:apply-templates select="$found_type"/>
            </xsl:otherwise>
        </xsl:choose>
    </xsl:if>
</xsl:template>

The complexType-search template is passed the complete allIncludes list and works its way through, recursively calling itself with an incremented list index until a match for the type is found or the end of the list is reached.

Template: complexType-search
<xsl:template name="complexType-search">
    <!-- The element type we are seeking. -->
    <xsl:param name="type"></xsl:param>
    <!-- The list of include files to search. -->
    <xsl:param name="includes"></xsl:param>
    <!-- The list index (starts from 1). -->
    <xsl:param name="pos" select="1"/>

    <!-- Get the name of the include file at the current list index. -->
    <xsl:variable name="include">
        <xsl:call-template name="get-include-element">
            <xsl:with-param name="includes" select="$includes"/>
            <xsl:with-param name="index" select="$pos"/>
        </xsl:call-template>
    </xsl:variable>

    <xsl:if test="string-length($include) > 0">
        <!-- Search for the type in the included schema. -->
        <xsl:variable name="found_type" select="document($include)/xsd:schema/xsd:complexType[@name=$type]"/>

        <xsl:choose>
            <xsl:when test="count($found_type)=0">
                <!-- If not found, increment the list index and recurse. -->
                <xsl:call-template name="complexType-search">
                    <xsl:with-param name="type" select="$type"/>
                    <xsl:with-param name="includes" select="$includes"/>
                    <xsl:with-param name="pos" select="$pos + 1"/>
                </xsl:call-template>
            </xsl:when>
            <xsl:otherwise>
                <!-- If found: process as normal. -->
                <xsl:apply-templates select="$found_type"/>
            </xsl:otherwise>
        </xsl:choose>
    </xsl:if>
</xsl:template>

The get-include-element template is used to extract the given allIncludes list element by index, assuming that the list elements are space-separated. As the <xsl:for-each> loop instruction only works on node sets, it can’t be used to iterate through our simple space-separated list. Instead, we recursively call get-include-element with progressively smaller sub-lists until the first element in the sub-list corresponds to the requested index.

Template: get-include-element
<xsl:template name="get-include-element">
    <!-- The includes list or sub-list. -->
    <xsl:param name="includes"></xsl:param>
    <!-- The current target index to be retrieved. -->
    <xsl:param name="index" select="1"/>
    <!-- The current position in the list (starting from 1). -->
    <xsl:param name="pos" select="1"/>

    <xsl:choose>
        <xsl:when test="$pos &lt; $index">
            <!-- If before the target index, recurse using the sub-list after the first space. -->
            <xsl:call-template name="get-include-element">
                <xsl:with-param name="includes" select="substring-after($includes, ' ')"/>
                <xsl:with-param name="index" select="$index"/>
                <xsl:with-param name="pos" select="$pos + 1"/>
            </xsl:call-template>
        </xsl:when>
        <xsl:otherwise>
            <!-- Else everything before the first space is out target include.  -->
            <xsl:value-of select="substring-before($includes, ' ')"/>
        </xsl:otherwise>
    </xsl:choose>
</xsl:template>

All that remains is to create a template to match the <xsd:complexType> and process its contents.