Saturday, March 14, 2009

My first WSDL 2.0 - generating Axis2 client code for a REST API

pencil icon, that"s clickable to start editing the post

The former InfoStructureBase (ISB) along with the catalogue of OIO standards has been replaced with digitaliser.dk. Closing up to a formal version 1.0 the current beta 4 has as the first release a public API. The ISB API was SOAP based with Web Services Security UsernameToken for authentication. The the WSDL's can still be seen in the former repository part:

As annonced in the last release note "Digitaliser.dk - nu med offentligt API og medlemspolitik for grupper og rig tekstredigering" this is based on (OIO)REST with POX responses. The documentation is in the form of endpoints and XML Schemas for the responses, and can be read online: http://api.digitaliser.dk/rest.

I have practically no experience with REST, so I started to google for hints on have to call a REST webservice with Axis2. I did'nt find much and it seem way to complex for likeing. I did found out that REST/POX services can be described with WSDL 2.0 Part: 1 and WSDL Version 2.0 Part 2: Adjuncts. I do not find these as easy reads so I looked for an example and found a post on Keith Chapmans blog "RESTfull Mashup with WSDL 2.0 - WSO2 Mashup Server". The WSDL for this exmaple. Another resource that I haven't really read yet is Lawrence Mandes article on IBM DeveloperWorks called "Describe REST Web services with WSDL 2.0".

Keith's example is quite complex and I wanted something simple to increase the likelihood of succes, so I choose to implement fetching a resource on digitaliser.dk. After cleaning up the example WSDL, removing complex parts, renaming and some trial and error I ended up with this WSDL:

    1 <?xml version="1.0"?>
    2 <wsdl2:description
    3   xmlns:wsdl2="http://www.w3.org/ns/wsdl"
    4   xmlns:wrpc="http://www.w3.org/ns/wsdl/rpc"
    5   xmlns:wsaw="http://www.w3.org/2006/05/addressing/wsdl"
    6   xmlns:whttp="http://www.w3.org/ns/wsdl/http"
    7   xmlns:wsdlx="http://www.w3.org/ns/wsdl-extensions"
    8   xmlns:xs="http://www.w3.org/2001/XMLSchema"
    9   xmlns:api1="http://api.digitaliser.dk/rest/1.0"
   10   xmlns:xtns="http://rep.oio.dk/sweetxml.org/xml/xsd/20090306/digitaliser.dk/1.0/"
   11   xmlns:wtns="http://rep.oio.dk/sweetxml.org/xml/wsdl/20090306/digitaliser.dk/1.0/"
   12   targetNamespace="http://rep.oio.dk/sweetxml.org/xml/wsdl/20090306/digitaliser.dk/1.0/">
   13   <wsdl2:types>
   14     <xs:import
   15       schemaLocation="http://api.digitaliser.dk/schemas/rest/1.0/resource.xsd"
   16       namespace="http://api.digitaliser.dk/rest/1.0" />
   17     <xs:schema
   18       attributeFormDefault="unqualified"
   19       elementFormDefault="qualified"
   20       targetNamespace="http://rep.oio.dk/sweetxml.org/xml/xsd/20090306/digitaliser.dk/1.0/">
   21       <xs:element
   22         name="identifier"
   23         type="xs:string" />
   24       <xs:complexType
   25         name="idWrapperType">
   26         <xs:sequence>
   27           <xs:element
   28             name="id"
   29             type="xs:string" />
   30         </xs:sequence>
   31       </xs:complexType>
   32       <xs:element
   33         name="idWrapper"
   34         type="xtns:idWrapperType" />
   35     </xs:schema>
   36   </wsdl2:types>
   37   <wsdl2:interface
   38     name="ResourceInterface">
   39     <wsdl2:operation
   40       name="getResourceOperation"
   41       pattern="http://www.w3.org/ns/wsdl/in-out"
   42       wsdlx:safe="true">
   43       <wsdl2:input
   44         element="xtns:idWrapper"
   45         wsaw:Action="" />
   46       <wsdl2:output
   47         element="api1:resource"
   48         wsaw:Action="" />
   49     </wsdl2:operation>
   50   </wsdl2:interface>
   51   <wsdl2:binding
   52     name="ResourceBinding"
   53     interface="wtns:ResourceInterface"
   54     type="http://www.w3.org/ns/wsdl/http">
   55     <wsdl2:operation
   56       ref="wtns:getResourceOperation"
   57       whttp:method="GET"
   58       whttp:location="resources/{id}" />
   59   </wsdl2:binding>
   60   <wsdl2:service
   61     name="ResourceService"
   62     interface="wtns:ResourceInterface">
   63     <wsdl2:endpoint
   64       name="RestEndpoint"
   65       binding="wtns:ResourceBinding"
   66       address="http://api.digitaliser.dk/rest/resources" />
   67   </wsdl2:service>
   68 </wsdl2:description>

The three major things for me where:

  • Eksternal schemas can/must now be imported directly in the types section - no need for a wrapper schema anymore (and it will not work). Look at example 3-4 in the WSDL 2.0 primer or Importing XML Schema in the standard.
  • Even though it would be the easiest just to have a simpletype element for the request parameter in this example, I have to use a wrapper. The reason I guess is that this model can handle any number of parameters, where the case with just one is the simplest.
  • @signature attribute does a reference to child elements in the wrapper defined in the former bullet.

With this WSDL I could generate Axis2 stub code with xmlbenas binding with the ant codegen utility:

<codegen wsdlfilename="${resource.wsdl}" databindingName="xmlbeans" output="${generated.dir}" wsdlVersion="2.0" />

from here it's easy writing a simple client program. The request param is a little clunky since I'm using XMLBeans, but that's fine with me.

    1 package org.sweetxml.rest;
    2 
    3 import java.rmi.RemoteException;
    4 import java.util.ArrayList;
    5 import java.util.Collection;
    6 import java.util.Iterator;
    7 
    8 import org.apache.axis2.Constants;
    9 import org.apache.xmlbeans.XmlOptions;
   10 import org.apache.xmlbeans.XmlValidationError;
   11 
   12 import dk.digitaliser.api.rest._1_0.ResourceDocument;
   13 import dk.oio.rep.sweetxml_org.xml.wsdl._20090306.digitaliser_dk._1_0.ResourceServiceStub;
   14 import dk.oio.rep.sweetxml_org.xml.xsd._20090306.digitaliser_dk._1_0.IdWrapperDocument;
   15 
   16 public class ResourceClient {
   17 
   18   public static void main(String[] args) throws RemoteException {
   19     String resourceIdentifier = "42079";
   20     ResourceServiceStub stub = new ResourceServiceStub();
   21     stub._getServiceClient().getOptions().setProperty(Constants.Configuration.TRANSPORT_URL, "http://localhost:9999/rest/");
   22 
   23     IdWrapperDocument requestIdentifierDoc = IdWrapperDocument.Factory.newInstance();
   24     requestIdentifierDoc.addNewIdWrapper().setId(resourceIdentifier);
   25 
   26     ResourceDocument respDoc = stub.getResourceOperation(requestIdentifierDoc);
   27     System.out.println("Title on resource " + resourceIdentifier + " is : " + respDoc.getResource().getTitle());
   28     validate(respDoc);
   29   }
   30 
   31   private static void validate(ResourceDocument resource) {
   32     Collection<XmlValidationError> errors = new ArrayList<XmlValidationError>();
   33 
   34     XmlOptions validationOptions = new XmlOptions();
   35     validationOptions.setErrorListener(errors);
   36     validationOptions.setLoadLineNumbers();
   37     validationOptions.setSaveNamespacesFirst();
   38     validationOptions.setSavePrettyPrint();
   39     validationOptions.setSaveAggressiveNamespaces();
   40     validationOptions.setUseDefaultNamespace();
   41 
   42     boolean isValid = false;
   43     isValid = resource.validate(validationOptions);
   44 
   45     if (!isValid) {
   46       Iterator<XmlValidationError> iter = errors.iterator();
   47       System.out.println(errors);
   48       while (iter.hasNext()) {
   49         XmlValidationError err = iter.next();
   50         System.out.println(err);
   51       }
   52     } else {
   53       System.out.println("Respons is XML Schema Valid");
   54     }
   55   }
   56 }

I added a very basic validation method that should be enforced if I were to use this for real. Notice that I changed the endpoint to let it run through TCPMon and now finally lets have a look at how it looks like on the wire.

The request

Since this is REST and I'm only fetching a resource htis is just a GET with only HTTP headers (and no SOAPAction):

GET /rest/resources/42079 HTTP/1.1
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
SOAPAction: ""
User-Agent: Axis2
Host: api.digitaliser.dk:9999

The reponse

The HTTP headers sent back is just plain, except that were informed that it's based on the Noelios Restlet Engine (NRE).

HTTP/1.1 200 OK
Server: Apache-Coyote/1.1
Date: Fri, 13 Mar 2009 19:11:44 GMT
Accept-Ranges: bytes
Server: Noelios-Restlet-Engine/1.1..2
Content-Type: text/xml;charset=UTF-8
Transfer-Encoding: chunked

Here's hte body sent back containg the resource strcuture as defined in the XML Schema.

    1 <?xml version="1.0" encoding="UTF-8" standalone="yes"?>
    2 <ns1:resource
    3   xmlns:ns1="http://api.digitaliser.dk/rest/1.0">
    4   <ns1:title>Demo WSDL for a simple service for OIOUBL Simpel Ordering</ns1:title>
    5   <ns1:body>A naive WSDL for a demonstration service based on OIOUBL Simple Ordering to get a
    6     realistic complexity.</ns1:body>
    7   <ns1:ownerGroup
    8     ns1:ref="http://api.digitaliser.dk/rest/groups/42078">
    9     <ns1:title>Sweetxml.org</ns1:title>
   10   </ns1:ownerGroup>
   11   <ns1:createdDate>2009-01-22T08:38:59.585+01:00</ns1:createdDate>
   12   <ns1:published
   13     ns1:state="published"
   14     ns1:date="2009-01-22T08:42:28.616+01:00" />
   15   <ns1:taggedBy>
   16     <ns1:tag
   17       ns1:taggedItem="http://api.digitaliser.dk/rest/resources/42079"
   18       ns1:owner="http://api.digitaliser.dk/rest/users/19802"
   19       ns1:ref="http://api.digitaliser.dk/rest/tags/246143">
   20       <ns1:label>service</ns1:label>
   21     </ns1:tag>
   22     <ns1:tag
   23       ns1:taggedItem="http://api.digitaliser.dk/rest/resources/42079"
   24       ns1:owner="http://api.digitaliser.dk/rest/users/19802"
   25       ns1:ref="http://api.digitaliser.dk/rest/tags/246139">
   26       <ns1:label>demo</ns1:label>
   27     </ns1:tag>
   28     <ns1:tag
   29       ns1:taggedItem="http://api.digitaliser.dk/rest/resources/42079"
   30       ns1:owner="http://api.digitaliser.dk/rest/groups/42078"
   31       ns1:ref="http://api.digitaliser.dk/rest/tags/246996">
   32       <ns1:label>WSDL</ns1:label>
   33     </ns1:tag>
   34     <ns1:tag
   35       ns1:taggedItem="http://api.digitaliser.dk/rest/resources/42079"
   36       ns1:owner="http://api.digitaliser.dk/rest/users/19802"
   37       ns1:ref="http://api.digitaliser.dk/rest/tags/246145">
   38       <ns1:label>webservice</ns1:label>
   39     </ns1:tag>
   40     <ns1:tag
   41       ns1:taggedItem="http://api.digitaliser.dk/rest/resources/42079"
   42       ns1:owner="http://api.digitaliser.dk/rest/groups/42078"
   43       ns1:ref="http://api.digitaliser.dk/rest/tags/246995">
   44       <ns1:label>OIOUBL</ns1:label>
   45     </ns1:tag>
   46   </ns1:taggedBy>
   47   <ns1:artefacts>
   48     <ns1:artefact
   49       ns1:ref="http://api.digitaliser.dk/rest/resources/42079/artefacts/SX_OIOUBLorderDemo.wsdl">
   50       <ns1:title>SX_OIOUBLorderDemo.wsdl</ns1:title>
   51     </ns1:artefact>
   52   </ns1:artefacts>
   53   <ns1:uri>http://api.digitaliser.dk/rest/resources/42079</ns1:uri>
   54   <ns1:versionGroup
   55     ns1:ref="http://api.digitaliser.dk/rest/versiongroups/ff5aba78-c20c-4c70-9190-84527b81d731" />
   56   <ns1:classificationInstance />
   57   <ns1:version></ns1:version>
   58 </ns1:resource>

Summary

I was very pleased that I was able to acces the REST API with Axis2, and that I was able to do it in contract-first style. Even more I was gladly surprised that WSDL 2.0 had the functionality to describe it well, since my only prior knowledge was from articles going against WSDL 2.0

2 comments :

Keith Chapman said...

Its nice to hear that you found my POST useful. The reason the example is complex is because I was trying to show how WSDL 2.0 could be used to describe this RESTfull scenario.

Here is another example scenario where I've used a hybrid approach.

Sweetxml said...

Hi Keith

Your post was certainly very helpful and a nice read. Please excuse me if it could be read as is your WSDL was unessary, you were realizing a much more complex scenario.
Speaking of complex I've run into a problem trying to create the WSDL2 for the publish part of the API. For any new resource it's a two step process, where the first involves caling "/rest/objectid" which is easy to describe BUT the response is just the new identifier in "text/plain". Do you know if this can be modelled in WSDL 2.0/Axis2, since this response is schema/element-less.