Friday Feb 08, 2008

Jersey Client API

Yesterday i finished refactoring the Jersey Client API and sprinkling it with some JavaDoc. This API was originally being used for Jersey unit testing but i think it is applicable in the larger context of a general RESTful client API.

With some HTTP-based client APIs you have to do a lot of crufty work to make even the simplest request and process a response. In addition they do not really capture the concept of resources and the uniform interface. I think this may be one reason why client-side code generation for creating a 'proxy' to a HTTP-based service is more prevalent than i think it should be. With this client API it is very easy to produce requests and consume responses with resources and the uniform interface at the fore of the API. Hopefully if you play with this API you will see how easy it is to use and why code-generation (and in general relying statically on specific server-side artifacts) is of less importance in this respect.

At the end of this blog is a complete example showing the basic features of the client-side API. This works with the latest Jersey 0.6 build. It contains a resource, called PropertiesResource, a message body reader/writer for reading and writing properties, and some client code in the Main.run method. When this code is executed it should complete successfully with no exception thrown. The example tests simple create, read, update and delete of properties where the server is in control of the property names and the client chooses the property values.

Let's go through the statements of the Main.run method.

Setting up the client:

ClientConfig cc = new DefaultClientConfig();
cc.getProviderClasses().add(PropertiesProvider.class);
Client c = Client.create(cc);

The client can utilize message body readers and writers just like the server-side (in addition to IoC frameworks). I have yet to implement the client-scanning parts so for now it is necessary to explicitly register readers and writers.

Create a resource proxy to the properties resource:

ResourceProxy pResource = c.proxy("http://localhost:9998/");

Now we can start calling methods on the resource proxy:

Properties p = pResource.get(Properties.class);
assertTrue(p.size() == 0);

A GET request should return a Properties instance that has no properties. Notice how the HTTP method is used a bit like RMI but since the interface is uniform it never varies for standard HTTP-based resources. Like the server-side the client side can easily avail of Java types for representations.

Creating a new property resource:

ClientResponse cr = pResource.
  type("text/plain").
  post(ClientResponse.class, "some stuff");
assertTrue(cr.getStatus() == 201);

A POST method is called on the properties resource. The builder pattern is used to set the content type of the request (it is possible for the client to specify what is acceptable using the accept method) and the post method is invoked with the requested return type and the request entity (a String instance). The status code of the response should be 201 (Created). Notice that the client can choose to get all the response information or the representation previously shown for the GET request. The same methods are used as it is the return type that specifies such behavior.

Verify the created property resource: 

ResourceProxy vResource = c.proxy(cr.getLocation());
String content = vResource.get(String.class);
assertTrue("some stuff".equals(content)); 

The response to the POST request (if successful) will contain the location to the newly created property resource. This can be used to create a new resource proxy. A get method is called on that resource proxy and it should return the same information that was POSTed.

Verify the contents of the created property resource are also present in the properties resource:

p = pResource.get(Properties.class);
assertTrue(p.size() == 1);
assertTrue(p.contains("some stuff"));

Delete the created resource:

try {
    vResource.delete();
} catch (UniformInterfaceException e) {
    assertTrue(false);
}

The DELETE method on the property resource is invoked. In this case the client does not send anything and does not request a response. If something other than a successful response occurs an exception will be thrown.

Verify that the property resource has been deleted: 

try {
    content = vResource.get(String.class);
} catch (UniformInterfaceException e) {
    assertTrue(e.getResponse().getStatus() == 404);
}

In this case we expect an exception to be thrown. The ClientResponse can be obtained from the exception to check the status code, which should be 404 (Not Found). But we can also do it like this if we wish:

cr = vResource.get(ClientResponse.class);
assertTrue(cr.getStatus() == 404);

So it is possible to verify the status code explicitly or work under the assumption that success is the norm and errors are the exception.

Hopefully that gives you a flavor of how to use the API from a resource and uniform interface perspective. Given the recent blogs on Jersey and Abdera (see here and here) it should be easy to apply Abdera on the Jersey client side as it was on the server side.

If you want you can still build client requests independently and pass them to the Client instance. You can also add filters for say performing authentication. There is a lot that still can be done. For example, a client requesting a CreatedResponse could verify the status code and location header and throw an exception if it does not conform. Asynchronous requests may be possible by the client requesting say Future<String> or Future<ClientResponse>. I already have a feature request to add request/response progress listeners. Since services may be clients too there is lots of opportunity for such clients to share resources with the server making things more efficient and scalable. So much to do! If you would like to help out just contact me :-)


import com.sun.net.httpserver.HttpServer;
import com.sun.ws.rest.api.client.Client;
import com.sun.ws.rest.api.client.ClientResponse;
import com.sun.ws.rest.api.client.ResourceProxy;
import com.sun.ws.rest.api.client.UniformInterfaceException;
import com.sun.ws.rest.api.client.config.ClientConfig;
import com.sun.ws.rest.api.client.config.DefaultClientConfig;
import com.sun.ws.rest.api.container.httpserver.HttpServerFactory;
import com.sun.ws.rest.spi.resource.Singleton;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URI;
import java.util.Properties;
import java.util.UUID;
import javax.ws.rs.ConsumeMime;
import javax.ws.rs.DELETE;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.PUT;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.ProduceMime;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.HttpContext;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.UriInfo;
import javax.ws.rs.ext.MessageBodyReader;
import javax.ws.rs.ext.MessageBodyWriter;
import javax.ws.rs.ext.Provider;

public class Main {

    @ProduceMime("text/plain")
    @Provider
    public static class PropertiesProvider implements
            MessageBodyWriter<Properties>,
            MessageBodyReader<Properties> {

        public void writeTo(Properties p, MediaType mediaType,
                MultivaluedMap<String, Object> headers, OutputStream out)
                throws IOException {
            p.store(out, null);
        }

        public boolean isWriteable(Class<?> type) {
            return Properties.class.isAssignableFrom(type);
        }

        public long getSize(Properties p) {
            return -1;
        }

        public boolean isReadable(Class<?> type) {
            return Properties.class.isAssignableFrom(type);
        }

        public Properties readFrom(Class<Properties> type, MediaType mediaType,
                MultivaluedMap<String, String> headers, InputStream in)
                throws IOException {
            Properties p = new Properties();
            p.load(in);
            return p;
        }
    }
   
    @Path("/")
    @ProduceMime("text/plain")
    @ConsumeMime("text/plain")
    @Singleton
    public static class PropertiesResource {
        @HttpContext UriInfo uriInfo;
       
        Properties p = new Properties();
       
        @GET public Properties get() {
            return p;
        }
       
        @POST public Response post(String in) {
            String id = UUID.randomUUID().toString();
           
            p.setProperty(id, in);
           
            URI u = uriInfo.getAbsolutePathBuilder().path(id).build();
            return Response.created(u).build();
        }
       
        @Path("{id}") @GET public synchronized String getContent(
                @PathParam("id") String id) {
            String content = p.getProperty(id);
            if (content == null) throw new WebApplicationException(404);
           
            return content;
        }
       
        @Path("{id}") @PUT public synchronized void updateContent(
                @PathParam("id") String id) {
            String content = p.getProperty(id);
            if (content == null) throw new WebApplicationException(404);
           
            p.setProperty(id, content);
        }
       
        @Path("{id}") @DELETE public synchronized void deleteContent(
                @PathParam("id") String id) {
            String content = p.getProperty(id);
            if (content == null) throw new WebApplicationException(404);
           
            p.remove(id);
        }
    }
   
    public static void run() throws Exception {
        // Create the client
        ClientConfig cc = new DefaultClientConfig();
        // Include the properties provider
        cc.getProviderClasses().add(PropertiesProvider.class);
        Client c = Client.create(cc);
       
        // Create the resource proxy to the resource
        ResourceProxy pResource = c.proxy("http://localhost:9998/");
       
        // Get the current properties and verify it is empty
        Properties p = pResource.get(Properties.class);       
        assertTrue(p.size() == 0);
       
        // Create a new property
        ClientResponse cr = pResource.
                type("text/plain").
                post(ClientResponse.class, "some stuff");
        assertTrue(cr.getStatus() == 201);

        // Verify the created resource
        ResourceProxy vResource = c.proxy(cr.getLocation());
        String content = vResource.get(String.class);
        assertTrue("some stuff".equals(content));
       
        // Get the current properties and verify the contents
        p = pResource.get(Properties.class);
        assertTrue(p.size() == 1);
        assertTrue(p.contains("some stuff"));
       
        // Delete the created resource
        try {
            vResource.delete();
        } catch (UniformInterfaceException e) {
            // This will occur when a status >= 300 occurs
            assertTrue(false);
        }
       
        // Verify resource is deleted
        try {
            content = vResource.get(String.class);
        } catch (UniformInterfaceException e) {
            // This will occur when a status >= 300 occurs
            assertTrue(e.getResponse().getStatus() == 404);
        }
       
        // Verify resource is deleted using ClientResponse
        cr = vResource.get(ClientResponse.class);
        assertTrue(cr.getStatus() == 404);
       
        // Verify properties resource contains no properties
        p = pResource.get(Properties.class);
        assertTrue(p.size() == 0);       
    }
   
    private static void assertTrue(boolean v) {
        if (v == false) throw new RuntimeException();
    }
   
    public static void main(String[] args) throws Exception {
        HttpServer server = HttpServerFactory.create("http://localhost:9998/");
        server.start();
       
        try {
            run();
        } finally {
            server.stop(0);
        }
    }
}

Tuesday Oct 30, 2007

Testing TLC

Last week i spent some time giving some needed TLC to Jersey's unit test infrastructure.

There was some really dreadful crufty code hacked together (the kind that you write in 5 minutes just to try something out and and you say i will change it later but somehow you never do and it gets used more and you spend silly amounts of time dealing with it) that was used to enable the unit testing of resource classes in-memory without depending on a HTTP container.

In addition there were unit tests using the light weight HTTP server, which used crufty HttpURLConnection code.

So there were two different sets of crufty code test infrastructure essentially doing the same thing, namely making HTTP-based client requests. Finally i got so fed up with this state of affairs i decided to do something about it.

The result is a simple RESTful client side API for making HTTP requests and processing HTTP responses that reuses classes and concepts from the JAX-RS API and Jersey. See here for a unit test that tests accept processing in memory, and see here for a unit test that tests matrix parameters using the Light Weight HTTP server. Notice that once a ResourceProxy has been obtained the code uses the same API.

Now that there is a common client-side API for making HTTP requests the next step is to further abstract out the configuration/deployment mechanism such that we can have one way of defining a unit test for testing one or more resource classes and be able to deploy them in any container (in-memory, LW HTTP server, or servlet).

Currently this client side API is part of the Jersey implementation at this location (and not the Jersey API). It was very instructive (and also very tedious!) to convert all the relevant unit tests over to using this API as it resulted in many little improvements and ideas to make further improvements (so it can be considered somewhat battle tested in the context of being used as a testing infrastructure API). Once those further improvements have been applied i plan to document it and move it over to the Jersey API for general use. But, if you are feeling brave you can still use it in the latest release. If you do let me know what you think.

While on the subject of testing TLC i watched a great interview on the Scoble show with ZFS inventors Jeff Bonwick and Bill Moore. At some point during the interview Jeff mentions that the testing code coverage of ZFS is over 90% (i cannot recall the exact number, it could be close to 99%) and it allowed them to make major changes to the ZFS code base without the fear of not knowing they had broken something. That point really resonated with me as i have found the Jersey unit tests have given me the confidence to make major internal changes (soon we will need to do some major refactoring of the URI dispatching). However, the code coverage of the Jersey unit tests is not at 99%. A Jersey release is built, using Hudson, every time the source code changes and that release is tested by running the unit tests. Emma code coverage is integrated and below is the trend graph generated by Hudson:

 

As you can see some more TLC is required to increase the code coverage. I have been told that the code coverage is not bad for a newish project, but still it makes me very nervous that at least 40% of methods are not being tested. Even if Jersey is early access we should strive for the highest quality possible for stable releases.

Thursday Jul 19, 2007

True Random Number Generator Goes Online

I read from /. that a true random number generator has gone online. It says:

To achieve high availability of the service, several network access modes are developed, or shall be developed. These include transparent acquisition of random numbers using C/C++ libraries, web services (access over the SOAP protocol), and Mathematica/MATLAB client add-ons.
To enable high security, in future, SSL protocol shall be supported, i.e. all data shall be encrypted, at users request, with user/service certificates.

Perhaps i am being naive (and i have not registered to check out the libraries)... why not make it accessible over HTTP/S using GET that returns a decimal number between 0 and 1?

C: GET /random

S: HTTP/1.1 200 OK
S: Content-Type: text/plain
S: [crlf]
S: 0.1243546575676575

If the client fails to get a response it just tries again, after all no two responses should be the same :-) [update: what i wrote was incorrect, two random numbers in sequence might be the same, especially depending on the range request as shown in the next paragraph, but in terms of the random numbers it still does not matter if a client does not get a response, especially if the lost response is caused by psuedo-random means :-)]

In fact this is already provided at random.org. The HTTP service is is really simple, sixteen random bits anyone?

About

sandoz

Search

Archives
« April 2014
SunMonTueWedThuFriSat
  
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
   
       
Today