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
- Download the latest version of the plugin
- cd to your project root
- run grails install-plugin <path-to-file grails-extended-data-binding-X.X.zip
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.GrailsDataBinderclass 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.CustomDateEditorclass 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 _PropertyEditor{_}s. 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.
No Comments Yet
Post a Comment
Site Login