Extended Data Binding Plugin

  • Authors : null
1 vote
Dependency :
compile ":extended-data-binding:0.5"

Documentation

Summary

Installation

In your application directory, run: grails install-plugin extended-data-binding

Description

Extended Data Binding Plugin

The extended data binding plugin allows configuring the DataBinder which controllers will use to parse the user input and populate objects, as well as wrapping objects to format data as strings in order to display data.

Contents

Features

  • Allow customization of the DataBinder that will be used to parse user-defined input and populate objects (typically domain objects) with custom PropertyEditors on both application-wide and controller-specific levels.
  • The same property editors are also used on BeanWrappers which will be used by a custom utility class, org.
  • Provide an utility class, br.inf.freeit.extendeddatabinding.WrappedBean, which uses a BeanWrapper to access properties and to retrieve PropertyEditors to format data as String.
  • Extend controllers with dynamic methods to allow data binding and bean wrapping.
  • Provide a custom tag to wrap beans on views.

Installation

In your application directory, run: grails install-plugin extended-data-binding

Application-wide DataBinder and BeanWrapper configuration

In order to configure application-wide PropertyEditors, you should set closures under the ServletContext (application) scope, under the following attributes:

  • newDataBinder: Takes the request and the object as parameters and should return a GrailsDataBinder instance
  • newBeanWrapper: Also takes the request and the object and should return a BeanWrapper instance
Those closures are optional, and the plugin will fall-back to a default GrailsDataBinder and a default BeanWrapperImpl

Controller-specific DataBinder and BeanWrapper configuration

To configure the DataBinder and BeanWrapper on the controller, you can define the following methods:

  • registerCustomEditors: Invoked to configure both DataBinder and BeanWrapper. Should be used to register PropertyEditors that are specific for that controller
  • initBinder: Only invoked when configuring a DataBinder, should be used to initialize other properties on the DataBinder, such as setDisallowedFields.

Controllers dynamic methods

This plugin adds some methods to controllers:

  • getBinder: Takes an object and returns a DataBinder for that object, setting it on the request under the attribute dataBinder
  • wrapBean: Takes an object and returns a WrappedBean instance. A WrappedBean is an utility class that uses a BeanWrapper and converts properties to String using registered PropertyEditors
  • bind: Takes an object, performs the data binding (using the getBinder() method) and returns the object itself

Custom tags

The following tags are added to the default g namespace:

  • wrap: creates a WrappedBean and exports it to a variable on a given scope. It accepts the following attributes:
    • bean: The object instance (required)
    • var: An attribute on a given scope to export the WrappedBean instance (required)
    • scope: The scope to export the WrappedBean instance (optional, defaults to page)
  • eachWrapped: works like g:each, but wraps each result before exporting it to a variable. Attributes:
    • in: The collection to iterate (required)
    • var: The name of the variable that will hold the wrapped instance (required)
    • status: The name of the variable that will hold the current loop index (optional)

Example

Here is an example application, that uses a sample domain object called Person, as follows:

class Person {
    String name
    Calendar birthDate
    BigDecimal income

static constraints = { name maxSize: 100 birthDate nullable: true income nullable: true } }

In order to edit and show the calendar according to the requested locale, the class src/groovy/GlobalPropertyEditorConfig was created:
import java.text.DateFormat
import java.text.DecimalFormat
import java.text.DecimalFormatSymbols
import org.springframework.beans.BeanWrapperImpl
import org.springframework.beans.propertyeditors.CustomNumberEditor
import org.codehaus.groovy.grails.web.binding.GrailsDataBinder

class GlobalPropertyEditorConfig { static newDataBinder = { request, object -> def binder = GrailsDataBinder.createBinder(object, GrailsDataBinder.DEFAULT_OBJECT_NAME, request) registerCustomEditors(request, binder) return binder }

static newBeanWrapper = { request, object -> def beanWrapper = new BeanWrapperImpl(object) registerCustomEditors(request, beanWrapper) return beanWrapper }

private static void registerCustomEditors(request, binder) { def numberFormat = new DecimalFormat("#,##0.00", new DecimalFormatSymbols(request.locale)) binder.registerCustomEditor(BigDecimal.class, new CustomNumberEditor(BigDecimal.class, numberFormat, true))

def dateFormat = DateFormat.getDateInstance(DateFormat.SHORT, request.locale) dateFormat.lenient = false binder.registerCustomEditor(Calendar.class, new CalendarEditor(dateFormat, true)) } }

The most relevant methods are newDataBinder and newBeanWrapper, as they will be used later. Both methods use a common one called registerCustomEditors.

Also, a custom PropertyEditor for Calendars is in src/groovy/CalendarEditor:

import java.text.DateFormat
import org.springframework.beans.propertyeditors.CustomDateEditor

class CalendarEditor extends CustomDateEditor { public CalendarEditor(DateFormat dateFormat, boolean allowEmpty) { super(dateFormat, allowEmpty) }

public void setAsText(String text) { super.setAsText(text) def value = this.value if (value instanceof Date) { Calendar cal = Calendar.instance cal.time = value this.value = cal } }

public String getAsText() { def value = this.value if (value instanceof Calendar) { this.value = value.time } return super.getAsText() } }

Then, we need to instruct the plugin to use both GlobalPropertyEditorConfig methods, and this is done here on the grails-app/conf/BootStrap class (better ideas?). Here is it:
class BootStrap {

def init = { servletContext -> servletContext.setAttribute("newDataBinder", GlobalPropertyEditorConfig.&newDataBinder) servletContext.setAttribute("newBeanWrapper", GlobalPropertyEditorConfig.&newBeanWrapper)

new Person(name:"John", birthDate:new GregorianCalendar(1970, 0, 18), income:5609.87).save() }

def destroy = { } }

Notice that both newDataBinder and newBeanWrapper static methods from GlobalPropertyEditorConfig are referenced and stored on the servletContext under those same attribute names (these names are required by the plugin). For the matter of example, a new person is also created.

That's all global configuration that is needed. Now, let's take a look on the PersonController class:

class PersonController {

def index = { redirect(action:list,params:params) }

// the delete, save and update actions only accept POST requests def allowedMethods = [delete:'POST', save:'POST', update:'POST']

def list = { if(!params.max) params.max = 10 [ personList: Person.list( params ) ] }

def show = { def person = Person.get( params.id )

if(!person) { flash.message = "Person not found with id ${params.id}" redirect(action:list) } else { return [ person : wrapBean(person) ] } }

def delete = { def person = Person.get( params.id ) if(person) { person.delete() flash.message = "Person ${params.id} deleted" redirect(action:list) } else { flash.message = "Person not found with id ${params.id}" redirect(action:list) } }

def edit = { def person = Person.get( params.id )

if(!person) { flash.message = "Person not found with id ${params.id}" redirect(action:list) } else { return [ person : wrapBean(person) ] } }

def update = { def person = Person.get( params.id ) if(person) { bind(person) if(!person.hasErrors() && person.save()) { flash.message = "Person ${params.id} updated" redirect(action:show,id:person.id) } else { render(view:'edit',model:[person:wrapBean(person)]) } } else { flash.message = "Person not found with id ${params.id}" redirect(action:edit,id:params.id) } }

def create = { def person = bind(new Person()) return ['person':wrapBean(person)] }

def save = { def person = bind(new Person()) if(!person.hasErrors() && person.save()) { flash.message = "Person ${person.id} created" redirect(action:show,id:person.id) } else { render(view:'create',model:[person:wrapBean(person)]) } } }

The controller is much like the vanilla class generated by the default scaffolding, but instead of using the model.properties = params statement, the bind(model) is used. Also, whenever a person is returned inside the model hash, a wrapBean(model) is used, which stores an instance of org.extendeddatabinding.WrappedBean, which converts properties to strings using registered PropertyEditors. So, the view needs no modification, as the normal output of model.property on the GSP page will be a formatted text. The one exception is the list, which uses the <g:eachWrapped> tag instead of <g:each> to wrap each model on the list.

Also, this model is using a custom editor for a specific model property. In this case, the person's name will be trimmed before saving. This demonstrates customizing both DataBinder and BeanWrapper in a controller-level.