Code for this blog can be found on github at
https://github.com/galapagosfinch

Friday, June 10, 2011

Grails Controllers and REST, part 2

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.



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 XMLJSON, 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.

4 comments:

  1. Part 3 is now available, discussing functional tests for REST.

    ReplyDelete
  2. during
    if (p.version) {
    def version = p.version.toLong()
    if (cowInstance.version > version) {

    another user may update the cow

    ReplyDelete
    Replies
    1. 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.

      Delete
    2. Whoops, I meant "cowInstance.merge(flush: true)" instead of "p.save(flush: true".

      Delete