Search code examples
grailsgroovyhttprequestgrails-controller

Can't read PUT XML request in Grails controller


I'm trying to parse XML in a Grails controller - I can successfully parse the result of a GET but when receiving a PUT, I can't get the values out of the request. Code follows.

Test: (PUTs a dummy Person so that I can test the parsing and saving)

import grails.test.mixin.*
import grails.test.mixin.domain.DomainClassUnitTestMixin
import org.junit.*
import com.mycompany.stuff.Person

@TestFor(ServiceController)
@TestMixin(DomainClassUnitTestMixin)
class ServiceControllerTests {
    void testCreateWithXML() {
        mockDomain(Person)
        request.method = "PUT"
        def controller = new ServiceController()
        controller.request.contentType = 'text/xml'
        controller.request.content = '''
                <person>
                    <refId>123-abc</refId>
                    <otherThing>some stuff</otherThing>
                </person>
                '''.stripIndent().getBytes() // note we need the bytes (copied from docs)
        def response = controller.create()
        assert Person.count() == 1
        assertEquals "123-abc", Person.get("123-abc").id
    }
}

Controller: Receives the put (correctly) after being mapped to the create method.

class ServiceController {
...
    def create() {
        if (request.format != "xml") {
            render 406 // Only XML expected
            return
        }

        def requestBody = request.XML
        def objectType  = requestBody.name() as String
        log.info "Received ${objectType} - ${requestBody}"
        if (!(objectType.toLowerCase() in ['person','personsubtype']))
        {
                render (status: 400, text: 'Unknown object type received in PUT')
                return
        }

        def person      = new Person(id: requestBody.person.refId.text())
        person.save()

        log.info "Saved ${person}"
        render 200
    }

Using the debugger, I can see that when the request is received, the variable requestBody is received as a NodeChild, and the name() is correct. I can also see that the requestBody.person.refId variable's metaClass is of GPathResult... yet the .text() (and .toString()) always return null. The first log.info prints the output:

2013-07-09 20:04:07,862 [main] INFO  client.ServiceController  - Received person - 123-abcsome stuff

so I know that the contents came across.

Any and all suggestions appreciated. I've been trying this for some time and am at my wits' end.


Solution

  • You are accessing refId inappropriately from requestBody. In your case <person> is itself represented by requestBody.

    requestBody.refId.text() will give you 123-abc.

    The controller implementation and the test case should be written this way:

    def create() {
            if (request.format != "xml") {
                render 406 // Only XML expected
                return
            }
    
            def requestBody = request.XML
            def objectType  = requestBody.name() as String
    
            //You can see here objectType is person which signifies
            //requestBody is represented as the parent tag <person> 
            log.info "Received ${objectType} - ${requestBody}"
            if (!(objectType.toLowerCase() in ['person','personsubtype'])) {
                render (status: 400, text: 'Unknown object type received in PUT')
                return
            }
    
            //Since <person> is represented by requestBody, 
            //refId can be fetched directly from requestBody
            def person = new Person(id: requestBody.refId.text())
            person.save()
    
            log.info "Saved ${person}"
            render 200
        }
    

    Test Class can be optimized and unwanted items can be removed:-

    //Test Class can be optimized
    import grails.test.mixin.*
    import org.junit.*
    import com.mycompany.stuff.Person
    
    @TestFor(ServiceController)
    //@Mock annotation does the mocking for domain classes
    @Mock(Person)
    class ServiceControllerTests {
        void testCreateWithXML() {
            //mockDomain(Person) //Not required, taken care by @Mock
            request.method = "PUT"
            //No need to initialize controller 
            //as @TestFor will provide controller.
            //def controller = new ServiceController()
            controller.request.contentType = 'text/xml'
            controller.request.content = '''
                    <person>
                        <refId>123-abc</refId>
                        <otherThing>some stuff</otherThing>
                    </person>
                    '''.stripIndent().getBytes() // note we need the bytes (copied from docs)
            controller.create()
    
            assert controller.response.contentAsString == 200
            assert Person.count() == 1
            assertEquals "123-abc", Person.get("123-abc").id
        }
    }