Wednesday, September 7, 2011

Improvements in the WADL client generator

I have recently been working on bringing the useful WADL client generator as originally penned by Marc Hadley more in line with currently technology. I have updated it so that it now uses the Jersey client and also I have made a few modifications to make it easier to use. There is plenty of documentation on the WADL site on how to run the tool in various configurations, but I thought it worth while just to show a simple example of what you might expect.

You can download a snapshot of version 1.1 from here, and hopefully soon a final version of 1.1 will be avaliable here if I have guessed the URL correctly. If you pick the version after the 6th of September it should contain all the bits your need.

So as an example we have a really simple hello world service that has a path param to select the correct languauge, the WADL and the Schema generated in Jersey 1.9 are as follows:


<!-- application.wadl -->

<?xml version = '1.0' encoding = 'UTF-8' standalone = 'yes'?>
<application xmlns="http://wadl.dev.java.net/2009/02">
    <doc xmlns:jersey="http://jersey.java.net/" jersey:generatedBy="Jersey: 1.9 09/02/2011 11:17 AM"/>
    <grammars>
        <include href="application.wadl/xsd0.xsd">
            <doc title="Generated" xml:lang="en"/>
        </include>
    </grammars>
    <resources base="http://localhost:7101/cr/jersey/">
        <resource path="webservice">
            <resource path="{lang}">
                <param xmlns:xs="http://www.w3.org/2001/XMLSchema" name="lang" style="template" type="xs:string"/>
                <resource path="greeting">
                    <method id="hello" name="GET">
                        <response>
                            <representation xmlns:ns2="http://example.com/hello" element="ns2:bean" mediaType="*/*"/>
                        </response>
                    </method>
                </resource>
            </resource>
        </resource>
    </resources>
</application>

<!-- application.wadl/xsd0.xsd -->

<?xml version = '1.0' standalone = 'yes'?>
<xs:schema version="1.0" targetNamespace="http://example.com/hello" xmlns:tns="http://example.com/hello" xmlns:xs="http://www.w3.org/2001/XMLSchema">

  <xs:element name="bean" type="tns:bean"/>

  <xs:complexType name="bean">
    <xs:sequence>
      <xs:element name="message" type="xs:string" minOccurs="0"/>
    </xs:sequence>
  </xs:complexType>
</xs:schema>

Running this through the wadl tool you end up with a java class that look like this along with the relevant JAX-B classes for the schema type Bean.

package client;

import java.util.HashMap;

import javax.annotation.Generated;

import javax.ws.rs.core.UriBuilder;

import com.example.hello.Bean;

import com.sun.jersey.api.client.Client;
import com.sun.jersey.api.client.GenericType;
import com.sun.jersey.api.client.WebResource;


/**
 *
 */
@Generated(value = { "wadl|http://localhost:7101/cr/jersey/application.wadl" },
           comments = "wadl2java, http://wadl.java.net", date = "2011-09-07T11:53:39.206+01:00")
public class Localhost_CrJersey {


    public static Localhost_CrJersey.Webservice webservice(Client client) {
        return new Localhost_CrJersey.Webservice(client);
    }

    public static Localhost_CrJersey.Webservice webservice() {
        return webservice(Client.create());
    }

    public static class Webservice {

        private Client _client;
        private UriBuilder _uriBuilder;
        private HashMap<String, Object> _templateAndMatrixParameterValues;

        /**
         * Create new instance using existing Client instance
         *
         */
        public Webservice(Client client) {
            _client = client;
            _uriBuilder = UriBuilder.fromPath("http://localhost:7101/cr/jersey/");
            _uriBuilder = _uriBuilder.path("webservice");
            _templateAndMatrixParameterValues = new HashMap<String, Object>();
        }

        /**
         * Create new instance
         *
         */
        public Webservice() {
            this(Client.create());
        }

        public Localhost_CrJersey.Webservice.Lang lang(String lang) {
            return new Localhost_CrJersey.Webservice.Lang(_client, lang);
        }

        public static class Lang {

            private Client _client;
            private UriBuilder _uriBuilder;
            private HashMap<String, Object> _templateAndMatrixParameterValues;

            /**
             * Create new instance using existing Client instance
             *
             */
            public Lang(Client client, String lang) {
                _client = client;
                _uriBuilder = UriBuilder.fromPath("http://localhost:7101/cr/jersey/");
                _uriBuilder = _uriBuilder.path("webservice");
                _uriBuilder = _uriBuilder.path("{lang}");
                _templateAndMatrixParameterValues = new HashMap<String, Object>();
                _templateAndMatrixParameterValues.put("lang", lang);
            }

            /**
             * Create new instance
             *
             */
            public Lang(String lang) {
                this(Client.create(), lang);
            }

            /**
             * Get lang
             *
             */
            public String getLang() {
                return ((String)_templateAndMatrixParameterValues.get("lang"));
            }

            /**
             * Set lang
             *
             */
            public Localhost_CrJersey.Webservice.Lang setLang(String lang) {
                _templateAndMatrixParameterValues.put("lang", lang);
                return this;
            }

            public Localhost_CrJersey.Webservice.Lang.Greeting greeting() {
                return new Localhost_CrJersey.Webservice.Lang.Greeting(_client,
                                                                       ((String)_templateAndMatrixParameterValues.get("lang")));
            }

            public static class Greeting {

                private Client _client;
                private UriBuilder _uriBuilder;
                private HashMap<String, Object> _templateAndMatrixParameterValues;

                /**
                 * Create new instance using existing Client instance
                 *
                 */
                public Greeting(Client client, String lang) {
                    _client = client;
                    _uriBuilder = UriBuilder.fromPath("http://localhost:7101/cr/jersey/");
                    _uriBuilder = _uriBuilder.path("webservice");
                    _uriBuilder = _uriBuilder.path("{lang}");
                    _uriBuilder = _uriBuilder.path("greeting");
                    _templateAndMatrixParameterValues = new HashMap<String, Object>();
                    _templateAndMatrixParameterValues.put("lang", lang);
                }

                /**
                 * Create new instance
                 *
                 */
                public Greeting(String lang) {
                    this(Client.create(), lang);
                }

                /**
                 * Get lang
                 *
                 */
                public String getLang() {
                    return ((String)_templateAndMatrixParameterValues.get("lang"));
                }

                /**
                 * Set lang
                 *
                 */
                public Localhost_CrJersey.Webservice.Lang.Greeting setLang(String lang) {
                    _templateAndMatrixParameterValues.put("lang", lang);
                    return this;
                }

                public Bean getAsBean() {
                    UriBuilder localUriBuilder = _uriBuilder.clone();
                    WebResource resource =
                        _client.resource(localUriBuilder.buildFromMap(_templateAndMatrixParameterValues));
                    WebResource.Builder resourceBuilder = resource.getRequestBuilder();
                    resourceBuilder = resourceBuilder.accept("*/*");
                    return resourceBuilder.method("GET", Bean.class);
                }

                public <T> T getAs(GenericType<T> returnType) {
                    UriBuilder localUriBuilder = _uriBuilder.clone();
                    WebResource resource =
                        _client.resource(localUriBuilder.buildFromMap(_templateAndMatrixParameterValues));
                    WebResource.Builder resourceBuilder = resource.getRequestBuilder();
                    resourceBuilder = resourceBuilder.accept("*/*");
                    return resourceBuilder.method("GET", returnType);
                }

                public <T> T getAs(Class<T> returnType) {
                    UriBuilder localUriBuilder = _uriBuilder.clone();
                    WebResource resource =
                        _client.resource(localUriBuilder.buildFromMap(_templateAndMatrixParameterValues));
                    WebResource.Builder resourceBuilder = resource.getRequestBuilder();
                    resourceBuilder = resourceBuilder.accept("*/*");
                    return resourceBuilder.method("GET", returnType);
                }

            }

        }

    }

}

This looks little bit wordy, but things become a little bit simpler if you just look at the interface that has been generated. For example you can now access each part of the path directly using the accessor methods. In this example we are asking for just the english version of the greeting:

public class Test
{
   public static void main(String[] args) {
 
      Bean bean = Localhost_CrJersey.webservice().lang("en").greeting().getAsBean();
      System.out.println(bean.getMessage());

   }
}

Finally for each method that returns a JAX-B element there are also two that take Class.class or GenericType as parameters. This allows the client to access the ClientResponse object, or any other mapping in a way that just wasn't possible in the old API:

public class Test
{
   public static void main(String[] args) {
 
      ClientResponse response = Localhost_CrJersey.webservice().lang("en").greeting().getAs(ClientResponse.class);
      System.out.println(response.getStatus());

   }
}

Now this client currently has limitations in that it doesn't know about HATEOAS and you can't control the URL with deployment descriptors; but it is a start that can hopefully evolve into a more generally useful tool. Also I have played with the constructors for Wadl2Java a little bit to make easier to integrate into IDE tooling, it is the work of a couple of afternoons to wrap this up in a wizard and integrate this into your IDE of choice.

2 comments:

oggie said...

Gerard,

The parameters to the methods are POJOs annotated with @XmlRootElement. That xsd is in the WADL.

The problem is the return object from the method. We return a Jersey Response object that contains a POJO with @XmlRootElement, but it isn't in the WADL.

We do this:

return Response.ok(result).build();

where result is the annotated POJO. But the xsd for the returned xsd isn't there since it's in the Response object.

Gerard Davison said...

Oggie,

You need the JAX-RS Response with the Jersey specific JResponse<> type which is a drop in replacement. This allows you to provide the generic type which can be used to generate the correct schema/WADL.

Gerard