Pimping Servlet and Jetty
Sometimes you just want a servlet, not a whole web application framework. But to a Scala programmer the servlet API is pretty ugly.
The Starting Point
Consider the GET service method from the servlet spec:
protected void doGet(HttpServletRequest req, HttpServletResponse resp)
Implementations create a response as a side-effect by mutating the HttpServletResponse object. The resulting code is not pretty. We really want to return our response explicitly. Here is an example from one of the lightweight scala web frameworks called Step:
get("/") {
<h1>Hello {params("yourname")}!</h1>
}
The part in braces is effectively a service function with type:
() => Any
We simply return an object as a response. Nice! But where did params(...)
come from? The request information is passed via a thread local variable instead of as a parameter of the service function. Not so nice.
The Desired Result
The aim of the pimped servlet API is to let you write:
get("/") {
request => <h1>Hello {request("yourname") getOrElse "stranger"}!</h1>
}
This is similar to the previous example, but it has a more functional style. Here is a larger example returning some KML for Google Earth to display:
implicit val contentType = ContentType("application/vnd.google-earth.kml+xml") // note 1
get("/cafe") {
case Params(BBox(w, s, e, n)) => // note 2
<kml xmlns="http://www.opengis.net/kml/2.2">
<Document>
{
for((name, lon, lat) <- findCafesIn(w, s, e, n)) yield
<Placemark>
<name>{name}</name>
<Point>
<coordinates>{lon},{lat},0</coordinates>
</Point>
</Placemark>
}
</Document>
</kml>
case _ => 404 // note 3
}
The example extracts the coordinates of a bounding box (w, s, e, n)
from a Google Earth request and uses them to find a collection of locations via findCafes(..)
, which is supposedly a spatial query on some data source. The locations are wrapped in KML markup and returned.
This shows how to set an implicit content type (note 1); the use of extractors to obtain request parameters (note 2); and implicit conversion of responses such as XML or integers. An integer gets converted to an error response (note 3).
The Pimping
Responses
The servlet API pimping starts by lifting the service method out of the servlet class as a standalone scala function:
(HttpServletRequest, HttpServletResponse) => Unit
Curry that to get;
HttpServletRequest => HttpServletResponse => Unit
Now lets call the second part of this the Responder.
type Responder = HttpServletResponse => Unit
So our service function is:
HttpServletRequest => Responder
The Responder
contains all the ugly side effects. But, conveniently, we can write these separately and a few canned Responders cover most needs. Implicit conversions will wrap result types such as XML in appropriate Responders. Here is the conversion from Int
to Responder
:
implicit def toErrorResponse(code: Int): Responder = { _.sendError(code) }
XML is handled by an only slightly more complicated Responder:
implicit def toXMLResponse(content: Node)(implicit contentType: ContentType): Responder = {
response =>
response.setContentType(contentType.mime)
response.getWriter.write(content.toString)
}
You can always include a specific Responder
definition right in the service function and deal with the HttpServletResponse
object directly, if you need to.
Requests
Similarly, the naked HttpServletRequest
object is passed to the service function and you can deal with it directly. However, extractors allow you to write the service function as a series of case clauses, dealing mainly with the contents of the request.
Here is an example where we extract the parameter map and query it:
get("/stock") {
case Params(params) if params contains "SKU" => ...
...
}
Here we set up an extractor for a string parameter and a, possibly repeated, integer parameter:
val Action = StringParam("ACTION")
val AssetNumber = LongParam("INUM")
post("/assets") {
case Params(Action("delete")&AssetNumber(inum)) => ... // delete an asset with number 'inum'
case Params(Action("allocate")&AssetNumber(from, until)) => ... // allocate assets in the range 'from' .. 'until'
}
When the pattern matching style is not suitable, an implicit wrapper, RichRequest, lets you query the HttpServletRequest using scala conventions:
get("/stock") {
request => request("SKU") map lookupSKU getOrElse generateSKUList
}
Jetty
The get and post methods shown in the examples have this signature:
def get( servletContext: String)( serviceFunction: HttpServletRequest => HttpServletResponse => Unit)
This binds the serviceFunction
to an HTTP GET
with the given servletContext
(ie the leading part of the path in the request). One way to implement this binding would be to write a central routing servlet. But designing routing schemes quickly goes beyond pimping the servlet API.
This is where Jetty comes in.
The implementation given here relies on the routing system provided by Jetty based on the standard concept of a servlet context. Jetty provides an API for defining servlet contexts that we can exploit instead of writing a web.xml configuration file. Jetty also provides virtual host routing. This is exposed by a more general binding method that underlies get() and post() :
def on(method: String, vhost: Option[String], path: String)(serviceFunction: HttpServletRequest => HttpServletResponse => Unit)
Putting it all Together
The pimping layer is very thin and does not prevent you from getting at the HttpServletRequest and HttpServletResponse if you need to. It is divided into small modules, which can be seen in this complete example:
package example
import au.com.langdale.webserver._
import Driver._ // start and stop Jetty
import Connectors._ // Jetty-specific connection methods such as listen()
import Handlers._ // Jetty-specific binding methods get("...") and post("...")
import Responders._ // generic servlet Responders and implicit conversions for responses
import Requests._ // generic servlet request extractors and wrapper
object HelloWorld {
val host = "127.0.0.1"
val port = 9986
// tell Jetty where to receive connections
listen(host, port)
// may be omitted since this is the default for XML responses
implicit val contentType = ContentType("application/xhtml+xml")
// hello world server
get("/test") { request =>
<html xmlns="http://www.w3.org/1999/xhtml">
<head><title>The Test Page</title></head>
<body>
<h1>Test</h1>
<p>Hello world.</p></body>
</html>
}
def main(args: Array[String]) = { start; join }
}
Get the Code
The code for all this is on github: http://github.com/arnolddevos/JettyS There is not much to it, feel free to embrace and extend.