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
).