73

Lets say I have an already functioning Play 2.0 framework based application in Scala that serves a URL such as:

http://localhost:9000/birthdays

which responds with a listing of all known birthdays

I now want to enhance this by adding the ability to restrict results with optional "from" (date) and "to" request params such as

http://localhost:9000/birthdays?from=20120131&to=20120229

(dates here interpreted as yyyyMMdd)

My question is how to handle the request param binding and interpretation in Play 2.0 with Scala, especially given that both of these params should be optional.

Should these parameters be somehow expressed in the "routes" specification? Alternatively, should the responding Controller method pick apart the params from the request object somehow? Is there another way to do this?

magicduncan
  • 1,376
  • 1
  • 10
  • 11

6 Answers6

60

Encode your optional parameters as Option[String] (or Option[java.util.Date], but you’ll have to implement your own QueryStringBindable[Date]):

def birthdays(from: Option[String], to: Option[String]) = Action {
  // …
}

And declare the following route:

GET   /birthday       controllers.Application.birthday(from: Option[String], to: Option[String])
Julien Richard-Foy
  • 9,434
  • 2
  • 33
  • 40
  • 8
    For me this wasn't enough to do routing with no parameters. I extended it to `GET /birthday controllers.Application.birthday(from: Option[String] ?= None, to: Option[String] ?= None)` – Marius Soutier Mar 28 '12 at 15:22
  • More succient: `GET /birthday controllers.Application.birthday(Option[from], Option[to])` – Paul Draper Jan 31 '14 at 18:48
  • 1
    Now documented at https://www.playframework.com/documentation/2.4.x/ScalaRouting#Parameters-with-default-values – matanster Feb 06 '16 at 10:31
  • Optional parameters are described here (v2.7+) => https://www.playframework.com/documentation/2.7.x/ScalaRouting#Optional-parameters – sentenza Aug 23 '19 at 10:09
19

A maybe less clean way of doing this for java users is setting defaults:

GET  /users  controllers.Application.users(max:java.lang.Integer ?= 50, page:java.lang.Integer ?= 0)

And in the controller

public static Result users(Integer max, Integer page) {...}

One more problem, you'll have to repeat the defaults whenever you link to your page in the template

@routes.Application.users(max = 50, page = 0)
Somatik
  • 4,599
  • 2
  • 35
  • 47
  • Can someone confirm that (max : java.lang.Integer = null) works? 'max' always ends up null for me... – ejain Mar 26 '12 at 23:14
  • 1
    I fixed the answer, should have been ?= – Somatik Mar 27 '12 at 09:02
  • 6
    In Java you can use `play.libs.F.Option` instead of Scala `Option` – Julien Richard-Foy Mar 28 '12 at 20:57
  • 1
    Do you have an example of the whole thing with using the Scala or F Option. What bothers me is the repetition of the defaults in the templates. I'm always getting this suggestion to use _ for partially applied functions – Somatik Mar 30 '12 at 08:39
12

In Addition to Julien's answer. If you don't want to include it in the routes file.

You can get this attribute in the controller method using RequestHeader

String from = request().getQueryString("from");
String to = request().getQueryString("to");

This will give you the desired request parameters, plus keep your routes file clean.

Andrii Abramov
  • 7,967
  • 8
  • 55
  • 79
Dave Ranjan
  • 2,616
  • 22
  • 44
  • how do your router file looks? I have something like `GET /url @com.mycompany.controllers.MyClass.fetchget()` and it cannot find the route – Zennichimaro Sep 26 '17 at 12:12
  • 1
    @Zennichimaro https://stackoverflow.com/questions/16301211/handling-freeform-get-url-parameters-in-play-2-routing take a look – iku Jul 02 '19 at 11:33
8

Here's Julien's example rewritten in java, using F.Option: (works as of play 2.1)

import play.libs.F.Option;
public static Result birthdays(Option<String> from, Option<String> to) {
  // …
}

Route:

GET   /birthday       controllers.Application.birthday(from: play.libs.F.Option[String], to: play.libs.F.Option[String])

You can also just pick arbitrary query parameters out as strings (you have to do the type conversion yourself):

public static Result birthdays(Option<String> from, Option<String> to) {
  String blarg = request().getQueryString("blarg"); // null if not in URL
  // …
}
Max
  • 10,510
  • 5
  • 22
  • 15
5

For optional Query parameters, you can do it this way

In routes file, declare API

GET   /birthdays     controllers.Application.method(from: Long, to: Long)

You can also give some default value, in case API doesn't contain these query params it will automatically assign the default values to these params

GET   /birthdays    controllers.Application.method(from: Long ?= 0, to: Long ?= 10)

In method written inside controller Application these params will have value null if no default values assigned else default values.

Himanshu Goel
  • 494
  • 8
  • 23
  • Thanks for this answer, now it shows up in swagger with a field with a default value. In my case I had a mandatory and an optional parameter. – karsten314159 Aug 20 '18 at 07:21
2

My way of doing this involves using a custom QueryStringBindable. This way I express parameters in routes as:

GET /birthdays/ controllers.Birthdays.getBirthdays(period: util.Period)

The code for Period looks like this.

public class Period implements QueryStringBindable<Period> {

  public static final String PATTERN = "dd.MM.yyyy";
  public Date start;

  public Date end;

  @Override
  public F.Option<Period> bind(String key, Map<String, String[]> data) {
      SimpleDateFormat sdf = new SimpleDateFormat(PATTERN);

      try {
          start = data.containsKey("startDate")?sdf.parse(data.get("startDate")  [0]):null;
          end = data.containsKey("endDate")?sdf.parse(data.get("endDate")[0]):null;
      } catch (ParseException ignored) {
          return F.Option.None();
      }
      return F.Option.Some(this);
  }

  @Override
  public String unbind(String key) {
      SimpleDateFormat sdf = new SimpleDateFormat(PATTERN);
      return "startDate=" + sdf.format(start) + "&amp;" + "endDate=" + sdf.format(end);
  }

  @Override
  public String javascriptUnbind() {
      return null;
  }

  public void applyDateFilter(ExpressionList el) {
      if (this.start != null)
          el.ge("eventDate", this.start);
      if (this.end != null)
          el.le("eventDate", new DateTime(this.end.getTime()).plusDays(1).toDate());
  }

}

applyDateFilter is just a convienence method i use in my controllers if I want to apply date filtering to the query. Obviously you could use other date defaults here, or use some other default than null for start and end date in the bind method.

Andrii Abramov
  • 7,967
  • 8
  • 55
  • 79
stian
  • 2,834
  • 3
  • 22
  • 38