Play
So I’ve learned Scala the language, and now I think it’s time to learn Play, a popular web framework for it. There is also a browsable API documentation.
Command
The play
command is similar to the rails
command. The new
option bootstraps a simple application with the following directory structure:
Directory | Purpose |
---|---|
app | application source |
conf | configuration files/data |
project | project build scripts |
public | static files |
test | tests |
The run
option starts the Play server and runs the application. Specifying it as ~run
performs compilation as files are modified, whereas regular run
recompiles upon the next HTTP request.
Running play
without any options starts the Play console. The console
command can then start a Scala console which provides access to the Play application.
Configuration
The conf/application.conf file is the master configuration file in Play. From there all aspects of Play and other third-party libraries like Akka can be configured. The configuration format is based on the config library which supports a superset of JSON, Java Properties, comments, file includes, file merging, units (e.g. days, MB, etc.), and so on.
Environment variables are often used for operating system-independent, machine-specific configuration. The ${}
syntax is used to interpolate environment variables and other properties as well. These can be interpolated into other values.
db.default.url = ${DATABASE_URL}
log.directory = /var/log
log.access = ${log.directory}/access.log
log.error = ${log.directory}/error.log
It’s also possible to include other files, which allows to create context-specific configuration files that are then included into the larger configuration file. Objects can be merged together as well based on the order in which they appear.
db: {
default: {
driver: "org.h2.Driver",
url: "jdbc:h2:mem:play",
user: "sa",
password: "",
}
}
include "db.conf"
db.default.user = products
db.default.password = clippy
There’s also a configuration API for type-safe access to configuration properties. Currently the supported types are String
, Int
, and Boolean
, where Boolean
can take on true
, yes
, and enabled
(and their opposites).
import play.api.Play.current
current.configuration.getString("db.default.url").map {
databaseUrl => Logger.info(databaseUrl)
}
Since the configuration is structured hierarchically, it’s possible to get sub-configuration values for a given level:
current.configuration.getConfig("db.default").map {
dbConfig =>
dbConfig.getString("driver").map(Logger.info(_))
dbConfig.getString("url").map(Logger.info(_))
}
Routing
The conf/routes file contains the mapping of routes to controller actions.
GET / controllers.Products.home()
# if parameter not passed, assume to be 1
GET /products controllers.Products.list(page: Int ?= 1)
# parameter is optional; may or may not appear
GET /apples controllers.Apples.show(page: Option[Int])
# parameter is fixed to 1
GET /oranges controllers.Oranges.show(page: Int = 1)
# route variable
GET /product/:ean controllers.Products.details(ean: Long)
# path parameter
GET /photo/*filepath controllers.Media.photo(file: String)
# path parameter with different external prefix
GET /assets/*filepath controllers.Assets.at(path="/public", file)
GET /product/$ean<\d{13}> controllers.Products.details(ean: Long)
Reverse routing is a means of programmatically generating routes for a given action method invocation.
def delete(ean: Long) = Action {
Product.delete(ean)
// or url with routes.Products.list().url
Redirect(routes.Products.list())
}
Environments
It’s straightforward to setup different configuration environments by using the include
directive in configuration files. Production configuration files would need to use the classpath
function to refer to files within the deployed archive. This can be used to define a configuration file at a system path such as /etc/paperclips/production.conf, which can then be imported with the -Dconfig.file
parameter.
include classpath("application.conf")
email.override.enabled=false
Models
A model tends to consist of a model class (definition and attributes), data access object (to access model data), and some test data. The model is typically defined as a case class and its data access object is represented by a companion object.
package models
case class Product(
ean: Long, name: String, description: String)
object Product {
var products = Set(
Product(1L, "House", "A huge house"),
Product(2L, "Boat", "A tug boat"),
Product(3L, "Car", "A luxurious car")
)
def findAll = products.toList.sortBy(_.ean)
}
Persistence
Play allows for evolutions which are similar to Rails database migrations, which are stored in conf/evolutions/default/ and are named #.sql
where #
is the revision number. Play automatically asks to apply the evolution the next time the application is accessed. Evolutions take the following form:
# --- !Ups
CREATE TABLE products (id long, ean long);
# --- !Downs
DROP TABLE IF EXISTS products;
Play comes with Squeryl, a DSL for generating SQL in a type-safe manner, and Anorm, which allows raw SQL queries to be written explicitly.
Note: Recent versions of Play allow the use of Slick, which is the preferred method of interfacing with databases going forward.
Anorm has three ways of processing results: Stream API, pattern matching, and parser combinators. SQL queries are constructed using the SQL
class. The apply
method of SQL
accepts an implicit parameter of type java.sql.Connection
, which Play
provides from DB.withConnection
. This apply
method returns a Stream[SqlRow]
which can be map
ped over.
The SQL query contains SqlRow
objects which have an apply
method that retrieves the field by name. An explicit type parameter is provided to ensure that the field gets cast to the correct Scala type.
import play.api.Play.current
import play.api.db.DB
import anorm.SQL
import anorm.SqlQuery
object Product {
def getAll: List[Product] = DB.withConnection { implicit connection =>
val sql: SqlQuery = SQL("select * from products order by name asc")
sql().map ( row =>
Product(row[Long]("id"), row[Long]("ean"),
row[String]("name"), row[String]("description"))
).toList
}
}
The above transformation can also be applied with pattern matching.
def getAll: List[Product] = DB.withConnection { implicit connection =>
import anorm.Row
val sql: SqlQuery = SQL("select * from products order by name asc")
sql().collect {
case Row(Some(id: Long), Some(ean: Long),
Some(name: String), Some(description: String)) =>
Product(id, ean, name, description)
}.toList
}
Finally, the query can also be transformed with parser combinators.
import anorm.RowParser
val productParser: RowParser[Product] = {
import anorm.~
import anorm.SqlParser._
long("id") ~
long("ean") ~
str("name") ~
str("description") map {
case id ~ ean ~ name ~ description =>
Product(id, ean, name, description)
}
}
This creates a RowParser
but Anorm expects a ResultSetParser
. The *
means to try to match zero or more rows.
import anorm.ResultSetParser
val productsParser: ResultSetParser[List[Product]] = {
productParser *
}
The ResultSetParser
can then be passed to the query with the as
method:
def getAllWithParser: List[Product] = DB.withConnection {
implicit connection =>
sql.as(productsParser)
}
A multirecord parser can be written to construct instances of objects that themselves contain other class instances from a different table. We start out by writing a StockItem
parser.
val stockItemParser: RowParser[StockItem] = {
import anorm.SqlParser._
import anorm.~
long("id") ~ long("product_id") ~
long("warehouse_id") ~ long("quantity") map {
case id ~ productId ~ warehouseId ~ quantity =>
StockItem(id, productId, warehouseId, quantity)
}
}
Now a parser is needed which can parse the combination of product and stock item. The SQL join query will return a list of single arrays containing the joined parts, [Product, StockItem]
. The flatten
method transforms each of the single arrays into a two element tuple.
def productStockItemParser: RowParser[(Product, StockItem)] = {
import anorm.SqlParser._
import anorm.~
productParser ~ StockItem.stockItemParser map (flatten)
}
Finally we can construct the method that will retrieve each of the values and transform them into a map keyed by the Product
and containing StockItem
values. This transformation is possible by using the groupBy
function to group the results by the first tuple element, the Product
. This still leaves the values containing Product
, specifically a Map[Product,List[(Product,StockItem)]]
, which can be removed by using mapValues
and mapping the tuples to replace themselves with the second tuple element, the StockItem
values, leaving a Map[Product,List[StockItem]]
.
def getAllProductsWithStockItems: Map[Product, List[StockItem]] = {
DB.withConnection { implicit connection =>
val sql = SQL("select p.*, s.*" +
"from products p" +
"inner join stock_items s on (p.id = s.product_id)")
val results: List[(Product, StockItem)] = sql.as(productStockItemParser *)
results.groupBy { _._1 }.mapValues { _.map { _._2 } }
}
}
It’s also possible to insert, update, and delete with Anorm. This is generally accomplished by preparing statements and calling executeUpdate
on them.
def insert(product: Product): Boolean =
DB.withConnection { implicit connection =>
val addedRows = SQL("""
insert into products
values ({id}, {ean}, {name}, {description})
""").on(
"id" -> product.id,
"ean" -> product.ean,
"name" -> product.name,
"description" -> product.description
).executeUpdate()
addedRows == 1
}
def update(product: Product): Boolean =
DB.withConnection { implicit connection =>
val updatedRows = SQL("""
update products
set name = {name},
ean = {ean},
description = {description},
where id = {id}
""").on(
"id" -> product.id,
"name" -> product.name,
"ean" -> product.ean,
"description" -> product.description
).executeUpdate()
updateRows == 1
}
def delete(product: Product): Boolean =
DB.withConnection { implicit connection =>
val updatedRows = SQL("delete from products where id = {id}").
on("id" -> product.id).
executeUpdate()
updatedRows == 0
}
Views
Templates in Play are type-safe and consist of interspersed Scala, avoiding the need to learn a template DSL. Templates can specify a parameter list as the first line, which can be used to provide data from the controller’s end.
The @
character is used to denote the start of a Scala expression, where the parser conservatively determines the extent of the expression. Sometimes it may be necessary to explicitly define this range, which is possible by using parentheses. If it’s necessary to print a @
character verbatim, it must be escaped by another @
. Comments are delimited with @* *@
.
Hello @name!
@* output the year *@
Next year, your age will be @(user.age + 1)
The template parser encloses all template expressions in curly braces, so it’s not possible to define a variable in one expression and use it in another in the same level. More specifically, the bodies of functions, for comprehensions, and so on are treated as “sub-templates,” so the body isn’t treated as XML literals in the case of HTML templates.
@{var i = 0}
@articles.map { article =>
@{i = i + 1} // yields not found: value i
<li>@i - @article.name</li>
}
@for((article, index) <- articles.zipWithIndex) {
<li>@(index + 1) - @article.name</li>
}
It’s possible to clean up a lot of value declarations by defining them inside the for comprehension:
@for((article, index) <- articles.zipWithIndex;
rank = index + 1;
first = index == 0;
last = index == articles.length - 1) {
// use values here
}
It’s also possible to define a scope with an associated value, to avoid having to re-evaluate that expression each time.
@defining(article.countReview()) { total =>
<p>@total @total @total</p>
}
Escaping
All Scala expressions’ output is escaped. Values can be output unescaped by wrapping them in the Html
type.
Includes
There is no distinction between partials, known as includes in Play, and regular templates. Any template can be embedded within another by simply calling the generated object’s apply method in a Scala expression.
@()
<strong>This is a partial</strong>
<div class="announcement">
@navigation()
</div>
Layouts
A layout can be created in a straightforward manner from the template concepts covered so far. First it’s necessary to add a parameter of type Html
to the layout template. Other templates that want to embed themselves in a layout simply call the layout template and use the braces {}
method call syntax to pass the entire template to the layout.
@(title="Default Title")(content: Html)
<!DOCTYPE html>
<html>
<head>
<title>@title</title>
</head>
<body>
@content
</body>
</html>
@main("Products") {
<strong>Some Products</strong>
}
It’s possible to define implicit parameters on template parameter lists to avoid having to explicitly pass parameters to the templates. A common pattern is to define a reusable trait with the appropriate implicit values. The WrappedRequest
class wraps a Request
and can be extended by a class that defines other values that should be accessible by a template.
Localization
Localization is pretty straightforward in Play. The application.conf file can take an application.langs option that defines a comma-separated list of languages in ISO 639-2 format optionally followed by an ISO 3166-1 alpha-2 country code.
application.langs="en,en-US,nl"
For each of these languages there should be a corresponding conf/messages.lang file which defines localized messages. These messages can then be referenced using the Messages
object which takes the message’s key as well as an implicit Lang
value which can be implicitly converted from a Request
in the scope.
Messages in the file are patterns formatted using java.text.MessageFormat, so that arguments can be inserted with {#}
where the #
is the position of the argument. It’s also possible to define different messages for zero, one, more items:
// cart=Cart {0,choice,0#is empty|1#has one item|1< has {0} items}.
<p>@Messages("cart", 0)</p> // is empty
<p>@Messages("cart", 1)</p> // has one item
<p>@Messages("cart", 2)</p> // has 2 items
Controllers
Controllers derive from Controller
and are basically generators of actions by defining methods that return an instance of Action
, which constructs a function that handles a request and returns a result. More specifically, this function is of type Request[A] => Result
where A
is the type of the request body.
They generally mark the request
object as implicit
so that it may be passed to functions implicitly.
package controllers
import play.api.mvc.{Action, Controller}
import models.Product
object Products extends Controller {
def list = Action { implicit request =>
val products = Product.findAll
Ok(views.html.products.list(products))
}
def details(ean: Long) = Action {
NotImplemented // HTTP 501
}
}
The as
method allows setting the mime-type in a convenient manner, and the withHeaders
method allows setting headers.
Ok("some result")
.as(JSON)
.withHeaders(LOCATION -> url)
Actions can be composed, facilitating the re-use of common handlers such as authentication checking and caching:
def list =
Authenticate {
Cached {
Action {
// process request
}
}
}
Session
The session is represented as a map Map[String, String]
in request.session
and can easily be modified with the withSession
method.
Ok(result).withSession(
request.session + ("some.data" -> value)
)
// later on
val data = request.session.get("some.data")
A flash scope is available as in most other web frameworks which is essentially stored in the session and the values only live on until the next request. The flashing
method makes this straightforward:
Redirect(routes.Products.list()).flashing("info" -> "Product deleted!")
// later on
val message = request.flash("info")
Assets
The assets controller allows for reverse routing to asset files.
<link href="@routes.Assets.at("images/favicon.png")" type="image/png">
The assets controller handles this automatically by adding an Etag
header. HTTP Entity Tags (ETags) allows a client to make a conditional HTTP request for a resource so that the server can tell it whether or not to use the cached copy instead.
Compression is also automatically enabled if:
- in production mode
- request is routed to assets controller
- HTTP request has
Accept-Encoding: gzip
- file with same name and
.gz
suffix is found
Play has built-in support for Less and CoffeeScript. Such files can easily be referenced using the assets reverse router by its target extension. The extension can be prefixed by min
to use a minified version. A file can opt-out of compilation by prefixing its name with an underscore.
Forms
Play provides a forms API which is used for general validation, not just HTML forms. A Mapping
is an object that constructs an object from an HTTP request, a process called binding, where the type of the object constructed is a type parameter of Mapping
.
The data from an HTTP request is transformed into a Map[String, String]
, and a mapping performs its construction off of this map. A mapping can also perform the reverse process, unbinding.
A mapping can also define constraints and errors to provide when data doesn’t satisfy the constraints.
Predefined mappings exist in the Forms
namespace, such as Forms.text
. Mappings can be composed together, for example using Forms.tuple
which can bind values to a tuple type.
val data = Map(
"name" -> "Box of paper clips",
"ean" -> "1234567890123",
"pieces" -> "300"
)
val mapping: Mapping[(String, String, Int)] =
Forms.tuple(
"name" -> Forms.text,
"ean" -> Forms.text,
"pieces" -> Forms.number
)
To be able to bind data, it’s necessary to wrap a mapping in a Form
, which can also contain the data that will eventually be bound. The Form
’s type parameter is the same as the Mapping
’s, representing the data that would be available if it validates. Once the form is created, the data can be bound using the bind
method. Since Form
is immutable, the bind
method returns a new Form
populated with the bound data.
val productForm = Form(mapping)
val processedForm = productForm.bind(data)
The form can be checked for errors with hasErrors
and if there are any the errors can be fetched with getErrors
, otherwise the data can be retrieved with get
.
An alternative method of processing the result of binding the data is to use the fold
method in the same manner that it’d be used on the Either
type, where the first function is the error handler which is passed the form, and the second function is the success handler which is passed the bound data:
processedForm.fold(
formWithErrors => BadRequest,
productTuple => {
Ok(views.html.product.show(product))
}
)
Aside from tuple mappings, it’s also possible to construct objects from mappings. This is possible by using the mapping
method which takes a map of mappings, a function used to construct the object, and one to deconstruct it. These last two functions are provided for free by case classes in the form of apply
and unapply
. The first takes a parameter for every field in the case class, whereas the second takes an object of that type and deconstructs it into an Option
tuple containing each of the fields.
import play.api.data.Forms._
case class Product(
name: String,
ean: String,
pieces: Int)
val productMapping = mapping(
"name" -> text,
"ean" -> text,
"pieces" -> number)(Product.apply)(Product.unapply)
)
Creating a mapping for a class allows the construction of a Form
parameterized by that class, making it much more natural to handle forms.
val productForm = Form(productMapping)
productForm.bind(data).fold(
formWithErrors => ...,
product => ...
)
So far forms have been found from maps, but yielding a map from an HTTP request isn’t very straightforward. For this reason, the bindFromRequest
form method exists which works the same way as bind
.
def processForm() = Action { implicit request =>
productForm.bindFromRequest.fold(
...
)
}
View Helpers
There are helper methods for generating forms whose output can be customized. As in Rails, the helpers can take extra parameters of type (Symbol, Any)
to specify attributes such as the class
.
@helper.form(action = routes.GeneratedForm.create) {
@helper.inputText(productForm("name"))
@helper.textarea(productForm("description"))
@helper.checkbox(productForm("active"))
<div class="form-actions">
<button type="submit">Create Product</button>
</div>
}
The action must then pass the form to the action so that it may be accessed by the helper:
def createForm() = Action {
Ok(views.html.products.form(productForm))
}
A custom input element is possible with the following:
@helper.input(myForm("mydatetime")) { (id, name, value, args) =>
<input type="datetime" name="@name"
id="@id" value="@value" @toHtmlArgs(args)>
}
It’s also possible to customize the HTML that’s generated from a particular helper. This HTML is generated by a FieldConstructor
which is an implicit parameter to helper methods. The FieldConstructor
trait has a single apply
method which takes a FieldElements
object and returns Html
. The FieldElements
object contains information about the data that gets passed through to the HTML. The simplest way to use this is by creating a template that takes a FieldElements
parameter.
@(elements: views.html.helper.FieldElements)
@import play.api.i18n._
@import views.html.helper._
<div class="control-group @elements.args.get('_class)
@if(elements.hasErrors) {error}"
id="@elements.args.get('_id).getOrElse(elements.id + "_field")">
<label class="control-label" for="@elements.id">
@elements.label(elements.lang)
</label>
@elements.input
</div>
However, although this template takes a FieldConstructor
and returns Html
, it doesn’t extend the FieldConstructor
trait. This can be remedied simply by creating a wrapper and defining an apply
method that calls the template.
package object bootstrap {
implicit val fieldConstructor = new FieldConstructor {
def apply(elements: FieldElements) =
bootstrap.bootstrapFieldConstructor(elements)
}
}
Validation
Although mappings are immutable, the verifying
method takes Constraint[T]*
which copies the mapping and adds the provided constraints. There are some predefined constraints:
// maximum value for an Int mapping
max(maxValue: Int): Constraint[Int]
// maximum length for a String mapping
maxLength(length: Int): Constraint[String]
// e.g.
"name" -> text.verifying(Constraints.nonEmpty)
Custom validators can be created by using the verifying
method on mappings, which accepts a function that gets passed the bound object and returns a boolean indicating whether the constraint is met. It’s possible to pass a string as the first parameter, which serves as the custom validation message.
def eanExists(ean: Long) = Product.findByEan(ean).isempty
"ean" -> longNumber.verifying(eanExists(_))
Since mappings can be composed of other mappings, we can put constraints on more than one constituent mapping by applying the verifying
method at the outer level. However, the error message for the top-level mapping has no key. Instead, if the top-level mapping produces an error, it’s called the global error which can be retrieved with the globalError
method on Form
, which returns an Option[Error]
.
Optional Values
Optional form values can be represented with the Option
type by using the Forms.optional
method which transforms Mapping[A]
into Mapping[Option[A]]
.
case class Person(name: String, age: Option[Int])
val personMapping = mapping(
"name" -> nonEmptyText,
"age" -> optional(number)
)(Person.apply)(Person.unapply)
Repeated Values
It’s also possible to repeat mappings to accept a list of values using the list
mapping transformer, which transforms a mapping from Mapping[A]
to Mapping[List[A]]
. The seq
transformer is similar, but using a Seq
instead of a List
.
/*
<input type="text" name="tags[0]">
<input type="text" name="tags[1]">
<input type="text" name="tags[2]">
*/
"tags" -> list(text)
Repeated mappings like these can be displayed easily with form helpers by using the repeat
helper.
@helper.repeat(form("tags"), min = 3) { tagField =>
@helper.inputText(tagField, '_label -> "Tag")
}
Nested Mappings
Nested mappings can be referred to using dot-notation similar to object dot notation. One reason for nesting mappings, aside from organizational purposes, is to avoid reaching the hard limit of 18 tuple fields which are used to back mappings.
val contactMapping = tuple(
"name" -> text,
"email" -> email
)
val contactsForm = Form(tuple(
"main_contact" -> contactMapping,
"technical_contact" -> contactMapping,
"administrative_contact" -> contactMapping,
))
@helper.inputText(form("main_contact.name"))
@helper.inputText(form("main_contact.email"))
Custom Mappings
There are two ways to construct a custom mapping. The first involves transforming from an existing one, and the second involves building one from scratch. Transforming an existing mapping can be done using the transform
method, which accepts a function from the existing type to the target type, and another for the reverse since mappings are bidirectional. However, transform
’s limitation is that it has no way of expressing failure.
val localDateMapping = text.transform(
(dateString: String) => LocalDate.parse(dateString),
(localDate: LocalDate) => localDate.toString
)
The process of creating a custom mapping from scratch involves implementing the Formatter
trait. The mapping can then be constructed using the Forms.of
method.
trait Formatter[T] {
def bind(key: String, data: Map[String, String]):
Either[Seq[FormError], T]
def unbind(key: String, value: T): Map[String, String]
val format: Option[(String, Seq[Any])] = None
}
implicit val localDateFormatter = new Formatter[LocalDate] {
def bind(key: String, data: Map[String, String]) =
data.get(key) map { value =>
Try {
Right(LocalDate.parse(value))
} getOrElse Left(Seq(FormError(key, "error.date", Nil)))
} getOrElse Left(Seq(FormError(key, "error.required", Nil)))
def unbind(key: String, ld: LocalDate) = Map(key -> ld.toString)
override val format = Some(("date.format", Nil))
}
val localDateMapping = Forms.of(localDateFormatter)
val localDateForm = Form(single(
"introductionDate" -> localDateMapping
))
File Uploads
File uploads require the multipart/form-data
encoding on forms.
It can be handled using the parse.multipartFormData
body parser.
def upload() = Action(parse.multipartFormData) { implicit request =>
val form = Form(tuple(
"description" -> text,
"image" -> ignored(request.body.file("image")).
verifying("File missing", _.isDefined)
))
form.bindFromRequest.fold(
formWithErrors => {
Ok(views.html.fileupload.uploadform(formWithErrors))
},
value => {
file.ref.moveTo(new File("/tmp/image"))
Ok("Retrieved file %s" format file.filename)
}
)
}
The above handler would correspond to the following.
@helper.form(action = routes.FileUpload.upload,
'enctype -> "multipart/form-data") {
@helper.inputText(form("description"))
@helper.inputFile(form("image"))
}
JSON
Play’s play.api.libs.json.Json module contains support for JSON. Default types can be converted to JSON with the toJson
method resulting in a JsValue
, which can be converted to a string with the stringify
method. The response methods such as Ok
know about JsValue
and automatically set the Content-Type
header to application/json
. The JSON types include:
JsString
JsNumber
JsBoolean
JsObject
JsArray
JsNull
Arbitrary objects can be converted to JSON using a JSON formatter. The way this works is that toJson
takes a second, implicit parameter of type Writes[T]
where T
is the type being serialized. Writes[T]
is a trait with a single method that takes an object of type T
and returns a JsValue
.
import play.api.libs.json._
case class Product(ean: Long, name: String, description: String)
implicit object ProductWrites extends Writes[Product] {
def writes(p: Product) = Json.obj(
"ean" -> Json.toJson(p.ean),
"name" -> Json.toJson(p.name),
"description" -> Json.toJson(p.description)
)
}
// alternatively
val adminProductWrites: Writes[Product] = (
(JsPath \ "ean").write[Long] and
(JsPath \ "name").write[String] and
(JsPath \ "description").write[String] and
(JsPath \ "price").write[BigDecimal] and
)(unlift(Product.unapply))
To go in the other direction, parsing an object from a JSON value, we use the Reads
trait.
implicit val productReads: Reads[Product] = (
(JsPath \ "ean").read[Long] and
(JsPath \ "name").read[String] and
(JsPath \ "description").read[String] and
)(Product.apply _)
val parsedProduct = JsValue.as[Product]
There are a couple of query methods that can be used on JSON values. The \
method selects an element. The \\
method selects a property anywhere in the JSON tree. The apply
method returns an element from a JsArray
.
val companyName = (json \ "company" \ "name").asOpt[String]
Since it’s common to want to implement Reads
and Writes
to serialize and deserialize objects, the Format[T]
exists that represents both.
case class PricedProduct(
name: String,
description: Option[String],
purchasePrice: BigDecimal,
sellingPrice: BigDecimal
)
implicit val productFormat = (
(JsPath \ "name").format[String] and
(JsPath \ "description").formatNullable[String] and
(JsPath \ "purchase_price").formatNullable[BigDecimal] and
(JsPath \ "selling_price").formatNullable[BigDecimal]
)(PricedProduct.apply, unlift(PricedProduct.unapply))
Formatters can also be created at compile time.
implicit val productReads = Json.reads[PricedProduct]
implicit val productWrites = Json.writes[PricedProduct]
// or
implicit val productFormat = Json.format[PricedProduct]
Constraints can be added to fields to perform validation. This is done by supplying a custom Reads
implementation to use for the validation as an implicit parameter to read
or readNullable
. The JSON can then be validated using the validate
method.
def save = Actions(parse.json) { implicit request =>
val json = request.body
json.validate[Product].fold(
valid = { product =>
Product.save(product)
Ok("Saved")
},
invalid = {
errors => BadRequest(JsError.toFlatJson(errors))
}
)
}
Authentication
One way to handle authentication is to create a wrapper around Action
that serves an HTTP Unauthorized error code if authentication fails.
def AuthenticatedAction(f: Request[AnyContent] => Result):
Action[AnyContent] = {
Action { request =>
if (authenticate(request)) {
f(request)
} else {
Unauthorized
}
}
}
Modules
Modules can be added via sbt, which the play
command is actually a wrapper of. Some modules may be found in repositories other than the default ones, in which case a resolver must be added. The following shows how to add the SecureSocial module.
import sbt._
import Keys._
import PlayProject._
object ApplicationBuild extends Build {
val appName = "product-details"
val appVersion = "1.0-SNAPSHOT"
val appDependencies = Seq(
"securesocial" %% "securesocial" % "2.1.0"
)
val main = PlayProject(appName, appVersion,
appDependencies, mainLang = SCALA
).settings(
resolvers += Resolver.url("SecureSocial Repository",
url("http://repo.scala-sbt.org/scalasbt/sbt-plugin-releases/")
)(Resolver.ivyStylePatterns)
)
}
Creating modules is also straightforward and begins by creating a new Play application and only keeping the necessary files. This means that if we can remove app/public and app/views if we don’t need them. It’s important to keep in mind naming collisions however, which is why it’s useful to create packages out of source files with the package
keyword in Scala.
Deployment
Play has two commands that make it very easy to deploy. The stage
command compiles the application to a JAR file together with all dependency JARs and places the file in target/staged along with script target/start which can be used to start the application. The dist
command packages up start script and dependencies into a zip archive which can easily be transferred.
Packaging up the application in this manner allows it to be deployed on any target that contains a Java runtime installation.
Web Services
The WS
object makes it simple to consume web services. The WS.url
method creates a WSRequestHolder
object can be constructed in method-chaining style, ultimately followed by a call to the request type, i.e. get
.
def itemList() = Action {
Async {
val response: Future[Response] =
WS.url("http://someservice.com/api.json")
.withQueryString("q" -> query)
.get
response.map { response =>
val items = Json.parse(response.body).\("results").as[Seq[Item]]
Ok(views.html.items.itemList(items))
}
}
}
Caching
There’s a caching API represented by the Cache
object that can be accessed by having an implicit play.api.Application in the scope, which can be fulfilled by importing play.api.Play.current. The getOrElse
method can get a value for a given key and if not found, compute it and store it with an optional expiration time.
Cache.set("user-key", User("John Doe"))
val userOption: Option[User] = Cache.getAs[User]("user-key")
It’s also possible to use the Cached
object to wrap an Action
that can take a key and optional expiration time.
def handler() = Cached("handler", 120) {
Action { ... }
}
It’s also possible to cache dynamic content by supplying a function to Cached
that determines the string key to use based on the RequestHeader
.
Iteratees
Iteratees are objects that received individual chunks of data and does something with them. They have two type parameters: the first indicating the type of chunks it accepts and the second indicates the type of the final result the iteratee produces. Consider an iteratee that logs every chunk using the foreach
function which takes a chunk of type A
and returns an Iteratee[A, Unit]
. It can be used with a WSRequestHolder
by passing a function of type ResponseHeaders => Iteratee
to the request type.
val loggingIteratee = Iteratee.foreach[Array[Byte]] { chunk =>
val chunkString = new String(chunk "UTF-8")
println(chunkString)
}
WS.url("https://stream.twitter.com/1/statuses/sample.json")
.sign(OAuthCalculator(consumerKey, accessToken))
.get(_ => loggingIteratee)
Alternatively we can create an iteratee that produces an end-result, such as summing up integer chunks. Enumerators are the counterpart to iteratees in that they are producers of chunks and can be applied to iteratees, yielding futures of the new iteratee. Iteratees are immutable, so the result is simply a new iteratee with a new state.
val summingIteratee = Iteratee.fold(0) { (sum: Int, chunk: Int) =>
sum + chunk
}
val intEnumerator = Enumerator(1, 2, 3, 4, 5)
val newIterateeFuture: Future[Iteratee[Int, Int]] =
intEnumerator(summingIteratee)
val resultFuture: Future[Int] = newIterateeFuture.flatMap(_.run)
resultFuture.onComplete(sum => println("Sum is %d" format sum))
WebSockets
WebSockets are created using a pair of an iteratee used to process the incoming data stream and an enumator used to send data to the client.
In a simple chat application, an Akka actor can be used to process users, specifically to keep track of all of the users in the room. A Concurrent.broadcast
yields a pair of an enumerator and a channel tied to that enumerator which allows one to push additional data to it after it has been created. The actor’s private state will therefore include a set of users and a broadcast channel and its associated enumerator.
class ChatRoom extends Actor {
val users = Set[String]()
val (enumerator, channel) = Concurrent.broadcast[String]
The receive
method will handle events where a user joins, leaves, or broadcasts a message. Joining is handled by first checking if that user exists, and if so, ignoring anything that user might say. Otherwise, a message is broadcast to the room announcing that the user joined and adding them to the set of user names taken. The mapDone
method is used to specify the behavior to perform when the iteratee has finished sending data.
def receive = {
case Join(nick) => {
if (!users.contains(nick)) {
val iteratee = Iteratee.foreach[String]{ message =>
self ! Broadcast("%s: %s" format (nick, message))
}.mapDone { _ =>
self ! Leave(nick)
}
users += nick
channel.push("User %s has joined the room, now %s users"
format(nick, users.size))
sender ! (iteratee, enumerator)
} else {
val enumerator = Enumerator(
"Nickname %s is already in use." format nick)
val iteratee = Iteratee.ignore
sender ! (iteratee, enumerator)
}
}
The Akka actor can also handle the case where a user leaves by simply removing the username from the set of usernames and broadcasting the event to the room.
case Leave(nick) => {
users -= nick
channel.push("User %s has left the room, %s users left"
format(nick, users.size))
}
Finally, the broadcast event simply adds a message to the channel that was created with Concurrent.broadcast
.
case Broadcast(msg: String) => channel.push(msg)
}
}
This actor can then be used in a controller by creating an instance of the actor. The showRoom
method will render the template showing the chat room. The chatSocket
is an action of type WebSocket.async
which accepts parameters of type Future[(Iteratee, Enumerator)]
.
The chatSocket
action sends the Join
message to the actor using the ?
method which essentially expects a response, which in this case is a pair of iteratee and enumerator, with the iteratee being the stream used to communicate with the server and the enumerator being the broadcast stream that is sent to the user. Finally, the mapTo
method is used to cast the Future[Any]
to the appropriate type of Future[(Iteratee, Enumerator)]
.
object Chat extends Controller {
implicit val timeout = Timeout(1 seconds)
val room = Akka.system.actorOf(Props[ChatRoom])
def showRoom(nick: String) = Action { implicit request =>
Ok(views.html.chat.showRoom(nick))
}
def chatSocket(nick: String) = WebSocket.async { request =>
val channelsFuture = room ? Join(nick)
channelsFuture.mapTo[(Iteratee[String, _], Enumerator[String])]
}
}
Body Parsers
Normally actions are only invoked when the request is complete, once the body parser is done parsing the body of the request. Suppose a user is uploading a large file only to be rejected once the upload is complete because the file is of the wrong type. A body parser can respond before the request is complete.
A body parser is an object that can process an HTTP request body, such as the json
or multipartFormData
body parsers. More specifically, a body parser is a function that takes a RequestHeader
parameter and returns an iteratee Iteratee[Array[Byte], Either[Result, A]]
, that is, it processes the body of type Array[Byte]
and returns an Either[Result, A]
, where an error Result
is return in the event of failure to construct A
.
Essentially, Play creates a RequestHeader
which is used to route to the appropriate Action
and the body parser is used to create an Iteratee
that’s fed the body of the request. When the body is finished constructing, it’s used to construct a complete Request
which is then fed to the Action
. If the construction fails, a Request
won’t be constructed and instead the Result
will be returned to the client.
Built-in body parsers allow a maximum body size to be specified, which defaults to 512 kilobytes, and if exceeded, yields a EntityTooLarge
HTTP error response.
The temporaryFile
body parser parses the body and put its in a temporary file, which can then be moved to a permanent location with the moveTo
method if the file satisfies any constraints. The Play.getFile
method can yield a path relative to the application root.
def upload = Action(parse.temporaryFile) { request =>
val destinationFile = Play.getFile("uploads/myfile")
request.body.moveTo(destinationFile)
Ok("File successfully uploaded!")
}
It’s possible to compose body parsers. For example, the when
method allows one to specify a predicate and a parser to use if it’s satisfied, as well as a failure result if not.
def fileWithContentType(filename: String, contentType: String) =
parse.when(
requestHeader => requestHeader.contentType == contentType,
parse.file(Play.getFile(filename)),
requestHeader => BadRequest(
"Expected a '%s' content type, but fuond %s".
format(contentType, requestHeader.contentType)))
def savePdf(filename: String) = Action(
fileWithContentType(filename, "application/pdf")) { request =>
Ok("Your file is saved!")
}
)
Body parsers also support the map
function to transform the body parser’s constructed type. This has the advantage of constructing the desired type and making it available within the action.