XSLT that transforms from XSD to WSDL

I don’t like writing WSDLs. I think they force a wrong model. Unfortunately, they are the standard. I do like contract-first Web service. I think the XML messages, and therefore the XSD, are most important. So, what I usually do when creating a WSDL

  1. start with the XML messages,
  2. create or infer a schema from that,
  3. copy all the defined elements in messages,
  4. create operations from the messages
  5. create a portType from the operations
  6. create a binding from the portType
  7. create a service from the binding
Step 2 requires some manual tuning, but steps 3-7 are pretty tedious, so I wondered if I couldn’t automate it in some way. Craig Walls also wondered about this.

I started out writing this in Java, but that got even more tedious. After some though, it seemed like perfect opportunity to use an XSLT stylesheet, since we have XML coming in (the schema), and XML coming out (the WSDL). Basically, the idea is to iterate over all elements that end with a certain suffix (’Request’ by default), and to create operations based on that. So if you have a GetHolidayRequest element in the schema, a GetHolidayRequestMessage is created, used by a GetHoliday operation.

Note that it is a XSLT 2.0 stylesheet, because I needed some of the new functionality (most importantly the new <xsl:namespace/> tag). You will need an XSLT 2.0 processor, like Saxon, to run it. As far as I know, Xalan doesn’t do XSLT 2.0 (yet). Also note that I’m no XSLT expert, so there are probably some improvements to be made. Please give them in the comments! The XSLT will eventually end up in Spring Web Services, so that you can dynamically create the WSDL based on your XSD.

Without further ado, here it is:

<!--
  ~ Copyright 2006 Arjen Poutsma.
  ~
  ~ Licensed under the Apache License, Version 2.0 (the "License");
  ~ you may not use this file except in compliance with the License.
  ~ You may obtain a copy of the License at
  ~
  ~      http://www.apache.org/licenses/LICENSE-2.0
  ~
  ~ Unless required by applicable law or agreed to in writing, software
  ~ distributed under the License is distributed on an "AS IS" BASIS,
  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  ~ See the License for the specific language governing permissions and
  ~ limitations under the License.
  -->
<xsl:stylesheet version="2.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
                xmlns:wsdl="http://schemas.xmlsoap.org/wsdl/"
                xmlns:wsdlsoap="http://schemas.xmlsoap.org/wsdl/soap/"
                xmlns:xsd="http://www.w3.org/2001/XMLSchema">
    <xsl:output method="xml" encoding="UTF-8" indent="yes"/>
    <!-- Parameters -->
    <!-- Name for the WSDL definition, which is also uses as a prefix for portType, binding, and service -->
    <xsl:param name="name"/>
    <!-- Location where the service can be found -->
    <xsl:param name="location"/>
    <!-- Namespace of the schema -->
    <xsl:param name="schemaNamespace" select="/xsd:schema/@targetNamespace"/>
    <!-- Prefix for namespace of the schema -->
    <xsl:param name="schemaPrefix" select="'schema'"/>
    <!-- WSDL Namespace, by default the same as the schema namespace -->
    <xsl:param name="targetNamespace" select="$schemaNamespace"/>
    <!-- Prefix for WSDL -->
    <xsl:param name="prefix" select="'tns'"/>
    <xsl:param name="requestSuffix" select="'Request'"/>
    <xsl:param name="responseSuffix" select="'Response'"/>
    <xsl:param name="messageSuffix" select="'Message'"/>
    <xsl:param name="inputSuffix" select="'Input'"/>
    <xsl:param name="outputSuffix" select="'Output'"/>
    <xsl:param name="portTypeSuffix" select="'PortType'"/>
    <xsl:param name="bindingSuffix" select="'Binding'"/>
    <xsl:param name="serviceSuffix" select="'Service'"/>
    <xsl:param name="portSuffix" select="'Port'"/>
    <xsl:param name="soapActionPrefix" select="$targetNamespace"/>

    <xsl:variable name="elements" select="/xsd:schema/xsd:element"/>

    <!-- Elements that end with the request suffix, keyed under operation name -->
    <xsl:key name="requestElements" match="/xsd:schema/xsd:element[ends-with(@name, $requestSuffix)]"
             use="substring-before(@name,$requestSuffix)"/>
    <!-- Elements that end with the response suffix, keyed under operation name -->
    <xsl:key name="responseElements" match="/xsd:schema/xsd:element[ends-with(@name, $responseSuffix)]"
             use="substring-before(@name,$responseSuffix)"/>

    <!-- Global template -->
    <xsl:template match="/">
        <xsl:call-template name="definitions"/>
    </xsl:template>

    <!-- WSDL definitions -->
    <xsl:template name="definitions">
        <wsdl:definitions>
            <xsl:attribute name="name">
                <xsl:value-of select="$name"/>
            </xsl:attribute>
            <xsl:namespace name="{$schemaPrefix}" select="$schemaNamespace"/>
            <xsl:namespace name="{$prefix}" select="$targetNamespace"/>
            <xsl:attribute name="targetNamespace">
                <xsl:value-of select="$targetNamespace"/>
            </xsl:attribute>
            <xsl:call-template name="types"/>
            <xsl:for-each select="$elements">
                <xsl:call-template name="message">
                    <xsl:with-param name="elementName">
                        <xsl:value-of select="@name"/>
                    </xsl:with-param>
                </xsl:call-template>
            </xsl:for-each>
            <xsl:call-template name="portType"/>
        </wsdl:definitions>
    </xsl:template>

    <!-- WSDL types  -->
    <xsl:template name="types">
        <wsdl:types>
            <xsl:copy-of select="/xsd:schema"/>
        </wsdl:types>
    </xsl:template>

    <!-- WSDL Message -->
    <xsl:template name="message">
        <xsl:param name="elementName"/>
        <wsdl:message>
            <xsl:attribute name="name">
                <xsl:value-of select="concat($elementName,$messageSuffix)"/>
            </xsl:attribute>
            <wsdl:part name="body">
                <xsl:attribute name="element">
                    <xsl:value-of select="concat($schemaPrefix,':',$elementName)"/>
                </xsl:attribute>
            </wsdl:part>
        </wsdl:message>
    </xsl:template>

    <!-- WSDL Port type -->
    <xsl:template name="portType">
        <xsl:variable name="portTypeName" select="concat($name, $portTypeSuffix)"/>
        <wsdl:portType>
            <xsl:attribute name="name">
                <xsl:value-of select="$portTypeName"/>
            </xsl:attribute>
            <!-- Find all elements that end with $requestSuffix, and start an operation for that  -->
            <xsl:for-each select="/xsd:schema/xsd:element[ends-with(@name, $requestSuffix)]">
                <xsl:call-template name="portTypeOperation">
                    <xsl:with-param name="operationName">
                        <xsl:value-of select="substring-before(@name,$requestSuffix)"/>
                    </xsl:with-param>
                </xsl:call-template>
            </xsl:for-each>
        </wsdl:portType>
        <xsl:call-template name="binding">
            <xsl:with-param name="portTypeName" select="$portTypeName"/>
        </xsl:call-template>
    </xsl:template>

    <xsl:template name="portTypeOperation">
        <xsl:param name="operationName"/>
        <wsdl:operation>
            <xsl:attribute name="name">
                <xsl:value-of select="$operationName"/>
            </xsl:attribute>
            <xsl:for-each select="key('requestElements', $operationName)">
                <wsdl:input>
                    <xsl:attribute name="name">
                        <xsl:value-of select="concat($operationName, $inputSuffix)"/>
                    </xsl:attribute>
                    <xsl:attribute name="message">
                        <xsl:value-of select="concat($prefix,':',@name,$messageSuffix)"/>
                    </xsl:attribute>
                </wsdl:input>
            </xsl:for-each>
            <xsl:for-each select="key('responseElements', $operationName)">
                <wsdl:output>
                    <xsl:attribute name="name">
                        <xsl:value-of select="concat($operationName, $outputSuffix)"/>
                    </xsl:attribute>
                    <xsl:attribute name="message">
                        <xsl:value-of select="concat($prefix,':',@name,$messageSuffix)"/>
                    </xsl:attribute>
                </wsdl:output>
            </xsl:for-each>
        </wsdl:operation>
    </xsl:template>

    <xsl:template name="binding">
        <xsl:param name="portTypeName"/>
        <xsl:variable name="bindingName" select="concat($name, $bindingSuffix)"/>
        <wsdl:binding>
            <xsl:attribute name="name">
                <xsl:value-of select="$bindingName"/>
            </xsl:attribute>
            <xsl:attribute name="type">
                <xsl:value-of select="concat($prefix,':',$portTypeName)"/>
            </xsl:attribute>
            <wsdlsoap:binding style="document" transport="http://schemas.xmlsoap.org/soap/http"/>
            <!-- Find all elements that end with $requestSuffix, and start an operation for that  -->
            <xsl:for-each select="/xsd:schema/xsd:element[ends-with(@name, $requestSuffix)]">
                <xsl:call-template name="bindingOperation">
                    <xsl:with-param name="operationName">
                        <xsl:value-of select="substring-before(@name,$requestSuffix)"/>
                    </xsl:with-param>
                </xsl:call-template>
            </xsl:for-each>
        </wsdl:binding>
        <xsl:call-template name="service">
            <xsl:with-param name="bindingName" select="$bindingName"/>
        </xsl:call-template>
    </xsl:template>

    <xsl:template name="bindingOperation">
        <xsl:param name="operationName"/>
        <wsdl:operation>
            <xsl:attribute name="name">
                <xsl:value-of select="$operationName"/>
            </xsl:attribute>
            <wsdlsoap:operation>
                <xsl:attribute name="soapAction">
                    <xsl:value-of select="concat($soapActionPrefix,'/',$operationName)"/>
                </xsl:attribute>
            </wsdlsoap:operation>
            <xsl:for-each select="key('requestElements', $operationName)">
                <wsdl:input>
                    <xsl:attribute name="name">
                        <xsl:value-of select="concat($operationName, $inputSuffix)"/>
                    </xsl:attribute>
                    <wsdlsoap:body use="literal"/>
                </wsdl:input>
            </xsl:for-each>
            <xsl:for-each select="key('responseElements', $operationName)">
                <wsdl:output>
                    <xsl:attribute name="name">
                        <xsl:value-of select="concat($operationName, $outputSuffix)"/>
                    </xsl:attribute>
                    <wsdlsoap:body use="literal"/>
                </wsdl:output>
            </xsl:for-each>
        </wsdl:operation>
    </xsl:template>

    <xsl:template name="service">
        <xsl:param name="bindingName"/>
        <wsdl:service>
            <xsl:attribute name="name">
                <xsl:value-of select="concat($name, $serviceSuffix)"/>
            </xsl:attribute>
            <wsdl:port>
                <xsl:attribute name="name">
                    <xsl:value-of select="concat($name, $portSuffix)"/>
                </xsl:attribute>
                <xsl:attribute name="binding">
                    <xsl:value-of select="concat($prefix,':', $bindingName)"/>
                </xsl:attribute>
                <wsdlsoap:address>
                    <xsl:attribute name="location">
                        <xsl:value-of select="$location"/>
                    </xsl:attribute>
                </wsdlsoap:address>
            </wsdl:port>
        </wsdl:service>
    </xsl:template>

</xsl:stylesheet>

4 Comments

  1. tomi vanek said,

    July 27, 2006 @ 14:45

    A very nice solution.

    Maybe you can find useful the WSDL-viewer:
    an XSLT that transforms WSDL in a “human readable format” for non-programmer business professionals (HTML):

    http://tomi.vanek.sk/index.php?page=wsdl-viewer
    
  2. Steve Loughran said,

    July 27, 2006 @ 20:23

    This is really cool. I was thinking of something similar that took a more minimal service description and generated valid WSDL, but to do everything from the XSD is very cool. Now, if we could mark up the XSD with extra annotations, we’d have something really bizarre :)

    Ant1.7, that will have an alpha release this week, beta-1 next month, has a new task that validates docs against XSD. We do contract-first XSD/WSDL in which we make test templates of what we think valid messages should look like, then run against them. This verifies the xsd works the way we think it should. Think of it as test-first XSD design.

  3. Standard Deviations » Generate WSDL from XML Schema (XSD) said,

    July 30, 2006 @ 8:17

    […] Stefan Tilkov points to an XSL transform that generates a WSDL from XML schema. You define your XML schema and this script creates the corresponding WSDL for you. Good stuff, contract first design and such. […]

  4. Claudio Perrone's Monologues said,

    August 11, 2006 @ 22:01

    Crested Butte - The Prologue

RSS feed for comments on this post