We will develop a sample application in Java and Scala, which will accompany us throughout this book. First, we will take a close look at the anatomy of a Play application. Then we will deal with user input. Finally, we will explore the different ways to talk to a database with Play.
In this section we will learn the basics of how to implement the MVC components of a typical Play application.
Our sample here is a prototype of a phone book application. We are maintaining our contacts with it, where the contacts have a name and a phone number. We are able to add, remove, and edit contacts. Additionally, we will implement a search for contacts.
All source code needed to run the example is found in this book. However, it is also available online at http://packtpub.com and http://bit.ly/PhoneBookSample for each step that we perform.
We already know that a Play application follows the MVC architectural pattern. It is up to us which component we implement first. We decide to start implementing the model. The model reflects the business requirements, in that it contains the business logic and our entities.
First of all, we have to ensure our applications are started in development mode with the ~run
command, as described previously.
We create the package models
in the app/
folder of our Java application and add a new entity class Entry.java
to it. As mentioned before, the package name models
can be changed if needed.
package models; public class Entry { public Long id; public String name; public String phone; }
The Entry
class represents phone book entries. Objects of this class contain a specific name
and phone
entity. Also we need a unique identifier, id
, to be able to distinguish phone book entries. Especially, it is allowed to create multiple entries with the same name and phone number. Please note that phone numbers are of type String
, because we also want to allow characters other than digits.
For the sake of simplicity, the visibility of the properties is public
to omit the implementation of getters and setters.
Next, we need to create our data access object (DAO). We choose to name it Entries.java
. To keep it simple, we place it in the same package:
package models; import static play.libs.Scala.toSeq; import java.util.*; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.atomic.AtomicLong; import scala.collection.Seq; public class Entries { private static Map<Long, Entry> entries = new ConcurrentHashMap<Long, Entry>(); private static AtomicLong uuid = new AtomicLong(); public static void delete(long id) { entries.remove(id); } public static Entry findById(long id) { return entries.get(id); } public static Seq<Entry> findByName(String filter) { List<Entry> result = new ArrayList<Entry>(); for (Entry entry : entries.values()) { if (entry.name.toLowerCase() .contains(filter.toLowerCase())) { result.add(entry); } } return toSeq(result); } public static void save(Entry entry) { if (entry.id == null) { entry.id = uuid.incrementAndGet(); } entries.put(entry.id, entry); } }
The DAO consists of static methods only and we will not create instances of that class. This pattern is also found in controllers and reflects a fundamental idea of the Play framework; to encourage us to follow a stateless programming model.
Phone book entries can be deleted by an entry id
entity. We search entries by their ID and by their name. The findByName
implementation returns a list of entries (of type Seq
) that contain a specific string in their name, ignoring the case. If the search string is empty, all entries are returned. The order of list items is undetermined here.
Tip
We use the Scala collection type Seq
here because it is shared by the views of our Java and Scala examples. Java developers typically use List
instead.
Finally, the entries can be saved. The save
method equips new entries with an ID. Existing entries are updated in the underlying storage. Our storage is a map associating IDs with the corresponding entries.
Our Scala implementation of the model resides in app/models/
in the file Entries.scala
:
package models import scala.collection.concurrent.TrieMap import java.util.concurrent.atomic.AtomicLong case class Entry(name: String = "", phone: String = "", id: Option[Long] = None) object Entries { val entries = TrieMap[Long, Entry]() val uuid: AtomicLong = new AtomicLong() def delete(id: Long) = entries.remove(id) def findById(id: Long) = entries.get(id) def findByName(filter: String) = entries.values.filter { _.name.toLowerCase.contains(filter.toLowerCase) }.toSeq def save(entry: Entry) = ( if (entry.id.isDefined) entry else entry.copy(id = Some(uuid.incrementAndGet())) ) match { case e@Entry(_, _, Some(id)) => entries += (id -> e) } }
This contains both the entry
entity and the entries
DAO. Entry
is a case class, which allows us to implement data beans in a very concise way, literally as a one-liner.
The functionality of our Java and Scala DAO implementations is the same here. The entity is containing the data and will be part of our interface between view and controller. Only the controller is permitted to use the DAO to access the data layer.
HTTP requests and responses are first-class citizens of Play. The HTTP interface forms the core of a Play application. It is defined in the conf/routes
file and contains a mapping of HTTP requests to the corresponding method calls. An entry of this file is called route and looks like this:
GET / controllers.Application.index
In particular, the left-hand side of the route consists of two parts: the HTTP method and the request path, which is relative to the base URL of the application.
Here, the Application.index
method is called when we open a browser and visit the base URL, that is, the request path /
, of our application. Such a request is sent with the GET
method.
Valid methods are GET
, POST
, PUT
, DELETE
, OPTIONS
, HEAD
, and WS
.
In our example the GET
and POST
methods are sufficient. Described in a nutshell, we use the GET
method to load a specific web page without modifying a resource and the POST
method to change a resource and navigate to a result page.
Please insert the following code lines behind the first route definition of the conf/routes
file of both the projects (Java and Scala):
GET /entries controllers.Entries.list(filter ?= "") GET /entries/new controllers.Entries.add GET /entries/:id controllers.Entries.edit(id: Long) POST /entries/:id/remove controllers.Entries.remove(id: Long) POST /entries controllers.Entries.save
Routes are processed in the order of their occurrence until a match is found. If no route matches the actual URL, Play returns a HTTP 404 Not Found page.
The first line reflects that we want to retrieve a list of phone book entries filtered by a specific search string. We are planning to add controls to the phone book list that will allow us to add new phone book entries, edit existing ones, and remove entries.
If we add or edit an entry, we navigate to a phone book entry details page. After the user has finished editing the entry on the client side, it can be saved or canceled. In both cases, we want to return back to the entry list page.
There is one thing we have not explained yet regarding our new routes. Play allows a special notation of the request path of the route. There are three kinds of placeholders for path segments we can use:
$name<regex>
: The dollar sign$
marks a placeholder for the match of a regular expressionregex
. The current value can be accessed byname
. The path segments are processed case sensitive.:name
: The colon:
matches exactly one request path segment. It is internally mapped to the regular expression$name<[^/]+>
.*name
: The asterisk*
matches the remaining path segments and can be used to express whole paths. It is internally mapped to the regular expression$name<.+>
.
The first two placeholders can be combined in a request path, such as:
/shop/:product/$number<[0-9]+>/details
Given that, /shop/socks/1234/details
is a valid request path, where product
is socks
and number
is 1234
. We pass these placeholders to a controller method, say:
controllers.Shop.showProductDetails(product: String, number: Int)
Also, please note that Play takes care of converting the text content of the extracted path segments to the appropriate parameter types declared in the controller method. The trailing parentheses are optional if the controller method has no arguments.
If we declare a controller method parameter that has no corresponding request path placeholder, Play tries to find that value in the HTTP request header. In other words, the parameter can be specified as a URL query string parameter shop?product=socks
.
Additionally, we can define a special handling for controller method parameters:
product: String = "socks"
: This defines the fixed valuesocks
product: String ?= "socks"
: This defines the default valuesocks
A controller
is a subclass of the Controller
class. It consists of a set of (generally static) methods that process an HTTP request by computing an HTTP response and sending it back to the web browser. The controller methods are called actions.
The Java API has no special representation of actions. In Scala, an action can be basically seen as a function of the following type:
(play.api.mvc.Request => play.api.mvc.Result)
The Java classes Controller
, Request
, and Result
that are used in the request-response cycle, are located in the package play.mvc
. The Scala API is located at play.api.mvc
.
Apparently, the routes file has errors because of the missing controller Entries
(not to be confused with models.Entries
).
To fix that, we create app/controllers/Entries.java
with the following content:
package controllers; import play.mvc.*; public class Entries extends Controller { public static Result list(String filter) { return TODO; } public static Result remove(long id) { return TODO; } public static Result add() { return TODO; } public static Result edit(long id) { return TODO; } public static Result save() { return TODO; } }
The corresponding Scala code looks almost the same:
package controllers import play.api.mvc._ object Entries extends Controller { def list(filter: String) = TODO def remove(id: Long) = TODO def add = TODO def edit(id: Long) = TODO def save = TODO }
We actually don't have provided real
implementations of the controller methods. Instead, we return Play's empty result (Java) and action (Scala) implementation, TODO. Now the controller is syntactically correct and can be compiled.
To trigger the routes generator, we are reloading the web pages of our Java and Scala applications. So now, the error should disappear.
Of course we can already test what happens when we visit a specific URL listed in our routes file, say localhost:9000/entries?filter=test
. According to the route, the controller method controllers.Entries.list("test")
is called, which returns the TODO dummy page:
The router can also be used to programmatically generate an URL. Each controller has a so-called reverse controller in a sub-package routes
of the corresponding controller class. For example, the controller controllers.Entries
has a reverse controller controllers.routes.Entries
, which contains (almost) the same methods. The difference is that the reverse controller methods return a Call instead of a Result, where a call corresponds to an URL.
As an example, we will redirect our base URL localhost:9000
to the list of phone book entries located at localhost:9000/entries
.
Technically, the Play server performs the redirect by sending an HTTP 302 response to the client, where the response header contains the new URL. In contrast to directly calling the controller method Entries.list("")
, the browser location is changed.
In Java, we replace the body of the Application
controller's index
method with:
public class Application extends Controller {
public static Result index() {
return redirect(routes.Entries.list(""));
}
}
The Scala solution allows us to omit the argument of the list
method and use the default method defined in the routes
definition:
object Application extends Controller {
def index = Action {
Redirect(routes.Entries.list())
}
}
The routes
file tells us that a request of localhost:9000/
invokes Application.index
, which in turn invokes Entries.list
with a redirect to localhost:9000/entries
.
We will also use reverse controllers to generate links in our view templates. We have all our URI patterns centralized in the routes file. Using reverse controllers instead of hard-coded links provides us with the certainty that all links are correct—which is one of the several advantages of using Play.
Now we have met all the requirements to implement our first two controller methods, list
and remove
.
We edit app/controllers/Entries.java
and insert the following method bodies. Please note that we will adjust the index
view template accordingly:
public static Result list(String filter) { Seq<Entry> entries = models.Entries.findByName(filter); return ok(views.html.index.render(entries)); }
The list
method first calls the findByName
method of our DAO models.Entries
, which returns all phone book entries where the given filter
is part of the entry name.
Then the views.html.index
view template renders the list of phone book entries. The result is an HTML page, which is returned by the action with the HTTP 200 OK
status and the correct content type text/html
. The content type is inferred based on the value passed to the status function (but could be changed by calling .as(…)
).
The implementation of remove
looks very similar to that of the list
method:
public static Result remove(long id) { models.Entries.delete(id); return redirect(routes.Entries.list("")); }
First, the DAO is called to delete the entry by it's ID. Then a redirect to the list of phone book entries is returned.
Because Entries.findByName
returns a sequence of phone book entries of type Seq<Entry>
, we additionally need to import the following classes:
import scala.collection.Seq; import models.Entry;
We edit app/controllers/Entries.scala
and insert the implementation of list
:
def list(filter: String) = Action { val entries = DAO.findByName(filter.trim) Ok(views.html.index(entries)) }
The implementation of remove
is also analogous to Java:
def remove(id: Long) = Action { DAO.delete(id) Redirect(routes.Entries.list()) }
Our DAO models.Entries
has the same name as our current controller. To resolve this name clash, we had to provide the fully-qualified name of the DAO in the Java implementation of the list
action. In Scala we have the ability to substitute the imported class name with a shortcut by specifying the following import:
import models.{Entries => DAO}
The views of a Play application are based on a safe template engine. View templates (views) are HTML files with dynamic parts using Scala as an expression language. Compared to other template languages there are no special tags or surrounding blocks for an embedded template syntax. Template expressions begin with the @
character and are simply mixed in to your HTML code.
With the help of Play's template parser, a Scala version of the template is generated. For example, we use this in our controllers. When a view is rendered, the compiled template expressions are evaluated. The occurrence of a template expression is replaced by its value. The result is a static HTML page.
Physically, templates are located in the folder app/views
or one of its subfolders. After views are compiled they are located in the package views.html
or one of its sub packages. For example, the fully-qualified name of app/views/index.scala.html
is views.html.index
.
The syntax of view templates are basically a combination of HTML for the static parts and Scala for the dynamic parts.
The first line of a view is mandatory and declares the template parameters. It starts with an @
character followed by one or more parameter blocks. A parameter block is denoted in parentheses ()
and contains a comma-separated list of parameter declarations.
Like in Scala, a parameter has a name followed by a colon :
and a type. Generic parameter types are denoted with []
instead of <>
. For example:
@(entries: Seq[models.Entry])
The declaration of view parameters is optionally followed by import declarations. Please remember that the underlying syntax is that of Scala. Use _
instead of *
as a wildcard. For example:
@import java.util.List @import views.html.helper._
Template expressions are Scala expressions, with one exception—a simplified for
expression. Strictly speaking, the HTML code is also a template expression, as follows:
<markup>…</markup>
: It is an HTML code, but contains template expressions
But it is not considered as such in our short overview, as follows:
@expression
: It is a simple expression such as@list.getSize()
@(complex expression)
: It indicates multiple tokens such as@(e1 + e2)
@{block expression}
: It represents block of expressions such as@{e1; e2}
@*comment*@
: It indicates a comment, but unlike<!--comment-->
it is not rendered@@
: It denotes a single@
character has to be escaped
Additionally, there are the following control structures:
@if(condition) {…} else {…}
: This is the if-expression, theelse
part is optional@for(to <- from; …) {…}
: This is thefor
loop, which returns the evaluated block
Sometimes it may also be helpful to declare the reusable blocks without introducing a new template:
@name(param: Type, …) = {…}
: This declares a functionname
For a complete list of template expressions, please refer to the Play framework documentation.
A view can be seen as a function. It computes an HTML fragment based on specific parameters. In that manner, views are reusable building blocks. They are referenced by their fully-qualified name, for example, @viewName(params){param}{…}
.
Parameters are of an arbitrary Java or Scala type. Especially, blocks of HTML can be passed to views. The type of an HTML block is play.api.templates.Html
for both Java and Scala.
When a view is called, it is evaluated. This is done by evaluating all of its contained template expressions and subsequently concatenating the results. The result is of type play.api.templates.Html
.
A new Play application has two views, main
and index
. The index
view calls the main
view, passing the required parameters. In general, templates are called to have the same content on all the pages of a website:
@(message: String) @main("Sample") { <h1>@message</h1> }
The main
view takes two parameters, which are used as simple template expressions:
@(title: String)(content: Html) <html> <head> <title>@title</title> </head> <body> @content </body> </html>
Visiting the base URL calls the Application.index
action, which renders the index
view.
The fact that functions such as the main
view can have multiple parameter blocks is new to programmers. This allows us to denote a multi-line block using {}
instead of ()
if there is only one parameter.
The main
template is a reusable building block, which is used within the index
template. The index
view renders some HTML code and passes it as parameter to the main
view. In general, the most specific parts of a page are rendered first and passed as an argument to more general and reusable building blocks (such as a menu bar) of a web page.
Let's get started with implementing the views of our phone book sample.
We use the CSS framework Twitter Bootstrap to style our application. These styles are annotated in class="…"
attributes of the HTML elements. The styles are not explained in detail here because they do not affect the functionality of the phone book application.
To install Twitter Bootstrap, please download it at http://twitter.github.com/bootstrap/ and extract its contents to the public/
folder of our Java and Scala application. The result should look similar to this:
/path/to/play-starter-scala/public |-bootstrap | |-css | |-img | |-js |-images |-javascripts |-stylesheets
Currently, the compiler complains about the wrong parameter type of the index
view located at app/views/index.scala.html
. We fix that by introducing the entries
parameter of type Seq[Entry]
. Also we replace the content passed to the main
view:
@(entries: Seq[Entry]) @main { <form action="@routes.Entries.list()" method="GET"> <div class="input-append"> <input type="text" name="filter" class="input-xxlarge" autofocus=""> <button type="submit" class="btn">Search</button> </div> </form> <a href="@routes.Entries.add()" class="btn btn-small btn-primary">New entry</a> @list(entries) }
The main
view does not take the title parameter any more. We replaced the welcome message of the initial view with the following:
<form>…</form>
: This is an input field to search phone book entries<a>…</a>
: This is a link to add new phone book entries, displayed as a button@list(entries)
: This is a list of current phone book entries
As we already stated, we use reverse controllers instead of hard-coded URLs.
The HTTP method of the form is GET
and it is used in order to append the form data to the request query string. This makes searches bookmarkable.
Instead of @views.html.list(entries)
, we can write @list(entries)
. The packages views.html
and models
are in scope by default.
The list of entries is rendered by a separate view app/views/list.scala.html
. We create it with the following content for our Java application:
@(entries: Seq[Entry]) <table class="table table-hover"> @for(entry <- entries) { <tr> <td> <strong>@entry.name</strong><br> <a href="tel:@entry.phone">@entry.phone</a> <form action="@routes.Entries.remove(entry.id)" method="POST"> <a href="@routes.Entries.edit(entry.id)" class="btn btn-small">Edit</a> <input type="submit" value="Remove" class="btn btn-small"> </form> </td> </tr> } </table>
Our Entry
implementations slightly differ in Java and Scala. In Scala, we avoid them to cope with null
value handling. Instead, we use the generic Option[T]
type, which has two implementations, Some[T]
and None[T]
.
Here we get Some(id)
if entry.id
is defined, otherwise, we get None
. The Option
type plays well together with the for
expression. This is our Scala implementation:
@(entries: Seq[Entry]) <table class="table table-hover"> @for(entry <- entries; id <- entry.id) { <tr> <td> <strong>@entry.name</strong><br> <a href="tel:@entry.phone">@entry.phone</a> <form action="@routes.Entries.remove(id)" method="POST"> <a href="@routes.Entries.edit(id)" class="btn btn-small">Edit</a> <input type="submit" value="Remove" class="btn btn-small"> </form> </td> </tr> } </table>
According to the call of the list
view in the index
view, there is one parameter entries
of type Seq[Entry]
. The phonebook entries are aligned in a table by iterating the given sequence and producing a table row for each Entry
.
In Scala we do not expect None
values here. Anyway, if entry.id
is None
, no table row is generated for that entry.
A table row consists of the phone book entry name, the phone number, and two buttons for editing and removing phone book entries. The phone number is displayed as a click-to-call link for mobile browsers. We use the reverse controllers for linking our edit
and remove
actions.
Finally, we have to provide a proper implementation of the main
view:
@(content: Html) @style(asset: String) = { <link rel="stylesheet" href="@routes.Assets.at(asset)"> } <!DOCTYPE html> <html> <head> <title>Play Framework Starter Guide</title> @style("bootstrap/css/bootstrap.css") @style("bootstrap/css/bootstrap-responsive.css") @style("stylesheets/main.css") </head> <body> <div class="navbar navbar-inverse navbar-fixed-top"> <div class="navbar-inner"> <div class="container"> <a class="brand" href="/">Phonebook</a> </div> </div> </div> <div class="container"> @content </div> </body> </html>
We removed the title
parameter of the initial main
view implementation and declared the style
function, which we use in the HTML head
element to include our CSS stylesheets.
The body consists of the popular Twitter Bootstrap navbar
, which displays our brand, Phonebook
, and the content
argument of the main
view.
We have to customize the Twitter Bootstrap style for our design. Please replace the content of public/stylesheets/main.css
with the following:
@media (min-width:979px) { body { padding-top:60px } } form { margin:0 } table { margin-top:8px } td { position:relative } td > form { position:absolute; right:8px; top:14px }
We ensure correct alignment to the top of big screens. The margin of tables and forms is also tweaked a little bit. Also, we align our Entry
buttons to the right-hand side of a table row.
Now reload our page. This is what it looks like:
Unfortunately, our data store is empty and we are currently unable to add entries. Nevertheless, to test the functionality we need to add some test data.
One easy way to achieve this is to intercept the application start-up and create some test data by implementing app/Global.java
in the Java project:
import models.*; import play.*; public class Global extends GlobalSettings { @Override public void onStart(Application app) { Entries.save(newEntry("Guillaume Bort", "+33 5 55 55 55 55")); Entries.save(newEntry("Sadek Drobi", "+33 5 55 55 55 55")); } private Entry newEntry(String name, String phone) { Entry entry = new Entry(); entry.name = name; entry.phone = phone; return entry; } }
The onStart
method is called by Play on the application startup. So we have the opportunity to initialize our data store by programmatically saving phone book entries.
For the Scala project, we create app/Global.scala
:
import models.{Entry, Entries => DAO} import play.api._ object Global extends GlobalSettings { override def onStart(app: Application) { DAO.save(Entry("Guillaume Bort", "+33 5 55 55 55 55")) DAO.save(Entry("Sadek Drobi", "+33 5 55 55 55 55")) } }
Feel free to play around with our first version of the phone book prototype!
There are two buttons displayed on our main page that currently have no implementation on the backend side, namely New entry and Edit. Our next goal is to implement these by creating a user interface for adding new phone book entries and for editing existing ones. The functionality of the Remove button on the search result page will be implemented later.
It is straightforward to add new functionality to our application. We need to implement two things, the HTML form with the input fields for a phone book entry and a corresponding form abstraction on the server side. We start with the server side.
Users submit HTML form fields on the client side. The data is sent as text to the server packaged within an HTTP request. It has to be parsed on the server side. A corresponding typed data structure is filled with the form field values.
Data binding is a common task in the world of web programming. Play provides an easy to use vehicle for this, simply called Form.
We need to add two standard imports to our Entries
controller before we can define a Form:
import play.data.Form; import static play.data.Form.form;
Given that, we declare the Form the following way. The existing code of our file is unaffected:
public class Entries extends Controller { final static Form<Entry> entryForm = form(Entry.class); // ... }
We wrapped the Entry
type in a Form
instance, which we use in our controller to bind the data from a request in the save
method:
public static Result save() { Entry entry = entryForm.bindFromRequest(request()).get(); models.Entries.save(entry); return redirect(routes.Entries.list("")); }
After persisting the entity, we redirect to the phone entry list.
The add
method is also implemented:
public static Result add() { Form<Entry> form = entryForm.fill(new Entry()); return ok(views.html.edit.render(form, "Add entry")); }
Because a new entry has no initial data, we fill our form with an empty Entry
and call the edit
view. The same view will be used to render the existing phone book entries, so we send the form data along with an appropriate label reflecting the current action.
Finally, we need to implement the edit
method:
public static Result edit(long id) { Entry entry = models.Entries.findById(id); if (entry != null) { Form<Entry> form = entryForm.fill(entry); return ok(views.html.edit.render(form, "Edit entry")); } else { return redirect(routes.Entries.list("")); } }
Here we render an existing phone book entry. Because we don't know if another user changed or removed our entry in the meanwhile, we load the current entry from our storage by its ID. If no entry is found, we will return to the list of entries.
Play for Scala provides a different API for dealing with user input. To use it, we have to add some imports to our Entries
controller:
import models.{Entry, Entries => DAO} import play.api.data._ import play.api.data.Forms._
Then we define the form:
object Entries extends Controller { val entryForm = Form( mapping( "name" -> text, "phone" -> text, "id" -> optional(longNumber) )(Entry.apply)(Entry.unapply)) // ... }
A form is constructed with a mapping of field names to field parsers, where a field parser is technically also a type of mapping. This allows us to easily define nested structures.
There are two types of mappings: composite mappings and value mappings. Simply put, composite mappings are a hint to the form parser on how to look for nested input fields. Value mappings provide the parser with information, including which type of data it has to deal with; for example, text or numeric fields.
Finally, we have to specify how objects are created from parsed form data and vice versa. The parsed data is a tuple. In our example, we use the apply
and unapply
methods of our case class Entry
. The apply
method takes a tuple and unapply
results in a tuple.
To bind the Entry
from the request in our save
method, we have to bring the request into scope. The easiest way to do this is by using the implicit
keyword:
def save = Action { implicit request => val entry = entryForm.bindFromRequest.get DAO.save(entry) Redirect(routes.Entries.list()) }
Adding phone book entries looks almost the same as in Java:
def add = Action { val form = entryForm.fill(Entry()) Ok(views.html.edit(form, "Add entry")) }
The difference with edit
between Java and Scala is that findById
returns an Option[Entry]
. Again, this means that we don't have to cope with the null
values.
def edit(id: Long) = Action { DAO.findById(id) map { entry => val form = entryForm.fill(entry) Ok(views.html.edit(form, "Edit entry")) } getOrElse { Redirect(routes.Entries.list()) } }
If the finder returns Some(Entry)
, the edit form is displayed. Otherwise, it returns None
and we redirect to the list of phone book entries.
The edit page displayed on the client side is created by rendering the edit
view template. Create app/views/edit.scala.html
with the following content:
@(entryForm: Form[Entry], heading: String) @main { <h1>@heading</h1> <form action="@routes.Entries.save()" method="POST"> <input type="hidden" name="id" value="@entryForm("id").value"> <div> <label for="name">Name</label> <div class="input"> <input type="text" id="name" name="name" value="@entryForm("name").value" autofocus=""> </div> </div> <div> <label for="phone">Phone</label> <div class="input"> <input type="text" id="phone" name="phone" value="@entryForm("phone").value"> </div> </div> <div> <button type="submit" class="btn btn-primary">Save</button> <a class="btn" href="@routes.Entries.list()">Cancel</a> </div> </form> }
The HTML form consists of one hidden input field containing the ID of the entry. The ID is needed on the server side to identify the entity to be saved to the data storage.
Furthermore, there are two input fields holding the name and the phone number of our entry. All fields are initialized with the corresponding form value of the given entryForm
parameter, for example, @entryForm("name").value
.
On form submission, the reverse controller method Entries.save
is called. Additionally there is a cancel link, which loads the list of phone book entries.
Now we are able to create and edit phone book entries.
It is necessary that we validate the user input before saving it to the data storage. Play does provide an elegant API for that. In Java, we annotate our entity fields with constraints or define a validate
method for more complex situations. In Scala we already used constraints without mentioning it explicitly.
Play's validation mechanism is built around the Form API and it is therefore executed on the server side. The client submits the form data to the server, where it is bound from the request. If validation constraints are defined, the form validation is triggered. We check if validation errors have occurred, and then either send the form including the validation errors back to the client or go ahead and process the form data.
On the server side, we have two tasks regarding validation, including attribution of our form fields with constraints, and adapting the control flow of the save
method.
The Java API allows us to annotate our constraints directly at the fields of our Entry
entity. The constraints are statically imported, as shown in the following:
package models; import static play.data.validation.Constraints.*; public class Entry { public Long id; @Required @MinLength(value = 2) public String name; @Required @Pattern( value = "\\+?[0-9\\s]+", message = "Optional +, followed by digits & spaces" ) public String phone; public Entry() {} }
We marked the fields name
and phone
as required. Additionally, the name
field has to have a minimum length of two characters. Because we use the tel:
URL scheme to enable click-to-dial links on mobile devices, we have to ensure that phone numbers have the right format.
There is actually no pre-defined constraint checking the validity of phone numbers. In this case, it is sufficient to implement our own constraint using the Pattern
annotation. This has two arguments: a regular expression and an error message.
During form validation, it is checked if the regular expression matches the current value of the phone
field. If it does not match the current value, the message is added to the list of error messages associated with the current form field.
Now that we have constraints defined, we need to adjust the control flow of the save
method.
public static Result save() { Form<Entry> form = entryForm.bindFromRequest(request()); if (form.hasErrors()) { return badRequest( views.html.edit.render(form, "Correct entry")); } else { models.Entries.save(form.get()); return redirect(routes.Entries.list("")); } }
When the form is bound from the request, the validation process is automatically started. If the form has errors, we send it back to the server in order to be corrected by the user. If the form contents are valid, the phone book entry is saved, and we redirect to the phone book list.
For Scala, the Play developers took a different approach. The constraints are specified directly at the form fields in our Entries
controller.
First, we have to import the constraints API:
import play.api.data.validation.Constraints._
Then we add the constraints to our form
fields:
val entryForm = Form( mapping( "name" -> nonEmptyText(minLength = 2), "phone" -> (nonEmptyText verifying pattern( """\+?[0-9\s]+""".r, error = "Optional +, followed by digits & spaces") ), "id" -> optional(longNumber) )(Entry.apply)(Entry.unapply))
The nonEmptyText
constraint replaces the text
constraint. It implies, that the field is required. The argument minLength
defines the minimum length of the name
field.
The phone
field is marked as required. We added the regular expression along with an appropriate error message for verifying phone numbers:
def save = Action { implicit request => entryForm.bindFromRequest.fold( errors => BadRequest(views.html.edit(errors, "Correct entry")), entry => { DAO.save(entry) Redirect(routes.Entries.list()) } ) }
The fold
method defined in the Form
class provides us with a convenient syntax for handling form errors and validation success.
Our program does already compile and run, but we also need to customize the edit
view in regards to the way that validation errors are displayed:
<form action="@routes.Entries.save()" method="POST"> <input type="hidden" name="id" value="@entryForm("id").value"> <div class="control-group @if(entryForm("name").hasErrors) {error}"> <label for="name">Name</label> <div class="input"> <input type="text" id="name" name="name" value="@entryForm("name").value" autofocus=""> <span class="help-inline">@(entryForm("name").errors.map{ e => play.api.i18n.Messages(e.message , e.args: _*) }.mkString(", "))</span> </div> </div> <div class="control-group @if(entryForm("phone").hasErrors) {error}"> <label for="phone">Phone</label> <div class="input"> <input type="text" id="phone" name="phone" value="@entryForm("phone").value"> <span class="help-inline">@(entryForm("phone").errors.map{e => play.api.i18n.Messages(e.message , e.args: _*) }.mkString(", "))</span> </div> </div> <div> <button type="submit" class="btn btn-primary">Save</button> <a class="btn" href="@routes.Entries.list()">Cancel</a> </div> </form>
Tip
Please note that internationalization (i18n) is not needed here, and that it is going beyond the topic of the book sample. For more information on i18n, please refer to the Play documentation.
This view compiles only in Scala. Because of a small discrepancy between the Java and Scala API, the string e.args
must be replaced with e.arguments
in the highlighted lines for the Java version.
We have added some additional error message handling to two visible input fields here, and to be honest, there is so much repetition already that the source code starts to look really horrible. Not to imagine how a more complex form would look like.
But we can do better…
We have a powerful template language at our hands that allows us to easily reduce unnecessary repetition. For example, we can define a function, which generates an input field given a specific form field. Play already comes with helpers, which exactly do this‑the form template helpers.
We replace our edit
form with this code, which is the same for Java and Scala:
@(entryForm: Form[Entry], heading: String) @import views.html.helper._ @import views.html.helper.twitterBootstrap._ @main { <h1>@heading</h1> @form(action=routes.Entries.save()) { <input type="hidden" name="id" value="@entryForm("id").value"> @inputText( entryForm("name"), '_class -> "control-group", '_label -> "Name", '_showConstraints -> false, 'autofocus -> "" ) @inputText( entryForm("phone"), '_class -> "control-group", '_label -> "Phone", '_showConstraints -> false ) <div> <button type="submit" class="btn btn-primary">Save</button> <a class="btn" href="@routes.Entries.list()">Cancel</a> </div> } }
First, the helpers are imported; we especially need the Twitter Bootstrap support here, to automatically apply the right style. Please note that the Twitter Bootstrap imports contain a FieldConstructor
implementation, which defines how to render a form field and which is implicitly used by all default Play view helpers, such as @form
and @inputText
.
We replaced the <form>
element with @form
. It is no longer necessary to specify the HTTP method, because the reverse controller we use already contains this information.
Next, we replaced all the elements around our visible input fields with @inputText
. This template has three different kinds of arguments. First, we specified the form
field, followed by special arguments of @inputText
(starting with '_
), and then followed at the end by a list of HTML attributes, which will be appended to the generated HTML input element.
The CSS style control-group
has to be set, because the current Twitter Bootstrap support of Play is a little bit outdated.
The imported helper twitterBootstrapFieldConstructor.scala.html
contains the HTML code of a Twitter Bootstrap form
field. This code can be compared with one of the Twitter Bootstrap form samples to get more details about the completeness of the helper implementation.
The '_label
argument contains the visible label of the input field. Finally, we set '_showConstraints
to false
because we don't want our field constraints to be displayed.
In this last part of the book, we are going to replace our self-implemented data store with a real database.
To ease database related development, Play has a built-in support for two database access layers, namely Ebean for Java and Anorm for Scala. In the near future, a third candidate will be bundled with Play; Slick for Scala. They all use JDBC under the hood and provide us with nice APIs to close the gap between our application code and the underlying database.
Ebean is an object-relational mapper for Java, based on an internal domain-specific language (DSL) for querying the database. That means native SQL statements are created automatically when executing queries.
By default, a new Play application for Java that has been created with the play new
command has all the required dependencies pre-defined in our SBT project definition project/Build.scala
:
val appDependencies = Seq( javaCore, javaJdbc, javaEbean )
We register the H2 in-memory JDBC driver for our database named default
by uncommenting these two lines in our configuration file conf/application.conf
:
db.default.driver=org.h2.Driver db.default.url="jdbc:h2:mem:play"
To activate the Ebean initialization on application startup, we have to uncomment the following line:
ebean.default="models.*"
With this property, we specify where to search for entities; in our case the class Entry.java
. Also we define which Ebean server is used to access our entities, in this case default, which corresponds to the connection properties previously given.
Now the configuration of Ebean is complete.
We have two choices when it comes to creating a database schema; we either do it manually or automatically. Ebean supports automatic DDL generation, that is, it produces SQL scripts based on our entities. This is good for testing or as a starting point for large databases, but it is discouraged for production quality applications.
Here we are going to create the database schema manually. Play supports us in tracking and executing our database changes. We call these changes evolutions; they are divided into files located in conf/evolutions/<database-name>/
. In our case <database-name>
is default
. The first evolution has the name 1.sql
, the second 2.sql
, and so on. These files contain the so-called up and down scripts, which are used to change and revert the database. A revert may occur during development if an existing evolution changes.
So let's create our first database evolution. As mentioned previously, it is located in conf/evolutions/default/1.sql
with the following content:
# --- !Ups create table entry ( id bigint not null, name varchar(255) not null, phone varchar(255) not null, constraint pk_entry primary key (id)) ; create sequence entry_seq start with 1; # --- !Downs drop table if exists entry; drop sequence if exists seq_entry;
The Ups
and the Downs
comments are mandatory. In the Ups
part we create a table entry; rows of phone book entries with a unique ID, a name, and a phone number. Additionally, we are using a sequence to generate our unique IDs. In the Downs
part we specify how to revert the changes, which in our case is simply by dropping the table and the sequence.
Before we start our application and create the schema, we have to implement our Ebean based model.
Ebean uses the same mapping annotations as the Java Persistence API (JPA). These are located in the package javax.persistence
. We use the @Entity
annotation that denotes that our Entry
class is mapped to a table with the same name. The @Id
annotation marks our Primary key field.
We implement app/models/Entry.java
as follows:
package models; import play.db.ebean.Model; import javax.persistence.Entity; import javax.persistence.Id; import static play.data.validation.Constraints.*; @Entity public class Entry extends Model { @Id public Long id; @Required @MinLength(value = 2) public String name; @Required @Pattern( value = "\\+?[0-9\\s]+", message = "Optional +, followed by digits & spaces" ) public String phone; public Entry() {} public static Finder<Long,Entry> find = new Finder<Long,Entry>(Long.class, Entry.class); }
Ebean has a default naming convention that maps Java names written in camel-case fooBarBaz
to database names in snake-case format foo_bar_baz
. Of course this convention can be customized.
We extend the Model
class, which is part of the Play Framework. It contains convenient methods and a Finder
class that we use to create a finder for our Entry
entity.
Next, we are using the query DSL of Ebean to implement our DAO methods. Please modify app/models/Entries.java
accordingly:
package models; import static play.libs.Scala.toSeq; import scala.collection.Seq; public class Entries { public static void delete(long id) { Entry.find.ref(id).delete(); } public static Entry findById(long id) { return Entry.find.byId(id); } public static Seq<Entry> findByName(String filter) { return toSeq(Entry.find .where() .ilike("name", "%"+filter+"%") .findList()); } public static void save(Entry entry) { if (entry.id == null) { entry.save(); } else { entry.update(); } } }
As you can see, it is pretty straightforward to query the database with Ebean.
delete
: This uses theFinder
ofEntry
to delete an entity referenced by its ID.findById
: This uses theFinder
ofEntry
to find an entity by its ID.findByName
: This finds all entities matching a specific criteria. We useilike
to find case insensitive matches of a given filter within the columnname
.save
: This persists an entity. If it is new, the ID is generated by our database sequence and automatically set by Ebean in ourmodel
class; else all fields of the associated database row are updated.
Ebean, by default, executes a query in an implicit transaction, so we don't have to care here about session and transaction management.
We can now start reloading our application in the browser:
Play checks on each page reload in development mode to see if database evolutions have changed. If there are new evolutions or an existing one has changed, Play asks to apply the scripts. We press the Apply this script now! button to execute the Ups
sections of the changed scripts in the right-hand side order.
Now we have a working Ebean implementation of our model.
Anorm for Scala takes a different approach than Ebean for Java. Queries are written in SQL, because Anorm assumes that SQL is already the best DSL to access a database. There is no need for an Object Relational Mapper. Scala has all the features needed to transform the JDBC data into Scala structures.
When creating a new Play application for Scala with the play new
command, the required dependencies jdbc
and anorm
are already configured in the SBT build definition file project/Build.scala
:
val appDependencies = Seq( jdbc, anorm )
Like for our Ebean example, we enable the in-memory H2 database in conf/application.conf
by uncommenting the following lines:
db.default.driver=org.h2.Driver db.default.url="jdbc:h2:mem:play"
There is no additional configuration needed.
Again we will create the database DDL script conf/evolutions/default/1.sql
.
# --- !Ups create table entry ( id bigint auto_increment primary key, name varchar(255) not null, phone varchar(255) not null) ; # --- !Downs drop table if exists entry;
We declare our primary key column as auto_increment
to automatically generate IDs in the database when inserting new phone book entries. Ebean needs a database sequence for ID generation, but Anorm also works well with an auto increment key.
The database access is implementation in the DAO through models/Entries.scala
. Our entity case class Entry
remains unchanged.
The necessary imports for using Anorm and JDBC connections are also added:
package models import anorm._ import anorm.SqlParser._ import play.api.Play.current import play.api.db.DB case class Entry(name: String = "", phone: String = "", id: Option[Long] = None) object Entries { val simple = { get[String]("entry.name") ~ get[String]("entry.phone") ~ get[Option[Long]]("entry.id") map { case name ~ phone ~ id => Entry(name, phone, id) } } def delete(id: Long) = DB.withConnection { implicit connection => SQL("delete from entry where id = {id}") .on('id -> id) .executeUpdate() } def findById(id: Long) = DB.withConnection { implicit connection => SQL("select * from entry where id = {id}") .on('id -> id) .singleOpt(simple) } def findByName(filter: String) = DB.withConnection { implicit connection => SQL("select * from entry where lower(name) like {filter}") .on('filter -> ("%"+filter.toLowerCase+"%")) .as(simple.*) } def save(entry: Entry) = DB.withConnection { implicit connection => if (entry.id.isDefined) { SQL("""update entry set name={name}, phone={phone} where id = {id}""") .on('name -> entry.name, 'phone -> entry.phone, 'id -> entry.id) .executeUpdate() } else { SQL("""insert into Entry(name, phone) values ({name}, {phone})""") .on('name -> entry.name, 'phone -> entry.phone) .executeInsert() } } }
The row parser simple
defines how rows of our database table Entry
are retrieved. For each column, we specify the type and the name, for example, get[String]("entry.name")
. These definitions are concatenated with the tilde character ~
. Finally, we tell Anorm how to create an Entry
object of a given row.
Unlike Ebean, we have to explicitly retrieve a new database connection when we execute database queries. We do this by calling DB.withConnection
. The Anorm SQL API needs an implicit connection to be in scope.
With the connection in place, we are able to implement our database queries based on plain SQL. In Anorm, we start a query by passing our SQL statement to the SQL
method, which returns an SqlQuery
object. Placeholders for variable values are denoted within curly braces {}
. On the SqlQuery
object, we call the on()
method to specify the placeholder values. Finally we execute the query.
Anorm provides us with different methods for querying the database. Here we use:
executeUpdate
: This is used to execute a SQLupdate
ordelete
statement.executeInsert
: This is used to execute a SQLinsert
statement.singleOpt(simple)
: This is used to execute a SQLselect
statement and retrieve an optional single result, returned as a ScalaOption
type. We specify our row parsersimple
to tell Anorm how to retrieve rows.as(simple.*)
: This is used to execute a SQL select statement and retrieve a list of result rows. Again, we specify our row parser and call the*
method to apply it to all the result rows.
Slick increases the degree of scalability and type-safety by allowing us to write database queries natively in Scala. Instead of writing SQL, we use an API that is very similar to that of Scala collections. Under the hood, Slick features a query compiler, which produces the SQL code for the underlying database.
As of Play 2.1, we have to add an external dependency to the SBT build definition project/Build.scala
in order to use Slick:
val appDependencies = Seq( jdbc, "com.typesafe.slick" %% "slick" % "1.0.0" )
The jdbc
module is also needed; the anorm
module can be removed.
Don't forget to regenerate your project files via play idea
or play eclipse
to update your class path accordingly if you use one of these IDEs.
Again we enable the in-memory H2 database in conf/application.conf
.
db.default.driver=org.h2.Driver db.default.url="jdbc:h2:mem:play"
As mentioned previously, Slick has a query compiler that generates SQL statements. We give Slick a hint about which SQL dialect to use by importing an appropriate Driver
and ExtendedProfile
respectively (replaced by JdbcDriver
with Slick 1.1).
Please create the file app/models/DAL.scala
with the following content:
package models trait Profile { val driver: slick.driver.ExtendedProfile val db: scala.slick.session.Database }
The driver
variable holds the specific Slick database driver. We also need to add a variable db
, which is used to create database sessions and transactions.
We wrap our Entries
data access object (DAO) in the EntriesComponent
trait and use the self-type mechanism to inject the profile properties into our component. Afterwards, the driver objects are imported.
This is the content of app/models/Entries.scala
:
package models case class Entry(name: String = "", phone: String = "", id: Option[Long] = None) trait EntriesComponent { self: Profile => import driver.simple._ import Database.threadLocalSession object Entries extends Table[Entry]("ENTRY") { def id = column[Long]("ID", O.PrimaryKey, O.AutoInc) def name = column[String]("NAME") def phone = column[String]("PHONE") def * = name ~ phone ~ id.? <> (Entry, Entry.unapply _) def delete(id: Long) = db withSession { Query(this).where(_.id is id).delete } def findById(id: Long) = db withSession { Query(this).where(_.id is id).firstOption } def findByName(filter: String) = db withSession { val search = "%"+filter.toLowerCase+"%" Query(this).where(_.name.toLowerCase like search).list } def save(entry: Entry) = db withSession { entry.id.fold { this.insert(entry) }{ id => Query(this).where(_.id is id).update(entry) } } } }
Similar to Anorm, we provide a mapping between objects and table rows. In particular, we define the type and name of each column along with additional attributes such as primary key and auto increment.
The *
method, the so-called "star-projection", is analog to the row parser of Anorm.
Queries are in the so-called "lifted" state, which means that no SQL has been executed so far. We finally do this by calling:
delete
: This deletes a specific phone book entryfirstOption
: This returnsOption[Entry]
, which may beNone
if no entry with the given ID existslist
: This returns phone book entries that contain a specific nameupdate
: This updates a specific phone book entry
We insert new entities into the database by calling insert
on the Entries
object.
The threadLocalSession
import attaches a database session to the local thread. This session is automatically available for queries.
We will now choose to add the concrete Slick driver binding to our existing data access layer (DAL) models/DAL.scala
.
import play.api.Play.current import play.api.db.DB import scala.slick.session.Database object DAL extends EntriesComponent /* with OtherComponent */ with Profile { val driver = scala.slick.driver.H2Driver val db = Database.forDataSource(DB.getDataSource("default")) val ddl = Entries.ddl // ++ Other.ddl }
All values of the Profile
trait are defined here. We use H2Driver
of Slick and the default
JDBC configuration found in application.conf
to retrieve a JDBC DataSource
value.
To prepare the automatic database schema generation, we additionally define the ddl
value containing the DDL script of our Entries
table.
The Entries
DAO is now part of our DAL. Therefore we have to adjust the imports section of our controller app/controllers/Entries.scala
:
import models.Entry import models.DAL.{Entries => DAO}
You should remember that we have inserted some test data on startup in our GlobalSettings
implementation. This is the right place to create our database on application startup.
import models.{DAL, Entry}, DAL._, DAL.driver.simple._
import play.api._
object Global extends GlobalSettings {
override def onStart(app: Application) {
DAL.db withSession { implicit s: Session =>
ddl.create
}
Entries.save(Entry("Guillaume Bort", "+33 5 55 55 55 55"))
Entries.save(Entry("Sadek Drobi", "+33 5 55 55 55 55"))
}
}
We use our DAL to bring a session into scope and create our database schema.
You can now run this application.
Now go ahead and start to implement amazing web applications with Play!