Writing a web service on Scalatra

Scalatra is a lightweight, high-performance web framework close to Sinatra , which can make your life much easier when moving from Ruby to Scala. In this article I want to fill the gap in the absence of manuals in Russian for this interesting framework on the example of creating a simple application with the ability to authenticate.

Installation


The official documentation suggests creating a project using giter8 from a pre-prepared template. However, if you want to do without extra tools, you can simply create an sbt project as follows:

project \ plugins.sbt

addSbtPlugin("com.earldouglas"  % "xsbt-web-plugin" % "1.1.0")

This plugin will allow you to start 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"lazyval 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 ones.

Routing


In order for the service to start responding to requests, you first need to specify which controllers will respond to requests. Let's create the following file for this:

src \ main \ webapp \ WEB-INF \ web.xml

<?xml version="1.0" encoding="UTF-8"?><web-appversion="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 are averse to xml, you can describe the same thing more compactly this way:

src / main / scala / ScalatraBootstrap.scala

import org.scalatra.example._
import org.scalatra._
import javax.servlet.ServletContextclassScalatraBootstrapextendsLifeCycle{
  overridedefinit(context: ServletContext) {
    context.mount(newUserController, "/user")
  }
}

Here we determined that org.scalatra.example.UserController will respond to requests starting with 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.JacksonJsonSupportimport scala.util.{Failure, Success, Try}
classUserControllerextendsScalatraServletwithAuthenticationSupportwithJacksonJsonSupport{
  protectedimplicitlazyval jsonFormats: Formats = DefaultFormats
  before() {
    contentType = formats("json")
    basicAuth()
  }
  get("/") {
    DB.getAllUsers
  }
  get("/:id") {
    Try {
      params("id").toInt
    } match {
      caseSuccess(id) => DB.getUserById(id)
      caseFailure(ex) => pass()
    }
  }
}

Let's analyze this code in more detail. First, all controllers in Scalatra must inherit from ScalatraServlet . To determine the paths for which the servlet will respond, 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, you can use the id parameter inside the get block using the params () method . Similarly, you can get the rest of the query parameters. If you pervert want to pass several parameters with the same name, for example / user / 52? Foo = uno & bar = dos & baz = three & foo = anotherfoo (note that the foo parameter is found here 2 times), you can use the multiParams () function , which allows you to uniformly process parameters, for example:

  multiParams("id") // => Seq("52")
  multiParams("foo") // => Seq("uno", "anotherfoo")
  multiParams("unknown") // => an empty Seq

Note that the Pass () method is used in the UserController . It allows you to skip processing on a given route and move on to the following routes (although in this case, there are no more handlers that this way falls under). If you want to interrupt the processing of the request and show the user the error page, use the halt () method , which can take various parameters, for example, a return code and an 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 require 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 a DB


In the previous section, objects obtained from the BD class are used as a controller response. However, in Scalatra there is no built-in framework for working with the database, in connection with which I left only an imitation of working with the database.

src \ main \ scala \ org \ scalatra \ example \ DB.scala

package org.scalatra.example
import org.scalatra.example.models.UserobjectDB{
  privatevar users = List(
    User(1, "scalatra", "scalatra"),
    User(2, "admin", "admin"))
  defgetAllUsers: List[User] = users
  defgetUserById(id: Int): Option[User] = users.find(_.id == id)
  defgetUserByLogin(login: String): Option[User] = users.find(_.login == login)
}

src \ main \ scala \ org \ scalatra \ example \ models \ User.scala

package org.scalatra.example.models
caseclassUser(id: Int, login:String, password: String)

However, do not think that there are any difficulties with this - the official documentation describes how to make Scalatra friends with the most popular databases and ORMs: Slick , MongoDB , Squeryl , Riak .

Json


Note that the controller returns directly the case class User , or rather, 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, i.e., for example, the response to the / user request will be like this:

List(User(1,scalatra,scalatra), User(2,admin,admin)).

In order for the servlet to start working with json, you must:
  • Add JacksonJsonSupport trait to it
  • Specify the conversion format to json. Scalatra uses json4s to work with json, which allows you to create custom conversion rules for json and vice versa. In our case, the default format will be enough:

    protectedimplicitlazyval jsonFormats: Formats = DefaultFormats
  • Add a header with return type:

    contentType = formats("json")

After performing these simple steps, the response to the same / user request will become:

[{"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. To do this, it is proposed to use the Scentry framework, which is a 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 will find that authentication is already implemented in it. To do this, the AuthenticationSupport trait is added to 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.Userimport org.scalatra.ScalatraBaseimport javax.servlet.http.{HttpServletResponse, HttpServletRequest}
classOurBasicAuthStrategy(protected override val app: ScalatraBase, realm: String) extendsBasicAuthStrategy[User](app, realm) {
  protecteddefvalidate(userName: String, password: String)(implicit request: HttpServletRequest, response: HttpServletResponse): Option[User] = {
    DB.getUserByLogin(userName).filter(_.password == password)
  }
  protecteddefgetUserId(user: User)(implicit request: HttpServletRequest, response: HttpServletResponse): String = user.id.toString
}
traitAuthenticationSupportextendsScentrySupport[User] withBasicAuthSupport[User] {
  self: ScalatraBase =>
  val realm = "Scalatra Basic Auth Example"protecteddeffromSession= {
    case id: String => DB.getUserById(id.toInt).get
  }
  protecteddeftoSession= {
    case usr: User => usr.id.toString
  }
  protectedval scentryConfig = newScentryConfig {}.asInstanceOf[ScentryConfiguration]
  overrideprotecteddefconfigureScentry() = {
    scentry.unauthenticated {
      scentry.strategies("Basic").unauthenticated()
    }
  }
  overrideprotecteddefregisterAuthStrategies() = {
    scentry.register("Basic", app => newOurBasicAuthStrategy(app, realm))
  }
}

The first thing to do is define an authentication strategy — a class that implements the ScentryStrategy interface . In this case, we used the BasicAuthStrategy [User] preform 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 incorrect data and getUserId () , which should return a string for further adding to the response headers.

The next thing to do is merge OurBasicAuthStrategy and ScentrySupport into the AuthenticationSupport trait., 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 user who is not logged in visits the page for which UserController is responsible for processing , he will first need to enter a username and password.

Conclusion


This article has shown only some of 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 still 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 nice study!

Also popular now: