Utiliser des types personnalisés dans ses routes Play! Framework 2

L’utilisation des routes de Play! Framework est d’une extrême simplicité lorsque l’on utilise des types « standards ». Lorsqu’il s’agit d’utiliser un type plus exotique (au hasard, quelque chose que l’on n’utilise jamais… un UUID par exemple), la chose n’est pas aussi évidente.

Pour ce faire, il est nécessaire de définir un binder en implémentant l’interface PathBindable. Ce binder, pour l’API Java, doit être auto-récursif.

class FooBinder extends PathBindable<FooBinder> {
  // Du contenu.
}

Implémentation

L’observation de cette interface lève une interrogation sur la méthode unbind : comment « unbinder » la valeur si celle-ci n’est pas passée en paramètre ? Piège.

/**
* Unbind a URL path parameter.
*
* @param key Parameter key
* @param value Parameter value.
*/
public String unbind(String key);

L’astuce (grand mot s’il en est) consiste à stocker la valeur au moment de la construction de l’ojet. Prenons comme exemple l’UUID :

public class UuidBinder implements PathBindable<UuidBinder> {

  private UUID uuid;

  /** {@inheritDoc} */
  @Override
  public UuidBinder bind(String key, String txt) {
    uuid = UUID.fromString(txt);
    return this;
  }

  /** {@inheritDoc} */
  @Override
  public String unbind(String key) {
    return uuid.toString();
  }

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

Celui-ci sera utilisable dès lors qu’il aura été déclaré dans le fichier Build.scala :

val main = PlayProject(appName, appVersion, appDependencies, mainLang = JAVA).settings(
  routesImport += "util.binders.UuidBinder.";
)

Utilisation

L’utilisation depuis les routes est immédiat :
GET /foo/:uuid controllers.MyFooController.viewBar(uuid: util.binders.UuidBinder)

L’utilisation depuis les templates l’est un peu moins. Une solution simple consiste à enrichir le modèle :

class Bar extends Model {
  @Id
  public UUID uuid;

  @Transient
  public UuidBinder bindUuid() {
    return UuidBinder.create(uuid);
  }
}

La méthode create de UuidBinder se contente d’instancier un nouvel objet et de l’alimenter :

public static UuidBinder create(final UUID uuid) {
  UuidBinder u = new UuidBinder();
  u.uuid = uuid;
  return u;
}
<a href="@routes.MyFooController.viewBar(bar.bindUuid)">View my bar!</a>

Il peut être utile également de pouvoir récupérer la valeur de l’UUID. Ajoutons pour cela dans UuidBinder un simple getter :

public UUID uuid() {
  return uuid;
}

Ainsi, on peut l’utiliser dans le contrôleur :

public static Result viewBar(UuidBinder id) {
  UUID uuid = id.uuid();
  Bar bar = BarService.getBar(uuid);
  return barDetail(bar);
}
TwitterFacebookGoogle+LinkedIn
  • Benco

    intéressant et est-il possible de mettre une regex sur le format de l’identifiant ?

    • http://twitter.com/adericbourg Alban

      C’est possible, oui.

      Dans ce cas, on fait quelque chose de la forme :
      GET /foo/$id controllers.MyFooController.viewBaz(id: Long)

    • Benco

      Ma question était un peu bête puisque c’est le binder qui va réussir ou non à exploiter la string qu’on lui passe, il n’y a donc plus de regex à mettre dans la route. Néanmoins, si le bind lève une exception c’est Play! qui prend en charge l’erreur et retourne un badrequest qui reprend le message de l’exception. Si bind retourne un null, c’est au controller de gérer.

      En conclusion:
      - regex dans la route => not found
      - bind + exception => badrequest
      - bind + null => ce que le controller souhaite
      Du coup, en retournant un null, il est plus facile de gérer finement la réponse.

      • http://twitter.com/adericbourg Alban

        Dans ce cas, si on est en Scala, je renverrais un Option[UUID] avec un None plutôt qu’un null pour bien indiquer qu’on doit gérer ce cas.

        D’ailleurs, je me dis que faire la même chose en Java pourrait éviter bien des NullPointerException…

        • Benco

          Oui le modèle de l’objet None est bien, à une condition : il faut qu’il existe ou qu’on puisse le créer. Dans mon cas c’est un org.bson.types.ObjectId et il n’existe pas. De plus, il ne fait que repousser le problème puisqu’il faut de toutes manières traiter le cas, et si ce n’est pas une NullPointerException ce sera un autre type d’erreur qui sortira ! Quant à Scala je ne m’y suis pas encore mis réellement.

          En tous cas merci pour ce post, j’étais un peu frustré de passer par des regex plutôt que par les ids, je vais de ce pas intégrer cela dans mon projet.

  • ludo #

    Dans la partie utilisation, la variable du chemin n’est-elle pas uuid plutôt que :id ?
    GET /foo/:uuid controllers.MyFooController.viewBar(uuid: util.binders.UuidBinder)

    Dans l’exemple de l’url de la vue, n’est-ce pas bar.bindUuid plutôt que foo.bindUuid puisque le model est Bar ?

    Est-ce vraiment utile de binder ce type de données sachant qu’il faut passer un UuidBinder dans la signature de méthode et faire id.uuid() alors que passer une String en signature permet d’être plus ouvert à la gestion des données web et dans l’action on fait UUID.fromString(uuid). Je ne vois pas le gain puisque les actions play ne sont destinées qu’à être utilisées par play et non à être exposées comme une API de facade.

    • http://twitter.com/adericbourg Alban

      Les deux remarques sont très justes. J’ai corrigé l’article.

      Quant à l’utilisation d’un binder ou d’une chaîne de caractères… j’ai presque envie de dire que c’est une question de goûts. Tout étant fortement typé, je trouve qu’on est plus dans la philosophie de l’ensemble en conservant le type (fût-il encapsulé) dans les routes.

      Dans tous les cas, il faudra convertir l’UUID en quelque chose et le quelque chose en UUID à un moment donné. Et, comme on a pu le voir, un binder n’est pas très coûteux à coder (et en Scala, c’est encore plus court).