1. Introduction
In this tutorial, we’ll show basic how-tos for the Scalatra framework.
Scalatra is a framework with a small core that enables us to build it upwards with various integrations for common tasks such as Akka, Twirl templates, database connections, and more.
2. Setup
We can generate a skeleton application using Gitter. Let’s see the initialization:
sbt new scalatra/scalatra.g8
After answering some standard Gitter questions such as organization name, version, and a few others, the project is generated.
3. Minimal Application
Now that we’ve generated the project, we can proceed to the first deployment:
sbt
>jetty:start
The application is deployed to port 8080 by default.
A simple MyScalatraServlet class returns the simple greeting – contained in the Twirl template hello.scala.html – that we get when visiting localhost:8080:
class MyScalatraServlet extends ScalatraServlet {
get("/") {
views.html.hello()
}
}
4. Configuration
While most frameworks don’t require any XML configuration, Scalatra requires some changes to web.xml. However, the developer has the choice to do most of the configuration with Scala code and not web.xml entries.
Let’s take a look at the web.xml:
<context-param>
<param-name>org.scalatra.LifeCycle</param-name>
<param-value>ScalatraDevelopmentBootstrap</param-value>
</context-param>
<listener>
<listener-class>org.scalatra.servlet.ScalatraListener</listener-class>
</listener>
Scalatra allows us to configure the application via bootstrap classes, which in this case is simply the ScalatraDevelopmentBootstrap class. Bootstrap classes enable us to set up initial configuration via ServletContext, mount servlets, and more.
Let’s view the ScalatraDevelopmentBootstrap code:
override def init(context: ServletContext) {
context.mount(new MyScalatraServlet, "/*")
context.setInitParameter("org.scalatra.environment", "development")
}
It goes without saying that we need environment-specific bootstrap classes. So, let’s have a look at the ScalatraProductionBootstrap code:
override def init(context: ServletContext) {
context.mount(new MyScalatraServlet, "/*")
context.setInitParameter("org.scalatra.environment", "production")
}
Of course, we can mount servlets and set context parameters via the web.xml file, but this is the Scalatra way.
5. Authentication
Authentication in Scalatra comes as an extension. The optional authentication system is Sentry, which is a port of Ruby’s Warden. We’ll need to add the dependency to our build.sbt file:
libraryDependencies += "org.scalatra" %% "scalatra-auth" % 2.8.2
To enforce authentication in our endpoints, we need to take two steps.
First, we need to implement a ScentryStrategy:
class CustomBasicAuthStrategy(protected override val app: ScalatraBase, realm: String)
extends BasicAuthStrategy[User](app, realm) {
override protected def getUserId(user: User)(implicit request: HttpServletRequest, response: HttpServletResponse): String = user.email
override protected def validate(userName: String, password: String)(implicit request: HttpServletRequest, response: HttpServletResponse): Option[User] = {
if (userName == "user" && password == "pwd") Some(User(Random.nextLong(), "scalatra"))
else None
}
}
Second, we need to implement a ScentrySuport trait that registers the ScentryStrategy we implemented:
trait AuthSupport extends ScentrySupport[User] with BasicAuthSupport[User] {
self: ScalatraBase =>
override protected def fromSession: PartialFunction[String, User] = {
email => User(Random.nextLong(), email)
}
override protected def toSession: PartialFunction[User, String] = {
user => user.email
}
override protected def registerAuthStrategies(): Unit = {
scentry.register("Basic", app => new CustomBasicAuthStrategy(app, realm))
}
override protected def configureScentry(): Unit = {
scentry.unauthenticated {
scentry.strategies("Basic").unauthenticated()
}
}
protected val scentryConfig = new ScentryConfig {}
override type ScentryConfiguration = ScentryConfig
val realm = "Scalatra Basic Auth Example"
}
Now, we can use the AuthSupport trait in Servlets:
class BasicAuthServlet extends ScalatraServlet with AuthSupport {
get("/") {
basicAuth()
"Basic auth protected page"
}
}
It’s important to note that authentication takes place only if we call the basicAuth() function.
We can also avoid calling basicAuth() to every endpoint by calling it in a before() filter in our controller or by setting it up as a before() filter in the AuthSupport trait.
6. JSON Support
It’s common for web frameworks to require some form of configuration or implementation for transforming HTTP responses.
Interestingly enough*, ScalatraServlet* converts HTTP responses to the string representation of the Scala source. The following controller demonstrates the default behavior:
case class User(id: Long, email: String)
object User {
def all = List(
User(1L, "[email protected]"),
User(2L, "[email protected]"),
User(3L, "[email protected]"),
User(4L, "[email protected]")
)
}
get("/") {
User.all
}
When we visit localhost:8080/user, we get the response:
List(User(1,[email protected]), User(2,[email protected]), User(3,[email protected]), User(4,[email protected]))
Therefore, we need to enable JSON responses for our controllers. First of all, let’s include the Scalatra JSON libraries:
libraryDependencies ++= Seq(
"org.scalatra" %% "scalatra-json" % "2.8.2",
"org.json4s" %% "json4s-jackson" % "4.0.1"
)
Now, in order to add JSON output support to UserController, we must mixin in the JacksonJsonSupport trait, apply a before() filter, and define implicit JSON formats.
Let’s see how our UserConrtoller looks with JSON support:
class UserServlet extends ScalatraServlet with JacksonJsonSupport {
protected implicit lazy val jsonFormats: Formats = DefaultFormats
before() {
contentType = formats("json")
}
get("/") {
User.all
}
Finally, let’s visit localhost:8080/user once more to get the JSON response:
[{"id":1,"email":"[email protected]"},{"id":2,"email":"[email protected]"},{"id":3,"email":"[email protected]"},{"id":4,"email":"[email protected]"}]
To receive JSON in the UserServlet, no further changes are required. Let’s see an example of reading JSON:
post("/:userId") {
val userId = params("userId")
val body = parsedBody.extract[User]
log(s"userId: $userId, body: $body")
...
7. Database
For the purpose of this tutorial, for database access, we’ll use the Slick data access library. First of all, let’s add the dependencies to the build.sbt file:
libraryDependencies ++= Seq(
"com.typesafe.slick" %% "slick" % "3.4.1",
"com.h2database" % "h2" % "2.1.214",
"com.mchange" % "c3p0" % "0.9.5.2"
)
Let’s see how to map a database table to a case class with Slick:
class Users(tag: Tag) extends Table[User](tag, "user") {
def id = column[Long]("id", O.PrimaryKey, O.AutoInc)
def email = column[String]("email")
override def * = (id, email) <> (User.tupled, User.unapply)
}
val users = TableQuery[Users]
def all = users.result
def insert(user: User) = users += user
Also, we’ve used c3p0 connection pooling and h2 as a database. We need to add the c3p0 properties file in our resources folder:
c3p0.driverClass=org.h2.Driver
c3p0.jdbcUrl=jdbc:h2:mem:test
c3p0.user=root
c3p0.password=
c3p0.minPoolSize=1
c3p0.acquireIncrement=1
c3p0.maxPoolSize=50
With the configuration out of the way, we can now initialize the database connections in our bootstrap classes. To do so, we’ll make some additions to ScalatraDevelopmentBootstrap:
val cpds = new ComboPooledDataSource
// code ...
override def init(context: ServletContext) {
val db = Database.forDataSource(cpds, None)
context.mount(new UserServlet(db), "/user/*")
context.mount(new DbServlet(db), "/db/*")
// code...
override def destroy(context: ServletContext): Unit = {
super.destroy(context)
cpds.close()
}
In the absence of a sophisticated database migration system, the DbServlet will play the role of the database management API for us.
Let’s have a look:
class DbServlet(val db: Database)
extends ScalatraServlet with DbSupport with FutureSupport {
post("/create-users") {
db.run(UserRepo.users.schema.create)
}
post("/insert-users") {
db.run(UserRepo.insertData)
}
post("/destroy-users") {
db.run(UserRepo.users.schema.drop)
}
}
Finally, we can use UserRepo in the UserServlet:
class UserServlet(val db: Database) extends ScalatraServlet with JacksonJsonSupport with DbSupport {
// ...
get("/") {
db.run(UserRepo.all)
}
// ...
8. Twirl
While we can just inline HTML in Scalatra Servlets, we chose to present HTML views with Twirl, the Play 2 templating engine.
To install Twirl, we simply need to enable the SbtTwirl plugin in the build.sbt file:
enablePlugins(SbtTwirl)
Twirl layouts can be added to the src/main/twirl/layouts folder and the views under src/main/twirl/views.
To demonstrate Twirl, we changed the hello.scala.html and now use the environment variable to print a greeting message:
@(env: String)
@layouts.html.default("Scalatra: a tiny, Sinatra-like web framework for Scala", "Welcome to Scalatra"){
<p>Hello, Twirl from @env!</p>
}
The default HTML layout that we used is:
@(title: String, headline: String)(body: Html)
<html>
<head>
<title>@title</title>
</head>
<body>
<h1>@headline</h1>
@body
</body>
</html>
Now, we just need to bind this view to a Scalatra servlet. Let’s add an endpoint to MyScalatraServlet which will serve the hello.scala.html view:
class MyScalatraServlet extends ScalatraServlet {
get("/") {
val env = getServletContext.getInitParameter("org.scalatra.environment")
views.html.hello(env)
}
}
This is the curl result to the localhost:8080 endpoint for the development configuration:
~ curl -X GET http://localhost:8080
<html>
<head>
<title>Scalatra: a tiny, Sinatra-like web framework for Scala</title>
</head>
<body>
<h1>Welcome to Scalatra</h1>
<p>Hello, Twirl from development!</p>
</body>
</html>
9. Conclusion
In this article, we implemented some of the most basic functionalities of a web framework using Scalatra.
As always, the code of the above examples is available over on GitHub.