table of contents »

Circumflex Web Framework Documentation

Table Of Contents

This document is also available in a multi-page format.

Overview

Circumflex Web Framework is a minimalistic DSL for quick and robust Web application development.

It makes no assumptions of your applications and maintains limited feature set allowing you to use your favorite tools and libraries. Due to its unobtrusive nature it also plays nicely with different frameworks and view technologies.

Here's the sample web application:

class Main extends RequestRouter {
  get("/") = "Hello world!"
  get("/posts/:id") = "Post #" + uri("id")
  post("/form") = {
    // do some work
    // render FreeMarker template:
    ftl("/done.ftl")
  }
}

Installation & Configuration

Circumflex web applications run in standard Servlet 2.5 containers. There's a couple of things you should do in order to start using Circumflex Web Framework.

If you use Maven for building your project, add following lines to pom.xml (or merge XML sections accordingly):

<properties>
  <cx.version><!-- desired version --></cx.version>
</properties>
<dependencies>
  <dependency>
    <groupId>ru.circumflex</groupId>
    <artifactId>circumflex-web</artifactId>
    <version>${cx.version}</version>
  </dependency>
</dependencies>

If you prefer SBT, make sure that libraryDependencies of your project contains following artifact:

"ru.circumflex" % "circumflex-web" % cxVersion % "compile->default"

where cxVersion points to desired Circumflex version. Here's the sample project configuration:

import sbt._

 class MyProject(info: ProjectInfo) extends DefaultWebProject(info) {
  val cxVersion = "2.0"

  override def libraryDependencies = Set(
      "ru.circumflex" % "circumflex-web" % cxVersion % "compile->default"
  ) ++ super.libraryDependencies

}

You can follow SBT Setup Guide to create a new project.

Note that first-time builds usually require a substantial amount of dependencies downloads.

Configure the main request router of your application by setting cx.router configuration parameter:

Please refer to Circumflex Configuration API for more information on how to configure your application.

Imports

All code examples assume that you have following import statement in code where necessary:

import ru.circumflex._, core._, web._

Basic Concepts

Request Routers

Each Circumflex web application is composed of one or more request routers. Request router is a subclass of RequestRouter which sequentionally defines routes directly within its body:

class Main extends RequestRouter {
  get("/") = "Hello world!"
  get("/posts/:id") = "Post #" + uri.get("id")
  post("/form") = {
    // Do some work
    // . . .
    // Render FreeMarker template:
    ftl("/done.ftl")
  }
}

Request routers are essentially the controllers of the application. Since Circumflex Web Framework employs the Front Controller pattern, each web application should have a single main router — a special RequestRouter which gets executed on every request. It dispatches all requests of web application.

Request routers can also be easily nested:

class MainRouter extends RequestRouter {
  // with matching
  any("/users/*") => new UsersRouter
  any("/posts/*") => new PostsRouter
  any("/mail/*") => new MailRouter
  any("/downloads/*") => new DownloadsRouter
  // unconditionally
  new MiscRouter
}

It is generally a good practice to have different routers for different tasks — it makes the code modular, more organized and easier to maintain.

Routes

Circumflex Web Framework is designed around the route concept. A route is an HTTP method with matching mechanism and attached block.

Routes are defined using one of the following members of RequestRouter:

Each route should define a matcher, which describes the conditions a request must satisfy to be matched by the route.

Each route also has an associated block which gets executed if matching succeeds. A block must evaluate to RouteResponse which will be sent to client (String and scala.xml.Node are converted to RouteResponse implicitly):

class MyRouter extends RequestRouter {
  get("/hello/:name.txt") = "Hello, " + param("name") + "!"
  get("/hello/:name.xml") = {
    val name = param("name")
    <hello to={name}/>
  }
}

Upon successful matching the block attached to corresponding route gets executed. Internally we use ResponseSentException, a special control throwable, to indicate that request processing has been finished. This exception is caught by CircumflexFilter which performs response flushing and finalizes request processing.

Various helpers throw ResponseSentException instead of yielding RouteResponse:

get("/") = redirect("/index.html")

Matchers

Request matching can be performed against request URI and zero or more request headers. The syntax is self-descriptive:

get("/")        // matches GET /
get("/posts")   // matches GET /posts
post("/posts")  // matches POST /posts

You can combine several matchers in one route using the & method:

get("/mail" & Accept("text/html") & Host("localhost"))
// matches following request:
// GET /mail
// Host: localhost
// Accept: text/html

Parameters

Routes can include patterns with named parameters which can be accessed in the attached block. The following route matches GET /posts/43 or GET /posts/foo; the construct uri("id") is used to capture the parameter from request URI:

get("/posts/:id") = "Post #" + uri("id")

Route patterns may also include wildcard parameters (* for zero or more characters, + for one or more characters), they can be accessed via index (starting with 1 like in regex groups):

get("/files/+") = "Downloading file " + uri(1)

You may also refer to the whole match with 0 index:

get("/files/:name.:ext") = {
  println("The URI is: " + uri(0))
  "Filename is " + uri("name") + ", extension is " + uri("ext")
}

Named parameters are indexed too:

get("*/:two/+/:four") = {
  // uri(2) == uri("two")
  // uri(4) == uri("four")
  (1 to 4).map(i => i + " -> " + uri(i)).mkString("\n")
}

Parameters can also be extracted using the param helper. Unlike uri, which represents match results from URI only, the param helper can extract named parameters from headers:

get("/" & Accept("text/:format")) = "The format is " + param("format")

You can also extract request parameters using param:

get("/") = "Limit is " + param("limit") + ", offset is " + param.getOrElse("offset", "0")
// >> GET /?limit=50&offset=10
// << Limit is 50, offset is 10
// >> GET /?limit=5
// << Limit is 5, offset is 0

Serving Static files

By default static files are served from <webapp_root>/public directory. You may override this by setting cx.public configuration parameter.

Redirecting & Forwarding

You can send 302 Found HTTP redirect:

get("/") = sendRedirect("/index.html")

You can also perform request forwarding (a.k.a. URI rewriting) — the request will be dispatched again, but with different URI):

get("/") = forward("/index.html")

Note that you should add <dispatcher>FORWARD</dispatcher> to CircumflexFilter mapping in your web.xml to make forwarding work. You should also avoid infinite forwarding loops manually.

Sending Errors

You can send errors with specific status code and optional message:

get("/") = sendError(500, "We don't work yet.")

Sending Files

You can use the sendFile helper to send arbitrary file to client:

get("/") = sendFile(new File("/path/to/file.txt"))

You can also specify optional filename so that Content-Disposition: attachment could be added to response:

get("/") = sendFile(new File("/path/to/file.txt"), "greetings.txt")

The content type of the file is guessed based on it's extension. You may override it:

get("/") = {
  response.contentType("text/plain")
  sendFile(new File("/path/to/file.text"), "greetings.txt")
}

You can also use the more efficient xSendFile helper to delegate the file transfering to your web server. This feature is configured via cx.XSendFile configuration parameter. Consult your web server documentation to obtain more information on this feature.

Handling AJAX Requests

You can determine if current request is XmlHttpRequest:

get("/") = if (request.body.xhr_?) "AJAX" else "plain old request"

Accessing Headers

You get the contents of request headers using the header helper:

get("/") = "Serving to host: " + headers("Host")

Accessing Session

Dealing with session attributes is fairly easy:

get("/") = {
  // get attribute
  session("attr1")
  // set attribute
  session("attr2") = "My value"
}

Flashes

Flashes provide a way to pass temporary objects between requests:

get("/") = flash.get("note") match {
  case Some(value) => "Having a note: " + value
  case None => "No notes for now..."
}

post("/") = {
  flash("note") = "Hello from POST, folks!"
  redirect("/")
}

Anything you place in flash helper will be exposed until the first lookup and then cleared out. This is a great way of dealing with notices and alerts which only need to be shown once.

Advanced Concepts

This topic reveals some nitty-gritty details about Circumflex Web Framework internals.

Matching

The central abstractions of the route matching mechanism are:

Match objects are designed to be used inside attached blocks of routes, where you can naturally assume that match succeeded:

get("/:name") = uri("name")

Match also has name which reflects the context where match has occured. URI-based matcher returns Match with name uri while headers-based matchers create Match objects with the name of corresponding HTTP header.

You can lookup certain Match object in CircumflexContext by it's name:

get("/foo" & Accept("text/:format")) = ctx("Accept") match {
  case m: Match => "Requested format is " + m("format")
  case _ => ""
}

In general you don't have to lookup Match objects, the param helper can retrieve named parameters from all matches that appear in CircumflexContext. So the previous example could be rewritten in much more convenient manner:

get("/foo" & Accept("text/:format")) = "Requested format is " + param("format")

However, there are situations where looking up a Matcher can come in handy. For example, you cannot access splats (wildcard matches) or indexed parameters with param:

get("/foo" & Accept("text/+")) = ctx("Accept") match {
  case m: Match => "Requested format is " + m(1)
  case _ => ""
}

Standard RegexMatcher can also accept an arbitrary regular expression, the groups will be accessible from Match by their index:

get("/posts/(\\d+)".r) = {
  val id = uri(1).toLong
  // lookup the post by id and render response
  "..."
}

Router Prefix

You may optionally specify the prefix for request router. All URI-based matchers inside the router will be prepended by this prefix:

class PostsRouter extends RequestRouter("/posts") {
  get("/") = "Showing posts"                  // matches GET /posts/
  get("/show/:id") = "Post " + param("id")    // matches GET /posts/show/149
}

Alternatively, you can let the enclosing router specify a prefix for subrouter:

class SubRouter(prefix: String) extends RequestRouter(prefix)

class MainRouter extends RequestRouter {
  new SubRouter("/prefix-a")
  new SubRouter("/prefix-b")
}

Note that main request routers should have the default zero-arguments constructor, so the prefix must be hardcoded. Generally, main routers have "" prefix (unless different filter mappings are specified in web.xml).