Writing modules for Play 2, part 2: Interceptors

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<LogMe>
{
    @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-1.1-sources.jar ...
[info] Done packaging.
[info] Wrote /tmp/mylogger/project-code/target/scala-2.9.1/mylogger_2.9.1-1.1.pom
[info] Updating {file:/tmp/mylogger/project-code/}mylogger...
[info] Done updating.                                                                  
[info] :: delivering :: mylogger#mylogger_2.9.1;1.1 :: 1.1 :: 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-1.1.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-1.1-javadoc.jar ...
[info] Done packaging.
[info] Packaging /tmp/mylogger/project-code/target/scala-2.9.1/mylogger_2.9.1-1.1.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/1.1/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/1.1/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/1.1/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/1.1/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/1.1/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<LogMe>
{
    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

Other parts to this tutorial series

Leave a comment ?

15 Comments.

  1. Thanks for the guide so far!

  2. Publishing Play 2 modules on github | Objectify - pingback on April 21, 2012 at 09:23
  3. Thanks for the great intro!
    Just one question: Is there a difference between a module and a plugin in Play, or is it just a matter of naming? And if there is any difference, when should one opt for the former or the later?

  4. A plugin is just a class that extends Play’s Plugin class. You can have plugins in your main code base or in modules.

    Modules are externalised, generic libraries that are specific to Play, but don’t implement any specific classes or interfaces.

  5. Hi Steve,
    That’s a great manual, thanks! I’m now trying to walk through, however getting an error in

    @With(LogMeAction.class)

    saying that types are incompatible:

    [info] Compiling 1 Scala source and 2 Java sources to /home/sskrobot/dev/stork-admin/target/scala-2.9.1/classes...
    [error] /home/sskrobot/dev/stork-admin/app/actions/RenderWith.java:16: incompatible types
    [error] found : java.lang.Class
    [error] required: java.lang.Class<? extends play.mvc.Action>
    [error] @With(LogMeAction.class)
    [error] ^
    [error] 1 error

    Did you see this? What’s the signature of the @With annotation you are using?

  6. @Sergey – hmm, that’s odd. Did you check the example implementation at http://www.objectify.be/wordpress/wp-content/uploads/2012/03/mylogger-2.zip ?

  7. Steve, I was inspired by your post and wrote my own on using “interceptors” to implement basic auth: Basic Authentication in the Play Framework Using A Custom Action Annotation

  8. @Shane – very nicely done…and it’s always nice to read that I’ve inspired something :)

  9. @sergey @steve the only thing wrong with the post is that LogAction.java class definition should go from

    public class LogMeAction extends Action

    to

    public class LogMeAction extends Action

    otherwise so good so far

  10. Thanks Nick – your comment got a bit mangled, for exactly the same the original post did (using angled brackets instead of &lt;, etc) but I got the point. The post has now been corrected.

  11. Content Security Policy in Play! Framework 2 - pingback on January 19, 2013 at 20:03
  12. Important tip for speed up your module development:

    Instead of using your local repository (need to republish every time you do a change in your module) you could use dependsOn in your sample Build.scala.

    Take a look at this site for a detailed example

  13. Shane, you shuld updte your code to Play FW 2.2

    F.Promise and

    return F.Promise.pure((SimpleResult) unauthorized());

    /Erik

Leave a Comment


NOTE - You can use these HTML tags and attributes:
<a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>

Trackbacks and Pingbacks: