Scalatra is a lightweight, high-performance web framework close to
Sinatra , which can make your life a lot easier when switching from Ruby to Scala. In this article, I want to fill the gap in the absence of manuals in Russian for this interesting framework using the example of creating a simple application with the ability to authenticate.
Installation
Official documentation suggests creating a project using
giter8 from a previously prepared template. However, if you want to do without extra tools, you can simply create a sbt project, as follows:
project \ plugins.sbtaddSbtPlugin("com.earldouglas" % "xsbt-web-plugin" % "1.1.0")
This plugin will allow you to run a web service using a special
sbt command:
')
$ sbt > container:start
build.sbt val scalatraVersion = "2.4.0-RC2-2" resolvers += "Scalaz Bintray Repo" at "https://dl.bintray.com/scalaz/releases" lazy val root = (project in file(".")).settings( organization := "com.example", name := "scalatra-auth-example", version := "0.1.0-SNAPSHOT", scalaVersion := "2.11.6", scalacOptions ++= Seq("-unchecked", "-deprecation", "-feature"), libraryDependencies ++= Seq( "org.scalatra" %% "scalatra-auth" % scalatraVersion, "org.scalatra" %% "scalatra" % scalatraVersion, "org.scalatra" %% "scalatra-json" % scalatraVersion, "org.scalatra" %% "scalatra-specs2" % scalatraVersion % "test", "org.json4s" %% "json4s-jackson" % "3.3.0.RC2", "javax.servlet" % "javax.servlet-api" % "3.1.0" % "provided" ) ).settings(jetty(): _*)
The purpose of the added libraries can be understood from their name, if you do not need
json or authentication - you can safely remove unnecessary.
Routing
In order for the service to start responding to requests, you first need to specify which controllers will respond to requests. Create this file for this:
src \ main \ webapp \ WEB-INF \ web.xml <?xml version="1.0" encoding="UTF-8"?> <web-app version="2.5" xmlns="http://java.sun.com/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"> <servlet> <servlet-name>user</servlet-name> <servlet-class> org.scalatra.example.UserController </servlet-class> </servlet> <servlet-mapping> <servlet-name>user</servlet-name> <url-pattern>/user/*</url-pattern> </servlet-mapping> </web-app>
If you have an aversion to xml, you can describe the same more compactly in this way:
src / main / scala / ScalatraBootstrap.scala import org.scalatra.example._ import org.scalatra._ import javax.servlet.ServletContext class ScalatraBootstrap extends LifeCycle { override def init(context: ServletContext) { context.mount(new UserController, "/user") } }
Here we have determined that
org.scalatra.example.UserController will respond to requests starting from the path
yoursite.example / user . Let's see how this file works:
src \ main \ scala \ org \ scalatra \ example \ UserController.scala package org.scalatra.example import org.json4s.{DefaultFormats, Formats} import org.scalatra._ import org.scalatra.json.JacksonJsonSupport import scala.util.{Failure, Success, Try} class UserController extends ScalatraServlet with AuthenticationSupport with JacksonJsonSupport { protected implicit lazy val jsonFormats: Formats = DefaultFormats before() { contentType = formats("json") basicAuth() } get("/") { DB.getAllUsers } get("/:id") { Try { params("id").toInt } match { case Success(id) => DB.getUserById(id) case Failure(ex) => pass() } } }
Let's sort this code in more detail. To begin with, all controllers in
Scalatra must inherit from
ScalatraServlet . To determine the paths that the servlet will respond to, you need to add a get, post, put, or delete block (depending on the type of request), for example:
get("/") { }
will respond to requests to
yoursite.example / user . If any of the parameters are part of the URL, you need to describe your parameters like this:
get("/:id") { params("id") }
As a result, inside the get block, you can use the id parameter using the
params () method. Similarly, you can get the rest of the query parameters. If you are a
pervert, you want to pass several parameters with the same name, for example
/ user / 52? Foo = uno & bar = dos & baz = three & foo = anotherfoo (note that the parameter foo occurs 2 times), you can use the
multiParams () function that allows you to process parameters, for example:
multiParams("id")
I note that the
User (Control) method uses the
pass () method. It allows you to skip processing along this route and go to the following routes (although in this case, there are no more handlers for which this path falls). If you want to interrupt the processing of the request and show the user an error page, use the
halt () method, which can accept various parameters, such as the return code and the error text.
Another possibility provided by the framework is to set pre- and post-handlers, for example, by writing:
before() { contentType = formats("json") basicAuth() }
You can specify the type of response (in this case
json ) and request authentication from the user (authentication and working with json will be discussed in the following sections).
More information about routing can be found in the
official documentation .
Work with DB
In the previous section, the objects received from the BD class are used as the controller's response. However,
Scalatra does not have a built-in framework for working with a database, and therefore I left only the imitation of working with the database.
src \ main \ scala \ org \ scalatra \ example \ DB.scala package org.scalatra.example import org.scalatra.example.models.User object DB { private var users = List( User(1, "scalatra", "scalatra"), User(2, "admin", "admin")) def getAllUsers: List[User] = users def getUserById(id: Int): Option[User] = users.find(_.id == id) def getUserByLogin(login: String): Option[User] = users.find(_.login == login) }
src \ main \ scala \ org \ scalatra \ example \ models \ User.scala package org.scalatra.example.models case class User(id: Int, login:String, password: String)
However, do not think that there are any difficulties with this - the official
documentation describes how to make friends with
Scalatra with the most popular databases and ORM:
Slick ,
MongoDB ,
Squeryl ,
Riak .
Json
Please note that the controller returns directly the
case class User , and more specifically even Option [User] and List [User]. By default,
Scalatra converts the return value to a string and uses it as a response to the request, that is, for example, the answer to the / user request is:
List(User(1,scalatra,scalatra), User(2,admin,admin)).
In order for the servlet to start working with json, you need to:
After performing these simple steps, the answer to the same query / user will become like this:
[{"id":1,"login":"scalatra","password":"scalatra"},{"id":2,"login":"admin","password":"admin"}]
Authentication
Finally, I would like to touch on a topic such as user authentication. For this, it is proposed to use the
Scentry framework, which is the
Warden framework ported to Scala, which can also make life easier for people familiar with Ruby.
If you look closely at the
UserController class, you can find that authentication has already been implemented in it. To do this, the
AuthenticationSupport treit is mixed into the class and the
basicAuth () method is called in the
before () filter. Take a look at the implementation of
AuthenticationSupport .
src \ main \ scala \ org \ scalatra \ example \ AuthenticationSupport.scala package org.scalatra.example import org.scalatra.auth.strategy.{BasicAuthStrategy, BasicAuthSupport} import org.scalatra.auth.{ScentrySupport, ScentryConfig} import org.scalatra.example.models.User import org.scalatra.ScalatraBase import javax.servlet.http.{HttpServletResponse, HttpServletRequest} class OurBasicAuthStrategy(protected override val app: ScalatraBase, realm: String) extends BasicAuthStrategy[User](app, realm) { protected def validate(userName: String, password: String)(implicit request: HttpServletRequest, response: HttpServletResponse): Option[User] = { DB.getUserByLogin(userName).filter(_.password == password) } protected def getUserId(user: User)(implicit request: HttpServletRequest, response: HttpServletResponse): String = user.id.toString } trait AuthenticationSupport extends ScentrySupport[User] with BasicAuthSupport[User] { self: ScalatraBase => val realm = "Scalatra Basic Auth Example" protected def fromSession = { case id: String => DB.getUserById(id.toInt).get } protected def toSession = { case usr: User => usr.id.toString } protected val scentryConfig = new ScentryConfig {}.asInstanceOf[ScentryConfiguration] override protected def configureScentry() = { scentry.unauthenticated { scentry.strategies("Basic").unauthenticated() } } override protected def registerAuthStrategies() = { scentry.register("Basic", app => new OurBasicAuthStrategy(app, realm)) } }
The first thing to do is to define an authentication strategy — a class that implements the
ScentryStrategy interface. In this case, we used the
BasicAuthStrategy [User] stub that implements some standard methods. After that, we need to define 2 methods -
validate () , which in case of a successful login should return Some [User], or None in case of invalid data and
getUserId () , which should return a string in order to further add it to the response headers.
The next thing to do is to merge
OurBasicAuthStrategy and
ScentrySupport into the
AuthenticationSupport treit, which we will mix with the controller. In it, we registered our authentication strategy and implemented (in the simplest way) ways to get a user object from a session and, conversely, add its id to the session.
As a result, if a non-logged-in user enters a page that
UserController is responsible for handling, he will first need to enter a login and password.
Conclusion
This article showed only a few, the selective features of
Scalatra . Although this framework is not very popular in the Russian-speaking community, a wide range of implemented functionality and ease of development make it very promising for writing both small web services and large sites.
If after reading the article you have any questions, I am ready to answer them in the comments or in the following articles.
All source code is available on
github .
Have a good learning!