Tuesday, July 21, 2009

RESTful clients with the Jersey Client API

[Note: This is one in a series of posts about JAX-RS. You may wish to start at the beginning]


Jersey, the reference implementation of the JAX-RS specification, is a framework that makes it very easy to implement RESTful web services. But there is more to Jersey than just the server side. In this post I will spend a little time exploring the Jersey client library.


Depending upon how you design your RESTful service, you may or may not want to have separate URLs for separate representations of the same resource. This presents a problem when trying to test with a browser. There is no way to tell popular browsers that you want a text/html representation or an image/jpeg representation. The browser has a list of preferred media types, but none that I'm aware of allow you to customize this (either in general or for a particular request). Even more importantly, we need to be able to build solid unit tests for our services. The Jersey client framework provides a good solution to this problem. It is designed to allow developers of RESTful web services to write good unit tests, but is more general purpose than that. It can also be used to write RESTful client applications.


There is an excellent tutorial on the Jersey client API, which you should download and read if you plan to use it. But I will give you a taste of the API here.


The Jersey client API is very clean, and requires a minimum of fuss to use. As an example, let's write a unit test which tests our web service serving up information about fighter planes. First, we write the code to set up and tear down our service implementation. This code is the same as that in our Main class before, just split up between the setup and tear-down methods.

public class Test3b {

private SelectorThread selector;

@org.junit.Before
public void createService() throws IOException {
Map initParams = new HashMap();
initParams.put("com.sun.jersey.config.property.packages", "net.gilstraps.server");
selector = GrizzlyWebContainerFactory.create("http://localhost:9998/", initParams);
}

@org.junit.After
public void DestroyService() {
selector.stopEndpoint();
selector = null;
}

The only difference in this case is we don't read from standard in to shut down the service, since we always want to shut it down at the end of the unit test.


Now, we can test that the text/html representation returned by invoking the service is what we expect. First, we do a bit of hoop jumping to read in a copy of the HTML we expect to receive:


@org.junit.Test
public void testF22Html() {
try {
File expected = new File("f22.html");
long fileSize = expected.length();
if (fileSize > Integer.MAX_VALUE) {
throw new IllegalArgumentException("File it larger than a StringWriter can hold");
}
int size = (int) fileSize;
BufferedReader r = new BufferedReader(new FileReader(expected), size);
char[] chars = new char[size];
int readChars = r.read(chars);
if (readChars != size) {
throw new RuntimeException("Failed to read all chars of the expected result html file");
}
final String expectedText = new String(chars);

At this point, the variable expectedText contains what we should receive back from our request. Making the request is quite straightforward. First, we create a JAX-RS client:

Client client = new Client();

These clients are 'heavyweight' objects (taking time to create and using significant resources). As such, in a production client we would want to create this client once and use it many times. For the sake of independent unit tests, we will go ahead and create a Client object for each test.


Next, we specify the resource we want to retrieve using a WebResource object:

WebResource f22 = client.resource("http://localhost:9998/planes/f22/f22.html");

And then we ask the client to retrieve the resource for us, specifying that we want a text/html representation (MediaType.TEXT_HTML_TYPE) and specifying that we want to get back a ClientResponse object:


ClientResponse response =
f22.accept(MediaType.TEXT_HTML_TYPE).get(ClientResponse.class);

This code is using the builder pattern, where we build up our request through a chain of method calls. In this case, the chain is only two calls long. First, we call the accept method to specify the media types we will accept in the response (text/html), then we call the get method to actually retrieve the resource. It is possible to chain together more calls to specify other characteristics of either the request or the expected response (for more information, see the whitepaper on the Jersey client API).


Now that we have gotten a representation of the resource in the form of an HTTP response, we can then retrieve the HTML entity contained within that response as a string:


String returnedHTML = response.getEntity(String.class);

And finally, since this is a unit test, we make sure that what we got back matches what we expected:

assertEquals("Expected and actual HTML don't match"
expectedText, returnedHTML);

The ClientResponse object is useful if you want to look at other characteristics of the response, such as the returned headers. In this case, we could just as well have asked for the string from the WebResource directly. To do so, we would replace these two lines:

ClientResponse response =
f22.accept(MediaType.TEXT_HTML_TYPE).get(ClientResponse.class);
String returnedHTML = response.getEntity(String.class);

With this one:

String returnedHTML = asHTML.accept(MediaType.TEXT_HTML_TYPE).get(String.class);

Testing for retrieval of the JPEG representation of an image is almost identical. The only differences are that we read in the image file as an array of bytes, ask for the response entity as an array of bytes, and compare the two as arrays of bytes. Here is the entire test method:


@org.junit.Test
public void testF22JPEG() {
try {
// Read in our expected result
File imageFile = new File("f22.jpg");
long fileSize = imageFile.length();
if (fileSize > Integer.MAX_VALUE) {
throw new IllegalArgumentException("File it larger than a StringWriter can hold");
}
int size = (int) fileSize;
byte[] expectedBytes = new byte[size];
BufferedInputStream bis = new BufferedInputStream(new FileInputStream(imageFile), size);
int bytesRead = bis.read(expectedBytes);
assertEquals(size, bytesRead);

// Request the representation
Client client = new Client();
WebResource f22 = client.resource("http://localhost:9998/planes/f22/f22.jpg");
ClientResponse response = f22.accept(MediaType.WILDCARD).get(ClientResponse.class);
MultivaluedMap headers = response.getHeaders();
for ( String key : headers.keySet() ) {
System.out.println( key + "=" + headers.get(key) );
}
byte[] returnedBytes = new byte[0];
returnedBytes = response.getEntity(returnedBytes.getClass());

// Compare the two sets of bytes to make sure they match
assertEquals(size, returnedBytes.length);
assertTrue(Arrays.equals(expectedBytes,returnedBytes));
}
catch (FileNotFoundException e) {
AssertionError ae = new AssertionError("File containing expected HTML not found!");
ae.initCause(e);
throw ae;
}
catch (IOException e) {
AssertionError ae = new AssertionError("Problems reading expected text!");
ae.initCause(e);
throw ae;
}
}

Just like the core of Jersey, the client library enables clear, short code that clearly expresses what the developer is trying to do. This is a marked departure from many other web frameworks and APIs. It is refreshing to be able to write web services clients in such a terse, yet clear style.

[Note: I plan to continue working with Jersey and continue posting about it. But I'm going to take a few weeks to look into some other topics first. I'll make sure to update the list of JAX-RS and Jersey posts as I add more]

6 comments:

Gadi Eichhorn said...

Thank you very much for the articles.

I have a question to do with Jersey security, we careated a secure REST server with JAAS and j_security_check where the username & password is post back to the server. We implemented a dojo client that consumes this interface. However, when we started using jersey client we discuvered the form authentication is not supported by jesrey client.

Any suggestion for server security and how to consume it from the jersey client?

Many thanks.
G.

Brian Gilstrap said...

Gadi,

Can you be a bit more specific please? When you say "form authentication", what do you mean?

Brian

Gadi Eichhorn said...

Hi Brian,

Yes, I am using j_security_check service on glassfish v3. I have configured the authentication to use FORM and defined all the roles and groups accordingly on the server.

I followed this -> http://download.oracle.com/javaee/6/tutorial/doc/gkbaa.html#bncbq

I noticed the Jersey client has no form authentication filter. There is some information about using the Apache http client but no examples how this can be used.

Many thanks in advance.
Gadi.

Brian Gilstrap said...

Gadi,

You are using a JavaScript library (Dojo) in a browser to talk to your server, right? If so, I suggest you not use an authentication system targeted to a human (form-based authentication), since that approach assumes the user is consuming responses directly instead of those responses being interpreted by client-side Javascript.

Instead, I would use something like SSL plus Basic Authentication to do your authentication. Your client-side code can prompt the user for their authentication information and then specify that information in each request to the server-side using the Basic Authentication approach (with the Authentication header). When doing this, you can use SSL to prevent eavesdropping.

Hope this helps.

Brian

Unknown said...

Hi Brian.
I'm Andrey Kovalsky working with Gadi on this project.

Currently server send html login form exactly as Gadi said. JavaScript app just check whether string "Please login" exists in html text returned from server, and if yes, it start authentication process.
I know it not very smart, but it works. The main problem is that we have different databases, with different access login : passwords. So if user already logged with DB_1, and client send request to DB_2, just empty JSON object returned, same as empty search result. But we want to have some note that new authentication requred. That is the main problem.

Thank you.
Andrey

Brian Gilstrap said...

Hi Andrey,

I think this discussion has gotten too detailed for blog comments. :-)

If you're interested in engaging me to help you, please send me an email( bgilstrap AT gmail.com).

Brian