Steve Chaloner, a Brit living in Belgium, has been developing in Java since 1996, and has been an avid user of the Play framework since 2010. Steve has introduced Play into several companies for projects ranging from the fairly small to the extremely large. He is the author of several Play modules, including the Deadbolt authorization system. Steve is a DZone MVB and is not an employee of DZone and has posted 18 posts at DZone. You can read more from them at their website. View Full User Profile

Writing modules for Play 2: Interceptors

04.30.2012
| 2569 views |
  • submit to reddit

In the first part of this tutorial, we looked at the bare basics for creating, publishing and calling a module. The module we created didn’t really do much, so now it’s time to look at expaning the functionality using some of Play’s features.

1. Interceptors

Interceptors allow you to intercept calls to controllers, and augment or block their behaviour. In the first sample application, we added an explicit call to MyLogger to log a message to the console. If we scale that up, and you want to use this oh-so-useful plugin for every controller method call, you’re going to be writing a lot of boilerplate code. Interceptors allow us to automatically apply actions, and so reduce boilerplate.

1.1 Add the code

In the app directory, create a new package called actions. In here, we’re going to add the LogMe annotation, and LogMeAction that will be executed whenever the annotation is present.

LogMe.java is, at this point, a very simple annotation that doesn’t take any parameters

package actions;

import play.mvc.With;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * @author Steve Chaloner (steve@objectify.be)
 */
@With(LogMeAction.class)
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD, ElementType.TYPE})
@Inherited
@Documented
public @interface LogMe
{
}

Take a look at the annotations, and you’ll With(LogInAction.class) – this lets Play know that when this annotation is encountered, it should execute an LogInAction before the actual target.

package actions;

import play.mvc.Action;
import play.mvc.Http;
import play.mvc.Result;

/**
 * @author Steve Chaloner (steve@objectify.be)
 */
public class LogMeAction extends Action
{
    @Override
    public Result call(Http.Context context) throws Throwable
    {
        System.out.println("MyLogger: " + context.request().path());
        return delegate.call(context);
    }
}

This is pretty elegant stuff – the action has a generic parameter type of LogMe, which gives access to any parameters given to the LogMe annotation. This allows you to customise behaviour of the action. We’ll see this in action when we add some extra features. Once your code – in this case, another class to System.out, is done then you return the result of delegate.class(context) to resume the normal execution flow. In the meantime, if @LogMe is added to a controller method, the path of the action will be logged to the console; if @LogMe is added to a controller, the invocation of any method in that controller will result in the path being logged to the console.

1.2 Update Build.scala

Since we have a new version of mylogger, we should change the version number. Open project/Build.scala and change

val appVersion      = "1.0-SNAPSHOT"

to

val appVersion      = "1.1"
1.3 Make sure your project changes are detected

If you’re already running the Play console in mylogger/project-code, you need to execute “reload” for the changes to Build.scala to be picked up. If don’t have the console open, open it now – the changes will be picked up automatically on start-up.

[mylogger] $ reload
[info] Loading project definition from C:\Temp\mylogger\project-code\project
[info] Set current project to mylogger (in build file:/C:/Temp/mylogger/project-code/)
1.4 Clean and publish

As noted earlier, it’s always a good idea to clean before publishing to ensure you’re not pushing out any objects that shouldn’t be there.

[mylogger] $ clean
[success] Total time: 0 s, completed Mar 19, 2012 9:17:25 PM
[mylogger] $ publish-local
[info] Packaging /tmp/mylogger/project-code/target/scala-2.9.1/mylogger_2.9.1-<strong>1.1</strong>-sources.jar ...
[info] Done packaging.
[info] Wrote /tmp/mylogger/project-code/target/scala-2.9.1/mylogger_2.9.1-<strong>1.1</strong>.pom
[info] Updating {file:/tmp/mylogger/project-code/}mylogger...
[info] Done updating.
[info] :: delivering :: mylogger#mylogger_2.9.1;1.1 :: <strong>1.1</strong> :: release :: Mon Mar 19 21:17:30 CET 2012
[info] Generating API documentation for main sources...
[info] Compiling 3 Java sources to /tmp/mylogger/project-code/target/scala-2.9.1/classes...
[info] 	delivering ivy file to /tmp/mylogger/project-code/target/scala-2.9.1/ivy-<strong>1.1</strong>.xml
model contains 7 documentable templates
[info] API documentation generation successful.
[info] Packaging /tmp/mylogger/project-code/target/scala-2.9.1/mylogger_2.9.1-<strong>1.1</strong>-javadoc.jar ...
[info] Done packaging.
[info] Packaging /tmp/mylogger/project-code/target/scala-2.9.1/mylogger_2.9.1-<strong>1.1</strong>.jar ...
[info] Done packaging.
[info] 	published mylogger_2.9.1 to /home/steve/development/play/play-2.0/framework/../repository/local/mylogger/mylogger_2.9.1/<strong>1.1</strong>/poms/mylogger_2.9.1.pom
[info] 	published mylogger_2.9.1 to /home/steve/development/play/play-2.0/framework/../repository/local/mylogger/mylogger_2.9.1/<strong>1.1</strong>/jars/mylogger_2.9.1.jar
[info] 	published mylogger_2.9.1 to /home/steve/development/play/play-2.0/framework/../repository/local/mylogger/mylogger_2.9.1/<strong>1.1</strong>/srcs/mylogger_2.9.1-sources.jar
[info] 	published mylogger_2.9.1 to /home/steve/development/play/play-2.0/framework/../repository/local/mylogger/mylogger_2.9.1/<strong>1.1</strong>/docs/mylogger_2.9.1-javadoc.jar
[info] 	published ivy to /home/steve/development/play/play-2.0/framework/../repository/local/mylogger/mylogger_2.9.1/<strong>1.1</strong>/ivys/ivy.xml
[success] Total time: 3 s, completed Mar 19, 2012 9:17:31 PM

Note the version of the module has changed in the logging. If you still see 1.0-SNAPSHOT, make sure you reloaded the project before publishing!

1.5 Update the sample application

Back in the sample application, change the module version you require in project/Build.scala

    val appDependencies = Seq(
      "mylogger" % "mylogger_2.9.1" % "1.1"
    )

Reload, and run “dependencies” to ensure you have the correct version. You can now update app/controllers/Application.java to use this new code:

package controllers;

import actions.LogMe;
import play.mvc.Controller;
import play.mvc.Result;
import views.html.index;

@LogMe
public class Application extends Controller
{
    public static Result index()
    {
        return ok(index.render("Your new application is ready."));
    }
}

Run this example, and you’ll now see MyLogger output applied through the annotation.

2. Added interceptor parameters

Just having the path of the request logged is not particulary useful or exciting. What if a specific log message should be given for each controller or controller method? In this case, we need to add some parameters.

2.1 Change the annotation signature

Upload actions/LogMe.java to take a value() parameter – this is the default annotation parameter, and so doesn’t need to be explicitly named when used. The value defaults to an empty string, so a standard message can be provided in the action if one isn’t present here.

public @interface LogMe
{
    String value() default "";
}

In the action, the inherited configuration field is typed to the generic parameter (in this case, LogMe) and gives access to the parameters. Update the call(Http.Context) method to take advantage of this.

public Result call(Http.Context context) throws Throwable
{
    String value = configuration.value();
    if (value == null || value.isEmpty())
    {
        value = context.request().path();
    }
    System.out.println("MyLogger: " + value);
    return delegate.call(context);
}
2.2 Publish the changes

Repeat steps 1.2 to 1.4 again, this time changing appVersion to 1.2

2.3 Update the sample application

Just like before, update the dependency version in Build.scala, reload and confirm with “dependencies”. Now you can add a message to the LogMe annotation:

@LogMe("This is my log message")
public class Application extends Controller

Run the application, and now you’ll see your annotation message in the console.

[info] play - Application started (Dev)
MyLogger: This is my log message

3. Make the interceptors interact

Now you (hopefully) have the hang of this, we’re going to speed up a bit. In this section, we’re going to look at how interceptors can interact with each other. Play applies interceptors first to the method, and then to controller, so if the same annotation is present at both the method and controller level it will be executed twice. The LogMe annotation can be applied to both the class level and the method level, but what if you have a general logging message for the entire controller except for one method that requires a different message? Also, we only want one logging message invocation per invocation. To achieve this, we can use the context that’s passed into each action.

3.1 Update the module

Update LogMeAction to give it awareness of previous invocations:

package actions;

import play.mvc.Action;
import play.mvc.Http;
import play.mvc.Result;

/**
 * @author Steve Chaloner (steve@objectify.be)
 */
public class LogMeAction extends Action
{
    public static final String ALREADY_LOGGED = "already-logged";

    @Override
    public Result call(Http.Context context) throws Throwable
    {
        Result result;

        if (context.args.containsKey(ALREADY_LOGGED))
        {
            // skip the logging, just continue the execution
            result = delegate.call(context);
        }
        else
        {
            // we're not using the value here, only the key, but this
            // mechanism can also be used to pass objects
            context.args.put(ALREADY_LOGGED, "");

            String value = configuration.value();
            if (value == null || value.isEmpty())
            {
                value = context.request().path();
            }
            System.out.println("MyLogger: " + value);

            result = delegate.call(context);
        }

        return result;
    }
}

Update the version number, clean, reload, and publish-local.

3.2 Update the sample application

We’re going to add a second annotation, this time to the index method. This will override the controller-level annotation. So, update the dependency number in Build.scala, reload and run.

package controllers;

import actions.LogMe;
import play.mvc.Controller;
import play.mvc.Result;
import views.html.index;

@LogMe("This is my log message")
public class Application extends Controller
{
    @LogMe("This is my method-specific log message")
    public static Result index()
    {
        return ok(index.render("Your new application is ready."));
    }
}

When you access http://localhost:9000, you will now see this in the console:

@LogMe("This is my log message")
[info] play - Application started (Dev)
MyLogger: This is my method-specific log message

4 It’s beer time again

You now have an infrastructure that supports parameterised actions. Remember that lots of things can be passed as annotation parameters, but – crucially – not everything. You may need to get creative for some of your tasks!

You can download the complete source code here.

A note on progress so far

This was originally planned to be a three-part tutorial. However, now I see how much detail you can go into when discussing just one subject, I think it’s going to be more of an ongoing series. If you have any feedback or questions on what there is so far, please let me know at steve@objectify.be

Published at DZone with permission of Steve Chaloner, author and DZone MVB. (source)

(Note: Opinions expressed in this article and its replies are the opinions of their respective authors and not those of DZone, Inc.)