In part one of this series, I looked at doing REST-ful GET commands for lists and elements by embedding the code in the same functions used for HTML. That was the easy part - expressing data as XML or JSON and rendering "Not Found" messages. In this part, we will look at the REST-ful ways to modify the system, through PUT, POST, and DELETE.
Let's start with a review of the HTTP verbs and how they relate to REST:
- GET
- This one is pretty simple: GET is a read request, and returns the resource specified by the URI.
- DELETE
- DELETE will remove the resource from the server. 'Nuff said.
- PUT
- PUT is more or less equivalent to an UPDATE, or an INSERT in some cases. By strict interpretation, PUT requires that the resource be modified as a whole, and calls are considered idempotent, which means, repeating the same call over and over will have the same effect. This is important- if a system generates identifiers which are part of the resource, then PUT should probably not be used as an INSERT. After all, repeated calls that do not contain the identifier would need to be reconciled with previous calls in order to avoid multiple inserts.
- POST
- "The POST method is used to request that the origin server accept the entity enclosed in the request as a new subordinate of the resource identified by the Request-URI in the Request-Line." (w3c) Ummm, what? In layman's terms, a POST can be used to modify a portion of a resource (like adding elements to a list), or to submit a block of data (like the contents of a form). Because its definition is so generic, it can be used for RPC-style function calls as well.
(Sidebar: before you go emailing me that you don't agree with the POST vs PUT interpretation, this is *my* interpretation of the spec. There are many bright people who agree, and many bright people who disagree. Too bad the w3c didn't write the spec more definitively...)
Now, on to the code.
Now, on to the code.
We did GET last time, so on to DELETE. First, we'll add "DELETE" to the
/rest/../element
UrlMappings:"/rest/$controller/element/$id"{ action = [GET:"show", DELETE: "delete"] }
Our controller starts with the standard, auto-generated flow: Get the cow to delete; if it's there, try to delete it, reporting back either success or failure; if it's not there, report back "not found".
def delete = { def cowInstance = Cow.get(params.id) if (cowInstance) { try { cowInstance.delete(flush: true) flash.message = "${message(code: 'default.deleted.message', args: [message(code: 'cow.label', default: 'Cow'), params.id])}" redirect(action: "list") } catch (org.springframework.dao.DataIntegrityViolationException e) { flash.message = "${message(code: 'default.not.deleted.message', args: [message(code: 'cow.label', default: 'Cow'), params.id])}" redirect(action: "show", id: params.id) } } else { flash.message = "${message(code: 'default.not.found.message', args: [message(code: 'cow.label', default: 'Cow'), params.id])}" redirect(action: "list") } }
First,we'll convert the happy path, the
try
block:try { cowInstance.delete(flush: true) withFormat { form { flash.message = "${message(code: 'default.deleted.message', args: [message(code: 'cow.label', default: 'Cow'), params.id])}" redirect(action: "list") } xml { response.status = 200 // OK render "${message(code: 'default.deleted.message', args: [message(code: 'cow.label', default: 'Cow'), params.id])}" } json { response.status = 200 // OK render "${message(code: 'default.deleted.message', args: [message(code: 'cow.label', default: 'Cow'), params.id])}" } } }
We add the
withFormat
block to differentiate XML
, JSON
, and... form
? What happened to html
? Well, the delete
method is accessed via a POST under normal circumstances, so the incoming format will be application/x-www-form-urlencoded
. That translates to form
in Config.groovy
. So, the normal html
block gets replaced in our withFormat
code.Oh, that reminds me - for REST, we won't be accessing
delete
via POST, will we? So, we need to make a change to the top of the controller, too:static allowedMethods = [save: "POST", update: "POST", delete: ["POST", "DELETE"]]
We add
DELETE
to the list of HTTP verbs allowed to access the delete
method. Otherwise, Grails will respond with a 405
response code (Method Not Allowed).For GET commands, we would always end a success block with
render cowInstance as blah
, but for DELETE, we have no cowInstance to render - we just deleted it! So, instead, we fill the gap with a simple message saying the delete was successful.Now, let's look at the unhappy path:
catch (org.springframework.dao.DataIntegrityViolationException e) { withFormat { form { flash.message = "${message(code: 'default.not.deleted.message', args: [message(code: 'cow.label', default: 'Cow'), params.id])}" redirect(action: "show", id: params.id) } xml { response.status = 409 // Conflict render "${message(code: 'default.not.deleted.message', args: [message(code: 'cow.label', default: 'Cow'), params.id])}" } json { response.status = 409 // Conflict render "${message(code: 'default.not.deleted.message', args: [message(code: 'cow.label', default: 'Cow'), params.id])}" } } }
More of the same.
form
feedback for normal HTML access, xml
and json
feedback for REST access.Let's finish it off with the "not found route":
else { withFormat renderNotFound }
Is this going to work?
renderNotFound
needs some editing, too, to add the form
block.def renderNotFound = { html { flash.message = "${message(code: 'default.not.found.message', args: [message(code: 'cow.label', default: 'Cow'), params.id])}" redirect(action: "list") } form { flash.message = "${message(code: 'default.not.found.message', args: [message(code: 'cow.label', default: 'Cow'), params.id])}" redirect(action: "list") } xml { response.status = 404 render "Cow ${params.id} not found." } json { response.status = 404 render "Cow ${params.id} not found." } }
(Anybody have a suggestion about compressing the equivalent blocks, short of nesting closures?)
On to
PUT
and POST
, for update
and save
, respectively. Add update
to the .../element/...
UrlMappings:"/rest/$controller/element/$id"(parseRequest:true){ action = [GET:"show", DELETE: "delete", PUT: "update"] }
and add
save
to the .../list
UrlMappings:"/rest/$controller/list"(parseRequest:true){
action = [GET:"list", POST: "save"] }
Yes, I added
POST
to the .../list
mapping, so we are implementing saves using the "adding elements to the list" paradigm. See my dissertation above...Notice the addition of
parseRequest
. It must be set to true
in these instances; otherwise, Grails will not interpret the payload in order to pass the values to the function via the params block.The
save
method is the easier of the two. It's original flow is "attempt to save; if success, report success; if failure, report failure". Yes, it's that easy.def save = { def cowInstance = new Cow(params) if (cowInstance.save(flush: true)) { flash.message = "${message(code: 'default.created.message', args: [message(code: 'cow.label', default: 'Cow'), cowInstance.id])}" redirect(action: "show", id: cowInstance.id) } else { render(view: "create", model: [cowInstance: cowInstance]) } }
We can translate this to our new paradigm pretty easily:
def save = { def cowInstance = new Cow(request.format == 'xml' ? params.cow : params) if (cowInstance.save(flush: true)) { withFormat { form { flash.message = "${message(code: 'default.created.message', args: [message(code: 'cow.label', default: 'Cow'), cowInstance.id])}" redirect(action: "show", id: cowInstance.id) } xml { response.status = 200 // OK render cowInstance as XML } json { response.status = 200 // OK render cowInstance as JSON } } } else { withFormat { form { render(view: "create", model: [cowInstance: cowInstance]) } xml { response.status = 400 // Bad Request render cowInstance.errors as XML } json { response.status = 400 // Bad Request render cowInstance.errors as JSON } } } }
Notice that the Cow instance is created using conditional inputs. This is because XML input will be inside a list, whereas JSON parameters will be directly injected into the params block. (Ed: anyone know a elegant way to get the XML directly injected into the params block?)
Success is mapped to a 200 with the new cowInstance rendered in the response; failure is mapped to a 400 (Bad Request) with the errors collection rendered to the response. (There are prettier ways to render the validation errors; perhaps we'll return to that later.)
The
update
method is more involved, as there are more things that can go wrong.def update = { def cowInstance = Cow.get(params.id) if (cowInstance) { if (params.version) { def version = params.version.toLong() if (cowInstance.version > version) { cowInstance.errors.rejectValue("version", "default.optimistic.locking.failure", [message(code: 'cow.label', default: 'Cow')] as Object[], "Another user has updated this Cow while you were editing") render(view: "edit", model: [cowInstance: cowInstance]) return } } cowInstance.properties = params if (!cowInstance.hasErrors() && cowInstance.save(flush: true)) { flash.message = "${message(code: 'default.updated.message', args: [message(code: 'cow.label', default: 'Cow'), cowInstance.id])}" redirect(action: "show", id: cowInstance.id) } else { render(view: "edit", model: [cowInstance: cowInstance]) } } else { flash.message = "${message(code: 'default.not.found.message', args: [message(code: 'cow.label', default: 'Cow'), params.id])}" redirect(action: "list") } }
The gist of it is, "get the instance to be updated; if it exists, check for optimistic lock failures, do the update, then report success or failure; if it doesn't exist, then report "Not Found".
def update = { def p = (request.format == 'xml' ? params.cow : params) def cowInstance = Cow.get(params.id) if (cowInstance) { if (p.version) { def version = p.version.toLong() if (cowInstance.version > version) { cowInstance.errors.rejectValue("version", "default.optimistic.locking.failure", [message(code: 'cow.label', default: 'Cow')] as Object[], "Another user has updated this Cow while you were editing") withFormat render409orEdit.curry(cowInstance) return } } cowInstance.properties = p if (!cowInstance.hasErrors() && cowInstance.save(flush: true)) { withFormat { form { flash.message = "${message(code: 'default.updated.message', args: [message(code: 'cow.label', default: 'Cow'), cowInstance.id])}" redirect(action: "show", id: cowInstance.id) } xml { response.status = 200 // OK render cowInstance as XML } json { response.status = 200 // OK render cowInstance as JSON } } } else { withFormat render409orEdit.curry(cowInstance) } } else { withFormat renderNotFound } }
where
render409orEdit
looks like:def render409orEdit = { cowInstance -> form { render(view: "edit", model: [cowInstance: cowInstance]) } xml { response.status = 409 // Conflict render cowInstance.errors.allErrors as XML } json { response.status = 409 // Conflict render cowInstance.errors.allErrors as JSON } }
Same flow as the original code, now with XML and JSON sprinkled in.
I've shown that it is possible to join the REST methods into the standard HTML methods, typically with minimal increase in complexity. And with a little refactoring, like I've shown with
renderNotFound
and render404orEdit
, we can increase the readability of the methods altogether. (And with a little curry sprinkled on top, we can make it even more generic. Maybe we'll come back to that later...)But wait - how do I know all this stuff works? Oh, look, we're out of time. I guess we'll have to take up testing in part 3 of our series, showing how to use
curl
for manual testing, as well as automated functional tests.
Part 3 is now available, discussing functional tests for REST.
ReplyDeleteduring
ReplyDeleteif (p.version) {
def version = p.version.toLong()
if (cowInstance.version > version) {
another user may update the cow
Good point! And the "p.save()" call won't properly deal with that situation. "p.merge(flush:true)" would have been better, coupled with catching OptimisticLockingFailureException.
DeleteWhoops, I meant "cowInstance.merge(flush: true)" instead of "p.save(flush: true".
Delete