
org.springframework.validation.DataBinder Maven / Gradle / Ivy
/*
* Copyright 2002-2005 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.validation;
import java.beans.PropertyEditor;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.BeanWrapper;
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.util.StringUtils;
/**
* Binder that allows for binding property values to a target object.
* The binding process can be customized through specifying allowed fields,
* required fields, and custom editors.
*
* Note that there are potential security implications in failing to set
* an array of allowed fields. In the case of HTTP form POST data for example,
* malicious clients can attempt to subvert an application by supplying values
* for fields or properties that do not exist on the form. In some cases this
* could lead to illegal data being set on command objects or their nested
* objects. For this reason, it is highly recommended to specify the
* {@link #setAllowedFields allowedFields} property on the DataBinder.
*
*
The binding results can be examined via the Errors interface,
* available as BindException instance. Missing field errors and property
* access exceptions will be converted to FieldErrors, collected in the
* Errors instance, with the following error codes:
*
*
* - Missing field error: "required"
*
- Type mismatch error: "typeMismatch"
*
- Method invocation error: "methodInvocation"
*
*
* Custom validation errors can be added afterwards. You will typically
* want to resolve such error codes into proper user-visible error messages;
* this can be achieved through resolving each error via a MessageSource.
* The list of message codes to try can be customized through the
* MessageCodesResolver strategy. DefaultMessageCodesResolver's javadoc
* gives details on the default resolution rules.
*
*
By default, binding errors are resolved through the binding error processor
* for required binding errors and property access exceptions. You can override
* those if needed, for example to generate different error codes.
*
*
This generic data binder can be used in any sort of environment.
* It is heavily used by Spring's web MVC controllers, via the subclass
* org.springframework.web.bind.ServletRequestDataBinder
.
*
* @author Rod Johnson
* @author Juergen Hoeller
* @see #setAllowedFields
* @see #setRequiredFields
* @see #registerCustomEditor
* @see #setMessageCodesResolver
* @see #setBindingErrorProcessor
* @see #bind
* @see #getErrors
* @see DefaultMessageCodesResolver
* @see DefaultBindingErrorProcessor
* @see org.springframework.context.MessageSource
* @see org.springframework.web.bind.ServletRequestDataBinder
*/
public class DataBinder {
/**
* We'll create a lot of DataBinder instances: Let's use a static logger.
*/
protected static final Log logger = LogFactory.getLog(DataBinder.class);
private final BindException errors;
private boolean ignoreUnknownFields = true;
private String[] allowedFields;
private String[] requiredFields;
private BindingErrorProcessor bindingErrorProcessor = new DefaultBindingErrorProcessor();
/**
* Create a new DataBinder instance.
* @param target target object to bind onto
* @param objectName name of the target object
*/
public DataBinder(Object target, String objectName) {
this.errors = createErrors(target, objectName);
}
/**
* Create a new Errors instance for this data binder.
* Can be overridden in subclasses to.
* Needs to be a subclass of BindException.
* @param target target object to bind onto
* @param objectName name of the target object
* @return the Errors instance
* @see #close
*/
protected BindException createErrors(Object target, String objectName) {
return new BindException(target, objectName);
}
/**
* Return the wrapped target object.
*/
public Object getTarget() {
return this.errors.getTarget();
}
/**
* Return the name of the bound object.
*/
public String getObjectName() {
return this.errors.getObjectName();
}
/**
* Return the Errors instance for this data binder.
* @return the Errors instance, to be treated as Errors or as BindException
* @see Errors
*/
public BindException getErrors() {
return errors;
}
/**
* Return the underlying BeanWrapper of the Errors object.
* To be used by binder subclasses that need bean property checks.
*/
protected BeanWrapper getBeanWrapper() {
return this.errors.getBeanWrapper();
}
/**
* Set whether to ignore unknown fields, i.e. whether to ignore request
* parameters that don't have corresponding fields in the target object.
*/
public void setIgnoreUnknownFields(boolean ignoreUnknownFields) {
this.ignoreUnknownFields = ignoreUnknownFields;
}
/**
* Return whether to ignore unknown fields, i.e. whether to ignore request
* parameters that don't have corresponding fields in the target object.
*/
public boolean isIgnoreUnknownFields() {
return ignoreUnknownFields;
}
/**
* Register fields that should be allowed for binding. Default is all
* fields. Restrict this for example to avoid unwanted modifications
* by malicious users when binding HTTP request parameters.
*
Supports "xxx*" and "*xxx" patterns. More sophisticated matching
* can be implemented by overriding the isAllowed method.
* @param allowedFields array of field names
* @see org.springframework.web.bind.ServletRequestDataBinder
* @see #isAllowed
*/
public void setAllowedFields(String[] allowedFields) {
this.allowedFields = allowedFields;
if (logger.isDebugEnabled()) {
logger.debug("DataBinder restricted to binding allowed fields [" +
StringUtils.arrayToCommaDelimitedString(allowedFields) + "]");
}
}
/**
* Return the fields that should be allowed for binding.
* @return array of field names
*/
public String[] getAllowedFields() {
return allowedFields;
}
/**
* Register fields that are required for each binding process.
*
If one of the specified fields is not contained in the list of
* incoming property values, a corresponding "missing field" error
* will be created, with error code "required" (by the default
* binding error processor).
* @param requiredFields array of field names
* @see #setBindingErrorProcessor
* @see DefaultBindingErrorProcessor#MISSING_FIELD_ERROR_CODE
*/
public void setRequiredFields(String[] requiredFields) {
this.requiredFields = requiredFields;
if (logger.isDebugEnabled()) {
logger.debug("DataBinder requires binding of required fields [" +
StringUtils.arrayToCommaDelimitedString(requiredFields) + "]");
}
}
/**
* Return the fields that are required for each binding process.
* @return array of field names
*/
public String[] getRequiredFields() {
return requiredFields;
}
/**
* Register the given custom property editor for all properties
* of the given type.
* @param requiredType type of the property
* @param propertyEditor editor to register
* @see org.springframework.beans.BeanWrapper#registerCustomEditor
*/
public void registerCustomEditor(Class requiredType, PropertyEditor propertyEditor) {
this.errors.getBeanWrapper().registerCustomEditor(requiredType, propertyEditor);
}
/**
* Register the given custom property editor for the given type and
* field, or for all fields of the given type.
*
If the field denotes an array or Collection, the PropertyEditor
* will get applied either to the array/Collection itself (the
* PropertyEditor has to create an array or Collection value) or to
* each element (the PropertyEditor has to create the element type),
* depending on the specified required type.
*
Note: Only one single registered custom editor per property path
* is supported. In case of a Collection/array, do not register an editor
* for both the Collection/array and each element on the same property.
* @param requiredType type of the property (can be null if a field is
* given but should be specified in any case for consistency checking)
* @param field name of the field (can also be a nested path), or
* null if registering an editor for all fields of the given type
* @param propertyEditor editor to register
* @see org.springframework.beans.BeanWrapper#registerCustomEditor
*/
public void registerCustomEditor(Class requiredType, String field, PropertyEditor propertyEditor) {
getBeanWrapper().registerCustomEditor(requiredType, field, propertyEditor);
}
/**
* Set the strategy to use for resolving errors into message codes.
* Applies the given strategy to the underlying errors holder.
*
Default is a DefaultMessageCodesResolver.
* @see BindException#setMessageCodesResolver
* @see DefaultMessageCodesResolver
*/
public void setMessageCodesResolver(MessageCodesResolver messageCodesResolver) {
this.errors.setMessageCodesResolver(messageCodesResolver);
}
/**
* Set the strategy to use for processing binding errors, that is,
* required field errors and PropertyAccessException
s.
*
Default is a DefaultBindingErrorProcessor.
* @see DefaultBindingErrorProcessor
*/
public void setBindingErrorProcessor(BindingErrorProcessor bindingErrorProcessor) {
this.bindingErrorProcessor = bindingErrorProcessor;
}
/**
* Return the strategy for processing binding errors.
*/
public BindingErrorProcessor getBindingErrorProcessor() {
return bindingErrorProcessor;
}
/**
* Bind the given property values to this binder's target.
*
This call can create field errors, representing basic binding
* errors like a required field (code "required"), or type mismatch
* between value and bean property (code "typeMismatch").
*
Note that the given PropertyValues should be a throwaway instance:
* For efficiency, it will be modified to just contain allowed fields if it
* implements the MutablePropertyValues interface; else, an internal mutable
* copy will be created for this purpose. Pass in a copy of the PropertyValues
* if you want your original instance to stay unmodified in any case.
* @param pvs property values to bind
* @see #doBind(org.springframework.beans.MutablePropertyValues)
*/
public void bind(PropertyValues pvs) {
MutablePropertyValues mpvs = (pvs instanceof MutablePropertyValues) ?
(MutablePropertyValues) pvs : new MutablePropertyValues(pvs);
doBind(mpvs);
}
/**
* Actual implementation of the binding process, working with the
* passed-in MutablePropertyValues instance.
* @param mpvs the property values to bind,
* as MutablePropertyValues instance
* @see #checkAllowedFields
* @see #checkRequiredFields
* @see #applyPropertyValues
*/
protected void doBind(MutablePropertyValues mpvs) {
checkAllowedFields(mpvs);
checkRequiredFields(mpvs);
applyPropertyValues(mpvs);
}
/**
* Check the given property values against the allowed fields,
* removing values for fields that are not allowed.
* @param mpvs the property values to be bound (can be modified)
* @see #getAllowedFields
* @see #isAllowed(String)
*/
protected void checkAllowedFields(MutablePropertyValues mpvs) {
List allowedFieldsList = (getAllowedFields() != null) ? Arrays.asList(getAllowedFields()) : null;
PropertyValue[] pvArray = mpvs.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]);
if (logger.isWarnEnabled()) {
logger.warn("Field [" + pvArray[i] + "] has been removed from PropertyValues " +
"and will not be bound, because it has not been found in the list of allowed fields " +
allowedFieldsList);
}
}
}
}
/**
* Return if the given field is allowed for binding.
* Invoked for each passed-in property value.
*
The default implementation checks for "xxx*" and "*xxx" matches.
* Can be overridden in subclasses.
*
If the field is found in the allowedFields array as direct match,
* this method will not be invoked.
* @param field the field to check
* @return if the field is allowed
* @see #setAllowedFields
*/
protected boolean isAllowed(String field) {
if (getAllowedFields() != null) {
String[] allowedFields = getAllowedFields();
for (int i = 0; i < allowedFields.length; i++) {
String allowed = allowedFields[i];
if ((allowed.endsWith("*") && field.startsWith(allowed.substring(0, allowed.length() - 1))) ||
(allowed.startsWith("*") && field.endsWith(allowed.substring(1, allowed.length())))) {
return true;
}
}
return false;
}
return true;
}
/**
* Check the given property values against the required fields,
* generating missing field errors where appropriate.
* @param mpvs the property values to be bound (can be modified)
* @see #getRequiredFields
* @see #getBindingErrorProcessor
* @see BindingErrorProcessor#processMissingFieldError
*/
protected void checkRequiredFields(MutablePropertyValues mpvs) {
if (getRequiredFields() != null) {
String[] requiredFields = getRequiredFields();
for (int i = 0; i < requiredFields.length; i++) {
PropertyValue pv = mpvs.getPropertyValue(requiredFields[i]);
if (pv == null || pv.getValue() == null ||
(pv.getValue() instanceof String && !StringUtils.hasText((String) pv.getValue()))) {
// Use bind error processor to create FieldError.
String field = requiredFields[i];
getBindingErrorProcessor().processMissingFieldError(field, getErrors());
// Remove property from property values to bind:
// It has already caused a field error with a rejected value.
mpvs.removePropertyValue(field);
}
}
}
}
/**
* Apply given property values to the target object.
*
Default implementation applies them all of them as bean property
* values via the corresponding BeanWrapper. Unknown fields will by
* default be ignored.
* @param mpvs the property values to be bound (can be modified)
* @see #getTarget
* @see #getBeanWrapper
* @see #isIgnoreUnknownFields
* @see #getBindingErrorProcessor
* @see BindingErrorProcessor#processPropertyAccessException
*/
protected void applyPropertyValues(MutablePropertyValues mpvs) {
try {
// Bind request parameters onto target object.
getBeanWrapper().setPropertyValues(mpvs, isIgnoreUnknownFields());
}
catch (PropertyAccessExceptionsException ex) {
// Use bind error processor to create FieldErrors.
PropertyAccessException[] exs = ex.getPropertyAccessExceptions();
for (int i = 0; i < exs.length; i++) {
getBindingErrorProcessor().processPropertyAccessException(exs[i], getErrors());
}
}
}
/**
* Close this DataBinder, which may result in throwing
* a BindException if it encountered any errors
* @return the model Map, containing target object and Errors instance
* @throws BindException if there were any errors in the bind operation
* @see BindException#getModel
*/
public Map close() throws BindException {
if (getErrors().hasErrors()) {
throw getErrors();
}
return getErrors().getModel();
}
}