Regular Expressions in Play2 route using UUID

974 views
Skip to first unread message

Jon Brule

unread,
May 28, 2013, 7:47:07 AM5/28/13
to play-fr...@googlegroups.com
Using Play2, I am trying to map a URI that has a java.util.UUID path variable using the following code, but only received a 404. Note that the second route "tag by name" works well. I am using a custom formatter in the model (see below). Can the regular expression mapping be used with the customer formatter or readers / writers?

Thoughts?

Thanks,
Jon

Routes in question:
GET  /api/tag/$id<\d{8}-\d{4}-\d{4}-\d{4}-\d{12}>    controllers.Tags.showById(id: java.util.UUID)
GET  /api/tag/:name    controllers.Tags.showByName(name: String)

Corresponding controller:
package controllers

import play.api.mvc.{ Action, Controller }
import models.Tag
import play.api.libs.json.Json
import java.util.UUID
import play.api.libs.json.JsError

object Tags extends Controller {

  def showById(id: UUID) = Action { implicit request =>
    Tag.findById(id).map { tag =>
      Ok(Json.toJson(tag))
    }.getOrElse(NotFound)
  }

  def showByName(name: String) = Action { implicit request =>
    Tag.findByName(name).map { tag =>
      Ok(Json.toJson(tag))
    }.getOrElse(NotFound)
  }

}

And the model as such:
package models

import play.api.libs.json._
import play.api.libs.json.util._
import play.api.libs.json.Reads._
import play.api.libs.functional.syntax._
import java.util.UUID

case class Tag(
  id: Option[UUID] = None,
  name: String,
  description: String)

object Tag {

  implicit object UUIDFormat extends Format[UUID] {
    def writes(uuid: UUID): JsValue = JsString(uuid.toString())
    def reads(json: JsValue): JsResult[UUID] = json match {
      case JsString(x) => JsSuccess(UUID.fromString(x))
      case _ => JsError("Expected UUID as JsString")
    }
  }

  implicit val tagRead = (
    (__ \ 'id).readNullable[UUID] and
    (__ \ 'name).read[String] and
    (__ \ 'description).read[String])(Tag.apply _)

  implicit val tagWrite = (
    (__ \ 'id).writeNullable[UUID] and
    (__ \ 'name).write[String] and
    (__ \ 'description).write[String])(unlift(Tag.unapply))

  var tags = Set(
    Tag(Some(UUID.randomUUID()), "Tag1", "First Tag"),
    Tag(Some(UUID.randomUUID()), "Tag2", "Second Tag"))

  def add(tag: Tag) {
    val newTag = Tag(Some(UUID.randomUUID()), tag.name, tag.description)
    this.tags = this.tags + newTag
  }

  def findAll = this.tags.toList.sortBy(_.name)

  def findById(id: UUID): Option[Tag] = this.tags.find(_.id == Option[UUID](id))

  def findByName(name: String): Option[Tag] = this.tags.find(_.name == name)

}



Jon Brule

unread,
May 28, 2013, 8:38:08 AM5/28/13
to play-fr...@googlegroups.com
Using Play 2.1.1

Manuel Bernhardt

unread,
May 28, 2013, 8:45:27 AM5/28/13
to play-fr...@googlegroups.com
Hi Jon,

for this purpose, I would recommend implementing a PathBindable:

http://www.playframework.com/documentation/api/2.1.1/scala/index.html#play.api.mvc.PathBindable

If you define the PathBindable in an object, you then need to add it
to the imports of the Router, like so:

routesImport += "my.Binders._",

Hth,

Manuel
> --
> You received this message because you are subscribed to the Google Groups
> "play-framework" group.
> To unsubscribe from this group and stop receiving emails from it, send an
> email to play-framewor...@googlegroups.com.
> For more options, visit https://groups.google.com/groups/opt_out.
>
>

Jon Brule

unread,
May 28, 2013, 8:11:08 PM5/28/13
to play-fr...@googlegroups.com
Manuel,

Thanks for your quick response!

Within a new application, I have implemented the following PathBinder and placed it under a util package
package util

import java.util.UUID
import play.api.mvc.PathBindable

object Binders {
  
  implicit def uuidPathBinder = new PathBindable[UUID] {
    override def bind(key: String, value: String): Either[String, UUID] = {
      Right(UUID.fromString(value))
    }
    override def unbind(key: String, id: UUID): String = {
      id.toString
    }
  }
  
}

I have added the following highlighted line to my project/Build.scala:
import sbt._
import Keys._
import play.Project._

object ApplicationBuild extends Build {

  val appName         = "things"
  val appVersion      = "1.0-SNAPSHOT"

  val appDependencies = Seq(
    // Add your project dependencies here,
    jdbc,
    anorm
  )

  val main = play.Project(appName, appVersion, appDependencies).settings(
    routesImport += "util.Binders._"      
  )

}

I have the following Model:

package models

import java.util.UUID
import play.api.libs.json.Json
import play.api.libs.json.JsSuccess
import play.api.libs.json.JsError
import play.api.libs.json.JsResult
import play.api.libs.json.JsString
import play.api.libs.json.JsValue
import play.api.libs.json.Format

case class Thing(id: Option[UUID] = None, name: String)

object Thing {

  implicit object UUIDFormat extends Format[UUID] {
    def writes(uuid: UUID): JsValue = JsString(uuid.toString())
    def reads(json: JsValue): JsResult[UUID] = json match {
      case JsString(x) => JsSuccess(UUID.fromString(x))
      case _ => JsError("Expected UUID as JsString")
    }
  }

  implicit val thingWrite = Json.writes[Thing]

  implicit val thingRead = Json.reads[Thing]

  var things = Set(
    Thing(Some(UUID.randomUUID()), "Thing1"),
    Thing(Some(UUID.randomUUID()), "Thing2"))

  def findAll = this.things.toList.sortBy(_.name)

  def findById(id: UUID): Option[Thing] = this.things.find(_.id == Option[UUID](id))

  def findByName(name: String): Option[Thing] = this.things.find(_.name == name)

}

And the following controller:
package controllers

import play.api._
import play.api.mvc._
import models.Thing
import play.api.libs.json.Json
import java.util.UUID

object Things extends Controller {
  
  def list = Action { implicit request =>
    val things = Thing.findAll
    Ok(Json.toJson(things))
  }
  
  def showById(id: UUID) = Action { implicit request =>
    Thing.findById(id).map { thing =>
      Ok(Json.toJson(thing))
    }.getOrElse(NotFound)
  }

  def showByName(name: String) = Action { implicit request =>
    Thing.findByName(name).map { thing =>
      Ok(Json.toJson(thing))
    }.getOrElse(NotFound)
  }

}

And the following routes:
# Routes
# This file defines all application routes (Higher priority routes first)
# ~~~~

# Home page
GET / controllers.Application.index

# Things
GET /api/things controllers.Things.list
GET /api/thing/$id<\d{8}-\d{4}-\d{4}-\d{4}-\d{12}> controllers.Things.showById(id: java.util.UUID)
GET /api/thing/:name controllers.Things.showByName(name: String)

# Map static resources from the /public folder to the /assets URL path
GET /assets/*file controllers.Assets.at(path="/public", file)


Unfortunately, I am still not able to resolve the fist /api/thing URI. Both the second, name-based fetch URI and the thing collection URI work fine. Can you think of something else that I have missed (I am very new to Play / Scala yet a seasoned Java developer).

In my searches, I came across bindableUUID within play.api.mvc.PathBindable under Play 2.1.1. Could I be spinning on this conversion for no reason when it might already exist? After all, I am already running 2.1.1...


Thanks so much,
Jon

Marius Soutier

unread,
May 29, 2013, 2:59:54 AM5/29/13
to play-fr...@googlegroups.com
I think you need to define the type explicitly in your Binder, e.g. type UUID = java.util.UUID.

And it helps to know what "doesn't work" mean. Do you get a compile error?

Jon Brule

unread,
May 29, 2013, 6:48:28 AM5/29/13
to play-fr...@googlegroups.com
Thanks for the reply, Marius.

I apologize for my lack of clarity. I mean to say that a URI of the form http://localhost:9000/api/thing/77de82f1-691a-4e97-880d-d662823b875e generates a 404, when a Thing record is defined as follows (used http://localhost:9000/api/thing/Thing1 to retrieve):

{
    "id": "77de82f1-691a-4e97-880d-d662823b875e",
    "name": "Thing1"
}

The routes are defined as follows:
GET / controllers.Application.index

# Things
GET /api/things controllers.Things.list
GET /api/thing/$id<\d{8}-\d{4}-\d{4}-\d{4}-\d{12}> controllers.Things.showById(id: java.util.UUID)
GET /api/thing/:name controllers.Things.showByName(name: String)

# Map static resources from the /public folder to the /assets URL path
GET /assets/*file controllers.Assets.at(path="/public", file)

I did try to update the binder as follows, but to no avail:
package util

import play.api.mvc.PathBindable

object Binders {
  
  implicit def uuidPathBinder = new PathBindable[java.util.UUID] {
    override def bind(key: String, value: String): Either[String, java.util.UUID] = {
      Right(java.util.UUID.fromString(value))
    }
    override def unbind(key: String, id: java.util.UUID): String = {
      id.toString
    }
  }
  
}

Any thoughts? I thought I read that URI overloading is possible and evaluated in the order defined in the routes file. Is this possible with 2.1.1?

Thanks,
Jon

Manuel Bernhardt

unread,
May 29, 2013, 7:46:19 AM5/29/13
to play-fr...@googlegroups.com
Hi Jon,

have you tried binding without the regular expression, just by using a
route of the kind

/api/thing/:id

?

Also I'm afraid your regular expression won't match because it limits
things to decimals, whereas UUIDs mix characters and decimals.

Manuel

Jon Brule

unread,
May 29, 2013, 9:30:10 AM5/29/13
to play-fr...@googlegroups.com
Well OK then (obvious revelation here)... A UUID is hexadecimal not decimal... 

When I change the route to the following, the UUID URI works fine:
GET /api/thing/$id<[0-9a-z]{8}-[0-9a-z]{4}-[0-9a-z]{4}-[0-9a-z]{4}-[0-9a-z]{12}> controllers.Things.showById(id: java.util.UUID)


Sometimes a swift boot to the head helps see the light... Or, a Clue x 4 as I like to call it..

Thanks so much to both Manuel and Marius,
Jon

Pablo Fernandez

unread,
May 29, 2013, 2:46:50 PM5/29/13
to play-fr...@googlegroups.com
| Well OK then (obvious revelation here)... A UUID is hexadecimal not decimal...

I guess you mean alphanumeric and not numeric :) 
Reply all
Reply to author
Forward
0 new messages