Yet Another Try: A REST Client with Jersey and Scala

[UPDATE] I pushed the code as SJersey to Github. For usage examples check out the test code here [/UPDATE]

The are many awesome REST frameworks out there, for Java and for Scala as well. Most of the concepts do cover the server side. Important I know, but where are the smart client side APIs/DSLs? There are some and I must confess, I dislike them all. So this is a try to create another one. One that I dislike to a lesser extend.

I like Jersey, a production ready REST client and server framework for REST. The server implementation follows the JAX-RS standard and has an awesome functionality. And since I know this framework best, it is my choice for the client side as well.

I wrote a little one-source-file Jersey wrapper for Scala. The REST services are operated by Neo4j Server, a NoSQL GraphDB. My specs tests are running against it. All objects have to be marshaled and unmarshaled in and to JSON. Jersey uses JAXB for that.

Unfortunately, this wrapper is far away from complete… Anyway, here is my proposal:

We have, at least, the REST methods GET, PUT, POST and DELETE. The following should be supported (not limited to):

  • there always is a base URI for all communication. In case of Neo4j it is http://localhost:7474/db/data/. But we should be able to overwrite it or to append a path for specific REST method calls
  • GET should return the expected JSON object, without this annoying classOf or .class stuff
  • PUT and POST should be able, additionally to GET, to take a request entity parameter to post f.e. a new node.
  • DELETE should support a Client Response object to check wether the deletion was successful

First of all, we have to extend from two traits SimpleWebResourceProvider, that supports the creation of Jersey Client and WebResource instances and trait Rest. Rest is the trait for all Jersey wrapping functionality.

Class MyRestServiceClient has to implement method baseUriAsString to provide the base URI within this class and to overwrite mediaType to set the media type from default to JSON.

class MyRestServiceClient extends Rest with SimpleWebResourceProvider {

  // base location of Neo4j server instance
  def baseUriAsString = "http://localhost:7474/db/data/"

  // all subsequent REST calls should use JSON notation
  override val mediaType = Some(MediaType.APPLICATION_JSON)
. . .
}

All REST method calls have to be enclosed in method rest(...) {}. rest can be parametrized with additional header parameter and a basePath that is appended to the global base URI. This is valid for all REST method calls within method rest.

rest(header = ("MyHeaderParameter1", "1") :: ("MyHeaderParameter2", "2") :: Nil) {}
rest(basePath = "node/1/") {}
rest{}

The following GetRoot is one example of a JAXB object declaration. GetRoot is returned by a GET call.

@XmlAccessorType(XmlAccessType.FIELD)
@XmlRootElement
class GetRoot {
  var index:String = _
  var node:String = _
  var reference_node:String = _
}

The declared JAXB objects can now be used in all REST methods. The following example returns Neo4j’s root entity and an index entity. Types are declared inside Scala’s [], so there is no classOf operator needed (BTW, you can not write val root:GetRoot = "".GET. Sad that I don’t know why…)

rest {
  implicit s =>
    val root = "".GET[GetRoot]
    val index = "/index".GET[GetIndex]
}

For POST we have to provide a request entity. It is PathRequest in this case, a JAXB object, that provides the parameter for the traversal of node 3 and depth 4. The POST call returns a JSONArray with the traversal data (unfortunately, JAXB thrown an exception using the usual JAXB objects). The “operator” <= is used to define the request entity.

rest{  
  implicit s =>
    val path = "node/3/traverse/path".POST[JSONArray] <= 
           PathRequest(order = "depth first", max_depth = 4, uniqueness = "node path")
}

Same for PUT. A PUT without response in this case. Request entity is MatrixNodeProperties, part of the common Neo4j graph example. The properties are set and read here.

rest(basePath = "node/1/") {
  implicit s =>
    "properties".PUT <= 
      MatrixNodeProperties(name = "Thomas Anderson Neo", profession = "Hacker")
    val properties = "properties".GET[MatrixNodeProperties]
}

The next more complex example shows how nodes can be created and deleted with a DELETE method call. Since the Location header of the POST response contains the whole URI of the created node, I had to provide a unary ! operator to overwrite all global paths and use the given one as absolute path.
(I hope that the comments are sufficient for explanation)

rest{
  implicit s =>

  // defining note names and profession
  val nodes = ("Mr. Andersson", "Hacker") ::
    ("Morpheus", "Hacker") ::
    ("Trinity", "Hacker") ::
    ("Cypher", "Hacker") ::
    ("Agent Smith", "Program") ::
    ("The Architect", "Whatever") :: Nil

  // for all notes
  val locations =
    for (node <- nodes;
         // create node
         cr = "node".POST[ClientResponse] <= MatrixNodeProperties(name = node._1, profession = node._2)
         // if creation was successful use yield
         if (cr.getStatus == ClientResponse.Status.CREATED.getStatusCode)
    // yield all created locations
    ) yield cr.getLocation

  // and remove them
  for (location <- locations) {
    // the unary ! is used to sign a absolute path (location here)
    val cr = (!location.toString).DELETE[ClientResponse]
    // no exception and No Content means successful
    cr.getStatus mustEqual ClientResponse.Status.NO_CONTENT.getStatusCode
  }
}

I hope you like it? What do you think?

One thought on “Yet Another Try: A REST Client with Jersey and Scala