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.