Skip to content

Using the Jersey Client with Scala (Revisited)

August 5, 2009
tags: ,

In my last post I noted that there is currently a Scala compiler bug that prohibits using the Jersey Client with Scala.

I wanted to learn Scala and the Jersey Client API, so I took a crack at creating a Scala replacement for the offending classes. This isn’t fully  plug compatible with the Java API – but assuming you just want to call the Jersey client from Scala (and not try and call these classes from other Java classs) – you should be OK.

The code is posted below. This is my first foray into Scala – so I am probably doing some bone headed things. Improvements are welcome.

First, let’s start with the Resource class (the name is intentionally different from the replacement WebResource – so we don’t collide on import).

package com.my2do.jersey.client

import  com.sun.jersey.api.client.filter.Filterable
import com.sun.jersey.api.client.config.ClientConfig
import com.sun.jersey.api.client._
import com.sun.jersey.client.impl.ClientRequestImpl
import _root_.java.net.URI
import javax.ws.rs.core.{Cookie, MediaType,MultivaluedMap,UriBuilder} 

/**
 * A Simplified Scala replacement for the Jersey Client WebResource class.
 *
 * This is needed because there is a bug in the scala compiler that
 * prevents the Jersey WebResource class from being used.
 *
 * see <a href="http://lampsvn.epfl.ch/trac/scala/ticket/1539">the bug report</a>
 *
 * This class does not implement UniformInterface. UniformInterface has many
 * methods that reference the GenericType class. The GenericType class
 * looks like it was added to Jersey to support a more conscise way of
 * referencing really nasty parameterized java types. Scala as the type keyword -
 * which serves the same purpose - so the GenericType class does not appear to be needed.
 *
 */

class Resource(clientHandler:ClientHandler, uri:URI) extends Filterable(clientHandler) {
 override def toString() = uri.toString()
 override def hashCode() = uri.hashCode()

 override def equals(other:Any):Boolean = other match {
 case that : Resource =>  isComparable(that) && this.uri == that.getURI()
 case _ => false
 }

 def isComparable(that:Any) = that.isInstanceOf[Resource]
 def getUriBuilder() = UriBuilder.fromUri(uri);
 def getURI() = uri

 /**
 * Create a Resource from this web resource with an additional
 * query parameter added to the URI of this web resource.
 *
 * @param key the query parameter name
 * @param value the query parameter value
 * @return the new web resource.
 */
 def queryParam(key:String , value:String ) = {
 val b = getUriBuilder();
 b.queryParam(key, value);
 new Resource(clientHandler, b.build());
 }

 /**
 * Create a new Resource from this web resource with additional
 * query parameters added to the URI of this web resource.
 *
 * todo: Creat a scala implicit that converts from MultivaluedMap to a scalafied Map?
 * @param params the query parameters.
 * @return the new web resource.
 */
 def queryParams(params: MultivaluedMap[String,String]) = {
 val b = getUriBuilder();
 val i = params.entrySet().iterator();
 while( i.hasNext() ) {
 val entry = i.next();
 val vals = entry.getValue().iterator();
 while( vals.hasNext() ) {
 b.queryParam(entry.getKey(),vals.next())
 }
 }
 new Resource(clientHandler, b.build());
 }

 /**
 * scalifed version of above
 */
 def queryParams(params: Map[String,List[String]] ) = {
 val b = getUriBuilder();
 params foreach { case(key,valueList) => valueList foreach { b.queryParam(key,_)}}
 new Resource(clientHandler, b.build());
 }

 def path(path:String) = new Resource(clientHandler, getUriBuilder().path(path).build());

 // uniform interface methods according to Roy
 def head(r:Request) =  method("HEAD",r);
 def options(r:Request) = method("OPTIONS",r)
 def get(r:Request) = method("GET",r)
 def put(r:Request) = method("PUT",r)
 def post(r:Request) = method("POST",r)
 def delete(r:Request) = method("DELETE",r)

 def method(method:String, r:Request) = {
 getHeadHandler().handle(new ClientRequestImpl(uri,method,r.entity, r.metaData))
 }
}

This is much simpler than the WebResource class it replaces (but admittedly not as functional).  All of the REST methods take the Request class, and all return a Jersey ClientResponse.   The Request class encapsulates the entity being sent (if there is one), and any request headers, cookies, etc:


package com.my2do.jersey.client

import com.sun.jersey.client.impl.ClientRequestImpl
import com.sun.jersey.core.header.OutBoundHeaders
import _root_.java.util.Locale
import _root_.java.net.URI

import javax.ws.rs.core.Cookie;
import javax.ws.rs.core.MediaType;

/**
 * The Request singleton has some syntactic sugar for creating Request objects
 * For example:
 *    resource.get(Request().acceptLanguageString("en")) // creates a request with a null entity
 *    resource.post(Request(myFile).mediaType("multipart/mime"))
 */

object Request {
 def apply() = new Request(null)
 def apply(entity:Object) = new Request(entity)
 def noEntity = new Request(null)
}

/**
 * Encapsulates a  web request
 * including the entity to be sent (optional - for example this will be null for a get)
 * and all of request cookies, accept headers, etc.
 */

class Request(val entity:Object) {

 var metaData = new OutBoundHeaders()

 def mediaType(t:MediaType) = {
 metaData.putSingle("Content-Type", t);
 this
 }

 def mediaType(t:String):Request = mediaType(MediaType.valueOf(t))

 def accept(mediaTypes:MediaType* ) = {
 for (val m <- mediaTypes)
 add("Accept", m)
 this
 }

 def acceptString(mediaTypes:String* ) = {
 for (val m <- mediaTypes)
 add("Accept", m)
 this
 }

 def acceptLanguage(locales:Locale*) = {
 for (val l <- locales)
 add("Accept-Language", l)
 this
 }

 def acceptLanguageString(locales:String*) = {
 for (val l <- locales)
 add("Accept-Language", l)
 this
 }

 def cookie(cookie:Cookie ) = add("Cookie", cookie)      

 def header(name:String, value:Object ) = add(name, value)

 private def add(name:String, value:Object ) = {
 metaData.add(name,value)
 this
 }
}
&#91;/sourcecode&#93;

One of the things that Jersey client API does for you is optionally converts the ClientResponse entity to a class of your choosing (assuming there exists a MessageBodyReader that can perform the conversion). For example <span style="color:#339966;"> resource.get(String.class)</span> will convert the ClientResponse entity to a String (or throw a UniformInterfaceException if the GET failed).

To accomplish the same thing in Scala, I experimented with implicit conversions. This is a neat (but potentially dangerous!) way of having the Scala compiler automagically convert from one type to another.   Here is an example snippet where we call the POST method and auto convert the returned entity to a String:


val multiPart = new MultiPart().
     bodyPart(new BodyPart(someTestBytes, MediaType.APPLICATION_OCTET_STREAM_TYPE));
val result:String = resource.path("/upload").post(Request(multiPart).mediaType("multipart/mixed"))

Note in above example we explicitly set the type of the result object. Normally in Scala we can let the compiler figure things out – but in thise case we don’t want a ClientResponse object -we just care about the entity being returned. The type hint triggers the implicit conversion from a ClientResponse to a String.  We could get the same result if we passed a ClientResult to some other Scala method that required a String (assuming the implicit conversions are in scope).

The conversions are declared like so:

/**
 * Implicit Conversions to convert from a ClientResponse to another type
 */

package com.my2do.jersey.client

import com.sun.jersey.api.client.{ClientResponse,UniformInterfaceException}
import java.io.InputStream

/**
 * Implicit Conversions from a ClientResponse to another type
 * Use with caution!
 * To use this, import com.my2do.jersey.client.Conversions._

 */

object Conversions {
     implicit def response2String(r:ClientResponse):String =  getClientResponseEntityAs(r,classOf[String])
     implicit def response2InputStream(r:ClientResponse):InputStream = r.getEntityInputStream()
     implicit def response2ByteArray(r:ClientResponse):Array[Byte] =  getClientResponseEntityAs(r,classOf[Array[Byte]])
     // todo: What others do we need?

    /**
     * unwrap the entity - thrown an exception if the entity is not valid
     */
     def getClientResponseEntityAs[T](r:ClientResponse, c:Class[T]):T = {
         if( r.getStatus() >= 300 )
            throw new UniformInterfaceException(r)
         r.getEntity(c)
     }
}

After using Scala for a few days, I have to say it is a lot of fun 🙂

Advertisements

Comments are closed.

%d bloggers like this: