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

2 comments:

Kevin Corby said...

Thanks for posting this, very helpful for me.

Anonymous said...

After developed the RestCliet.put() and encoder, I tested it. I coded a println request.dump() on the rest controller class, I couldn't find the File, How could I get the File on the rest controller? I appreciate your help.