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 domain object is exposed as resources represented by a URI.
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 virtuals, 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 the 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 principles of the Web are not fundamentally object oriented. So a layer is needed to adapt HTTP to your favorite language.
A 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 any 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 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 name of the Java parameter must be the same as the HTTP parameter.
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(id);
}
}
or even a collection type:
public static void show(List<Long> id) {
for(String anId : id) {
System.out.println(id);
}
}
Exceptions
If the HTTP parameter corresponding to the action method argument is not found, the corresponding method argument is set to it 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 type are automatically bound:
int, long, boolean, char, byte, float, double, Integer, Long, Boolean, Char, String, Float, Double.
Note that if the parameter is missing in the HTTP Request, or that automatic conversion fails, Object type will be set to null and native types will be set to their default value.
Date
A date object can be automatically bound if the string representation of the date 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
For example:
archives?from=21/12/1980
public static void articlesSince(Date from) {
List<Article> articles = Article.findBy("date >= ?", from);
render(articles);
}
Files
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.
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) {
...
}
POJO objects 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 parameters name to properties on the Client object. Unresolved parameters are safely ignored. Types 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
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);
}
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 path of the default template 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 during the render(...) call. Just pass it as the first parameter of the render method:
Example:
public static void show(Long id) {
Client client = Client.findById(id);
render("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 browsers POST to the /clients URL.
- The Router invokes the create action of the Clients controller.
- The action method call 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.
- ...
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 method 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 Application {
@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 Application {
@Before(unless="login")
static void checkAuthentification() {
if(session.get("user") == null) login();
}
public static void index() {
List<User> users = User.findAll();
render(users);
}
...
}
@After
Methods annotated with the @After annotation are executed after each action call for this Controller.
public class Admin extends Application {
@After
static void log() {
Logger.info("Action executed ...");
}
public static void index() {
List<User> users = User.findAll();
render(users);
}
...
}
@Finally
Methods annotated with the @Finally annotation are executed after each action result is applied from for this Controller.
public class Admin extends Application {
@Finally
static void log() {
Logger.info("Response contains : " + response.out);
}
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 Application {
...
}
Session and Flash scope
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 4Ko) 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 cache has a different semantic than 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.