Today I needed some code to assess if an HTTP request matched certain criteria, based on its declared content type. Essentially, I needed to know if the user-agent wanted XML (or JSON), and that it wanted only that type.
Requirements:
- Check the
Accept request header for a given content type.
- Support a non-standard
X-Accept header serving the same purpose as Accept (IE doesn’t let XmlHttpRequest specify the Accept header).
- The header value is specifically required to be one, and only one MIME type, without an explicit “q” value.
Assuming we have an HttpServletRequest instance that only accepts application/xml:
scala> req.getHeader("Accept")
res6: String = application/xml
and that we have an object with a method implementing the match algorithm, we should see this behavior:
scala> matcher accepts(req, "application/xml")
res7: Boolean = true
scala> matcher accepts(req, "application/json")
res8: Boolean = false
The complete code:
type HttpServletRequest = {
def getHeader(h: String): String
}
object req {
def getHeader(h: String) = h match {
case "Accept" => "application/xml"
case "X-Accept" => "text/html"
case _ => null
}
}
trait Acceptor {
val headers = "Accept" :: "X-Accept" :: Nil
def accepts(request: HttpServletRequest, contentType: String) =
headers map (request.getHeader) filter(_ != null) map {
_ == contentType
} reduceLeft(_ || _)
}
object matcher extends Acceptor
Not much, but let’s look at it more closely.
type HttpServletRequest = {
def getHeader(h: String): String
}
A type alias. This is a structural type, aliased as HttpServletRequest, with a method getHeader(String): String. This makes it easy to prototype the code in the scala REPL, or compile it against javax.servlet.http.HttpServletRequest.
Next, a singleton instance of the aliased type:
object req {
def getHeader(h: String) = h match {
case "Accept" => "application/xml"
case "X-Accept" => "text/html"
case _ => null
}
}
This conforms to our requirements above, and also behaves like a real HttpServletRequest in that it returns the dreaded null if no header by the provided name exists.
Next is the trait:
trait Acceptor {
val headers = "Accept" :: "X-Accept" :: Nil
def accepts(request: HttpServletRequest, contentType: String) =
headers map (request.getHeader) filter(_ != null) map {
_ == contentType
} reduceLeft(_ || _)
}
Notice the headers val. This is a list of strings containing the header names to read from the request and interpret as Accept header values.
The fun part is the accepts method, which takes a servlet request (conveniently aliased for REPL play), and a content (MIME) type. This is purely functional; there are no side-affects, it is deterministic, and the algorithm is a series of four transformations occurring on one collection after another. Through the transformations, the collection is reduced to a single boolean value. Let’s start with the first line:
headers map (request.getHeader) filter(_ != null)
Starting with the list of header names to inspect in the request, we map it with request.getHeader. This creates a new list, where each element is the result of applying request.getHeader to each element in the original headers list. The original list contains 2 elements, “Accept” and “X-Accept”, and so the resulting list now looks like this: List(application/xml, text/html). Our request object responded to the query for “Accept” with “application/xml”, and “text/html” to “X-Accept.” Had either of those header names not been present, a null value would now be present in the list.
Now we filter this resulting list. Filter, like map, applies a function to each element in the collection and results in a new collection. The function receives each element and returns a boolean value. For each invocation that results in true, that element is added to the resulting list (you filter a collection using a function). We use a literal shorthand to assert that the element is not null, and so the resulting list will contain non-null strings. This filtering is simply protection against null. Our transformed list doesn’t contain any nulls, so the new list looks the same as the old one: List(application/xml, text/html).
Next we have another mapping operation, using brackets instead of parentheses.
map {
_ == contentType
}
The use of brackets here is a style choice, and in this case is the same as writing map(_ == contentType). This part also evolved from a partial function, where the body is a case statement, matching on the single argument. The function being mapped simply checks if each element is equal to the contentType argument. Remember that == in scala, contrary to java, is an object equality test and not an identity test (eq is for identity tests). We left off at List(application/xml, text/html), and after this mapping transformation, arrive at List(true, false).
The final transformation, simple as it is, actually took me a bit to realize. The function itself had undergone a few iterations, each representing in some way, a group of header values in the request that did or didn’t match a given content type. I couldn’t seem to reach a solution that felt natural. I thought about filtering on true, and then checking that the result list wasn’t empty, but that didn’t really express the essence of the computation. Same for a find (and then matching on Option(true)). They would have worked, but they missed the point and, as all point-missers do, cluttered the algorithm.
Then it hit me:
reduceLeft(_ || _)
Exactly what I wanted. reduceLeft receives a 2 argument function, walks the contents of the collection, and returns the accumulated result. On each invocation, it passes the current and the next element as arguments, progressing from left to right, until the collection is exhausted. The result of one invocation becomes the input to the next. As a simple example, consider list summations:
scala> List(1,2,3) reduceLeft(_ + _)
res14: Int = 6
scala> List(1,2,3,4) reduceLeft(_ + _)
res15: Int = 10
Back to our exmaple, using function-literal shorthand, I OR the two arguments. This means that if either of the two are true, the resulting value is true. Reducing a list of booleans with this function results in a single boolean value. Because I’m using OR, a single true value in the list will result in a true result at the end of the reduction. We left off at a List(true, false), and after our reduction, arrive at a single true result. Perfect.
Last of all, a singleton that mixes in the Acceptor trait.
object matcher extends Acceptor
This just gives us an object with Acceptor behavior mixed in, so we can play in the REPL.