You are using SOAP and WSDL to build a service-based enterprise architecture. You want developers to be able to find complete and pertinent information on available services.
This solution constructs a
WSDL-based service
documentation server: a service that provides information about
services.[29] This example will use a Perl-based CGI that invokes XSLT
processing on a single WSDL file containing or including information
about all services available in an enterprise. Here the CGI invokes
the XSLT processor in a system
call. This clumsy
setup is good for prototyping but not production use. A better
solution would use the Perl modules XML::LibXML
and XML::LibXSLT
. An even better architecture
would use a more sophisticated server-side XSLT-enabled solution such
as Cocoon. To focus
on the XSLT and WSDL aspects of this example, and not the CGI
architecture, we took a simplistic approach.
The site’s main page is generated by a CGI that shows the user available services and ports. See the discussion for explanations of services and ports. It uses the following Perl CGI frontend to Saxon:
#!c:/perl/bin/perl print "Content-type: text/html " ; system "saxon StockServices.wsdl wsdlServiceList.xslt" ;
The transformation builds a form containing all available services, ports, bindings, and port types:
<?xml version="1.0" encoding="UTF-8"?> <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:wsdl="http://schemas.xmlsoap.org/wsdl/" xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/" xmlns:http="http://schemas.xmlsoap.org/wsdl/http/" xmlns:mime="http://schemas.xmlsoap.org/wsdl/mime/" exclude-result-prefixes="wsdl soap http mime"> <xsl:output method="html"/> <xsl:template match="/"> <html> <head> <title>Available Services at ACME Web Services, Inc.</title> <xsl:comment>WSDL Documentation: Generated by wsdlServiceList.xslt </xsl:comment> </head> <body> <xsl:apply-templates/> </body> </html> </xsl:template> <xsl:template match="wsdl:definitions"> <h1>Available Services at ACME Web Services, Inc.</h1> <br/> <form name="QueryServiceForm" method="post" action="QueryService.pl"> <table> <tbody> <tr> <td>Services</td> <td> <select name="service"> <option value="ALL">ALL</option> <xsl:apply-templates select="wsdl:service"/> </select> </td> </tr> <tr> <td>Ports</td> <td> <select name="port"> <option value="ALL">ALL</option> <xsl:apply-templates select="wsdl:service/wsdl:port"/> </select> </td> </tr> <tr> <td>Bindings</td> <td> <select name="binding"> <option value="ALL">ALL</option> <xsl:apply-templates select="wsdl:binding"/> </select> </td> </tr> <tr> <td>Port Types</td> <td> <select name="portType"> <option value="ALL">ALL</option> <xsl:apply-templates select="wsdl:portType"/> </select> </td> </tr> </tbody> </table> <br/> <button type="submit" name="submit">Query Services</button> </form> </xsl:template> <xsl:template match="wsdl:service | wsdl:port | wsdl:binding | wsdl:portType"> <option value="{@name}"><xsl:value-of select="@name"/></option> </xsl:template> </xsl:stylesheet>
Users can select a combination of service, port, bind, and port type on which they want more detailed information. When submitted, another small Perl CGI extracts the input and invokes another XSLT transform:
#!/perl/bin/perl use warnings; use strict; use CGI qw(:standard); my $query = new CGI ; my $service = $query->param('service'), my $port = $query->param('port'), my $binding = $query->param('binding'), my $portType = $query->param('portType'), print $query->header('text/html'), system "saxon StockServices.wsdl QueryService.xslt service=$service port=$port binding=$binding portType=$portType"
This transformation extracts the services that match the
user’s criteria and displays detailed information
about the service. The first part of the stylesheet normalizes some
parameters. It does so because an attribute reference in a WSDL file
could contain a namespace prefix, but the
@name
attribute of the element it references does not. The same reasoning
applies to key construction:
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE xslt [ <!ENTITY TNSPREFIX "'acme:'"> ]> <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:xsd="http://www.w3.org/2000/10/XMLSchema" xmlns:wsdl="http://schemas.xmlsoap.org/wsdl/" xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/" xmlns:http="http://schemas.xmlsoap.org/wsdl/http/" xmlns:mime="http://schemas.xmlsoap.org/wsdl/mime/"> <!--Query parameters --> <xsl:param name="service" select="'ALL'"/> <xsl:param name="port" select="'ALL'"/> <xsl:param name="binding" select="'ALL'"/> <xsl:param name="portType" select="'ALL'"/> <!-- A technique (or a hack) to make variables empty --> <!-- if the corrsponding parameter is ALL or otherwise --> <!-- the concatenation of &TNSPREFIX and the paramter --> <!-- For example, number($service = 'ALL') * 99999 + 1 --> <!-- will be 1 if $service is not equal to 'ALL' but --> <!-- 100000 if it is and hence beyond the lenght of the--> <!-- string which causes substring to return empty --> <!-- This is all to simplify cross-referencing. --> <xsl:variable name="serviceRef" select="substring(concat(&TNSPREFIX;,$service), number($service = 'ALL') * 99999 + 1)"/> <xsl:variable name="portRef" select="substring(concat(&TNSPREFIX;,$port), number($port = 'ALL') * 99999 + 1)"/> <xsl:variable name="bindingRef" select="substring(concat(&TNSPREFIX;,$binding), number($binding = 'ALL') * 99999 + 1)"/> <xsl:variable name="portTypeRef" select="substring(concat(&TNSPREFIX;,$portType), number($portType = 'ALL') * 99999 + 1)"/> <!-- These keys simplify and speed up querying --> <xsl:key name="bindings_key" match="wsdl:binding" use="concat(&TNSPREFIX;,@name)"/> <xsl:key name="portType_key" match="wsdl:portType" use="concat(&TNSPREFIX;,@name)"/> <xsl:output method="html"/> <xsl:template match="/"> <html> <head> <title>ACME Web Services, Inc. Query Result</title> <xsl:comment>WSDL Documentation: Generated by wsdlServiceList.xslt </xsl:comment> </head> <body> <xsl:apply-templates select="wsdl:definitions"/> </body> </html> </xsl:template>
Here you simplify the problem of
determining whether
the query found matching services by capturing the result in a
variable and testing its normalized value. If it is empty, then you
know the query failed, and it displays an appropriate message. The
query is a little tricky because the service’s
portType
can be determined only by making two hops
from the service to its binding and then to the
binding’s port type:
<xsl:template match="wsdl:definitions"> <xsl:variable name="result"> <!-- Query services that match the query parameters --> <!-- The portType match is the only complicated part --> <!-- We need to traverse form the service's port, to the binding--> <!-- and then to the binding's type to do the match. Hence --> <!-- the nested key( ) calls --> <xsl:apply-templates select="wsdl:service[ (not($serviceRef) or @name = $service) and (not($portRef) or wsdl:port/@name = $port) and (not($bindingRef) or wsdl:port/@binding = $bindingRef) and (not($portTypeRef) or key('portType_key', key('bindings_key', wsdl:port/@binding)/@type) /@name = $portType)]"/> </xsl:variable> <xsl:choose> <xsl:when test="normalize-space($result)"> <xsl:copy-of select="$result"/> </xsl:when> <xsl:otherwise> <p><b>No Matching Services Found</b></p> </xsl:otherwise> </xsl:choose> </xsl:template>
The rest of the stylesheet is mostly logic that renders the WSDL as HTML table entries. A service may contain more than one port, so be sure to select ports based on the query parameters once more:
<xsl:template match="wsdl:service" mode="display"> <h1><xsl:value-of select="@name"/></h1> <p><xsl:value-of select="wsdl:documentation"/></p> <table border="1" cellpadding="5" cellspacing="0" width="600"> <tbody> <xsl:apply-templates select="wsdl:port[(not($portRef) or @name = $port) and (not($bindingRef) or @binding = $bindingRef)]" mode="display"/> </tbody> </table> </xsl:template>
Use your keys to traverse (do a join) between port and binding, as well as between binding and port type:
<xsl:template match="wsdl:port" mode="display"> <tr> <td style="font-weight:bold" colspan="2" align="center">Port</td> </tr> <tr> <td colspan="2"><h3><xsl:value-of select="@name"/></h3></td> </tr> <tr> <td style="font-weight:bold" width="50">Binding</td> <td><xsl:value-of select="substring-after(@binding,':')"/></td> </tr> <tr> <td style="font-weight:bold" width="50">Address</td> <td><xsl:value-of select="soap:address/@location"/></td> </tr> <tr> <th colspan="2" align="center">Operations</th> </tr> <xsl:apply-templates select="key('bindings_key',@binding)" mode="display"/> </xsl:template> <xsl:template match="wsdl:binding" mode="display"> <xsl:apply-templates select="key('portType_key',@type)" mode="display"> <xsl:with-param name="operation" select="wsdl:operation/@name"/> </xsl:apply-templates> </xsl:template> <xsl:template match="wsdl:portType" mode="display"> <xsl:param name="operation"/> <xsl:for-each select="wsdl:operation[@name = $operation]"> <tr> <td colspan="2"><h3><xsl:value-of select="@name"/></h3></td> </tr> <xsl:if test="wsdl:input"> <tr> <td style="font-weight:bold" width="50">Input</td> <td><xsl:value-of select="substring-after(wsdl:input/@message,':')"/></td> </tr> <xsl:variable name="msgName" select="substring-after(wsdl:input/@message,':')"/> <xsl:apply-templates select="/*/wsdl:message[@name = $msgName]" mode="display"/> </xsl:if> <xsl:if test="wsdl:output"> <tr> <td style="font-weight:bold" width="50">Output</td> <td><xsl:value-of select="substring-after(wsdl:output/@message,':')"/></td> </tr> <xsl:variable name="msgName" select="substring-after(wsdl:output/@message,':')"/> <xsl:apply-templates select="/*/wsdl:message[@name = $msgName]" mode="display"/> </xsl:if> </xsl:for-each> </xsl:template> <xsl:template match="wsdl:message" mode="display"> <xsl:variable name="dataType" select="substring-after(wsdl:part[@name='body']/@element,':')"/> <tr> <td colspan="2"> <xsl:apply-templates select="/*/wsdl:types/*/*[@name=$dataType]" mode="display"> <xsl:with-param name="initial-newline" select="false( )"/> </xsl:apply-templates> </td> </tr> </xsl:template>
This code renders the messages’ XSD schema as XML within HTML. The actual schema is probably the most informative piece of information you can display in order to document the data types associated with the service input and output messages:
<xsl:template match="*" mode="display"> <xsl:param name="initial-newline" select="true( )"/> <xsl:if test="$initial-newline"> <xsl:call-template name="newline"/> </xsl:if> <!-- open tag --> <a><</a> <a><xsl:value-of select="name(.)" /> </a> <!-- Output attributes --> <xsl:for-each select="@*"> <a><xsl:text> </xsl:text><xsl:value-of select="name(.)" /> </a> <a>="</a> <xsl:value-of select="." /> <a>"</a> </xsl:for-each> <xsl:choose> <xsl:when test="child::node( )"> <!-- close start tag --> <a>></a> <xsl:apply-templates mode="display"/> <xsl:call-template name="newline"/> <!-- closing tag --> <a><</a> <a>/<xsl:value-of select="name(.)" /></a> </xsl:when> <xsl:otherwise> <a>/</a> </xsl:otherwise> </xsl:choose> <a>></a> </xsl:template> <!-- Add a newline and then indent based on depth within the schema --> <xsl:template name="newline"> <br/> <xsl:for-each select="ancestor::xsd:*[not(self::xsd:schema)]"> <xsl:text>    </xsl:text> </xsl:for-each> </xsl:template> </xsl:stylesheet>
Chapter 10 spoke of a message repository that can capture metadata about messages exchanged in a client-server architecture. WSDL is an example of a kind of message-repository syntax for a web-service-based architecture. Here you can also use the same WSDL to generate code and documentation. This example focuses on serving up documentation.
WSDL describes web services in terms of associations between services
and ports. In WSDL terms,
a
port
is a single end point defined as a
combination of a binding and a network address. A
binding
is a concrete protocol and data-format
specification for a particular port type. A port
type
defines an abstract set of operations supported by
one or more end point. An operation
is an action
that a service can perform, and it is defined in terms of a message
with a corresponding data payload. The data payload is usually
described in terms of an XSD schema. The following code is a partial
example of a simple WSDL describing a set of services offered within
the Acme Corporation:
<definitions name="StockServices" targetNamespace="http://acme.com/services.wsdl" xmlns:acme="http://acme.com/services.wsdl" xmlns:xsd1="http://acme.com/stockquote. xsd" xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/" xmlns="http://schemas. xmlsoap.org/wsdl/">
WSDL uses XSD schema as the default method of specifying the message data types exchanged in a service architecture:
<types> <schema targetNamespace="http://acme.com/services.xsd" xmlns="http://www.w3.org/2000/10/XMLSchema"> <element name="Ticker"> <complexType> <all> <element name="tickerSymbol" type="string"/> </all> </complexType> </element> <element name="TradePrice"> <complexType> <all> <element name="price" type="float"/> </all> </complexType> </element> <element name="CompanyName"> <complexType> <all> <element name="company" type="string"/> </all> </complexType> </element> <element name="ServicesInfo"> <complexType> <sequence> <element name="Services"> <complexType> <sequence> <element name="Service" type="string" minOccurs="0" maxOccurs="unbounded"/> </sequence> </complexType> </element> <element name="Ports"> <complexType> <sequence> <element name="Port" type="string" minOccurs="0" maxOccurs="unbounded"/> </sequence> </complexType> </element> <element name="Bindings"> <complexType> <sequence> <element name="Binding" type="string" minOccurs="0" maxOccurs="unbounded"/> </sequence> </complexType> </element> <element name="PortTypes"> <complexType> <sequence> <element name="Port" type="string" minOccurs="0" maxOccurs="unbounded"/> </sequence> </complexType> </element> </sequence> </complexType> </element> <element name="ServicesQuery"> <complexType> <all> <element name="Service" type="string"/> <element name="Port" type="string"/> <element name="Binding" type="string"/> <element name="PortType" type="string"/> </all> </complexType> </element> <element name="ServicesResponse"> <complexType> <sequence> <any/> </sequence> </complexType> </element> </schema> </types>
Messages bind names to data types and are used to specify what is communicated within the service architecture:
<message name="GetLastTradePriceInput"> <part name="body" element="xsd1:Ticker"/> </message> <message name="GetLastTradePriceOutput"> <part name="body" element="xsd1:TradePrice"/> </message> <message name="GetCompanyInput"> <part name="body" element="xsd1:Ticker"/> </message> <message name="GetCompanyOutput"> <part name="body" element="xsd1:CompanyName"/> </message> <message name="GetTickerInput"> <part name="body" element="xsd1:CompanyName"/> </message> <message name="GetTickerOutput"> <part name="body" element="xsd1:Ticker"/> </message> <message name="GetServicesInput"/> <message name="GetServicesOutput"> <part name="body" element="xsd1:ServicesInfo"/> </message> <message name="QueryServicesInput"> <part name="body" element="xsd1:ServicesQuery"/> </message> <message name="QueryServicesOutput"> <part name="body" element="xsd1:ServicesRespose"/> </message>
The port types specify which operations are available at a particular service end point (port). This section describes two such port types: one that gives stock quote information and another that describes a service discovery service. The service discovery service is the service you implement in this example, except that it would be used by a program other than a browser:
<portType name="StockPortType"> <operation name="GetLastTradePrice"> <input message="acme:GetLastTradePriceInput"/> <output message="acme:GetLastTradePriceOutput"/> </operation> <operation name="GetTickerFromCompany"> <input message="acme:GetTickerInput"/> <output message="acme:GetCompanyOutput"/> </operation> <operation name="GetCompanyFromTicker"> <input message="acme:GetCompanyInput"/> <output message="acme:GetTickerOutput"/> </operation> </portType> <portType name="ServicePortType"> <operation name="GetServices"> <input message="acme:GetServicesInput"/> <output message="acme:GetServicesOutput"/> </operation> <operation name="QueryServices"> <input message="acme:QueryServicesInput"/> <output message="acme:QueryServicesOutput"/> </operation> </portType>
The bindings tie operations to specific protocols. Here you declare that operations are bound to the SOAP protocol:
<binding name="StockQuoteSoapBinding" type="acme:StockPortType"> <soap:binding style="document" transport="http://schemas.xmlsoap.org/soap/http"/> <operation name="GetLastTradePrice"> <soap:operation soapAction="http://acme.com/GetLastTradePrice"/> <input> <soap:body use="literal"/> </input> <output> <soap:body use="literal"/> </output> </operation> </binding> <binding name="StockTickerSoapBinding" type="acme:StockPortType"> <soap:binding style="document" transport="http://schemas.xmlsoap.org/soap/http"/> <operation name="GetTickerFromCompany"> <soap:operation soapAction="http://acme.com/GetTickerSymbol"/> <input> <soap:body use="literal"/> </input> <output> <soap:body use="literal"/> </output> </operation> </binding> <binding name="StockNameSoapBinding" type="acme:StockPortType"> <soap:binding style="document" transport="http://schemas.xmlsoap.org/soap/http"/> <operation name="GetCompanyFromTicker"> <soap:operation soapAction="http://acme.com/GetCompanyName"/> <input> <soap:body use="literal"/> </input> <output> <soap:body use="literal"/> </output> </operation> </binding> <binding name="ServiceListSoapBinding" type="acme:ServicePortType"> <soap:binding style="document" transport="http://schemas.xmlsoap.org/soap/http"/> <operation name="GetServices"> <soap:operation soapAction="http://acme.com/GetServices"/> <input> <soap:body use="literal"/> </input> <output> <soap:body use="literal"/> </output> </operation> </binding>
Two services are advertised here. One is related to stock quotes, and the other to service discovery. Notice how the services organize a set of ports to specify the functionality available though the service:
<service name="StockInfoService"> <documentation>Provides information about stocks.</documentation> <port name="StockQuotePort" binding="acme:StockQuoteSoapBinding"> <soap:address location="http://acme.com/stockquote"/> </port> <port name="StockTickerToNamePort" binding="acme:StockTickerSoapBinding"> <soap:address location="http://acme.com/tickertoname"/> </port> <port name="StockNameToTickerPort" binding="acme:StockNameSoapBinding"> <soap:address location="http://acme.com/nametoticker"/> </port> </service> <service name="ServiceInfoService"> <documentation>Provides information about avaialable services.</documentation> <port name="ServiceListPort" binding="acme:ServiceListSoapBinding"> <soap:address location="http://acme.com/stockquote"/> </port> <port name="ServiceQueryPort" binding="acme:ServiceQuerySoapBinding"> <soap:address location="http://acme.com/tickertoname"/> </port> </service> </definitions>
Using your CGI-based server, you get a service-query screen. You can select the parameters of the query as shown in Figure 11-6.
When you submit the query, you obtain the result shown in Figure 11-7.
The specifications for WSDL are available at http://www.w3.org/TR/wsdl. WSDL is not yet an official W3C recommendation; however, it is quickly gaining recognition and support in the industry. An official Web Services Description Working Group (http://www.w3.org/2002/ws/desc/) is working on what will eventually be a W3C-sanctioned recommendation.
See Recipe 12.16 for ways of embedding XSLT processing into Perl rather than forking off a separate process.