Controllers
Business logic is managed in the domain model layer. As a client (typically a web browser) cannot directly invoke this code, the functionality of a domain object is exposed as resources represented by URIs.
A client uses the uniform API provided by the HTTP protocol to manipulate these resources, and by implication the underlying business logic. However, this mapping of resources to domain objects is not a bijection: the granularity can be expressed at different levels, some resources may be virtual, for some resources aliases may be defined…
This is precisely the role played by the Controller layer: providing a glue between the domain model objects and transport layer events. As the Model layer, controllers are written in pure Java, making it easy to access or modify Model objects. Like the HTTP interface, Controllers are procedural and Request/Response oriented.
The Controller layer reduces the impedance mismatch between HTTP and the Domain Model.
Note
There are different architectural models with different strategies. Some protocols give you direct access to the domain model objects. This is typically what EJB or Corba protocols do. In these cases, the architectural style used is RPC (Remote Procedure Call). These communication styles are hardly compatible with web architecture.
Some technologies like SOAP try to give access to the model object domain through the Web. However, SOAP is just another RPC-style protocol, in this case using HTTP as a transport protocol. It is not an application protocol.
The web’s principles are not fundamentally object-oriented. So a layer is needed to adapt HTTP to your favorite language.
Controller overview
A Controller is a Java class, hosted by the controllers
package, and subclassing play.mvc.Controller
.
This is a Controller:
package controllers;
import models.Client;
import play.mvc.Controller;
public class Clients extends Controller {
public static void show(Long id) {
Client client = Client.findById(id);
render(client);
}
public static void delete(Long id) {
Client client = Client.findById(id);
client.delete();
}
}
Each public, static method in a Controller is called an action. The signature for an action method is always:
public static void action_name(params...);
You can define parameters in the action method signature. These parameters will be automatically resolved by the framework from the corresponding HTTP parameters.
Usually, an action method doesn’t include a return statement. The method exit is done by the invocation of a result method. In this example, render(…)
is a result method that executes and displays a template.
Retrieving HTTP parameters
An HTTP request contains data. This data can be extracted from:
- The URI path: in
/clients/1541
, 1541 is the dynamic part of the URI Pattern. - The Query String:
/clients?id=1541
. - The request body: if the request was sent from an HTML form, the request body contains the form data encoded as
x-www-urlform-encoded
.
In all cases, Play extracts this data and builds a Map<String, String[]>
which contains all the HTTP parameters. The key is the parameter name. The parameter name is derived from:
- The name of the dynamic part of the URI (as specified in the route)
- The name portion of a name-value pair taken from the Query String
- The contents of a x-www-urlform-encoded body.
Using the params map
The params
object is available to any Controller class (it is defined in the play.mvc.Controller
super class). This object contains all the HTTP parameters found for the current request.
For example:
public static void show() {
String id = params.get("id");
String[] names = params.getAll("names");
}
You can also ask Play to do the type conversion for you:
public static void show() {
Long id = params.get("id", Long.class);
}
But wait, there are better ways to do this :)
From the action method signature
You can retrieve HTTP parameters directly from the action method signature. The Java parameter’s name must be the same as the HTTP parameter’s.
For example, in this request:
/clients?id=1451
An action method can retrieve the id
parameter value by declaring an id
parameter in its signature:
public static void show(String id) {
System.out.println(id);
}
You can use other Java types than String. In this case the framework will try to cast the parameter value to the correct Java type:
public static void show(Long id) {
System.out.println(id);
}
If the parameter is multivalued, you can declare an Array argument:
public static void show(Long[] id) {
for(String anId : id) {
System.out.println(anid);
}
}
or even a collection type:
public static void show(List<Long> id) {
for(String anId : id) {
System.out.println(anid);
}
}
Exceptions
If the HTTP parameter corresponding to the action method argument is not found, the corresponding method argument is set to its default value (typically null for objects and 0 for primitive numeric types). If a value is found but can’t be properly cast to the required Java type, an error is added to the validation error collection and the default value is used.
Advanced HTTP to Java binding
Simple types
All the native and common Java types are automatically bound:
int
, long
, boolean
, char
, byte
, float
, double
, Integer
, Long
, Boolean
, Char
, String
, Byte
, Float
, Double
.
Note that if the parameter is missing in the HTTP Request, or if automatic conversion fails, Object types will be set to null and native types will be set to their default values.
Date
A date object can be automatically bound if the date’s string representation matches one of the following patterns:
- yyyy-MM-dd’T’hh:mm:ss’Z' // ISO8601 + timezone
- yyyy-MM-dd’T’hh:mm:ss" // ISO8601
- yyyy-MM-dd
- yyyyMMdd’T’hhmmss
- yyyyMMddhhmmss
- dd'/‘MM’/'yyyy
- dd-MM-yyyy
- ddMMyyyy
- MMddyy
- MM-dd-yy
- MM'/‘dd’/'yy
Using the @As
annotation, you can specify the date format.
For example:
archives?from=21/12/1980
public static void articlesSince(@As("dd/MM/yyyy") Date from) {
List<Article> articles = Article.findBy("date >= ?", from);
render(articles);
}
You can of course refine the date format according to the language. For example:
public static void articlesSince(@As(lang={"fr,de","*"},
value={"dd-MM-yyyy","MM-dd-yyyy"}) Date from) {
List<Article> articles = Article.findBy("date >= ?", from);
render(articles);
}
In this example, we specified that for the French and German languages the date format is dd-MM-yyyy
and for all the other languages it’s MM-dd-yyyy
. Please note that the lang value can be comma separated. The important thing is that the number of parameters for lang matches the number of parameters for value.
If no @As
annotation is specified, then Play! uses the default date format according to your locale. The date.format configuration specifies the default date format to use.
Calendar
The calendar binding works exactly as with the date, except that Play is choosing the Calendar object according to your locale. The @Bind
annotation can also be used.
File
File upload is easy with Play. Use a multipart/form-data
encoded request to post files to the server, and then use the java.io.File
type to retrieve the file object:
public static void create(String comment, File attachment) {
String s3Key = S3.post(attachment);
Document doc = new Document(comment, s3Key);
doc.save();
show(doc.id);
}
The created file has the same name as the original file. It’s stored in a temporary directory and deleted at the end of the request. So you have to copy it in a safe directory or it will be lost.
The uploaded file’s MIME type should normally be specified by the HTTP request’s Content-type
header. However, when uploading files from a web browser, this might not happen for uncommon types. In this case, you can map the file name’s extension to a MIME type, using the play.libs.MimeTypes
class.
String mimeType = MimeTypes.getContentType(attachment.getName());
The play.libs.MimeTypes
class looks up the MIME type for the given file name’s extension in the file $PLAY_HOME/framework/src/play/libs/mime-types.properties
You can also add your own types using the Custom MIME types configuration.
Arrays or collections of supported types
All supported types can be retrieved as an Array or a collection of objects:
public static void show(Long[] id) {
…
}
or:
public static void show(List<Long> id) {
…
}
or:
public static void show(Set<Long> id) {
…
}
Play also handles the special case of binding a Map<String, String> like this:
public static void show(Map<String, String> client) {
…
}
A query string like the following:
?client.name=John&client.phone=111-1111&client.phone=222-2222
would bind the client variable to a map with two elements. The first element with key name
and value John
, and the second with key phone
and value 111-1111, 222-2222
.
POJO object binding
Play also automatically binds any of your model classes using the same simple naming convention rules.
public static void create(Client client ) {
client.save();
show(client);
}
A query string to create a client using this action would look like:
?client.name=Zenexity&[email protected]
Play creates a Client instance and resolves HTTP parameter names to properties on the Client object. Unresolved parameters are safely ignored. Type mismatches are also safely ignored.
Parameter binding is done recursively, which means you can address complete object graphs:
?client.name=Zenexity
&client.address.street=64+rue+taitbout
&client.address.zip=75009
&client.address.country=France
In order to update a list of model objects, use array notation and reference the object’s ID. For example imagine the Client model has a list of Customer models declared as List Customer customers
. To update the list of Customers you would provide a query string like the following:
?client.customers[0].id=123
&client.customers[1].id=456
&client.customers[2].id=789
JPA object binding
You can automatically bind a JPA object using the HTTP to Java binding.
You can provide the user.id
field yourself in the HTTP parameters. When Play finds the id
field, it loads the matching instance from the database before editing it. The other parameters provided by the HTTP request are then applied. So you can save it directly.
public static void save(User user) {
user.save(); // ok with 1.0.1
}
You can use JPA binding to modify complete object graphs in the same way as POJO mapping works, but you have to supply the ID for each sub object you intend to modify:
user.id = 1
&user.name=morten
&user.address.id=34
&user.address.street=MyStreet
Custom binding
The binding system now supports more customization.
@play.data.binding.As
The first thing is the new @play.data.binding.As
annotation that makes it possible to contextually configure a binding. You can use it for example to specify the date format that must be used by the DateBinder
:
public static void update(@As("dd/MM/yyyy") Date updatedAt) {
…
}
The @As
annotation also has internationalisation support, which means that you can provide a specific annotation for each locale:
public static void update(
@As(
lang={"fr,de","en","*"},
value={"dd/MM/yyyy","dd-MM-yyyy","MM-dd-yy"}
)
Date updatedAt
) {
…
}
The @As
annotation can work with all binders that support it, including your own binder. For example, using the ListBinder
:
public static void update(@As(",") List<String> items) {
…
}
This binds a simple comma separated String
as a List
.
@play.data.binding.NoBinding
The new @play.data.binding.NoBinding
annotation allows yous to mark non-bindable fields, resolving potential security issues. For example:
public class User extends Model {
@NoBinding("profile") public boolean isAdmin;
@As("dd, MM yyyy") Date birthDate;
public String name;
}
public static void editProfile(@As("profile") User user) {
…
}
In this case, the isAdmin
field will never be bound from the editProfile
action, even if an malicious user includes a user.isAdmin=true
field in a fake form post.
play.data.binding.TypeBinder / TypeUnbinder
The @As annotation also allows you to define a completely custom binder and/or unbinder. A custom binder / unbinder is subclass of TypeBinder
/ TypeUnbinder
that you define in your project. For example:
public class MyCustomStringBinder implements TypeBinder<String>, TypeUnbinder<String> {
public Object bind(String name, Annotation[] anns, String value,
Class clazz) {
return "!!" + value + "!!";
}
@Override
public void unBind(Map<String, Object> result, Object src, Class<?> srcClazz,
String name, Annotation[] annotations) throws Exception {
String value = (String) src;
if(value != null){
value = value.replaceAll("^!!", "").replaceAll("!!$", "");
}
result.put(name, value);
}
}
You can use it in any action, like:
public static void anyAction(@As(binder=MyCustomStringBinder.class)
String name) {
…
}
@play.data.binding.Global
Alternatively, you can define a global custom binder that will apply for the corresponding type. For example, you define a binder for the java.awt.Point
class like this:
@Global
public class PointBinder implements TypeBinder<Point> {
public Object bind(String name, Annotation[] anns, String value,
Class class) {
String[] values = value.split(",");
return new Point(
Integer.parseInt(values[0]),
Integer.parseInt(values[1])
);
}
}
As you see a global binder is a classical binder annotated with @play.data.binding.Global. An external module can contribute binders to a project, which makes it possible to define reusable binder extensions.
Result types
An action method has to generate an HTTP response. The easiest way to do this is to emit a Result object. When a Result object is emitted, the normal execution flow is interrupted and the method returns.
For example:
public static void show(Long id) {
Client client = Client.findById(id);
render(client);
System.out.println("This message will never be displayed !");
}
The render(…)
method emits a Result object and stops further method execution.
Return some textual content
The renderText(…)
method emits a simple Result event which writes some text directly to the underlying HTTP Response.
Example:
public static void countUnreadMessages() {
Integer unreadMessages = MessagesBox.countUnreadMessages();
renderText(unreadMessages);
}
You can format the text message using the Java standard formatting syntax:
public static void countUnreadMessages() {
Integer unreadMessages = MessagesBox.countUnreadMessages();
renderText("There are %s unread messages", unreadMessages);
}
Return a JSON String
Play comes equiped with a method to simply return a JSON string, by making use of the renderJSON(…)
methods. These methods return a JSON string and set the response content type to application/json
.
You can specify your own JSON string, or you can pass in an Object
, which will be serialised by the GsonBuilder
.
Example:
public static void countUnreadMessages() {
Integer unreadMessages = MessagesBox.countUnreadMessages();
renderJSON("{\"messages\": " + unreadMessages +"}");
}
Alternatively, if you had a more complex object structure, you may wish to build the JSON using the GsonBuilder
.
public static void getUnreadMessages() {
List<Message> unreadMessages = MessagesBox.unreadMessages();
renderJSON(unreadMessages);
}
If you need more control over the JSON builder when passing an Object
to the renderJSON(…)
method, you can also pass in GSON serialisers and Type
objects to customise the output.
Return an XML String
As with the JSON methods, there are several methods for rendering XML directly from the controller. The renderXml(…)
methods return XML strings with the content type set to text/xml
.
Here you can specify your own XML string, pass a org.w3c.dom.Document
object, or pass a POJO which will be serialised by the XStream serialiser.
Example:
public static void countUnreadMessages() {
Integer unreadMessages = MessagesBox.countUnreadMessages();
renderXml("<unreadmessages>"+unreadMessages+"</unreadmessages>");
}
Alternatively, you can use a org.w3c.dom.Document
object.
public static void getUnreadMessages() {
Document unreadMessages = MessagesBox.unreadMessagesXML();
renderXml(unreadMessages);
}
Return binary content
To serve binary data, such as a file stored on the server, use the renderBinary
method. For example, if you have a User
model with a play.db.jpa.Blob photo
property, add a controller method to load the model object and render the image with the stored MIME type:
public static void userPhoto(long id) {
final User user = User.findById(id);
response.setContentTypeIfNotSet(user.photo.type());
java.io.InputStream binaryData = user.photo.get();
renderBinary(binaryData);
}
Download a file as an attachment
You can set an HTTP header to instruct the web browser to treat a binary response as an ‘attachment’, which generally results in the web browser downloading the file to the user’s computer. To do this, pass a file name as a parameter to the renderBinary
method, which causes Play to set the Content-Disposition
response header, providing a file name. For example, supposing the User
model from the previous example as a photoFileName
property:
renderBinary(binaryData, user.photoFileName);
Execute a template
If the generated content is complex, you should use a template to generate the response content.
public class Clients extends Controller {
public static void index() {
render();
}
}
A template name is automatically deduced from the Play conventions. The default template path is resolved using the Controller and action names.
In this example the invoked template is:
app/views/Clients/index.html
Add data to the template scope
Often the template needs data. You can add these data to the template scope using the renderArgs
object:
public class Clients extends Controller {
public static void show(Long id) {
Client client = Client.findById(id);
renderArgs.put("client", client);
render();
}
}
During template execution, the client
variable will be defined.
For example:
<h1>Client ${client.name}</h1>
A simpler way to add data to the template scope
You can pass data directly to the template using render(…)
method arguments:
public static void show(Long id) {
Client client = Client.findById(id);
render(client);
}
In this case, the variables accessible by the template have the same name as the local Java variables.
You can pass more than one variable:
public static void show(Long id) {
Client client = Client.findById(id);
render(id, client);
}
Important!
You can only pass local variables in this way.
Specify another template
If you don’t want to use the default template, you can specify your own template file using the renderTemplate(…)
method, by passing the template name as the first parameter:
Example:
public static void show(Long id) {
Client client = Client.findById(id);
renderTemplate("Clients/showClient.html", id, client);
}
Redirect to another URL
The redirect(…)
method emits a Redirect event that in turn generates an HTTP Redirect response.
public static void index() {
redirect("http://www.zenexity.fr");
}
Action chaining
There is no equivalent to the Servlet API forward
. An HTTP request can only invoke one action. If you need to invoke another action, you have to redirect the browser to the URL able to invoke that action. In this way, the browser URL is always consistent with the executed action, and the Back/Forward/Refresh management is much easier.
You can send a Redirect response to any action, simply by invoking the action method in a Java way. The Java call is intercepted by the framework and the correct HTTP Redirect is generated.
For example:
public class Clients extends Controller {
public static void show(Long id) {
Client client = Client.findById(id);
render(client);
}
public static void create(String name) {
Client client = new Client(name);
client.save();
show(client.id);
}
}
With these routes:
GET /clients/{id} Clients.show
POST /clients Clients.create
- The browser sends a POST to the
/clients
URL. - The Router invokes the
Clients
controller’screate
action. - The action method calls the
show
action method directly. - The Java call is intercepted and the Router reverse route generation creates the URL needed to invoke Clients.show with an id parameter.
- The HTTP Response is
302 Location:/clients/3132
. - The browser then issues
GET /clients/3132
. - …
Customise web encoding
Play emphasises the use of UTF-8, but there are situations where some responses, or the whole application, must use a different encoding.
Custom encoding for current response
To change encoding for current response, you can do it like this in your controller:
response.encoding = "ISO-8859-1";
When posting a form using an encoding other than the server default, you should include the encoding/charset twice in the form, both in the accept-charset
attribute and in a special hidden form field named _charset_
. The accept-charset
attribute tells the browser which encoding to use when posting the from, and the form-field _charset_
tells Play what that encoding is:
<form action="@{application.index}" method="POST" accept-charset="ISO-8859-1">
<input type="hidden" name="_charset_" value="ISO-8859-1">
</form>
Custom encoding for the entire application
Configure application.web_encoding to specify which encoding Play uses when communicating with the browser.
Interceptions
A controller can define interception methods. Interceptors are invoked for all actions of the controller class and its descendants. It’s a useful way to define treatments that are common to all actions: verifying that a user is authenticated, loading request-scope information…
These methods have to be static
but not public
. You have to annotate these methods with a valid interception marker.
@Before
Methods annotated with the @Before
annotation are executed before each action call for this Controller.
So, to create a security check:
public class Admin extends Controller {
@Before
static void checkAuthentification() {
if(session.get("user") == null) login();
}
public static void index() {
List<User> users = User.findAll();
render(users);
}
…
}
If you don’t want the @Before method to intercept all action calls, you can specify a list of actions to exclude:
public class Admin extends Controller {
@Before(unless="login")
static void checkAuthentification() {
if(session.get("user") == null) login();
}
public static void index() {
List<User> users = User.findAll();
render(users);
}
…
}
Or if you want the @Before method to intercept a list of action calls, you can specify a only param :
public class Admin extends Controller {
@Before(only={"login","logout"})
static void doSomething() {
…
}
…
}
The unless
and only
parameters are available for the @After
, @Before
and @Finally
annotations.
@After
Methods annotated with the @After
annotation are executed after each action call for this Controller.
public class Admin extends Controller {
@After
static void log() {
Logger.info("Action executed ...");
}
public static void index() {
List<User> users = User.findAll();
render(users);
}
…
}
@Catch
Methods annotated with @Catch
are called if another action method throws the specified exception. The thrown exception is passed as a parameter to the @Catch method.
public class Admin extends Controller {
@Catch(IllegalStateException.class)
public static void logIllegalState(Throwable throwable) {
Logger.error("Illegal state %s…", throwable);
}
public static void index() {
List<User> users = User.findAll();
if (users.size() == 0) {
throw new IllegalStateException("Invalid database - 0 users");
}
render(users);
}
}
As with normal Java exception handling, you can catch a super-class to catch more exception types. If you have more than one catch method, you can specify their priority so that they are executed in order of priority (priority 1 is executed first).
public class Admin extends Controller {
@Catch(value = Throwable.class, priority = 1)
public static void logThrowable(Throwable throwable) {
// Custom error logging…
Logger.error("EXCEPTION %s", throwable);
}
@Catch(value = IllegalStateException.class, priority = 2)
public static void logIllegalState(Throwable throwable) {
Logger.error("Illegal state %s…", throwable);
}
public static void index() {
List<User> users = User.findAll();
if(users.size() == 0) {
throw new IllegalStateException("Invalid database - 0 users");
}
render(users);
}
}
@Finally
Methods annotated with the @Finally
annotation are always executed after each action call to this Controller.
@Finally-methods are called both after successful action calls and if an error occurred.
public class Admin extends Controller {
@Finally
static void log() {
Logger.info("Response contains : " + response.out);
}
public static void index() {
List<User> users = User.findAll();
render(users);
}
…
}
If the method annotated with @Finally takes one argument of type Throwable, The Exception will be passed in if available:
public class Admin extends Controller {
@Finally
static void log(Throwable e) {
if( e == null ){
Logger.info("action call was successful");
} else{
Logger.info("action call failed", e);
}
}
public static void index() {
List<User> users = User.findAll();
render(users);
}
…
}
Controller hierarchy
If a Controller class is a subclass of another Controller class, interceptions are applied to the full Controller hierarchy.
Adding more interceptors using the @With annotation
Because Java does not allow multiple inheritance, it can be very limiting to rely on the Controller hierarchy to apply interceptors. But you can define some interceptors in a totally different class, and link them with any controller using the @With
annotation.
Example:
public class Secure extends Controller {
@Before
static void checkAuthenticated() {
if(!session.containsKey("user")) {
unAuthorized();
}
}
}
And on another Controller:
@With(Secure.class)
public class Admin extends Controller {
…
}
Session and Flash scopes
If you have to keep data across multiple HTTP Requests, you can save them in the Session or the Flash scope. Data stored in the Session are available during the whole user session, and data stored in the flash scope are available to the next request only.
It’s important to understand that Session and Flash data are not stored in the server but are added to each subsequent HTTP Request, using the Cookie mechanism. So the data size is very limited (up to 4 KB) and you can only store String values.
Of course, cookies are signed with a secret key so the client can’t modify the cookie data (or it will be invalidated). The Play session is not aimed to be used as a cache. If you need to cache some data related to a specific session, you can use the Play built-in cache mechanism and use the session.getId() key to keep them related to a specific user session.
Example:
public static void index() {
List messages = Cache.get(session.getId() + "-messages", List.class);
if(messages == null) {
// Cache miss
messages = Message.findByUser(session.get("user"));
Cache.set(session.getId() + "-messages", messages, "30mn");
}
render(messages);
}
The session expires when you close your web browser, unless you configure application.session.maxAge.
The cache has different semantics to the classic Servlet HTTP session object. You can’t assume that these objects will be always in the cache. So it forces you to handle the cache miss cases, and keeps your application fully stateless.
Continuing the discussion
The next important layer of the MVC model is the View layer, for which Play provides an efficient templating system with its Template engine.