Play Framework Actions

Posted by n3integration on December 30, 2015

If you are unfamiliar with the Play! Framework, it’s an MVC framework written in Scala and definitely worth checking out. This holds true especially if you’re a Java or Scala developer. If you are unfamiliar with Scala, the language allows developers to blend both functional and object-oriented programming (OOP) techniques, providing the best of both paradigms.

Actions

You may be asking yourself, what is an action and why would I use one? If you are familiar with Express.js, actions are similar to middleware. If you’re unfamiliar with Express, the behavior is akin to the decorator pattern, in that actions can delegate to another action or to the underlying Controller method. Some examples of behavior that can be implemented as an action include:

  • Logging
  • Metrics
  • Authorization
Example Action

As a trivial example, we can wrap a call to a Controller and log the elapsed time. For trivial actions, the Play Framework provides an abstract implementation through Action.Simple, where simple implies that it does not require any configuration parameters.

public class Logging extends Action.Simple {
  public Promise<Result> call(Context ctx) throws Throwable {
    long now = System.currentTimeMillis();
    try {
      return delegate.call(ctx);  
    }
    finally {
      Http.Request request = ctx.request();
      long elapsedTime = System.currentTimeMillis() - now;
      Logger.debug("[{}] elapsed_time {}", request.path(), elapsedTime);
    }
  }
}

To make use of the new action, each Controller (or method) should be annotated with the Logging action.

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

Now, every time that the index method is invoked, a log message will be emitted to the Play log file with the request path and the elapsed time required to render the response.

Example Annotation

As mentioned above, an action may take configuration parameters. One use case that I have found this to be useful is to handle authorization. For example, suppose that you have services or pages that you’d like to restrict to administrators. Controllers or cherry-picked Controller methods can be annotated to ensure that users are authorized to access this functionality.

First, we need to define an enumeration to hold the possible roles.

public enum Role {
  Public, Admin
}

Then, we need to define a custom Java annotation.

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface Authorized {
    Role value() default Role.Public;
}

Next, we’ll need to create a AuthorizedAction implementation with the caveat that the following example is by no means complete or secure, but only meant to provide an example of a configurable action. The key takeaway to identify in the example below is the use of configuration.value(). This allows us to retrieve the configured Role for the current route.

public class AuthorizedAction extends Action<Authorized> {
  private static final String ADMIN    = "admin";
  private static final String UNKNOWN  = "unknown";
  private static final String USER_KEY = "SESSION_USER";

  public Promise<Result> call(Context ctx) throws Throwable {
    if(configuration.value() == Role.Admin) {
      Http.Session session = ctx.session();
      if(!Objects.equals(ADMIN, session.getOrDefault(USER_KEY, UNKNOWN))) {
        return F.Promise.pure(unauthorized());
      }
    }
    return delegate.call(ctx);  
  }  
}

There are three remaining steps:

Update the Authorized annotation to include the @With annotation.

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

    @Authorized(Role.Admin)
    public Result admin() {
      return ok(index.render("Your new admin application is ready."));
    }
}

Annotate the Controller or method(s) with the new annotation.

@With(AuthorizedAction.class)

Update the routes definition with the new admin route.

GET   /admin  Application.admin()