10.13 Customizing Binding of Resources

The framework provides a sophisticated but simple mechanism for binding REST requests to domain objects and command objects. One way to take advantage of this is to bind the request property in a controller the properties of a domain class. Given the following XML as the body of the request, the createBook action will create a new Book and assign "The Stand" to the title property and "Stephen King" to the authorName property.

  1. <?xml version="1.0" encoding="UTF-8"?>
  2. <book>
  3. <title>The Stand</title>
  4. <authorName>Stephen King</authorName>
  5. </book>
  1. class BookController {
  2. def createBook() {
  3. def book = new Book()
  4. book.properties = request
  5. // ...
  6. }
  7. }

Command objects will automatically be bound with the body of the request:

  1. class BookController {
  2. def createBook(BookCommand book) {
  3. // ...
  4. }
  5. }
  6. class BookCommand {
  7. String title
  8. String authorName
  9. }

If the command object type is a domain class and the root element of the XML document contains an id attribute, the id value will be used to retrieve the corresponding persistent instance from the database and then the rest of the document will be bound to the instance. If no corresponding record is found in the database, the command object reference will be null.

  1. <?xml version="1.0" encoding="UTF-8"?>
  2. <book id="42">
  3. <title>Walden</title>
  4. <authorName>Henry David Thoreau</authorName>
  5. </book>
  1. class BookController {
  2. def updateBook(Book book) {
  3. // The book will have been retrieved from the database and updated
  4. // by doing something like this:
  5. //
  6. // book == Book.get('42')
  7. // if(book != null) {
  8. // book.properties = request
  9. // }
  10. //
  11. // the code above represents what the framework will
  12. // have done. There is no need to write that code.
  13. // ...
  14. }
  15. }

The data binding depends on an instance of the DataBindingSource interface created by an instance of the DataBindingSourceCreator interface. The specific implementation of DataBindingSourceCreator will be selected based on the contentType of the request. Several implementations are provided to handle common content types. The default implementations will be fine for most use cases. The following table lists the content types which are supported by the core framework and which DataBindingSourceCreator implementations are used for each. All of the implementation classes are in the org.grails.databinding.bindingsource package.

Content Type(s)Bean NameDataBindingSourceCreator Impl.
application/xml, text/xmlxmlDataBindingSourceCreatorXmlDataBindingSourceCreator
application/json, text/jsonjsonDataBindingSourceCreatorJsonDataBindingSourceCreator
application/hal+jsonhalJsonDataBindingSourceCreatorHalJsonDataBindingSourceCreator
application/hal+xmlhalXmlDataBindingSourceCreatorHalXmlDataBindingSourceCreator

In order to provide your own DataBindingSourceCreator for any of those content types, write a class which implementsDataBindingSourceCreator and register an instance of that class in the Spring application context. If youare replacing one of the existing helpers, use the corresponding bean name from above. If you are providing ahelper for a content type other than those accounted for by the core framework, the bean name may be anything thatyou like but you should take care not to conflict with one of the bean names above.

The DataBindingSourceCreator interface defines just 2 methods:

  1. package org.grails.databinding.bindingsource
  2. import grails.web.mime.MimeType
  3. import grails.databinding.DataBindingSource
  4. /**
  5. * A factory for DataBindingSource instances
  6. *
  7. * @since 2.3
  8. * @see DataBindingSourceRegistry
  9. * @see DataBindingSource
  10. *
  11. */
  12. interface DataBindingSourceCreator {
  13. /**
  14. * `return All of the {`link MimeType} supported by this helper
  15. */
  16. MimeType[] getMimeTypes()
  17. /**
  18. * Creates a DataBindingSource suitable for binding bindingSource to bindingTarget
  19. *
  20. * @param mimeType a mime type
  21. * @param bindingTarget the target of the data binding
  22. * @param bindingSource the value being bound
  23. * @return a DataBindingSource
  24. */
  25. DataBindingSource createDataBindingSource(MimeType mimeType, Object bindingTarget, Object bindingSource)
  26. }

AbstractRequestBodyDataBindingSourceCreatoris an abstract class designed to be extended to simplify writing custom DataBindingSourceCreator classes. Classes whichextend AbstractRequestbodyDatabindingSourceCreator need to implement a method named createBindingSourcewhich accepts an InputStream as an argument and returns a DataBindingSource as well as implementing the getMimeTypesmethod described in the DataBindingSourceCreator interface above. The InputStream argument to createBindingSourceprovides access to the body of the request.

The code below shows a simple implementation.

src/main/groovy/com/demo/myapp/databinding/MyCustomDataBindingSourceCreator.groovy

  1. package com.demo.myapp.databinding
  2. import grails.web.mime.MimeType
  3. import grails.databinding.DataBindingSource
  4. import org...databinding.SimpleMapDataBindingSource
  5. import org...databinding.bindingsource.AbstractRequestBodyDataBindingSourceCreator
  6. /**
  7. * A custom DataBindingSourceCreator capable of parsing key value pairs out of
  8. * a request body containing a comma separated list of key:value pairs like:
  9. *
  10. * name:Herman,age:99,town:STL
  11. *
  12. */
  13. class MyCustomDataBindingSourceCreator extends AbstractRequestBodyDataBindingSourceCreator {
  14. @Override
  15. public MimeType[] getMimeTypes() {
  16. [new MimeType('text/custom+demo+csv')] as MimeType[]
  17. }
  18. @Override
  19. protected DataBindingSource createBindingSource(InputStream inputStream) {
  20. def map = [:]
  21. def reader = new InputStreamReader(inputStream)
  22. // this is an obviously naive parser and is intended
  23. // for demonstration purposes only.
  24. reader.eachLine { line ->
  25. def keyValuePairs = line.split(',')
  26. keyValuePairs.each { keyValuePair ->
  27. if(keyValuePair?.trim()) {
  28. def keyValuePieces = keyValuePair.split(':')
  29. def key = keyValuePieces[0].trim()
  30. def value = keyValuePieces[1].trim()
  31. map<<key>> = value
  32. }
  33. }
  34. }
  35. // create and return a DataBindingSource which contains the parsed data
  36. new SimpleMapDataBindingSource(map)
  37. }
  38. }

An instance of MyCustomDataSourceCreator needs to be registered in the spring application context.

grails-app/conf/spring/resources.groovy

  1. beans = {
  2. myCustomCreator com.demo.myapp.databinding.MyCustomDataBindingSourceCreator
  3. // ...
  4. }

With that in place the framework will use the myCustomCreator bean any time a DataBindingSourceCreator is neededto deal with a request which has a contentType of "text/custom+demo+csv".