Introduction
Essential Play is aimed at beginner-to-intermediate Scala developers who want to get started using the Play 2 web framework. The material presented focuses on Play version 2.3, although the approaches introduced are generally applicable to Play 2.2+.
By the end of the course we will have a solid foundation in each of the main libraries Play provides for building sites and services:
- Routing, controllers, and actions
- Manipulating requests and responses
- Generating HTML
- Parsing and validating form data
- Reading and writing JSON
- Asynchronous request handling
- Calling external web services
Many thanks to Richard Dallaway, Jonathan Ferguson, and the team at Underscore for their invaluable contributions and extensive proof reading.
Conventions Used in This Book
This book contains a lot of technical information and program code. We use the following typographical conventions to reduce ambiguity and highlight important concepts:
Typographical Conventions
New terms and phrases are introduced in italics. After their initial introduction they are written in normal roman font.
Terms from program code, filenames, and file contents, are written in monospace font
. Note that we do not distinguish between singular and plural forms. For example, might write String
or Strings
to refer to the java.util.String
class or objects of that type.
References to external resources are written as hyperlinks. References to API documentation are written using a combination of hyperlinks and monospace font, for example: scala.Option
.
Source Code
Source code blocks are written as follows. Syntax is highlighted appropriately where applicable:
object MyApp extends App {
println("Hello world!") // Print a fine message to the user!
}
Some lines of program code are too wide to fit on the page. In these cases we use a continuation character (curly arrow) to indicate that longer code should all be written on one line. For example, the following code:
println("This code should all be written ↩
on one line.")
should actually be written as follows:
println("This code should all be written on one line.")
Callout Boxes
We use three types of callout box to highlight particular content:
Tip callouts indicate handy summaries, recipes, or best practices.
Advanced callouts provide additional information on corner cases or underlying mechanisms. Feel free to skip these on your first read-through—come back to them later for extra information.
Warning callouts indicate common pitfalls and gotchas. Make sure you read these to avoid problems, and come back to them if you’re having trouble getting your code to run.
1 Getting Started
In this chapter we will discuss how to get started with Play. Our main focus will be on building and running the exercises in this book, but we will also discuss installing and using SBT, the Scala Build System, to compile, test, run, and deploy Play projects.
1.1 Installing the Exercises
The exercises and sample code in this book are all packaged with a copy of SBT. All you need to get started are Git, a Java runtime, and an Internet connection to download other dependencies.
Start by cloning the Github repository for the exercises:
bash$ git clone https://github.com/underscoreio/essential-play-code.git
bash$ cd essential-play-code
dave@Jade ~/d/p/essential-play-code> git status
# On branch exercises...
bash$ ls -1
chapter1-hello
chapter2-calc
chapter2-chat
# And so on...
The repository has two branches, exercises
and solutions
, each containing a set of self-contained Play projects in separate directories. We have included one exercise to serve as an introduction to SBT. Change to the chapter1-hello
directory and start SBT using the shell script provided:
bash$ cd chapter1-hello
bash$ ./sbt.sh
# Lots of output here...
# The first run will take a while...
[app] $
“Downloading the Internet”
The first commands you run in SBT will cause it to download various dependencies, including libraries for Play, the Scala runtime, and even the Scala compiler. This process can take a while and is affectionately known to Scala developers as “downloading the Internet”.
These files are only downloaded once, after which SBT caches them on your system. Be prepared for delays of up to a few minutes:
- the first time you start SBT;
- the first time you compile your code;
- the first time you compile your unit tests.
Things will speed up considerably once these files are cached.
Once SBT is initialised, your prompt should change to [app] $
, which is the name of the Play project we’ve set up for you. You are now interacting with SBT. Compile the project using the compile
command to check everything is working:
[app] $ compile
# Lots of output here...
# The first run will take a while...
[info] Updating {file:/Users/dave/dev/projects/essential-play-code/}app...
[info] Resolving jline#jline;2.12 ...
[info] Done updating.
[info] Compiling 3 Scala sources and 1 Java source to ↩
/Users/dave/dev/projects/essential-play-code/ ↩
target/scala-2.11/classes...
[success] Total time: 7 s, completed 13-Jan-2015 11:15:39
[app] $
If the project compiles successfully, try running it. Enter run
to start a development web server and access it at http://localhost:9000 to test out the app:
[app] $ run
--- (Running the application from SBT, auto-reloading is enabled) ---
[info] play - Listening for HTTP on /0:0:0:0:0:0:0:0:9000
(Server started, use Ctrl+D to stop and go back to the console...)
# Play waits until we open a web browser...
[info] play - Application started (Dev)
If everything worked correctly you should see the message "Hello world!"
in your browser. Congratulations—you have run your first Play web application!
1.1.1 Other Exercises in this Book
The process you have used here is the same for each exercise in this book:
- change to the relevant exercise directory;
- start SBT;
- issue the relevant SBT commands to compile and run your code.
You will find instructions for each exercise in the text of the book. Also look out for comments like the following in the exercise source code:
// TODO: Complete this bit!
These tell you where you need to modify the code to complete the exercises. There are complete solutions to each exercise in the solutions
branch of the repository.
Getting Help
Resist the temptation to look at the solutions if you get stuck! You will make mistakes when you first start programming Play applications, but mistakes are the best way to teach yourself.
If you do get stuck, join our Gitter chat room to get help from the authors and other students.
Try to get the information you need to solve the immediate problem without gaining complete access to the solution code. You’ll proceed slower this way but you’ll learn a lot faster and the knowledge will stick with you longer.
1.2 Installing SBT
As we discussed in the previous section, each exercise and solution is bundled with its own scripts and binaries for SBT. This is a great setup for this book, but after you’ve finished the exercises you will want to install SBT properly so you can work on your own applications. In this section we will discuss the options available to you to do this.
1.2.1 How Does SBT Work?
SBT relies heavily on account-wide caches to store project dependencies. By default these caches are located in two folders:
~/.sbt
contains configuration files and account-wide SBT plugins; and~/.ivy2
contains cached library dependencies (similar to~/.m2
for Maven).
SBT downloads dependencies on demand and caches them for future use in ~/.ivy2
. In fact, the JAR we run to boot SBT is actually a launcher (typically named sbt-launch.jar
) that downloads and caches the correct versions of SBT and Scala needed for our project.
This means we can use a single launcher to compile and run projects with different version requirements for libraries, SBT, and Scala. We are can use separate launchers for each project, or a single launcher that covers all projects on our development machine. The shared caches allow multiple SBT launchers to work indepdently without conflict.
Despite the convenience of these account-wide caches, they have two important drawbacks to be aware of:
the first time we build a project we must be connected to the Internet for SBT to download the required dependencies; and
as we saw in the previous section, the first build of a project may take a long time.
1.2.2 Flavours of SBT
SBT is available from a number of sources under a variety of different names. Here are the main options available, any of which is a suitable starting point for your own applications:
System-wide vanilla SBT—We can install a system-wide SBT launcher using the instructions on the SBT web site. Linux and OS X users can download copies via package managers such as Apt, MacPorts, and Homebrew.
Project-local vanilla SBT—We can bundle the SBT launcher JAR with a project and create shell scripts to start it with the correct command line arguments. This is the approach used in the exercises and solutions for this book. ZIP downloads of the required files are available from the SBT web site.
Typesafe Activator—Activator, available from Typesafe’s web site, is a tool for getting started with the Typesafe Stack. The
activator
command is actually just an alias for SBT, although the activator distribution comes pre-bundled with a global plugin for generating new projects from templates (theactivator new
command).“SBT Extras” script—Paul Philips released an excellent shell script that acts as a front-end for SBT. The script does the bootstrapping process of detecting Scala and SBT versions without requiring a launcher JAR. Linux and OS X users can download the script from Paul’s Github page.
Legacy Play Distributions
Older downloads from http://playframework.com shipped with a built-in play
command that was also an alias for SBT. However, the old Play distributions configured SBT with non-standard cache directories that meant it did not play nicely with other installs.
We recommend replacing any copies of the legacy play
command with one of the other options described above. Newer versions of Play are shipped with Activator, which interoperates well with other locally installed copies of SBT.
1.3 Using SBT
At the beginning of this chapter we cloned the Git repository of the exercises for this book and ran our first SBT commands on the chapter1-hello
sample project. Let’s revisit this project to investigate the standard SBT commands for compiling, running, and deploying Play applications.
Change to the chapter1-hello
directory if you are not already there and start SBT using the shell script provided:
bash$ cd essential-play-code/chapter1-hello
bash$ ./sbt.sh
[app] $
SBT with and without Play
Play is distributed in two components:
a set of libraries used by our web applications at runtime;
an SBT plugin that customises the default behaviour of SBT, adding and altering commands to help us build applications for the web.
This section covers the behaviour of SBT with the Play plugin activated. We have included callout boxes like this one to highlight the differences from vanilla SBT.
1.3.1 Interative and Batch Modes
We can start SBT in two modes: interactive mode and batch mode. Batch mode is useful for continuous integration and delivery, while interactive mode is faster and more convenient for use in development. Most of our time in this book will be spent in interactive mode.
We start interactive mode be running SBT with no command line arguments. SBT displays a command prompt where we can enter commands such as compile
, run
, and clean
to build our code. Pressing Ctrl+D
quits SBT when we’re done:
bash$ ./sbt.sh
[app] $ compile
# SBT compiles our code and we end up back in SBT...
[app] $ ^D
# Ctrl+D quits back to the OS command prompt
bash$
We start SBT in batch mode by issuing commands as arguments on the OS command line. SBT executes the commands immediately and then exits back to the OS. The commands—compile
, run
, clean
and so on—are the same in both modes:
bash$ ./sbt.sh compile
# SBT compiles our code and we end up back on the OS command prompt...
bash$
The SBT command prompt
The default SBT command prompt is a single echelon:
>
Play changes this to the name of the project surrounded by square brackets:
[app] $
You will find the prompt changing as you switch back and forth between Play projects and vanilla Scala projects.
Directory structure of non-Play projects
By default SBT uses two directories to store application and test code:
src/main/scala
—Scala application code;src/test/scala
—Scala unit tests.
Play replaces these with the app
, app/assets
, views
, public
, conf
, and test
directories, providing locations for the extra files required to build a web application.
1.3.2 Common SBT Commands
The following table contains a summary of the most useful SBT commands for working with Play. Each command is covered in more detail below.
Many commands have dependencies listed in the right-hand column. For example, compile
depends on update
, run
depends on compile
, and so on. When we run a command SBT automatically runs its dependencies as well. For example, whwnever we run the compile
command, SBT will run update
for us automatically.
SBT Command | Purpose | Notes and Dependencies |
---|---|---|
update |
Resolves and caches library dependencies | No dependencies |
compile |
Compiles application code, including code under app , app/assets , and views |
Depends on update |
run |
Runs application in development mode, continuously recompiles on demand | Depends on compile |
console |
Starts an interactive Scala prompt | Depends on compile |
test:compile |
Compiles all unit tests | Depends on compile |
test |
Compiles and runs all unit tests | Depends on test:compile |
testOnly foo.Bar |
Compiles and runs unit tests defined in the class foo.Bar |
Depends on test:compile |
stage |
Gathers all dependencies into a single stand-alone directory | Depends on compile |
dist |
Gathers staged files into a ZIP file | Depends on stage |
clean |
Deletes temporary build files under ${projecthome}/target |
No dependencies |
eclipse |
Generates Eclipse project files | No dependencies, requires the sbteclipse plugin |
1.3.3 Compiling and Cleaning Code
The compile
and test:compile
commands compile our application and unit tests respectively. The clean
command deletes the generated class files in case we want to rebuild from scratch (clean
is not normally required as we shall see below).
Let’s clean the example project from the previous section and recompile the code as an example:
bash$ ./sbt.sh
[info] Loading project definition from ↩
/Users/dave/dev/projects/essential-play-code/project
[info] Set current project to app (in build file:/.../essential-play-code/)
[app] $ clean
[success] Total time: 0 s, completed 13-Jan-2015 11:15:32
[app] $ compile
[info] Updating {file:/Users/dave/dev/projects/essential-play-code/}app...
[info] Resolving jline#jline;2.12 ...
[info] Done updating.
[info] Compiling 3 Scala sources and 1 Java source to ↩
/Users/dave/dev/projects/essential-play-code/ ↩
target/scala-2.11/classes...
[success] Total time: 7 s, completed 13-Jan-2015 11:15:39
[app] $
In the output from compile
SBT tells us how many source files it compiled and how long compilation took—7 seconds in this case! Fortunately we normally don’t need to wait this long. The compile
and test:compile
commands are incremental—they automatically recompile only the files that have changed since the last time we compiled the code. We can see the effect of incremental compilation by changing our application and running compile
again. Open app/controllers/AppController.scala
in an editor and change the "Hello World!"
line to greet you by name:
package controllers
import play.api.Logger
import play.api.Play.current
import play.api.mvc._
import models._
object AppController extends Controller {
def index = Action { request =>
Ok("Hello Dave!")
}
}
Now re-run the compile
command:
[app] $ compile
[info] Compiling 1 Scala source to ↩
/Users/dave/dev/projects/essential-play-code/ ↩
target/scala-2.11/classes...
[success] Total time: 1 s, completed 13-Jan-2015 12:26:16
[app] $
One Scala file compiled in one second. Much better! Incremental compilation means we can rely on compile
and test:compile
to do the right thing to recompile our code—we rarely need to use clean
to rebuild from scratch.
Compiling in interactive mode
Another reason our first compile
command was slow was because SBT spent a lot of time loading the Scala compiler for the first time. If we keep SBT open in interactive mode, subsequent compile
commands become much faster.
1.3.4 Watch Mode
We can prefix any SBT command with a ~
to run the command in watch mode. SBT watches our codebase and reruns the specified task whenever we change a source file. Type ~compile
at the prompt to see this in action:
[app] $ ~compile
[success] Total time: 0 s, completed 13-Jan-2015 12:31:09
1. Waiting for source changes... (press enter to interrupt)
SBT tells us it is “waiting for source changes”. Whenever we edit a source file it will trigger the compile
task and incrementally recompile the changed code. Let’s see this by introducing a compilation error to AppController.scala
. Open the source file again and delete the closing "
from "Hello Name!"
. As soon as we save the file we see the following in SBT:
[info] Compiling 1 Scala source to ↩
/Users/dave/dev/projects/essential-play-code/ ↩
target/scala-2.11/classes...
[error] /Users/dave/dev/projects/essential-play-code/app/ ↩
controllers/AppController.scala:11: unclosed string literal
[error] Ok("Hello Dave!)
[error] ^
[error] /Users/dave/dev/projects/essential-play-code/app/ ↩
controllers/AppController.scala:12: ')' expected but '}' found.
[error] }
[error] ^
[error] two errors found
[error] (compile:compile) Compilation failed
[error] Total time: 0 s, completed 13-Jan-2015 12:32:45
2. Waiting for source changes... (press enter to interrupt)
The compiler has picked up the error and produced some error messages as a result. If we fix the error again and save the file, the error messages disappear:
[success] Total time: 0 s, completed 13-Jan-2015 12:33:55
3. Waiting for source changes... (press enter to interrupt)
Watch mode is extremely useful for getting instant feedback during development. Simply press Enter when you’re done to return to the SBT command prompt.
Watch mode and other tasks
We can use watch mode with any SBT command. For example:
~compile
watches our code and recompiles it whenever we change a file;~test
watches our code and reruns the unit tests whenever we change a file; and~dist
watches our code and builds a new distributable ZIP archive whenever we change a file.
This behaviour is built into SBT and works irrespective of whether we’re using Play.
1.3.5 Running a Development Web Server
We can use the run
command to run our application in a development environment. This command starts a development web server, watches for incoming connections, and recompiles our code whenever an incoming request is received.
Let’s see this in action. First clean
the codebase, then enter run
at the SBT prompt:
[app] $ clean
[success] Total time: 0 s, completed 13-Jan-2015 12:44:07
[app] $ run
[info] Updating {file:/Users/dave/dev/projects/essential-play-code/}app...
[info] Resolving jline#jline;2.12 ...
[info] Done updating.
--- (Running the application from SBT, auto-reloading is enabled) ---
[info] play - Listening for HTTP on /0:0:0:0:0:0:0:0:9000
(Server started, use Ctrl+D to stop and go back to the console...)
SBT starts up a web server on /0:0:0:0:0:0:0:0:9000
(which means localhost:9000
in IPv6-speak) and waits for a browser to connect. Open up http://localhost:9000
in a web browser and watch the SBT console to see what happens. Play receives the incoming request and recompiles and runs the application to respond:
[info] Compiling 3 Scala sources and 1 Java source to ↩
/Users/dave/dev/projects/essential-play-code/ ↩
target/scala-2.11/classes...
[info] play - Application started (Dev)
If we reload the web page without changing any source code, Play simply serves up the response again. However, if we edit the code and reload the page, Play recompiles the application before responding.
Differences between run
and watch mode
The run
command is a great way to get instant feedback when developing an application. However, we have to send a request to the web browser to get Play to recompile the code. In contrast, watch mode recompiles the application as soon as we change a file.
Sometimes using ~compile
or ~test
can be a more efficient way of working. It depends on how much code we’re rewriting and how many compile errors we are likely to introduce during coding.
Running non-Play applications
SBT’s default run
command is much simpler than the command provided by Play. It simply runs a command line or graphical application and exits when it terminates. Play provides the development web server and continuous compilation functionality.
1.3.5.1 Running Unit Tests
The test
and testOnly
commands are used to run unit tests. test
runs all unit tests for the application; testOnly
runs a single test suite. Let’s use test
to test our sample application:
[app] $ test
[info] Compiling 1 Scala source to ↩
/Users/dave/dev/projects/essential-play-code/ ↩
target/scala-2.10/test-classes...
[info] ApplicationSpec:
[info] AppController
[info] - must respond with a friendly message
[info] ScalaTest
[info] Run completed in 934 milliseconds.
[info] Total number of tests run: 1
[info] Suites: completed 1, aborted 0
[info] Tests: succeeded 1, failed 0, canceled 0, ignored 0, pending 0
[info] All tests passed.
[info] Passed: Total 1, Failed 0, Errors 0, Passed 1
[success] Total time: 2 s, completed 14-Jan-2015 14:02:45
[app] $
Because this is the first time we’ve run test
, SBT starts by compiling the test suite. It then runs our sample code’s single test suite, controllers.AppControllerSpec
. The suite contains a single test that checks whether our greeting starts with the word "Hello"
.
We don’t have many tests for our sample application so testing is fast. If we had lots of test suites we could focus on a single suite using the testOnly
command. testOnly
takes the fully qualified class name of the desired suite as an argument:
[app] $ testOnly controllers.AppControllerSpec
[info] ScalaTest
[info] Run completed in 44 milliseconds.
[info] Total number of tests run: 0
[info] Suites: completed 0, aborted 0
[info] Tests: succeeded 0, failed 0, canceled 0, ignored 0, pending 0
[info] No tests were executed.
[info] Passed: Total 0, Failed 0, Errors 0, Passed 0
[info] No tests to run for test:testOnly
[success] Total time: 1 s, completed 14-Jan-2015 14:06:42
[app] $
As with compile
, both of these commands can run in watch mode by prefixing them with a ~
. Whenever we change and save a file, SBT will recompile it and rerun our tests for us.
1.3.6 Packaging and Deploying the Application
The stage
command bundles the compiled application and all of its dependencies into a single directory under the directory target/universal/stage
. Let’s see this in action:
[app] $ stage
[info] Packaging /Users/dave/dev/projects/essential-play-code/ ↩
target/scala-2.10/app_2.10-0.1-SNAPSHOT-sources.jar ...
[info] Done packaging.
[info] Packaging /Users/dave/dev/projects/essential-play-code/ ↩
target/scala-2.10/app_2.10-0.1-SNAPSHOT.jar ...
[info] Main Scala API documentation to /Users/dave/dev/projects/ ↩
essential-play-code/target/scala-2.10/api...
[info] Done packaging.
[info] Wrote /Users/dave/dev/projects/essential-play-code/ ↩
target/scala-2.10/app_2.10-0.1-SNAPSHOT.pom
[info] Packaging /Users/dave/dev/projects/essential-play-code/ ↩
target/app-0.1-SNAPSHOT-assets.jar ...
[info] Done packaging.
model contains 10 documentable templates
[info] Main Scala API documentation successful.
[info] Packaging /Users/dave/dev/projects/essential-play-code/ ↩
target/scala-2.10/app_2.10-0.1-SNAPSHOT-javadoc.jar ...
[info] Done packaging.
[success] Total time: 1 s, completed 14-Jan-2015 14:08:14
[app] $
Now press Ctrl+D
to quit SBT and take a look at the package created by the stage
command:
bash$ ls -l target/universal/stage/
total 0
drwxr-xr-x 4 dave staff 136 14 Jan 14:11 bin
drwxr-xr-x 3 dave staff 102 14 Jan 14:11 conf
drwxr-xr-x 44 dave staff 1496 14 Jan 14:11 lib
drwxr-xr-x 3 dave staff 102 14 Jan 14:08 share
bash$ ls -l target/universal/stage/bin
total 40
-rwxr--r-- 1 dave staff 12210 14 Jan 14:11 app
-rw-r--r-- 1 dave staff 6823 14 Jan 14:11 app.bat
SBT has created a directory target/universal/stage
containing all the dependencies we need to run the application. It has also created two executable scripts under target/universal/stage/bin
to set an appropriate classpath and run the application from the command prompt. If we run one of these scripts, the app starts up and allows us to connect as usual:
bash$ target/universal/stage/bin/app
Play server process ID is 22594
[info] play - Application started (Prod)
[info] play - Listening for HTTP on /0:0:0:0:0:0:0:0:9000
The contents of target/universal/stage
can be copied onto a remote web server and run as a standalone application. We can use standard Unix commands such as rsync
and scp
to achieve this. Sometimes, however, it is more convenient to have an archive to distribute. We can use the dist
command to create a ZIP of target/universal/stage
for easy distribution:
[app] $ dist
[info] Wrote /Users/dave/dev/projects/essential-play-code/ ↩
target/scala-2.10/app_2.10-0.1-SNAPSHOT.pom
[info]
[info] Your package is ready in /Users/dave/dev/projects/ ↩
essential-play-code/target/universal/app-0.1-SNAPSHOT.zip
[info]
[success] Total time: 2 s, completed 14-Jan-2015 14:15:50
Packaging non-Play applications
The stage
and dist
commands are specific to the Play plugin. SBT contains a built-in package
command for building non-Play projects, but this functionality is beyond the scope of this book.
1.3.7 Working With Eclipse
The sample SBT project includes a plugin called sbteclipse that generates project files for Eclipse. Run the eclipse
command to see this in action:
[app] $ eclipse
[info] About to create Eclipse project files for your project(s).
[info] Successfully created Eclipse project files for project(s):
[info] app
[app] $
Now start Eclipse and import your SBT project using File menu > Import… > General > Existing files into workspace and select the root directory of the project source tree in the Select root directory field. Click Finish to add a project called app
to the Eclipse workspace.
1.3.8 Working With Intellij IDEA
Newer versions of the Scala plugin for Intellij IDEA support direct import of SBT projects from within the IDE. Choose File menu > Import… > SBT and select the root directory of the project source tree. The import wizard will do the rest automatically.
1.3.9 Configuring SBT
A full discussion of how to write SBT project configurations is beyond the scope of this book. For more information we recommend reading the tutorial on the SBT web site and the build documentation on the Play web site. The sample projects and exercises for this book will provide a good starting point for your own projects.
2 The Basics
In this chapter we will introduce five fundamental concepts used to process web requests in Play: actions, controllers, routes, requests, and results. With these concepts we will be able to read incoming HTTP requests, pass them to the correct module of the application code, extract appropriate information, and send a response back to the client.
2.1 Directory Structure
Play projects use the following directory structure, which is quite different to the standard structure of an SBT project:
root/
+- app/ # Scala application code
| |
| +- assets/ # client assets for compilation by SBT
| # (Javascript, Coffeescript, Less CSS, and so on)
|
+- views/ # Twirl templates for compilation by SBT
|
+- public/ # static assets to be served by the application
| # (HTML, Javascript, CSS, and so on)
|
+- conf/ # runtime configuration files bundled with the
| # deployed application (route config, logs, DB config)
|
+- test/ # Scala unit tests
|
+- logs/ # logs generated by the development server
|
+- project/ # configuration files and temporary files
|
+- target/ # temporary directory used to store completed builds
Most of our time in this book will be spent editing Scala files in the app
and test
directories and the routes
configuration file in the conf
directory. You can find out more about the asset directories and configuration files in the Play documentation.
2.2 Actions, Controllers, and Routes
We create Play web applications from actions, controllers, and routes. In this section we will see what each part does and how to wire them together.
2.2.1 Hello, World!
Actions are objects that handle web requests. They have an apply
method that accepts a play.api.mvc.Request
and returns a play.api.mvc.Result
. We create them using one of several apply
methods on the play.api.mvc.Action
companion:
import play.api.mvc.Action
Action { request =>
Ok("Hello, world!")
}
We package actions inside Controllers
. These are singleton objects that contain action-producing methods:
package controllers
import play.api.mvc.{ Action, Controller }
object HelloController extends Controller {
def hello = Action { request =>
Ok("Hello, world!")
}
def helloTo(name: String) = Action { request =>
Ok(s"Hello, $name!")
}
}
We use routes to dispatch incoming requests to Actions
. Routes choose Actions
based on the HTTP method and path of the request. We write routes in a Play-specific DSL that is compiled to Scala by SBT:
GET / controllers.HelloController.hello
GET /:name controllers.HelloController.helloTo(name: String)
We’ll learn more about this DSL in the next section. By convention we place controllers in the controllers
package in the app/controllers
folder, and routes in a conf/routes
configuration file.
The structure of our minimal Play application is as follows:
root/
+- app/
| +- controllers/
| +- HelloController.scala # Controller and actions (Scala code)
+- conf/
| +- routes # Routes (Play routing DSL)
+- project/
| +- plugins.sbt # Load the Play plugin (SBT code)
+- build.sbt # Configure the build (SBT code)
2.2.2 The Anatomy of a Controller
Let’s take a closer look at the controller in the example above. The code in use comes from two places:
- the
play.api.mvc
package; - the
play.api.mvc.Controller
trait (via inheritance).
The controller, called HelloController
, is a subtype of play.api.mvc.Controller
. It defines two Action
-producing methods, hello
and helloTo
. Our routes specify which of these methods to call when a request comes in.
Note that Actions
and Controllers
have different lifetimes. Controllers
are created when our application boots and persist until it shuts down. Actions
are created and executed in response to incoming Requests
and have a much shorter lifespan. Play passes parameters from our routes to the method that creates the Action
, not to the action itself.
Each of the example Actions
creates an Ok
response containing a simple message. Ok
is a helper object inherited from Controller
. It has an apply
method that creates Results
with HTTP status 200. The actual return type of Ok.apply
is play.api.mvc.Result
.
Play uses the type of the argument to Ok.apply
to determine the Content-Type
of the Result
. The String
arguments in the example create a Results
of type text/plain
. Later on we’ll see how to customise this behaviour and create results of different types.
2.2.3 Take Home Points
The backbone of a Play web application is made up of Actions
, Controllers
, and routes:
Controllers
are collections of action-producing methods;Routes map incoming
Requests
toAction
-producing method calls on ourControllers
.
We typically place controllers in a Controllers
package in the app/controllers
folder. Routes go in the conf/routes
file (no filename extension).
In the next section we will take a closer look at routes.
2.2.4 Exercise: Time is of the Essence
The chapter2-time
directory in the exercises contains an unfinished Play application for telling the time.
Complete this application by filling in the missing actions and routes. Implement the three missing actions described in the comments in app/controllers/TimeController.scala
and complete the conf/routes
file to hook up the specified URLs.
We’ve written this project using the Joda Time library to handle time formatting and time zone conversion. Don’t worry if you haven’t used the library before—the TimeHelpers
trait in TimeController.scala
contains all of the functionality needed to complete the task at hand.
Test your code using curl
if you’re using Linux or OS X or a browser if you’re using Windows:
bash$ curl -v 'http://localhost:9000/time'
# HTTP headers...
4:18 PM
bash$ curl -v 'http://localhost:9000/time/zones'
# HTTP headers...
Africa/Abidjan
Africa/Accra
Africa/Addis_Ababa
# etc...
bash$ curl -v 'http://localhost:9000/time/CET'
# HTTP headers...
5:21 PM
bash$
Be agile!
Complete the exercises by coding small units of end-to-end functionality. Start by implementing the simplest possible action that you can test on the command line:
// Action:
def time = Action { request =>
Ok("TODO: Complete")
}
// Route:
GET /time controllers.TimeController.time
Write the route for this action and test it using curl
before you move on. The faster you get to running your code, the faster you will learn from any mistakes.
Answer the following questions when you’re done:
What happens when you connect to the application using the following URL? Why does this not work as expected and how can you work around the behaviour?
bash$ curl -v 'http://localhost:9000/time/Africa/Abidjan'
What happens when you send a
POST
request to the application?bash$ curl -v -X POST 'http://localhost:9000/time'`
The main task in the actions in TimeController.scala
is to convert the output of the various methods in TimeHelpers
to a String
so we can wrap it in an Ok()
response:
def time = Action { request =>
Ok(timeToString(localTime))
}
def timeIn(zoneId: String) = Action { request =>
val time = localTimeInZone(zoneId)
Ok(time map timeToString getOrElse "Time zone not recognized.")
}
def zones = Action { request =>
Ok(zoneIds mkString "\n")
}
Hooking up the routes would be straightforward, except we included one gotcha to trip you up. You must place the route for TimeController.zones
above the route for TimeController.timeIn
:
GET /time controllers.TimeController.time
GET /time/zones controllers.TimeController.zones
GET /time/:zone controllers.TimeController.timeIn(zone: String)
If you put these two in the wrong order, Play will treat the word zones
in /time/zones
as the name of a time zone and route the request to TimeController.timeIn("zones")
instead of TimeController.zones
.
The answers to the questions are as follows:
The mistake here is that we haven’t escaped the
/
inAfrica/Abidjan
. Play interprets this as a path with three segments but our route will only match two. The result is a 404 response.If we encode the value as
Africa%2FAbidjan
the application will respond as desired. The%2F
is decoded by Play before the argument is passed totimeIn
:bash$ curl 'http://localhost:9000/time/Africa%2FAbidjan' 4:38 PM
- Our routes are only configured to match incoming
GET
requests soPOST
requests result in a 404 response.
2.3 Routes in Depth
The previous section introduced Actions
, Controllers
, and routes. Actions
and Controllers
are standard Scala code, but routes are something new and specific to Play.
We define Play routes using a special DSL that compiles to Scala code. The DSL provides both a convenient way of mapping URIs to method calls and a way of mapping method calls back to URIs. In this section we will take a deeper look at Play’s routing DSL including the various ways we can extract parameters from URIs.
2.3.1 Path Parameters
Routes associate URI patterns with action-producing method calls. We can specify parameters to extract from the URI and pass to our controllers. Here are some examples:
# Fixed route (no parameters):
GET /hello controllers.HelloController.hello
# Single parameter:
GET /hello/:name controllers.HelloController.helloTo(name: String)
# Multiple parameters:
GET /send/:msg/to/:user ↩
controllers.ChatController.send(msg: String, user: String)
# Rest-style parameter:
GET /download/*filename ↩
controllers.DownloadController.file(filename: String)
The first example assocates a single URI with a parameterless method. The match must be exact—only GET
requests to /hello
will be routed. Even a trailing slash in the URI (/hello/
) will cause a mismatch.
The second example introduces a single-segment parameter written using a leading colon (‘:’). Single-segment parameters match any continuous set of characters excluding forward slashes (‘/’). The parameter is extracted and passed to the method call—the rest of the URI must match exactly.
The third example uses two single-segment parameters to extract two parts of the URI. Again, the rest of the URI must match exactly.
The final example uses a rest-parameter written using a leading asterisk (’*’). Rest-style parameters match all remaining characters in the URI, including forward slashes.
2.3.2 Matching Requests to Routes
When a request comes in, Play attempts to route it to an action. It examines each route in turn until it finds a match. If no routes match, it returns a 404 response.
Routes match if the HTTP method has the relevant value and the URI matches the shape of the pattern. Play supports all eight HTTP methods: OPTIONS
, GET
, HEAD
, POST
, PUT
, DELETE
, TRACE
, and CONNECT
.
HTTP method and URI | Scala method call or result |
---|---|
GET /hello |
controllers.HelloController.hello |
GET /hello/dave |
controllers.HelloController.helloTo("dave") |
GET /send/hello/to/dave |
controllers.ChatController.send("hello", "dave") |
GET /download/path/to/file.txt |
controllers.DownloadController.file("path/to/file.txt") |
GET /hello / |
404 result (trailing slash) |
POST /hello |
404 result (POST request) |
GET /send/to/dave |
404 result (missing path segment) |
GET /send/a/message/to/dave |
404 result (extra path segment) |
Play Routing is Strict
Play’s strict adherance to its routing rules can sometimes be problematic. Failing to match the URI /hello/
, for example, may seem overzealous. We can work around this issue easily by mapping multiple routes to a single method call:
GET /hello controllers.HelloController.hello # no trailing slash
GET /hello/ controllers.HelloController.hello # trailing slash
POST /hello/ controllers.HelloController.hello # POST request
# and so on...
2.3.3 Query Parameters
We can specify parameters in the method-call section of a route without declaring them in the URI. When we do this Play extracts the values from the query string instead:
# Extract `username` and `message` from the path:
GET /send/:message/to/:username ↩
controllers.ChatController.send(message: String, username: String)
# Extract `username` and `message` from the query string:
GET /send ↩
controllers.ChatController.send(message: String, username: String)
# Extract `username` from the path and `message` from the query string:
GET /send/to/:username ↩
controllers.ChatController.send(message: String, username: String)
We sometimes want to make query string parameters optional. To do this, we just have to define them as Option
types. Play will pass Some(value)
if the URI contains the parameter and None
if it does not.
For example, if we have the following Action
:
object NotificationController {
def notify(username: String, message: Option[String]) =
Action { request => /* ... */ }
}
we can invoke it with the following route:
GET /notify controllers.NotificationController. ↩
notify(username: String, message: Option[String])
We can mix and match required and optional query parameters as we see fit. In the example, username
is required and message
is optional. However, path parameters are always required—the following route fails to compile because the path parameter :message
cannot be optional:
GET /notify/:username/:message controllers.NotificationController. ↩
notify(username: String, message: Option[String])
# Fails to compile with the following error:
# [error] conf/routes:1: No path binder found for Option[String].
# Try to implement an implicit PathBindable for this type.
HTTP method and URI | Scala method call or result |
---|---|
GET /send/hello/to/dave |
ChatController.send("hello", "dave") |
GET /send?message=hello&username=dave |
ChatController.send("hello", "dave") |
GET /send/to/dave?message=hello |
ChatController.send("hello", "dave") |
2.3.4 Typed Parameters
We can extract path and query parameters of types other than String
. This allows us to define Actions
using well-typed arguments without messy parsing code. Play has built-in support for Int
, Double
, Long
, Boolean
, and UUID
parameters.
For example, given the following route and action definition:
GET /say/:msg/:n/times controllers.VerboseController.say(msg: String, n: Int)
object VerboseController extends Controller {
def say(msg: String, n: Int) = Action { request =>
Ok(List.fill(n)(msg) mkString "\n")
}
}
We can send requests to URLs like /say/Hello/5/times
and get back appropriate responses:
bash$ curl -v 'http://localhost:9000/say/Hello/5/times'
# HTTP headers...
Hello
Hello
Hello
Hello
Hello
bash$
Play also has built-in support for Option
and List
parameters in the query string (but not in the path):
GET /option-example controllers.MyController.optionExample(arg: Option[Int])
GET /list-example controllers.MyController.listExample(arg: List[Int])
Optional
parameters can be specified or omitted and List
parameters can be specified any number of times:
/option-example # => MyController.optionExample(None)
/option-example?arg=123 # => MyController.optionExample(Some(123))
/list-example # => MyController.listExample(Nil)
/list-example?arg=123 # => MyController.listExample(List(123))
/list-example?arg=12&arg=34 # => MyController.listExample(List(12, 34))
If Play cannot extract values of the correct type for each parameter in a route, it returns a 400 Bad Request response to the client. It doesn’t consider any other routes lower in the file. This is standard behaviour for all types of path and query string parameter.
Custom Parameter Types
Play parses route parameters using instances of two different type classes:
play.api.mvc.PathBindable
to extract path parameters;play.api.mvc.QueryStringBindable
to extract query parameters.
We can implement custom parameter types by creating implicit values these type classes.
2.3.5 Reverse Routing
Reverse routes are objects that we can use to generate URIs. This allows us to create URIs from type-checked program code without having to concatenate Strings
by hand.
Play generates reverse routes for us and places them in a controllers.routes
package that we can access from our Scala code. Returning to our original routes for HelloController
:
GET /hello controllers.HelloController.hello
GET /hello/:name controllers.HelloController.helloTo(name: String)
The route compiler generates a controllers.routes.HelloController
object with reverse routing methods as follows:
package routes
import play.api.mvc.Call
object HelloController {
def hello: Call =
Call("GET", "/hello")
def helloTo(name: String): Call =
Call("GET", "/hello/" + encodeURIComponent(name))
}
We can use reverse routes to reconstruct play.api.mvc.Call
objects containing the information required to address hello
and helloTo
over HTTP:
import play.api.mvc.Call
val methodAndUri: Call =
controllers.routes.HelloController.helloTo("dave")
methodAndUri.method // "GET"
methodAndUrl.url // "/hello/dave"
Play’s HTML form templates, in particular, make use of Call
objects when writing HTML for <form>
tags. We’ll see these in more detail next chapter.
2.3.6 Take Home Points
Routes provide bi-directional mapping between URIs and Action
-producing methods within Controllers
.
We write routes using a Play-specific DSL that compiles to Scala code. Each route comprises an HTTP method, a URI pattern, and a corresponding method call. Patterns can contain path and query parameters that are extracted and used in the method call.
We can type the path and query parameters in routes to simplify the parsing code in our controllers and actions. Play supports many types out of the box, but we can also write code to map our own types.
Play also generates reverse routes that map method calls back to URIs. These are placed in a synthetic routes
package that we can access from our Scala code.
2.3.7 Exercise: Calculator-as-a-Service
The chapter2-calc
directory in the exercises contains an unfinished Play application for performing various mathematical calculations. This is similar to the last exercise, but the emphasis is on defining more complex routes.
Complete this application by filling in the missing actions and routes. Implement the missing actions marked TODO
in app/controllers/CalcController.scala
, and complete conf/routes
to hook up the specified URLs:
CalcController.add
andCalcController.and
are examples ofActions
involving typed parameters;CalcController.concat
is an example involving a rest-parameter;CalcController.sort
is an example involving a parameter with a parameterized type;CalcController.howToAdd
is an example of reverse routing.
Test your code using curl
if you’re using Linux or OS X or a browser if you’re using Windows:
bash$ curl 'http://localhost:9000/add/123/to/234'
357
bash$ curl 'http://localhost:9000/and/true/with/true'
true
bash$ curl 'http://localhost:9000/concat/foo/bar/baz'
foobarbaz
bash$ curl 'http://localhost:9000/sort?num=1&num=3&num=2'
1 2 3
bash$ curl 'http://localhost:9000/howto/add/123/to/234'
GET /add/123/to/234
Answer the following questions when you’re done:
What happens when you add a URL-encodeD forward slash (
%2F
) to the argument toconcat
? Is this the desired behaviour?bash$ curl 'http://localhost:9000/concat/one/thing%2Fthe/other'
How does the URL-decoding behaviour of Play differ for normal parameters and rest-parameters?
Do you need to use the same parameter name in
conf/routes
and in your actions? What happens if they are different?Is it possible to embed a parameter of type
List
orOption
in the path part of the URL? If it is, what do the resulting URLs look like? If it is not, what error message do get?
As with the previous exercise the add
, and
, concat
, and sort
Actions
simply involve manipulating types to build Results
:
def add(a: Int, b: Int) = Action { request =>
Ok((a + b).toString)
}
def and(a: Boolean, b: Boolean) = Action { request =>
Ok((a && b).toString)
}
def concat(args: String) = Action { request =>
Ok(args.split("/").map(decode).mkString)
}
def sort(numbers: List[Int]) = Action { request =>
Ok(numbers.sorted mkString " ")
}
howToAdd
is more interesting. We can avoid hard-coding the URL for add
by using its reverse route:
def howToAdd(a: Int, b: Int) = Action { request =>
val call = routes.CalcController.add(a, b)
Ok(call.method + " " + call.url)
}
The routes
file is straightforward if you follow the examples above:
GET /add/:a/to/:b controllers.CalcController.add(a: Int, b: Int)
GET /and/:a/with/:b controllers.CalcController.and(a: Boolean, b: Boolean)
GET /concat/*args controllers.CalcController.concat(args: String)
GET /sort controllers.CalcController.sort(num: List[Int])
GET /howto/add/:a/to/:b controllers.CalcController.howToAdd(a: Int, b: Int)
The answers to the questions are as follows:
If we pass a
%2F
to the route here, we end up with the same undesirable%2F
in the result.This happens because
args
is a rest-parameter. Play treats rest-parameters differently from regular path and query string parameters.Because regular parameters are always a single path segment, we know there will never be a reserved URL character such as a
/
,?
,&
or=
in the content. Play is able to reliably decode any URL encoded characters for us without fear of ambiguity, and does so automatically before calling ourAction
.Rest-parameters, on the other hand, can contain unencoded
/
characters. Play cannot decode the content without causing ambiguity so it passes the raw string captured from the URL without decoding.To correctly handle URL encoded characters, we have to split the rest parameter on instances of
/
and apply theurlDecode
function to each segment:args.split("/").map(urlDecode)
In example in the question, the controller should remove the
/
characters from the parameter and decode the%2F
, yielding a response ofonething/theother
.Play matches parameters in routes by position rather than by name, so we don’t have to use the same names in our routes and our controllers.
In certain circumstances this behaviour can be useful. In
sort
, for example, we want a singular parameter name in the URL:curl 'http://localhost:9000/sort?num=1&num=3&num=2'
and a plural name in the action:
def sort(numbers: List[Int]) = ???
This can beome confusing when using named arguments on reverse routes. Reverse routes take their parameter names from the
conf/routes
file, not from ourActions
. Calls to the action and the reverse route may therefore look different:// Direct call to the Action: controllers.CalcController.sort(numbers = List(1, 3, 2)) // Call to the reverse route: routes.CalcController.sort(num = List(1, 3, 2))
Play uses two different type classes for encoding and decoding URL parameters:
PathBindable
for path parameters andQueryStringBindable
for query string parameters.Play provides default implementations of
QueryStringBindable
forOptional
andList
parameters, but it doesn’t providePathBindables
.If we attempt to create a path parameter of type
List[...]
:# We've added `:num` to the `sort` route from the solution # to change the required type class from QueryStringBindable to PathBindable: GET /sort/:num controllers.CalcController.sort(num: List[Int])
we get a compile error because of the failure to find a
PathBindable
:[error] /Users/dave/dev/projects/essential-play-code/ ↩ chapter2-calc/conf/routes:4: ↩ No URL path binder found for type List[Int]. ↩ Try to implement an implicit PathBindable for this type.
Now we have seen what we can do with routes, let’s look at the code we can write to handle Request
and Result
objects in our applications. This will arm us with all the knowledge we need to start working with HTML and forms in the next chapter.
2.4 Parsing Requests
So far we have seen how to create Actions
and map them to URIs using routes. In the rest of this chapter we will take a closer look at the code we write in the actions themselves.
The first job of any Action
is to extract data from the HTTP request and turn it into well-typed, validated Scala values. We have already seen how routes allow us to extract information from the URI. In this section we will see the other tools Play provides for the rest of the Request
.
2.4.1 Request Bodies
The most important source of request data comes from the body. Clients can POST
or PUT
data in a huge range of formats, the most common being JSON, XML, and form data. Our first task is to identify the content type and parse the body.
Confession time. Up to this point we’ve been telling a white lie about Request
. It is actually a generic type, Request[A]
. The parameter A
indicates the type of body, which we can retrieve via the body
method:
def index = Action { request =>
val body: ??? = request.body
// ... what type is `body`? ...
}
Play contains an number of body parsers that we can use to parse the request and return a body
of an appropriate Scala type.
So what type does request.body
return in the examples we’ve seen so far? We haven’t chosen a body parser, nor have we indicated the type of body anywhere in our code. Play cannot know the Content-Type
of a request at compile time, so how is this handled? The answer is quite clever—by default our actions handle requests of type Request[AnyContent]
.
play.api.mvc.AnyContent
is a sealed trait with subtypes for several common content types and a set of convenience methods that return Some
if the request matches the relevant type and None
if it does not:
Method of AnyContent |
Request content type | Return type |
---|---|---|
asText |
text/plain |
Option[String] |
asFormUrlEncoded |
application/x-www-form-urlencoded |
Option[Map[String, Seq[String]]] |
asMultipartFormData |
multipart/form-data |
Option[MultipartFormData] |
asJson |
application/json |
Option[JsValue] |
asXml |
application/xml |
Option[NodeSeq] |
asRaw |
any other content type | Option[RawBuffer] |
We can use any of these methods to read the body as a specific type and process it in our Action
. The Optional
return types force us to deal with the possibility that the client sent us the wrong content type:
def exampleAction = Action { request =>
request.body.asXml match {
case Some(xml) => // Handle XML
case None => BadRequest("That's no XML!")
}
}
We can alternatively implement handlers for multiple content types and chain them together with calls to map
, flatMap
, orElse
, and getOrElse
:
def exampleAction2 = Action { request =>
(request.body.asText map handleText) orElse
(request.body.asJson map handleJson) orElse
(request.body.asXml map handleXml) getOrElse
BadRequest("You've got me stumped!")
}
def handleText(data: String): Result = ???
def handleJson(data: JsValue): Result = ???
def handleXml(data: NodeSeq): Result = ???
Custom Body Parsers
AnyContent
is a convenient way to parse common types of request bodies. However, it suffers from two drawbacks:
- it only caters for a fixed set of common data types;
- with the exception of multipart form data, requests must be read entirely into memory before parsing.
If we are certain about the data type we want in a particular Action
, we can specify a body parser to restrict it to a specific type. Play returns a 400 Bad Request response to the client if it cannot parse the request as the relevant type:
import play.api.mvc.BodyParsers.parse
def index = Action(parse.json) { request =>
val body: JsValue = request.body
// ... no need to call `body.asJson` ...
}
If the situation demands, we can even implement our own custom body parsers to parse exotic formats:
object myDataParser new BodyParser[MyData] {
// ...
}
def action = Action(myDataParser) { request =>
val body: MyData = request.body
// ...
}
See Play’s documentation on body parsers for more information.
2.4.2 Headers and Cookies
Request
contains two methods for inspecting HTTP headers:
the
headers
method returns aplay.api.mvc.Headers
object for inspecting general headers;and
cookies
method returns aplay.api.mvc.Cookies
object for inspecting theCookies
header.
These take care of common error scenarios: missing headers, upper- and lower-case names, and so on. Values are treated as Strings
throughout. Play doesn’t attempt to parse headers as dedicated Scala types. Here is a synopsis:
object RequestDemo extends Controller {
def headers = Action { request =>
val headers: Headers = request.headers
val ucType: Option[String] = headers.get("Content-Type")
val lcType: Option[String] = headers.get("content-type")
val cookies: Cookies = request.cookies
val cookie: Option[Cookie] = cookies.get("DemoCookie")
val value: Option[String] = cookie.map(_.value)
Ok(Seq(
s"Headers: $headers",
s"Content-Type: $ucType",
s"content-type: $lcType",
s"Cookies: $cookies",
s"Cookie value: $value"
) mkString "\n")
}
}
Case sensitivity
The Headers.get
method is case insensitive. We can grab the Content-Type
using headers.get("Content-Type")
or headers.get("content-type")
. Cookie names, on the other hand, are case sensitive. Make sure you define your cookie names as constants to avoid case errors!
2.4.3 Methods and URIs
Routes are the recommended way of extracting information from a method or URI. However, the Request
object also provides methods that are of occasional use:
// The HTTP method ("GET", "POST", etc):
val method: String = request.method
// The URI, including path and query string:
val uri: String = request.uri
// The path of the URI, without the query string:
val path: String = request.path
// The query string, split into name/value pairs:
val query: Map[String, Seq[String]] = request.queryString
2.4.4 Take Home Points
Incoming web requests are represented by objects of type Request[A]
. The type parameter A
indicates the type of the request body.
By default, Play represents bodies using a type called AnyContent
that allows us to parse bodies a set of common data types.
Reading the body may succeed or fail depending on whether the content type matches the type we expect. The various body.asX
methods such as body.asJson
return Options
to force us to deal with the possibility of failure.
If we’re only concerned with one type of data, we can choose or write custom body parsers to process the body as a specific type.
Request
also contains methods to access HTTP headers, cookies, and various parts of the HTTP method and URI.
2.5 Constructing Results
In the previous section we saw how to extract well-typed Scala values from an incoming request. This should always be the first step in any Action
. If we tame incoming data using the type system, we remove a lot of complexity and possibility of error from our business logic.
Once we have finished processing the request, the final step of any Action
is to convert the result into a Result
. In this section we will see how to create Results
, populate them with content, and add headers and cookies.
2.5.1 Setting The Status Code
Play provides a convenient set of factory objects for creating Results
. These are defined in the play.api.mvc.Results
trait and inherited by play.api.mvc.Controller
Constructor | HTTP status code |
---|---|
Ok |
200 Ok |
NotFound |
404 Not Found |
InternalServerError |
500 Internal Server Error |
Unauthorized |
401 Unauthorized |
Status(number) |
number (an Int )—anything we want |
Each factory has an apply
method that creates a Result
with a different HTTP status code. Ok.apply
creates 200 responses, NotFound.apply
creates 404 responses, and so on. The Status
object is different: it allows us to specify the status as an Int
parameter. The end result in each case is a Result
that we can return from our Action
:
val result1: Result = Ok("Success!")
val result2: Result = NotFound("Is it behind the fridge?")
val result3: Result = Status(401)("Access denied, Dave.")
2.5.2 Adding Content
Play adds Content-Type
headers to our Results
based on the type of data we provide. In the examples above we provide String
data creating three results of Content-Type: text/plain
.
We can create Results
using values of other Scala types, provided Play understands how to serialize them. Play even sets the Content-Type
header for us as a convenience. Here are some examples:
Using this Scala type… | Yields this result type… |
---|---|
String |
text/plain |
play.twirl.api.Html (see Chapter 2) |
text/html |
play.api.libs.json.JsValue (see Chapter 3) |
application/json |
scala.xml.NodeSeq |
application/xml |
Array[Byte] |
application/octet-stream |
The process of creating a Result
is type-safe. Play determines the method of serialization based on the type we give it. If it understands what to do with our data, we get a working Result
. If it doesn’t understand the type we give it, we get a compilation error. As a consequence the final steps in an Action
tend to be as follows:
- Convert the result of action to a type that Play can serialize:
- HTML using a Twirl template, or;
- a
JsValue
to return the data as JSON, or; - a Scala
NodeSeq
to return the data as XML, or; - a
String
orArray[Byte]
.
Use the serializable data to create a
Result
.Tweak HTTP headers and so on.
Return the
Result
.
Custom Result Types
Play understands a limited set of result content types out-of-the-box. We can add support for our own types by defining instances of the play.api.http.Writeable
type class. See the Scaladocs for more information:
// We have a custom library for manipulating iCal calendar files:
case class ICal(/* ... */)
// We implement an implicit `Writeable[ICal]`:
implicit object ICalWriteable extends Writeable[ICal] {
// ...
}
// Now our actions can serialize `ICal` results:
def action = Action { request =>
val myCal: ICal = ICal(/* ... */)
Ok(myCal) // Play uses `ICalWriteable` to serialize `myCal`
}
The intention of Writeable
is to support general data formats. We wouldn’t create a Writeable
to serialize a specific class from our business model, for example, but we might write one to support a format such as XLS, Markdown, or iCal.
2.5.3 Tweaking the Result
Once we have created a Result
, we have access to a variety of methods to alter its contents. The API documentation for play.api.mvc.Result
shows this:
we can change the
Content-Type
header (without changing the content) using theas
method;we can add and/or alter HTTP headers using
withHeaders
;we can add and/or alter cookies using
withCookies
.
These methods can be chained, allowing us to create the Result
, tweak it, and return it in a single expression:
def ohai = Action { request =>
Ok("OHAI").
as("text/lolspeak").
withHeaders(
"Cache-Control" -> "no-cache, no-store, must-revalidate",
"Pragma" -> "no-cache",
"Expires" -> "0",
// etc...
).
withCookies(
Cookie(name = "DemoCookie", value = "DemoCookieValue"),
Cookie(name = "OtherCookie", value = "OtherCookieValue"),
// etc...
)
}
2.5.4 Take Home Points
The final step of an Actions
is to create and return a play.api.mvc.Result
.
We create Results
using factory objects provided by play.api.mvc.Controller
. Each factory creates Results
with a specific HTTP status code.
We can Results
with a variety of data types. Play provides built-in support for String
, JsValue
, NodeSeq
, and Html
. We can add our own data types by writing instances of the play.api.http.Writeable
type class.
Once we have created a Result
, we can tweak headers and cookies before returning it.
2.5.5 Exercise: Comma Separated Values
The chapter2-csv
directory in the exercises contains an unfinished Play application for converting various data formats to CSV. Complete the application by filling in the missing action in app/controllers/CsvController.scala
.
The action is more complicated than in previous exercises. It must accept data POSTed to it by the client and convert it to CSV using the relevant helper method from CsvHelpers
.
We have included several files to help you test the code: test.formdata
and test.tsv
are text files containing test data, and the various run-
shell scripts make calls to curl
with the correct command line parameters.
Your code should behave as follows:
Form data (content type
application/x-url-form-url-encoded
) should be converted to CSV in columnar orientation and returned withtext/csv
content type:bash$ ./run-form-data-test.sh # This script submits `test.formdata` with content type # `application/x-url-form-url-encoded`. # # Curl prints HTTP data from request and response including... < HTTP/1.1 200 OK < Content-Type: text/csv A,B,C 100,200,300 110,220,330 111,222,
Post data of type
text/plain
ortext/tsv
should be treated as tab separated values. The tabs should be replaced with commas and the result returned with content typetext/csv
:bash$ ./run-tsv-test.sh # This script submits `test.tsv` with content type `text/tsv`. # # Curl prints HTTP data from request and response including... < HTTP/1.1 200 OK < Content-Type: text/csv A,B,C 1,2,3 bash$ ./run-plain-text-test.sh # This script submits `test.tsv` with content type `text/plain`. # # Curl prints HTTP data from request and response including... < HTTP/1.1 200 OK < Content-Type: text/csv A,B,C 1,2,3
Any other type of post data should yield a 400 response with a sensible error message:
bash$ ./run-bad-request-test.sh # This script submits `test.tsv` with content type `foo/bar`. # # Curl prints HTTP data from request and response including... < HTTP/1.1 400 Bad Request < Content-Type: text/plain Expected application/x-www-form-url-encoded, text/tsv, or text/plain
Answer the following question when you are done:
Are your handlers for text/plain
and text/tsv
interchangeable? What happens when you remove one of the handlers and submit a file of the corresponding type? Does play compensate by running the other handler?
There are several parts to this solution: create handler functions for the various content types, ensure that the results have the correct status code and content type, and chain the handlers together to implement our Action
. We will address each part in turn.
First let’s create handlers for each content type. We have three types to consider: application/x-www-form-url-encoded
, text/plain
, and text/tsv
. Play has built-in body parsers for the first two. The methods in CsvHelpers
do most of the rest of the work:
def formDataResult(request: Request[AnyContent]): Option[Result] =
request.body.asFormUrlEncoded map formDataToCsv map csvResult
def plainTextResult(request: Request[AnyContent]): Option[Result] =
request.body.asText map tsvToCsv map csvResult
The text/tsv
conten type is trickier, however. We can’t use request.body.asText
—it returns None
because Play assumes the request content is binary. We have to use request.body.asRaw
to get a RawBuffer
, extract the Array[Byte]
within, and create a String
:
def rawBufferResult(request: Request[AnyContent]): Option[Result] =
request.contentType flatMap {
case "text/tsv" => request.body.asRaw map rawBufferToCsv map csvResult
case _ => None
}
Note the pass-through clause for content types other than "text/tsv"
. We have no control over the types of data the client may send our way, so we always have to provide a mechanism for dealing with the unexpected.
Also note that the conversion method in rawBufferToCsv
assumes unicode character encoding—make sure you check for other encodings if you write code like this in your production applications!
Each of the handler functions uses a common csvResult
method to convert the String
CSV data to a Result
with the correct status code and content type:
def csvResult(csvData: String): Result =
Ok(csvData).withHeaders("Content-Type" -> "text/csv")
We also need a handler for the case where we don’t know how to parse the request. In this case we return a BadRequest
result with a content type of "text/plain"
:
val failResult: Result =
BadRequest("Expected application/x-www-form-url-encoded, " +
"text/tsv, or text/plain")
Finally, we need to put these pieces together. Because each of our handlers returns an Option[Result]
, we can use the standard methods to chain them together:
def toCsv = Action { request =>
formDataResult(request) orElse
plainTextResult(request) orElse
rawBufferResult(request) getOrElse
failResult
}
The answer to the question is as follows. Although we are using "text/plain"
and "text/tsv"
interchangeably, Play treats the two content types differently:
"text/plain"
is parsed as plain text.request.body.asText
returnsSome
andrequest.body.asRaw
returnsNone
;"text/tsv"
is parsed as binary data.request.body.asText
returnsNone
andrequest.body.asRaw
returnsSome
.
In lieu of writing a custom BodyParser
for "text/tsv"
requests, we have to work around Play’s (understandable) misinterpretation of the format. We read the data as a RawBuffer
and convert it to a String
. The example code for doing this is error-prone because it doesn’t deal with character encodings correctly. We would have to address this ourselves in a production application. However, the example demonstrates the principle of dispatching on content type and parsing the request appropriately.
2.6 Handling Failure
At this point we have covered all the basics for this chapter. We have learned how to set up routes, write Actions
, handle Requests
, and create Results
.
In this final section of the chapter we will take a first look at a theme that runs throughout the course—failures and error handling. In future chapters we will look at how to generate good error messages for our users. In this section we will see what error messages Play provides for us.
2.6.1 Compilation Errors
Play reports compilation errors in two places: on the SBT console, and via 500 error pages. If you’ve been following the exercises so far, you will have seen this already. When we run a development web server using sbt run
and make a mistake in our code, Play responds with an error page:
While this behaviour is useful, we should be aware of two drawbacks:
The web page only reports the first error from the SBT console. A single typo in Scala code can create several compiler errors, so we often have to look at the complete output from SBT to trace down a bug.
When we use
sbt run
, Play only recompiles our code when we refresh the web page. This sometimes slows down development because we have to constantly switch back and forth between editor and browser.
We can write and debug code faster if we use SBT’s continuous compilation mode instead of sbt run
. To start continuous compilation, type ~compile
on the SBT console:
[hello-world] $ ~compile
[success] Total time: 0 s, completed 11-Oct-2014 11:46:28
1. Waiting for source changes... (press enter to interrupt)
In continuous compilation mode, SBT recompiles our code every time we change a file. However, we have to go back to sbt run
to see the changes in a browser.
2.6.2 Runtime Errors
If our code compiles but fails at runtime, we get a similar error page that points to the source of the exception. The exception is reported on the SBT console as well as on the page:
2.6.3 Routing Errors
Play generates a 404 page if it can’t find an appropriate route for an incoming request. This error doesn’t appear on the console: