Showing posts with label REST. Show all posts
Showing posts with label REST. Show all posts

Saturday, August 29, 2009

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" );
}
}

Wednesday, April 29, 2009

Enabling the "Accept" header for Grails/REST

I've been trying to implement a RESTful web service using Grails, following along a number of articles on the web for examples (Scott Davis has a particularly good one here: RESTful Grails). To get back XML from a Grails app that will be serving up both HTML and XML, I was setting the HTTP "Accept" header, then using content negotiation in my controller classes to determine what to send back to the client. For some reason, though, my app kept spitting back HTML even when I was specifically requesting XML.

Well, it turns out that, according to this Jira post, as of Grails 1.1, using the Accept header is disabled by default. To enable it, open up the Grails app's Config.groovy file (grails-app/conf/Config.groovy), and set "grails.mime.use.accept.header" to true.

Once I got that figured out, everything is running really smoothly.

Monday, September 22, 2008

Adding HTTP Basic Authorization to GroovyHTTP

A while back, I discovered a great little Groovy utility by Tony Landis called GroovyHTTP which allows you to generate web requests in Groovy. I've used it with great success on a number of projects.

Lately, I've been playing with a neat GTD web app named Tracks, which has a nice RESTful API for adding tasks, etc. To interact with the application via the API, though, you need to be able to authenticate via HTTP Basic authentication, and GroovyHTTP doesn't seem to support that. Since I really want to interact with Tracks on my internal network with Groovy, I've taken a stab at adding HTTP Basic authentication to GroovyHTTP. Of course, I realize that HTTP Basic is not the most secure authentication method (that's an understatement!), but when you need it, you need it.

It really boils down to adding one method to set the user and login on the GroovyHTTP object, then adding a Base64-encoded authorization string to the HTTP request headers. The extra header line ends up looking like this (for the user/password "Aladdin/open sesame"):

Authorization: Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==


To use the modified GroovyHTTP class to access a URL protected by Basic authentication, you can do something like this:

def h = new GroovyHTTP(protectedUrl)
h.setMethod('POST')
h.setAuthorization(login, password)
h.setParam('p1', p1)
h.setParam('p2', p2)


Since I can't find any contact information for Tony Landis on his site, I figured I'd post the Groovy code here since he's released it under the BSD license. The Groovy class is available here: GroovyHTTP with HTTP Basic Authentication. NOTE: It looks like Google Docs has messed up the formatting in the Groovy class. When I get some time, I'll try to neaten it up.