4

I am writing a module for the Play Framework. In part of my module I have the following code

abstract class SecurityFiltering extends GlobalSettings{
  override def onRequestReceived(request: RequestHeader) = {
    play.Logger.debug("onRequestReceived: " + request)
    super.onRequestReceived(request)
  }

  override def doFilter(next: RequestHeader => Handler): (RequestHeader => Handler) = {
    request => {
      play.Logger.debug("doFilter: " + request)
      super.doFilter(next)(request)
    }
  }

  override def onRouteRequest(request: RequestHeader): Option[Handler] = {
    play.Logger.debug("onRouteRequest: " + request)
    super.onRouteRequest(request)
  }
}

In the doFilter method I am able to determine the following useful information

ROUTE_PATTERN = /x/$name<[^/]+>/$age<[^/]+>
ROUTE_CONTROLLER = controllers.Application
ROUTE_ACTION_METHOD = tester
ROUTE_VERB = GET
path = /x/hello

What I need in addition to this is the values for the named parts of the URL before the QueryString. So given the following route in my test application I need to retrieve Name=Pete and Age=41

localhost:9000/x/Pete/41

There is surely some code in the Play Framework which already does this but I am unable to find it. Can someone suggest how I achieve this goal, or point me at which Play class extracts these values?

Peter Morris
  • 20,174
  • 9
  • 81
  • 146

2 Answers2

2

package models.com.encentral.tattara.web.util;

 import java.util.HashMap;
 import java.util.Map;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;


 public class RouteExtractor {

//something like "/foo/$id<[^/]+>/edit/$james<[^/]+>"
private String routePattern;
private String path;

//something like /$id<[^/]+>
private static final String INDEX_PATTERN = "\\$(.+?)\\<\\[\\^\\/\\]\\+\\>";

public RouteExtractor(String routePattern, String path) {

    this.routePattern = routePattern;
    this.path = path;
}

private Map<Integer, String> extractPositions() {
    Pattern pattern = Pattern.compile(INDEX_PATTERN);
    Matcher matcher = pattern.matcher(this.routePattern);
    Map<Integer, String> results = new HashMap<>();
    int index = 0;
    while (matcher.find()) {
        results.put(index++, matcher.group(1));
    }
    return results;
}

private String replaceRoutePatternWithGroup() {
    Pattern pattern = Pattern.compile(INDEX_PATTERN);
    Matcher matcher = pattern.matcher(this.routePattern);
    return matcher.replaceAll("([^/]+)");
}

public Map<String, String> extract() {
    Pattern pattern = Pattern.compile(this.replaceRoutePatternWithGroup());
    Matcher matcher = pattern.matcher(this.path);
    final Map<String, String> results = new HashMap<>();
    if (matcher.find()) {
        this.extractPositions().entrySet().stream().forEach(s -> {
            results.put(s.getValue(), matcher.group(s.getKey() + 1));
        });
    }
    return results;
}

}
1

As per this GitHub issue response via JRoper

onRequestReceived is the thing that does the routing and tags the request, so of course it's not going to have any of the routing information when it's first invoked, only after it's invoked.

val p = """\$([^<]+)<([^>]+)>""".r
override def onRequestReceived(request: RequestHeader) = {
  val (taggedRequest, handler) = super.onRequestReceived(request)
  val pattern = taggedRequest.tags("ROUTE_PATTERN")
  val paramNames = p.findAllMatchIn(pattern).map(m => m.group(1)).toList
  val pathRegex = ("^" + p.replaceAllIn(pattern, m => "(" + m.group(2) + ")") + "$").r
  val paramValues = pathRegex.findFirstMatchIn(request.path).get.subgroups  
  val params: Map[String, String] = paramNames.zip(paramValues).toMap
  // ^ your params map, will be Map("name" -> "Pete", "age" -> "41")
  (taggedRequest, handler)
}

That said, there are usually better, more typesafe ways to achieve whatever you're trying to achieve. If you depend on there being specific parameters in the URL, then a filter is not the right thing, because filters apply to all requests, whether they have those parameters or not. Rather, you should probably be using action composition or a custom action builder, like so:

case class MyAction(name: String, age: Int) extends ActionBuilder[Request] {
  def invokeBlock[A](request: Request[A], block: (Request[A]) => Future[Result]) = {
    // Do your filtering here, you have access to both the name and age above
    block(request)
  }
}

def foo(name: String, age: Int) = MyAction(name, age) { request =>
  Ok("Hello world")
}

def bar(name: String, age: Int) = MyAction(name, age).async { request =>
  Future.successful(Ok("Hello world"))
}
Peter Morris
  • 20,174
  • 9
  • 81
  • 146