In this section we will complement the Bed Nap summary page with a view page for displaying visit details. To do so we will need to convert the request URL into meaningful Visit objects.
We have an index / summary page that lists all the visits. Next we'll add a details page that will display all the information for a given visit.
To do this we need to identify each Visit, so we'll give each one an ID. Then we'll create a view page that responds to a RESTful URL in the format:
/view/<visit-id>
That way, just by changing the URL, we can view the details of different visits. It also means each visit page is bookmarkable.
Similar to how it may work if our VisitService was backed by a database, we'll automatically generate our IDs from a numeric sequence.
Update Visit Entity
Entity objects should always know who they are (you know who you are, right!?) so we'll add an ID field to Visit.
To allow a Visit entity to be created before it is saved by the service, we will make the ID field optional / nullable. We'll then add an optional / default ID parameter to the ctor. This means we don't have to change our sample visit data code.
const class Visit {
const Int? id
const Str name
const Date date
const Int rating
const Str comment
new make(Str name, Date date, Int rating, Str comment, Int? id := null) {
this.id = id
this.name = name
this.date = date
this.rating = rating
this.comment = comment
}
}
Update Visit Service
In our VisitService we'll change the SynchronizedList to a SynchronizedMap so we can easily retrieve individual Visit objects by using the ID as a key.
The save() method will check to see the given Visit entity has an ID or not. If not, it will clone the entity and set a newly generated ID at the same time.
Note we are only cloning the entity because it is const, and it is only const because it is being stored in a different thread via SynchronizedMap. If VisitService were to be backed by a database or other means, Visit would probably be non-const and we'd just set the ID on the object.
We'll use a simple AtomicInt to assign sequential and numerical IDs.
VisitService now looks like:
using concurrent::AtomicInt
using afConcurrent::SynchronizedMap
using afIoc::Inject
const class VisitService {
@Inject { id="bednap.visits"; type=Int:Visit# }
private const SynchronizedMap visits
private const AtomicInt lastId := AtomicInt()
new make(|This| f) { f(this) }
Visit[] all() {
visits.vals
}
Void save(Visit visit) {
if (visit.id == null) {
nextId := lastId.incrementAndGet
visit = Visit(visit.name, visit.date, visit.rating, visit.comment, nextId)
}
visits[visit.id] = visit
}
Visit get(Int id) {
visits[id]
}
}
Create View Page
Our ViewPage code will look very similar to IndexPage except the render() method will take the ID of the entity to render. The ID will simply be the last path segment from request URL.
Note how we turn the ID into an Int so we can query the VisitService with it.
The returned Visit object is used to populate the HTML. In this fashion, the ViewPage is used as a template for ALL the visit pages.
using afIoc::Inject
using afBedSheet::Text
using web::WebOutStream
const class ViewPage {
@Inject private const VisitService visitService
new make(|This|in) { in(this) }
Text render(Str strId) {
id := strId.toInt
visit := visitService.get(id)
htmlBuf := StrBuf()
html := WebOutStream(htmlBuf.out)
html.docType5
html.html
html.head
html.title.w("Bed Nap View Page").titleEnd
html.headEnd
html.body
html.h1.w("Bed Nap Tutorial").h1End
html.h2.w("Visit View Page").h2End
html.div.w("${visit.name} said:").divEnd
html.div.w(visit.comment).divEnd
html.div.w("on ${visit.date}").divEnd
html.div.w("${visit.rating} / 5 stars").divEnd
html.div
html.a(`/`).w("< Back").aEnd
html.divEnd
html.tableEnd
html.bodyEnd
html.htmlEnd
return Text.fromHtml(htmlBuf.toStr)
}
}
Update Routing
Now we have to tell BedSheet how to route URLs to our new ViewPage. Following the documentation for Route we can convert a segment of the request URL into a method parameter by using /** notation. So we'll update our AppModule:
@Contribute { serviceType=Routes# }
Void contributeRoutes(Configuration config) {
config.add(Route(`/`, IndexPage#render))
config.add(Route(`/view/**`, ViewPage#render))
}
Run It
We can now run the Bed Nap web app and view our new detail page. If we go to http://localhost:8069/view/2 we should see:

Wow! Stunning!
Now, what if we type in a URL for a Visit entity that doesn't exist? Like http://localhost:8069/view/69?
Or what if we type in a non-numeric Visit ID, like http://localhost:8069/view/Oops!?

Urgh! A 500 Error Page!
Not very nice. In the ValueEncoders section we'll see an easy way to avoid this.
Tidy Up
Our web application works - but it could be better!
TypeCoercing
When passing method arguments to Route Handlers, such as our page render() methods, BedSheet is clever about it. Using TypeCoercer from BeanUtils it tries to convert the Str segments from the request URL into whatever is needed by the route handlers.
That means our render() method could just take an Int as the ID parameter:
Text render(Int id) {
visit := visitService.get(id)
...
}
The conversion from Str to Int has now been taken out of our hands. So what happens now if we access a ViewPage with a non-numerical ID? Let's try it:

Wow, a 404 page! That's exactly what we want. We tried to access information about a Visit that doesn't exist. And because nothing exists for that particular URL, a 404 should be returned to the browser.
Don't worry about all that debug information being returned to your users. BedSheet only adds that when it is run in development mode. Try running BedSheet in production mode and refresh your browser.
Hint, change the -env parameter in Main.main(), or just omit it entirely. (BedSheet defaults to production mode when no env parameter is supplied.)
BsMain().main("-env prod bednap 8069".split)

ValueEncoders
Having URL segments type coerced to fit our method parameters is nice, but what if we could go further? What if BedSheet could just pass us our Visit entity?
Well, BedSheet can! We just need to tell it how! And that's the job of a ValueEncoder. A ValueEncoder converts an object to and from a Str. So we'll write one for converting Visit objects:
using afIoc::Inject
using afBedSheet::ValueEncoder
const class VisitEncoder : ValueEncoder {
@Inject private const VisitService visitService
new make(|This|in) { in(this) }
override Str toClient(Obj? value) {
visit := (Visit) value
return visit.id.toStr
}
override Obj? toValue(Str clientValue) {
id := clientValue.toInt
return visitService.get(id)
}
}
We can use IoC injection because we're going to autobuild an instance of VisitEncoder it when we contribute it to the ValueEncoders service in our AppModule:
@Contribute { serviceType=ValueEncoders# }
Void contributeValueEncoders(Configuration config) {
config[Visit#] = config.autobuild(VisitEncoder#)
}
Note how the config key is the object type that the encoder converts.
Our ViewPage now becomes greatly simplified, for it no longer needs to reference VisitService:
using afBedSheet::Text
using web::WebOutStream
const class ViewPage {
Text render(Visit visit) {
htmlBuf := StrBuf()
html := WebOutStream(htmlBuf.out)
...
return Text.fromHtml(htmlBuf.toStr)
}
}
Because BedSheet is now handling the entire conversion of a URL Str to a Visit instance, even out of range Int IDs now return a 404 page (and not an error page).

Update Index Page
As we've been concentrating on the ViewPage we've neglected the IndexPage! The summary table needs to be updated with links to the individual detail pages. This is where it becomes handy to have the ID bundled with the entity.
Text render() {
htmlBuf := StrBuf()
html := WebOutStream(htmlBuf.out)
...
visitService.all.each {
html.tr
html.td.w(it.name).tdEnd
html.td.w(it.date).tdEnd
html.td.w(it.rating).tdEnd
html.td
html.a(`/view/${it.id}`).w("view").aEnd
html.tdEnd
html.trEnd
}
...
return Text.fromHtml(htmlBuf.toStr)
}

Source Code
All the source code for this tutorial is available on the Bed Nap Tutorial Bitbucket Repository.
Code for this particular article is available on the 04-IDs-and-ValueEncoders branch.
Use the following commands to check the code out locally:
C:\> hg clone https://bitbucket.org/fantomfactory/bed-nap-tutorial C:\> cd bed-nap-tutorial C:\> hg update 04-IDs-and-ValueEncoders
Don't forget, you can trial the finished tutorial application at Bed Nap.
Have fun!
Edits
- 4 Aug 2016 - Updated tutorial to use BedSheet 1.5 & IoC 3.0.
- 4 Aug 2015 - Updated tutorial to use BedSheet 1.4.
- 29 Aug 2014 - Original article.