Technology Musings

Creating a WSDL file for a RESTful Web Service

JB

NOTE - this is about XML-based REST services.  There *might* be a way to write WSDL for JSON, but I don't know it yet.

Most of my work is done in RESTful environments.  However, the Microsoft world seems to favor SOAP.  REST is simple.  It is so simple, that you don't even really need special APIs for the different services.  You just need the URL and the documentation.  However, SOAP is complex, and requires complex tooling. SOAP services are described using the WSDL format.  Usually what happens is that a program reads the WSDL file and spits out stubs that you can code against directly.

In any case, the people that come from a SOAP environment don't realize that you can do cool stuff without tooling, and the idea that I could just give them a URL to hit and they could just hit it directly boggles their mind.  So, if I have a handy-dandy REST service, they still want to ask me for the WSDL file.  There's really no reason, but that's what they want.  So, thankfully, WSDL 2 allows for us to create WSDL descriptions of RESTful services.

So, before I describe how WSDL works, let me first say that, if you are used to the simplicity of REST coding, WSDL will blow your mind.  WSDL is built around the complexities of SOAP.  There are a lot of things where you will say, "why did they put in that extra layer?"  The answer is, "because the SOAP protocol was built by crack addicts."

WSDL is grouped into 4 parts - services, bindings, interfaces, and types.

An interface is a list of operations you might want to perform.  This is, essentially, the API itself.  Our API will be for retrieving form submissions to our website.  The interface is a list of operations, where each operation is an individual API call.  In our example, we will have a "list" and a "get" operation in our inteface.  The interface relies on types in order to define the input and output of the operations.

A binding tells you how to make an API call for an operation.  This is significantly simpler in REST than in SOAP, as you really only need the URL path and the method (this is the relative URL - the absolute path will be defined in the service).  The binding will have a operation tag for each operation it binds.

A service defines where on the web you can go to in order to find the interfaces.  It defines a set of endpoints, each of which tells the top-level URL of the resources, like http://www.example.com/api, and what interfaces they support and which bindings to use on them (telling both the interface and the binding seems redundant to me, but then again so does writing a WSDL file to begin with).

So, below, is a simple REST web service.  I've excluded the "types" section, which I will discuss later.

<?xml version="1.0" ?>
<wsdl2:description
xmlns:wsdl2="http://www.w3.org/ns/wsdl"
xmlns:wsaw="http://www.w3.org/2006/05/addressing/wsdl"
xmlns:whttp="http://www.w3.org/ns/wsdl/http"
xmlns:xs="http://www.w3.org/2001/XMLSchema"
xmlns:myexample="http://www.example.com/api"
targetNamespace="http://www.example.com/api"
<wsdl2:types>
<xs:schema
attributeFormDefault="unqualified"
elementFormDefault="unqualified">
<!-- omitted -->
</xs:schema>
</wsdl2:types>

<wsdl2:interface name="FormSubmissionsInterface">
<wsdl2:operation
name="listFormSubmissionsOperation"
pattern="http://www.w3.org/ns/wsdl/in-out"
whttp:method="GET">
<wsdl2:input element="formSubmissionSearchParameters" />
<wsdl2:output element="response" />
</wsdl2:operation>
<wsdl2:operation
name="getFormSubmissionOperation"
pattern="http://www.w3.org/ns/wsdl/in-out"
whttp:method="GET">
<wsdl2:input element="formSubmissionIdentifier" />
<wsdl2:output element="response" />
</wsdl2:operation>
</wsdl2:interface>

<wsdl2:binding name="FormSubmissionBinding"
interface="myexample:FormSubmissionBinding"
type="http://www.w3.org/ns/wsdl/http">
<wsdl2:operation
ref="myexample:getFormSubmissionOperation"
whttp:method="GET"
whttp:location="form_submissions/{id}.xml"
/>
<wsdl2:operation
ref="myexample:listFormSubmissionsOperation"
whttp:method="GET"
whttp:location="form_submissions.xml"
/>
</wsdl2:binding>

<wsdl2:service
name="FormSubmissionsServiceSSL"
interface="myexample:FormSubmissionsInterface">
<wsdl2:endpoint
name="FormSubmissionEndpoint"
binding="myexample:FormSubmissionBinding"
address="https://www.example.com/api"
/>
</wsdl2:service>
<wsdl2:service
name="FormSubmissionsService"
interface="myexample:FormSubmissionsInterface">
<wsdl2:endpoint
name="FormSubmissionEndpoint"
binding="myexample:FormSubmissionBinding"
address="http://www.example.com/api"
/>
</wsdl2:service>
</wsdl2:description>

So, starting at the bottom, we have 2 services defined - an HTTP service and an HTTPS service.  They both use the same binding and interface.  So, they are called exactly the same way, buy have a different base URL.  Depending on your client, the service name often corresponds to the main class name used to access the service.

The services implement the myexample:FormSubmissionsInterface.  This interface defines two operations: listFormSubmissionsOperation and getFormSubmissionsOperation.  Each one defines its input and output parameters.  Now, the output parameter is just a regular XML element from your schema (defined in the "types" section).  The input parameter is special.  It, too, is defined in your types section, but, when bound to an HTTP endpoint, rather than being treated as an XML document, it will be treated as a combination of URL pieces and query string parameters.  Here is mine:

                       <xs:element name="formSubmissionSearchParameters">
<xs:complexType>
<xs:all>
<xs:element name="since" type="xs:dateTime" minOccurs="0" />
<xs:element name="since_id" type="xs:integer" minOccurs="0" />
<xs:element name="until" type="xs:dateTime" minOccurs="0" />
<xs:element name="until_id" type="xs:integer" minOccurs="0" />
</xs:all>
</xs:complexType>
</xs:element>

Each of the elements under xs:all actually become query-string parameters.  WSDL treats it like an XML document, because of its SOAP roots.  But, when we specify an HTTP binding, then this gets transmogrified from an XML document into query-string parameters.  I have a separate type for the "get" operation, which defines the ID number of the form submission I want:

			<xs:element name="formSubmissionIdentifier">
<xs:complexType>
<xs:sequence>
<xs:element name="id" type="xs:integer" />
</xs:sequence>
</xs:complexType>
</xs:element>

As we will see, in the bindings section, we can move a named parameter into the URL itself, rather than just being sent as a query string.

The output is simply an XML document, whose structure is defined in the types section.  I would need to teach you XSD documents to tell you how this works, but you can google it yourself.  The one thing I will say is that because my result document does not use namespaces at all, in the xs:schema declaration I set elementFormDefault to "unqualified".

The binding is where we tell WSDL that we are using REST rather than SOAP.  We tell it what interface we are binding, and the fact that we are using an HTTP binding.  Then, for each operation, we define the HTTP method and relative path for the binding.  We can substitute in our parameters into the URL path by saying {name-of-xml-input-element} in the location.  Any parameters given which are not inserted into the URL path will be put in the query string.  So, for instance

whttp:location="form_submissions/{id}.xml"

Says to take the "id" element and substitute it in the URL.  The other location,

whttp:location="form_submissions.xml"

just puts all of the parameters in the query string.

So that's it!  That's a WSDL file for a REST service!

I tested my service and WSDL using AXIS 2's WSDL2JAVA command:

wsdl2java.sh -uri path/to/wsdl -wv 2

Then I created a simple TestMe.java to make sure it ran right.  The example also shows how to use HTTP Basic Authentication with the service:

import java.rmi.RemoteException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.math.*;

import org.apache.axis2.databinding.ADBBean;
import org.apache.axis2.Constants;
import org.apache.xmlbeans.XmlOptions;
import org.apache.xmlbeans.XmlValidationError;
import org.apache.axis2.client.*;


import com.example.www.FormSubmissionsServiceStub;
import com.example.www.FormSubmissionsServiceStub.*;

import org.apache.axis2.transport.http.*;

public class TestMe {
public static void main(String[] args) throws RemoteException {
/* Setup Service */
FormSubmissionsServiceStub stub = new FormSubmissionsServiceStub();

/* If you need authentication */
HttpTransportProperties.Authenticator basicAuth = new HttpTransportProperties.Authenticator();
basicAuth.setUsername("USERNAMEHERE");
basicAuth.setPassword("PASSHERE");
basicAuth.setPreemptiveAuthentication(true);
final Options clientOptions = stub._getServiceClient().getOptions();
clientOptions.setProperty(HTTPConstants.AUTHENTICATE, basicAuth);

/* Setup Single Get */
BigInteger subm_id = BigInteger.valueOf(500);
FormSubmissionIdentifier myid = new FormSubmissionIdentifier();
myid.setId(subm_id);
System.out.println(TestMe.debugAxisObject(myid));

/* Run Single Get */
Response r = stub.getFormSubmissionOperation(myid);

/* Setup Multi Get */
FormSubmissionSearchParameters p = new FormSubmissionSearchParameters();
p.setSince_id(BigInteger.valueOf(3200));
System.out.println(TestMe.debugAxisObject(p));

/* Run Multi Get */
Response r2 = stub.listFormSubmissionsOperation(p);
}
}

Note that "Response" is not a generic Java class, it is actually com.example.www.FormSubmissionsServiceStub.Response, because the name of the output element is "response".

Other Resources: