This document is also available in a multi-page format.
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")
}
}
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:
via src/main/resources/cx.properties file:
cx.router=com.myapp.web.MainRouter
pom.xml using Circumflex Maven Plugin.Please refer to Circumflex Configuration API for more information on how to configure your application.
All code examples assume that you have following import statement in code where necessary:
import ru.circumflex._, core._, web._
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.
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:
get (matches HTTP GET requests);post (matches HTTP POST requests);put (matches HTTP PUT requests);patch (matches HTTP PATCH requests);delete (matches HTTP DELETE requests);options (matches HTTP OPTIONS requests);head (matches HTTP HEAD requests);getOrHead (matches either HTTP GET or HTTP HEAD);getOrPost (matches either HTTP GET or HTTP POST);any (matches any HTTP request).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")
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
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
By default static files are served from <webapp_root>/public directory. You may override this by setting cx.public configuration parameter.
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.
You can send errors with specific status code and optional message:
get("/") = sendError(500, "We don't work yet.")
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.
You can determine if current request is XmlHttpRequest:
get("/") = if (request.body.xhr_?) "AJAX" else "plain old request"
You get the contents of request headers using the header helper:
get("/") = "Serving to host: " + headers("Host")
Dealing with session attributes is fairly easy:
get("/") = {
// get attribute
session("attr1")
// set attribute
session("attr2") = "My value"
}
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.
This topic reveals some nitty-gritty details about Circumflex Web Framework internals.
The central abstractions of the route matching mechanism are:
Match — holds parameters extracted during successful match (already familiar helper, uri, is Match actually);Matcher — performs actual matching and yields a sequence of Match objects on success.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
"..."
}
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).