Saturday, August 29, 2009

Groovy RESTClient, HTTPBuilder, and PUT responses with no body

In working with a RESTful web service that allows PUT requests to upload files, I came across an slight annoyance with the put() convenience method in the Groovy RESTClient. Up until now, I've been absolutely amazed with how easy RESTClient makes my life in working with RESTful web services, and I'll continue to use it in doing REST development. However, in this one case, RESTClient wouldn't work nicely for me.

Here's the scenario: I upload a zip file to the service so that it can use the data in that file to populate some things (see my post Groovy, RESTClient, and PUTting zip files for one way to get RESTClient to handle zip files). I watch the log on the server and see that it's handling that file just fine; however, my Groovy script errors out! What's going on?

Here's what the Groovy code looked like at this point:

def file = new File("data_file.zip")
def rest = new RESTClient( 'http://localhost:8080/server/rest/' )
rest.encoder.'application/zip' = { ... handle zip files ... }
rest.put( path: "data/data_file.zip", body: file, requestContentType: 'application/zip' )


It turns out that the RESTful service, once it gets the file, just sends back an HTTP "201 Created" response code. No body to the response at all because, really, there's none needed. However, by default, the RESTClient put() method wants a response body to parse and return, so it throws a NullPointerException when it tries to parse that "nothing".

Since I didn't see a way of telling RESTClient not to expect a response body, I could have just lived with the exception, catching it and moving along. But the exception was being thrown before the response object was passed back, I couldn't even check the status of my request to see if the request was successful or if there were problems. Fortunately, RESTClient is really just a set of convenience wrappers around the main HTTPBuilder, so I was able to implement my request using HTTPBuilder.

HTTPBuilder has the nice feature of allowing you to assign different response handlers to different HTTP response codes, so I could basically tell it not to bother trying to parse '201' responses and just print a "success" message. Here's how I ended up setting up the PUT request using HTTPBuilder:

import groovyx.net.http.HTTPBuilder
import groovyx.net.http.Method

def file = new File("data_file.zip")

def http = new HTTPBuilder('http://localhost:8080/server/rest/')
http.encoder.'application/zip' = { ... handle zip file ... }

http.request(Method.PUT) { req ->
uri.path = 'data/data_file.zip'
send("application/zip", file)

response.'201' = { resp ->
println resp.status
}

response.success = { resp, object ->
println resp.status
}

response.failure = { resp ->
println resp.statusLine
}
}


Of course, we could do a lot more with the response objects, but at this point, all we want to know is whether our request failed or succeeded, without having to worry about a response body. While it's definitely not as convenient as RESTClient, with the HTTPBuilder implementation in place, the file uploaded just fine, and there were no errors on the Groovy end of things.

Groovy RESTClient and PUTting zip files

I'm currently working with a RESTful web service that requires a client to upload a zip file with a content-type of 'application/zip'. Since we're using more and more Groovy in our shop, we initially tried to PUT the zip file using Groovy's HTTPBuilder/RESTClient. Our initial attempt looked like this:

def file = new File("data_file.zip")
def rest = new RESTClient( 'http://localhost:8080/server/rest/' )
rest.put( path: "data/data_file.zip", body: file, requestContentType: 'application/zip' )


When we first tried to run it, we kept getting NullPointerExceptions from RESTClient/HTTPBuilder trying to set the body of the request. Digging into the code, it looked like, by default, HTTPBuilder doesn't know how to handle zip files. It can do other kinds of binary encoding, but the content-type needs to be 'application/octet-stream', something the server we were using doesn't understand.

What we had to do was actually create our own encoding process and register that with the RESTClient. Using the HTTPBuilder EncoderRegistry.encodeStream() method (which returns InputStreamEntity instances of org.apache.http.HttpEntity) as a starting point, here's what we came up with:

/**
* Request encoder for a zip file.
* @param data a File object pointing to a Zip file on the file system
* @return an {@link FileEntity} encapsulating this request data
* @throws UnsupportedEncodingException
*/
def encodeZipFile( Object data ) throws UnsupportedEncodingException {
if ( data instanceof File ) {
def entity = new org.apache.http.entity.FileEntity( (File) data, "application/zip" );
entity.setContentType( "application/zip" );
return entity
} else {
throw new IllegalArgumentException(
"Don't know how to encode ${data.class.name} as a zip file" );
}
}


Basically, what we're doing is, instead of returning an InputStreamEntity, we return a FileEntity and set it's content-type to 'application/zip'. Of course, in the above code, we could do more strenuous checking on the data to make sure that it's actually a zip file and such. As well, we could probably expand it to handle other specific binary types. But in this case, we knew we were getting a zip file and nothing else, so YAGNI.

Once we had that method in place, all we needed to do was register it with HTTPBuilder/RESTClient. HTTPBuilder/RESTClient allows access to its encoders Map, and its propertyMissing() setter implementation automatically registers an encoder to that Map, so it was easy to attach our zip file encoder to our client object:

rest.encoder.'application/zip' = this.&encodeZipFile


With that in place, uploading the zip file to the RESTful web service worked. Our final code looked something like this:

def file = new File("data_file.zip")

def rest = new RESTClient( 'http://localhost:8080/server/rest/' )
rest.encoder.'application/zip' = this.&encodeZipFile
rest.put( path: "data/data_file.zip", body: file, requestContentType: 'application/zip' )

def encodeZipFile( Object data ) throws UnsupportedEncodingException {
if ( data instanceof File ) {
def entity = new FileEntity( (File) data, "application/zip" );
entity.setContentType( "application/zip" );
return entity
} else {
throw new IllegalArgumentException(
"Don't know how to encode ${data.class.name} as a zip file" );
}
}

Friday, August 7, 2009

Grails, JSecurity, and View URL Mapping in JBoss

I've been trying to get my app created in Grails 1.1.1 using the JSecurity plugin to deploy in JBoss 4.2.3.GA. Everything worked fine when I test it using 'grails run-app', but when I deployed my WAR file to JBoss, I got an IllegalStateException any time I tried to access something where I mapped a URL to a view in my UrlMappings.groovy file like this:

"/"(view:"index")
"500"(view:"error")


It turns out that there's a bug open in the JSecurity plugin: GRAILSPLUGINS-1117. Basically, there's not much explanation as to why, but there is a work-around. Instead of mapping to a view in the UrlMappings file, you map to a controller and action. In the case of the error view, it would look something like this:

"500"(controller:"errors", action:"serverError")

However, there isn't a nice way (that I've found, at least) to map the default "index" view. Instead, you'll either have to create a custom "home" controller that forwards to your generic index page, or map the "/" URL to one of your existing controller actions. In the case of my application, since the main entry point will eventually be through one of the existing controllers anyway, I ended up mapping "/" to that:

"/"(controller:"entryController", action: "list")


With those changes, the app works just fine under JBoss 4.2.3.GA. The JSecurity plugin bug is currently marked as major, so I hope at least it's getting some attention and will be fixed in the near future.