:idea: I've updated petclinic to switch between two meanings of required:- :arrow: validate as though request parameter was required regardless of whether the request parameter existed.
- :arrow: validate required only if request parameter exists.
This functionality is implemented as follows: - :arrow: AbstractClinicForm overrides createBinder() to create SchemaDataBinder.
- :arrow: SchemaDataBinder which subclasses ServletRequestDataBinder to implement bind(PropertyValues) as described above
Listing of SchemaDataBinder
Particularly note the highlighted (********) section:
Code:
package org.springframework.validation;
import java.util.Arrays;
import java.util.List;
import org.springframework.beans.MutablePropertyValues;
import org.springframework.beans.PropertyAccessException;
import org.springframework.beans.PropertyAccessExceptionsException;
import org.springframework.beans.PropertyValue;
import org.springframework.beans.PropertyValues;
import org.springframework.web.bind.ServletRequestDataBinder;
/**
* This data binder ignores the "required" validation
* if the value to be validated is not returned in the
* request parameters.
*
* @author Blotto [scott.smith@wamu.net]
* @version $Id$
*/
public class SchemaDataBinder extends ServletRequestDataBinder
{
/**
* If true, then if a required field isn't represented
* in the request parameters, then don't apply the required validation.
* If false, then apply the required validation in all cases.
*/
private boolean validateExisting;
/**
* The constructor.
*
* @param target target object to bind onto
* @param objectName objectName of the target object
* @param validateExisting whether to test only existing
* request parameters
*/
public SchemaDataBinder(Object target, String objectName, boolean validateExisting)
{
super(target, objectName);
this.validateExisting = validateExisting;
}
/**
* Like the super method except that this
* one doesn't create a required validation error if the
* value to be validated isn't in the request parameters.
*
* @param pvs property values to bind.
* @see DataBinder#bind(PropertyValues)
*/
public void bind(PropertyValues pvs) {
// check for fields to bind
String[] allowedFields = getAllowedFields();
List allowedFieldsList = (allowedFields != null) ? Arrays.asList(allowedFields) : null;
MutablePropertyValues mpvs = (pvs instanceof MutablePropertyValues) ?
(MutablePropertyValues) pvs : new MutablePropertyValues(pvs);
PropertyValue[] pvArray = pvs.getPropertyValues();
for (int i = 0; i < pvArray.length; i++) {
String field = pvArray[i].getName();
if (!((allowedFieldsList != null && allowedFieldsList.contains(field)) || isAllowed(field))) {
mpvs.removePropertyValue(pvArray[i]);
}
}
pvs = mpvs;
// check for missing fields
String[] requiredFields = getRequiredFields();
if (requiredFields != null) {
for (int i = 0; i < requiredFields.length; i++) {
PropertyValue pv = pvs.getPropertyValue(requiredFields[i]);
// *************************************************
// Here is where the modification is performed:
boolean requiredFailed = (validateExisting)
? (pv != null && "".equals(pv.getValue()))
: (pv == null || "".equals(pv.getValue()) || pv.getValue() == null);
if (requiredFailed) {
// End of modification
// *************************************************
// create field error with code "required"
String field = requiredFields[i];
BindException errors = getErrors();
errors.addError(
new FieldError(errors.getObjectName(), field, "", true,
errors.resolveMessageCodes(MISSING_FIELD_ERROR_CODE, field),
getArgumentsForBindingError(field), "Field '" + field + "' is required"));
}
}
}
try {
// bind request parameters onto params, ignoring unknown properties
getErrors().getBeanWrapper().setPropertyValues(pvs, true);
}
catch (PropertyAccessExceptionsException ex) {
PropertyAccessException[] exs = ex.getPropertyAccessExceptions();
for (int i = 0; i < exs.length; i++) {
// create field with the exceptions's code, e.g. "typeMismatch"
String field = exs[i].getPropertyChangeEvent().getPropertyName();
BindException errors = getErrors();
errors.addError(
new FieldError(errors.getObjectName(), field, exs[i].getPropertyChangeEvent().getNewValue(), true,
errors.resolveMessageCodes(exs[i].getErrorCode(), field),
getArgumentsForBindingError(field), exs[i].getLocalizedMessage()));
}
}
}
}
Here are the modified methods in AbstractClinicForm.
First, the property which defines which
required behavior is in effect:
Code:
private boolean requiredValidateExisting;
public boolean isRequiredValidateExisting()
{
return requiredValidateExisting;
}
/**
* sets whether or not to suppress <b>required</b> validation
* when the value to be validated isn't in the HTTP request
* parameters.
*
* @param requiredValidateExisting
*/
public void setRequiredValidateExisting(boolean requiredValidateExisting)
{
this.requiredValidateExisting = requiredValidateExisting;
}
Here's the override to utilize a different binder object:
Code:
/**
* Create a new binder instance for the given command and request.
* Called by bindAndValidate. This overrides the custom
* ServletRequestDataBinder with a SchemaDataBinder.
* @param request current HTTP request
* @param command the command to bind onto
* @return the new binder instance
* @throws Exception in case of invalid state or arguments
* @see #bindAndValidate
* @see #initBinder
* @see #setMessageCodesResolver
*/
protected ServletRequestDataBinder createBinder(HttpServletRequest request, Object command)
throws Exception {
SchemaDataBinder binder = new SchemaDataBinder(command, getCommandName(), requiredValidateExisting);
MessageCodesResolver messageCodesResolver = getMessageCodesResolver();
if (messageCodesResolver != null) {
binder.setMessageCodesResolver(messageCodesResolver);
}
initBinder(request, binder);
return binder;
}
Here is initBinder() which adds the required fields from the schema:
Code:
/**
* Set up a custom property editor for the application's date format.
* Also, retrieve the "required" fields from the corresponding not-null
* columns in the database schema.
*/
protected void initBinder(HttpServletRequest request, ServletRequestDataBinder binder)
throws Exception {
super.initBinder(request, binder);
registerDateFormat(binder);
registerRequired(request, binder);
}
/**
* Retrieves the "required" fields from the corresponding not-null
* columns in the database schema.
*
* @param binder what binder to assign the "required" fields to.
*/
private void registerRequired(HttpServletRequest request, ServletRequestDataBinder binder)
throws Exception
{
Object currentDataObject = binder.getTarget();
String requiredPropertyNames[] = getSchemaInfo().requiredFields(currentDataObject.getClass());
binder.setRequiredFields(requiredPropertyNames);
}
/**
* registers a date property editor
*
* @param binder the binder to register to.
*/
protected void registerDateFormat(ServletRequestDataBinder binder)
{
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
dateFormat.setLenient(false);
binder.registerCustomEditor(Date.class, null, new CustomDateEditor(dateFormat, false));
}
Finally, the section of petclinic-servlet.xml that configures AbstractClinicForm:
Code:
<bean id="abstractClinicForm" abstract="true"
class="org.springframework.samples.petclinic.web.AbstractClinicForm">
<!-- The class that implements the SchemaInfo interface to -->
<!-- answer the required fields for this data object: -->
<property name="schemaInfo"><ref bean="hibernateSchemaInfo"/></property>
<!-- Whether or not to ignore "required" validation of values -->
<!-- that are not in the request parameters -->
<property name="requiredValidateExisting"><value>true</value></property>
</bean>
This code has been tested and works for HTML input forms. It has not yet been checked w/ checkboxes and radio buttons.