Using XSLT to inline an imported schema in WSDL.

For a recent project, I created a simple web service, with a one-way operation using a simple, reasonably-typedTM message. Rather than inline the XML schema for the message types, I chose to import the externally defined schema, using a relative path on my local filesystem. Here is the WSDL:

<?xml version="1.0" encoding="utf-8"?>
<wsdl:definitions
    xmlns:wsdl="http://schemas.xmlsoap.org/wsdl/" 
    xmlns:tns="http://ws.hccp.org/cc-build" 
    xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/" 
    xmlns:xs="http://www.w3.org/2001/XMLSchema" 
    targetNamespace="http://ws.hccp.org/cc-build">
    <wsdl:types>
        <xs:import namespace="http://ws.hccp.org/cc-build" schemaLocation="../schema/build-notification.xsd"/>
    </wsdl:types>
    <wsdl:message name="buildNotificationMessage">
        <wsdl:part name="message" element="tns:build-notification"/>
    </wsdl:message>
    <wsdl:portType name="buildNotificationPortType">
        <wsdl:operation name="buildNotificationOperation">
            <wsdl:input message="tns:buildNotificationMessage"/>
        </wsdl:operation>
    </wsdl:portType>
    <wsdl:binding name="buildNotificationBinding" type="tns:buildNotificationPortType">
        <soap:binding transport="http://schemas.xmlsoap.org/soap/http" style="document"/>
        <wsdl:operation name="buildNotificationOperation">
            <soap:operation style="document" soapAction="buildNotification"/>
            <wsdl:input>
                <soap:body use="literal"/>
            </wsdl:input>
        </wsdl:operation>
    </wsdl:binding>
</wsdl:definitions>

While the use of the {http://www.w3.org/2001/XMLSchema}import element allows me a certain amount of flexibility during development, it is a solution that is less than acceptable from a deployment perspective. In many cases, deployment of services can occur in environments and server configurations in which externally defined schemata are not accessible. So, rather than hardcode the schema definition in the original WSDL, I'd like to automatically replace the reference with the actual schema during the build or deployment of the service, thereby retaining the indirection and normalization of my schema definitions and service descriptions.

A solution can be provided through the creation of a simple XSLT. First, let's start with a simple identity transformation. This stylesheet will output, for any XML document, a copy of the input document. The identity transform is the basic building block for many transformations:

<xsl:stylesheet 
    xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
    version="1.0">
    <xsl:output method="xml"/>
    <xsl:template match="node() | @*">
        <xsl:copy>
            <xsl:apply-templates select="node() | @*"/>
        </xsl:copy>
    </xsl:template>
</xsl:stylesheet>

Running the above transformation will, as expected, return the original WSDL, unchanged. Now, with a simple addition of a template matching the {http://www.w3.org/2001/XMLSchema}import element, we can extract the value of the schemaLocation attribute and use the document function retrieve the referenced schema. We then continue to apply the identity transformation template to the schema document:

<xsl:stylesheet
    xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
    xmlns:xs="http://www.w3.org/2001/XMLSchema"
    version="1.0">
    <xsl:output method="xml" indent="no"/>
    <xsl:template match="node() | @*">
        <xsl:copy>
            <xsl:apply-templates select="node() | @*"/>
        </xsl:copy>
    </xsl:template>    
    <xsl:template match="xs:import">
        <xsl:variable name="schema" select="@schemaLocation"/>
        <xsl:apply-templates select="document($schema)"/>
    </xsl:template>
</xsl:stylesheet>

Now, if we run the above transformation, we get output like this:

<?xml version="1.0" encoding="utf-8"?>
<wsdl:definitions xmlns:wsdl="http://schemas.xmlsoap.org/wsdl/"
    xmlns:tns="http://ws.hccp.org/cc-build" 
    xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/"
    xmlns:xs="http://www.w3.org/2001/XMLSchema"
    targetNamespace="http://ws.hccp.org/cc-build">
    <wsdl:types>
        <xs:schema xmlns:hccp="http://ws.hccp.org/build-notification"
            xmlns="http://ws.hccp.org/cc-build" elementFormDefault="qualified"
            targetNamespace="http://ws.hccp.org/cc-build">
            <xs:element name="build-notification" type="hccp:build-notification"/>
            <xs:complexType name="build-notification">
                <xs:sequence>
                    <xs:element name="status" maxOccurs="1" minOccurs="1" type="hccp:build-status"/>
                </xs:sequence>
            </xs:complexType>
            <xs:simpleType name="build-status">
                <xs:restriction base="xs:string">
                    <xs:enumeration value="PASS"/>
                    <xs:enumeration value="FAIL"/>
                </xs:restriction>
            </xs:simpleType>
        </xs:schema>
    </wsdl:types>
    <wsdl:message name="buildNotificationMessage">
        <wsdl:part name="message" element="tns:build-notification"/>
    </wsdl:message>
    <wsdl:portType name="buildNotificationPortType">
        <wsdl:operation name="buildNotificationOperation">
            <wsdl:input message="tns:buildNotificationMessage"/>
        </wsdl:operation>
    </wsdl:portType>
    <wsdl:binding name="buildNotificationBinding" type="tns:buildNotificationPortType">
        <soap:binding transport="http://schemas.xmlsoap.org/soap/http" style="document"/>
        <wsdl:operation name="buildNotificationOperation">
            <soap:operation style="document" soapAction="buildNotification"/>
            <wsdl:input>
                <soap:body use="literal"/>
            </wsdl:input>
        </wsdl:operation>
    </wsdl:binding>
</wsdl:definitions>

So now we have a simple set of steps for creating a deployable WSDL. Plus it's trivial to add the above steps to an Ant task:

<project...>
    <target...>
    ... 
        <xslt in="${wsdl.src.dir}/build-notification-endpoint.wsdl"
              out="${build.dir}/build-notification-endpoint.wsdl"
              style="${xslt.src.dir}/import-schema.xsl"/>
    ...
    </target>
</project>