Tutorial: Getting started with scala and scalatra - Part II
In the previous part of the tutorial we created a simple application from scratch and setup Eclipse so we could edit the scala files for scalatra. In this second part of the tutorial you’ll learn how to do the following:
- How to start scalatra with embedded Jetty for easy testing and debugging
- Create a simple REST API that returns JSON data
- Test your service using specs2
We will start by changing the way we start scalatra. Instead of running it using sbt like we did in the previous part, we’ll start scalatra directly from Eclipse. You can download the project for this tutorial from here. Remember to run the following from the project directory before importing in Eclipse.
$ sbt
> update
> eclipse
How to start scalatra with embedded Jetty for easy testing and debugging
We’ve seen that you can start scalatra (and your service) directly using sbt.
$ sbt
> container:start
> ~ ;copy-resources;aux-compile
This will start a Jetty server and automatically copy over the resources and compile them. Even though this works fine you can sometimes run into memory problems where reloading stops, debugging is very hard to do and when an exception is thrown you can’t just click on the exception to jump to the relevant source code. This is something we can easily fix. Scalatra uses Jetty internally, and is itself nothing more than a servlet. So what we can do is just run an embedded Jetty instance that points to the servlet. For this we create the following scala object.
package org.smartjava.scalatra.server;
import org.eclipse.jetty.server.Server
import org.eclipse.jetty.webapp.WebAppContext
object JettyEmbedded {
def main(args: Array[String]) {
val server = new Server(9080)
val context: WebAppContext = new WebAppContext();
context.setServer(server)
context.setContextPath("/");
context.setWar("src/main/webapp")
server.setHandler(context);
try {
server.start()
server.join()
} catch {
case e: Exception => {
e.printStackTrace()
System.exit(1)
}
}
}
}
Before you run this, also create a logback.xml file to control the logging. This is just a basic logging configuration that only logs the message at info level or greater. If you don’t have this, you’ll see a whole lot of Jetty log messages. For our own log messages we set the level to debug.
<configuration>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<!-- encoders are assigned the type
ch.qos.logback.classic.encoder.PatternLayoutEncoder by default -->
<encoder>
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<logger name="org.smartjava.scalatra" level="DEBUG"/>
<root level="info">
<appender-ref ref="STDOUT" />
</root>
</configuration>
You can run this as an application directly from Eclipse: “Run->Run As->Scala Application”. The output you’ll see will be something like this:
21:37:33.421 [main] INFO org.eclipse.jetty.server.Server - jetty-8.1.5.v20120716
21:37:33.523 [main] INFO o.e.j.w.StandardDescriptorProcessor - NO JSP Support for /, did not find org.apache.jasper.servlet.JspServlet
21:37:33.589 [main] INFO o.e.j.server.handler.ContextHandler - started o.e.j.w.WebAppContext{/,file:/Users/jos/Dev/scalatra/firststeps/hello-scalatra/src/main/webapp/},src/main/webapp
21:37:33.590 [main] INFO o.e.j.server.handler.ContextHandler - started o.e.j.w.WebAppContext{/,file:/Users/jos/Dev/scalatra/firststeps/hello-scalatra/src/main/webapp/},src/main/webapp
21:37:33.631 [main] INFO o.scalatra.servlet.ScalatraListener - Initializing life cycle class: Scalatra
21:37:33.704 [main] INFO o.e.j.server.handler.ContextHandler - started o.e.j.w.WebAppContext{/,file:/Users/jos/Dev/scalatra/firststeps/hello-scalatra/src/main/webapp/},src/main/webapp
21:37:33.791 [main] INFO o.f.s.servlet.ServletTemplateEngine - Scalate template engine using working directory: /var/folders/mc/vvzshptn22lg5zpp7fdccdzr0000gn/T/scalate-6431313014401266228-workdir
21:37:33.812 [main] INFO o.e.jetty.server.AbstractConnector - Started SelectChannelConnector@0.0.0.0:9080
Now you can work with scalatra directly from Eclipse. Well that was easy. Next step, lets create a REST API. Before we do that though, if you work with Chrome, install “Dev HTTP Client”. This is a great HTTP Client that works directly from Chrome. Very easy to use.
Create a simple REST API that returns JSON data
For this part of the tutorial we’ll start by creating a very simple REST API. We’ll create an API that allows us to bid on items. A sort of mini eBay. We won’t make it very large at the moment and only provide three operations:
- Get auction item based on id.
- Make a bid on a specific item.
- Get a bid a user has made.
We won’t add persistency yet (that’s something for the next tutorial), we’ll only look at the API side of things. We’ll start with the first one.
Get auction item based on id
For this we want to be able to do the following:
Request:
GET /items/123
Response:
200 OK
Content-Length: 434
Server: Jetty(8.1.5.v20120716)
Content-Type: application/vnd.smartbid.item+json;charset=UTF-8
{
"name":"Monty Python and the search for the holy grail",
"id":123,
"startPrice":0.69,
"currency":"GBP",
"description":"Must have item",
"links":[
{
"linkType":"application/vnd.smartbid.item",
"rel":"Add item to watchlist",
"href":"/users/123/watchlist"
},
{
"linkType":"application/vnd.smartbid.bid",
"rel":"Place bid on item",
"href":"/items/123/bid"
},
{
"linkType":"application/vnd.smartbid.user",
"rel":"Get owner's details",
"href":"/users/123"
}
]
}
As you can see we make a simple GET request to a specific URL and what we get back are the details of an item. This item has some properties and a number of links. These links can be followed by the user of your API to explore other resources or execute some action on your API. I won’t go into much detail here, but if you want to know what to do to create an easy to use and flexible API look at my presentation.
We know what our client needs to do to get this resource. The scalatra code is very easy:
package org.smartjava.scalatra
import grizzled.slf4j.Logger
import org.scalatra._
import scalate.ScalateSupport
import net.liftweb.json.compact
import net.liftweb.json.render
import net.liftweb.json.JsonDSL._
import net.liftweb.json.Serialization.{read, write}
import org.smartjava.scalatra.repository.ItemRepository
import net.liftweb.json.Serialization
import net.liftweb.json.NoTypeHints
import org.scalatra.Ok
import org.scalatra.NotFound
import org.smartjava.scalatra.repository.BidRepository
import org.scalatra.Created
import scala.collection.immutable.Map
import org.smartjava.scalatra.model.Bid
class HelloScalatraServlet extends ScalatraServlet with ScalateSupport {
// simple logger
val logger = Logger(classOf[HelloScalatraServlet]);
// repo stores our items
val itemRepo = new ItemRepository;
val bidRepo = new BidRepository;
// implicit value for json serialization format
implicit val formats = Serialization.formats(NoTypeHints);
get("/items/:id") {
// set the result content type
contentType = "application/vnd.smartbid.item+json"
// convert response to json and return as OK
itemRepo.get(params("id").toInt) match {
case Some(x) => Ok(write(x));
case None => NotFound("Item with id " + params("id") + " not found");
}
}
}
For this first REST operation I list the complete class, for the rest I’ll only show the relevant functions. To handle the request we need to define a ‘route’.
get("/items/:id") {
// set the result content type
contentType = "application/vnd.smartbid.item+json"
// convert response to json and return as OK
itemRepo.get(params("id").toInt) match {
case Some(x) => Ok(write(x));
case None => NotFound("Item with id " + params("id") + " not found");
}
This route listens for GET operations on the /items/:id url. Whenever a request is received, this function is invoked. In this function we first set the resulting content-type. I’m a proponent of creating custom media-types for my resources, so we set our result content-type to “application/vnd.smartbid.item+json”. Next we need to retrieve our item from our repository and serialize it to JSON.
For JSON serialization I’ve used lift-json. With this library you can automatically serialize case classes (or create and parse the json by hand). To use lift-json you need to add the following line to the libraryDependencies in build.sbt file and update the eclipse project from sbt..
"net.liftweb" %% "lift-json" % "2.4",
The code that writes our the class files as json is this one-liner
case Some(x) => Ok(write(x));
If we can find the item in the repository we write it out as json using the write function. We return this JSON as a “200 OK” response by using the scalatra OK function. If the resource can’t be find, we sent out a 404 using this one-liner.
case None => NotFound("Item with id " + params("id") + " not found");
For completeness sake I’ll list model and dummy repo implementation:
Model:
case class Item(
name:String,
id: Number,
startPrice: Number,
currency: String,
description: String,
links: List[Link]
);
case class Link(
linkType: String,
rel: String,
href: String
);
case class Bid(
id: Option[Long],
forItem: Number,
minimum: Number,
maximum: Number,
currency: String,
bidder: String,
date: Long
);
Dummy repo:
class ItemRepository {
def get(id: Number) : Option[Item] = {
id.intValue() match {
case 123 => {
val l1 = new Link("application/vnd.smartbid.item","Add item to watchlist","/users/123/watchlist");
val l2 = new Link("application/vnd.smartbid.bid","Place bid on item","/items/" + id + "/bid");
val l3 = new Link("application/vnd.smartbid.user","Get owner's details","/users/123");
val item = new Item(
"Monty Python and the search for the holy grail",
id,
0.69,
"GBP",
"Must have item",
List(l1,l2,l3));
Option(item);
};
case _ => Option(null);
}
}
def delete(item: Item) = println("deleting user: " + item)
}
With this code we’ve got our first REST operation finished. We can test this service easily using Chrome’s Dev HTTP client I mentioned earlier:
In the response you can see a number of links, one of them is the following:
{
"linkType":"application/vnd.smartbid.bid",
"rel":"Place bid on item",
"href":"/items/123/bid"
}
You can see a href attribute here. We can follow this link to place a bid.
Make a bid on a specific item.
To do this we need to make a POST to “/items/123/bid” with a bid of type “application/vnd.smartbid.bid”. The format looks like this:
{
"forItem":123,
"minimum":20,
"maximum":10,
"currency":"GBP",
"bidder":"jdirksen",
"date":1347269593301
}
Let’s look once again at the code for this operation.
post("/items/:id/bid", request.getContentType == "application/vnd.smartbid.bid+json") {
contentType = "application/vnd.smartbid.bid+json"
var createdBid = bidRepo.create(read[Bid](request.body));
Created(write(createdBid), Map("Location"->("/users/" + createdBid.bidder + "/bids/"+createdBid.id.get)));
}
As you can see by the name, this operation listens for a POST on “/items/:id/bid”. Since I want the API to be media-type driven I added an extra condition to this route. With “ request.getContentType == “application/vnd.smartbid.bid+json” we require the clients of this operation to indicate that the type of resource they sent is of this specific type. The operation itself isn’t so complex. We set the content-type of the result and use a repository to create a bid. For this we use the read operation from lift-json to convert the incoming JSON to a scala object. The created object is returned with a “201 Created” status message and contains a location header that points to the resource that we just created.
Get a bid a user has made.
The final operation we support for now is a simple one where we can view the bid we just created. We know where to look, because the location of the just created resource was returned in the location header. The scala code for this function is shown here:
/**
* Route that matches retrieval of bids
*/
get("/users/:user/bids/:bid") {
contentType = "application/vnd.smartbid.bid+json"
bidRepo.get(params("bid").toInt,params("user")) match {
case Some(x) => Ok(write(x));
case None => NotFound("Bid with id " + params("bid") + " not found for user: " + params("user") );
}
}
Pretty much the same as we’ve seen for the retrieval of the items. We retrieve the resource from a repository, if it exists we return it with a “200 OK”, if not we return a 404.
Test your service using specs2
In the final section for this part of the tutorial we’ll have a quick look at testing. When you create a new scalatra project as we’ve shown in the previous part, we also get a stub test we can extend to test our service. For this exampe I won’t write low level, simple JUnit tests, but we’ll create a specification that describes how our API should work. The code for a (part) of the specification is listed here:
package org.smartjava.scalatra
import org.scalatra.test.specs2._
import org.junit.runner.RunWith
import org.scalatra.test.Client
import org.specs2.SpecificationWithJUnit
import org.eclipse.jetty.util.log.Log
/**
* Set of JUnit test to test our API
*/
class HelloScalatraServletSpec extends ScalatraSpec {
// add the servlet so we can start testing
addServlet(classOf[HelloScalatraServlet], "/*")
// some constants
val EXPECTED_BID = """{"id":345,"forItem":123,"minimum":20,"maximum":10,"currency":"GBP","bidder":"jdirksen","date":1347285103671}"""
val BID_URL = "/users/jdirksen/bids/345";
val MED_TYPE = "application/vnd.smartbid.bid+json"
def is =
"Calling an unknown url on the API " ^
"returns status 404" ! statusResult("/unknown",404)^
end ^ p ^
"Calling a GET on " + BID_URL + " should" ^
"return status 200" ! statusResult(BID_URL,200)^
"and body should equal: " + EXPECTED_BID ! {get(BID_URL){response.body must_== EXPECTED_BID}}^
"and media-type should equal: " + MED_TYPE ! {get(BID_URL){response.getContentType must startWith(MED_TYPE)}}
end
def statusResult(url:String,code:Int) =
get(url) {
status must_== code
}
}
For a more complete introduction into specs2 look at their website, I’ll just explain the code shown here. In this code we create a scenario the “def is” part. “is” contains a number of statements that must be true.
"Calling an unknown url on the API " ^
"returns status 404" ! statusResult("/unknown",404)^
end ^ p ^
The first test we do is checking what happens when we call an unknown URL on our API. We define that for this we expect a 404. We check this by calling the statusResult function. If 404 is returned this check will pass, if not we’ll see this in the results. The actual “statusResult” function is also defined in this file. This function uses the build in “get” function to make a call to our API, which runs embedded from this test.
Next we’re going to check how the get bid URL should work.
"Calling a GET on " + BID_URL + " should" ^
"return status 200" ! statusResult(BID_URL,200)^
"and body should equal: " + EXPECTED_BID ! {get(BID_URL){response.body must_== EXPECTED_BID}}^
"and media-type should equal: " + MED_TYPE ! {get(BID_URL){response.getContentType must startWith(MED_TYPE)}}
As you can see, the same basic structure is followed. We run a number of checks that should pass. If we run this we can instantly see how our API should behave (instant documentation) and whether it confirms to our specification. This is the output from the test.
HelloScalatraServletSpec
Calling an unknown url on the API
+ returns status 404
Calling a GET on /users/jdirksen/bids/345 should
+ return status 200
+ and body should equal: {"id":345,"forItem":123,"minimum":20,"maximum":10,"currency":"GBP","bidder":"jdirksen","date":1347285103671}
+ and media-type should equal: application/vnd.smartbid.bid+json
Total for specification HelloScalatraServletSpec
Finished in 846 ms
4 examples, 0 failure, 0 error
Specs2 has a number of different ways it can be run. It can be run directly as a JUnit testcase, from maven or using it’s own launcher. Since I’m developping in Eclipse I wanted to run these tests directly from Eclipse. So I started out with the JUnit testrunner. The problem, however, with this runner is that it seems to conflict with the internally used Jetty from Eclipse. When I rant this the test tried to contact a Jetty instance on port 80, instead of using the embedded one it had started itself. To fix this I created a simple launcher that ran this test directly. To do this make the following launch configuration to get the output I just showed.
Run configuration part 1
Run configuration part 2
Now whenever you run this configuration the specs2 tests are run.
That’s it for this part of the tutorial. In the next part we’ll look at database access and using akka.