Book Image

Instant Play Framework Starter

By : Daniel Dietrich
Book Image

Instant Play Framework Starter

By: Daniel Dietrich

Overview of this book

Play is a full-featured Java and Scala web framework for building modern, high-performance web applications. It is characterized by its simplicity and scalability. With its lightweight, stateless, and web-friendly architecture, Play focuses on developer experience to make web application development fun.Instant Play Framework Starter is the ideal companion to start developing web applications with Play. The building blocks of a typical web application are carefully designed following an on-going example.Instant Play Framework Starter starts with a quick setup and running a first sample. Then, the anatomy of a typical Play application is outlined. More features are added step by step to an example application. The result is the prototype of a highly scalable web application.The example is implemented in Java and in Scala. It consists of building blocks you will find in every Play application. In particular, you will learn how views are rendered with the template engine, how HTTP routes are used to define the navigation rules, and how to separate the application logic of controllers from the business logic of the model. This separation is the result of a careful application design, which makes it easy to add features like data binding and validation. Finally you will see how easy it is to adapt different database access libraries. Instant Play Framework Starter will help you to get started with Play and develop your first application. Packed with examples, it is easy to follow the design of a real-world application. You are able to compare the difference between a Java- and a Scala-based Play application and to decide which language fits your needs best. All topics covered in the book are described with the aim to serve as a reference for future web application development with Play.
Table of Contents (7 chapters)

Top features you need to know about


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.

1 – Designing a Play application

In this section we will learn the basics of how to implement the MVC components of a typical Play application.

Introducing our sample 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.

Defining the domain model

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.

The Java model

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.

The Scala model

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.

Designing the HTTP interface with the routes file

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.

Defining the application routes

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.

Dynamic request paths and variables

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 expression regex. The current value can be accessed by name. 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 value socks

  • product: String ?= "socks": This defines the default value socks

Testing the routes

We need to get back to our phone book example if we want to test our newly created routes. The routes file is compiled and executed when we visit localhost:9000.

Play provides us with a hint that the Entries controller is missing. So, we will implement it next.

Handling HTTP requests by controller actions

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.

Providing dummy implementations

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:

URL redirection with reverse routes

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.

Implementing controller actions

Now we have met all the requirements to implement our first two controller methods, list and remove.

The Java code

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;
The Scala code

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}
What comes next

When we reload the page, the compiler complains that the index view template currently expects a string instead of a list of entries. We will fix that when implementing our view templates.

Composing the UI from view templates

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.

Syntax of view templates

The syntax of view templates are basically a combination of HTML for the static parts and Scala for the dynamic parts.

View parameters

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])
View imports

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._
View expressions

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, the else part is optional

  • @for(to <- from; …) {…}: This is the for 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 function name

For a complete list of template expressions, please refer to the Play framework documentation.

Composing view templates

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.

Example

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.

Nesting view templates

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.

Implementing the phone book views

Let's get started with implementing the views of our phone book sample.

Prerequisites

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
The index view template

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 view template

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.

The main view template

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.

Fine-tuning the CSS style

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:

Providing some test data

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!

2 – Dealing with user input

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.

Defining a form

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.

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.

Binding data with a Java form

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.

Binding data with a Scala form

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 client side

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.

Validating user input

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.

The server side

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.

Defining constraints in Java

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.

Defining constraints in Scala

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.

The client side

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…

Using form template helpers

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.

3 – Connecting to a database

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.

Accessing data with Ebean for Java

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.

Customizing the configuration

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.

Creating the database schema

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.

Implementing the 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 the Finder of Entry to delete an entity referenced by its ID.

  • findById: This uses the Finder of Entry to find an entity by its ID.

  • findByName: This finds all entities matching a specific criteria. We use ilike to find case insensitive matches of a given filter within the column name.

  • save: This persists an entity. If it is new, the ID is generated by our database sequence and automatically set by Ebean in our model 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.

Running the application

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.

Accessing data with Anorm for Scala

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.

Customizing the configuration

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.

Creating the database schema

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.

Implementing the model

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 SQL update or delete statement.

  • executeInsert: This is used to execute a SQL insert statement.

  • singleOpt(simple): This is used to execute a SQL select statement and retrieve an optional single result, returned as a Scala Option type. We specify our row parser simple 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.

Running the application

When running our application, we are asked to apply the new database evolution. We do so by pressing the Apply this script now! button.

Accessing data with Slick for Scala

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.

Customizing the configuration

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"
Implementing the model

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.

The data access object

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 entry

  • firstOption: This returns Option[Entry], which may be None if no entry with the given ID exists

  • list: This returns phone book entries that contain a specific name

  • update: 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.

The data access layer

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}
Generating the database schema

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!