Fixed checkbox binding bug

This commit is contained in:
Keith Donald
2008-05-14 21:49:38 +00:00
parent 67359d186f
commit bd95b63656
3 changed files with 211 additions and 12 deletions

View File

@@ -47,8 +47,9 @@ public interface Expression extends Serializable {
* context.
* @param context the context to evaluate
* @return the most general type of value that can be set on this context
* @throws EvaluationException an exception occurred during expression evaluation
*/
public Class getValueType(Object context);
public Class getValueType(Object context) throws EvaluationException;
/**
* Returns the original string used to create this expression, unmodified.

View File

@@ -16,19 +16,25 @@
package org.springframework.webflow.mvc.view;
import java.io.IOException;
import java.lang.reflect.Array;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.binding.convert.ConversionExecutionException;
import org.springframework.binding.convert.ConversionExecutor;
import org.springframework.binding.expression.EvaluationException;
import org.springframework.binding.expression.Expression;
import org.springframework.binding.expression.ExpressionParser;
import org.springframework.binding.expression.ParserContext;
import org.springframework.binding.expression.support.FluentParserContext;
import org.springframework.binding.expression.support.StaticExpression;
import org.springframework.binding.format.Formatter;
import org.springframework.binding.format.FormatterRegistry;
import org.springframework.binding.format.InvalidFormatException;
@@ -63,6 +69,8 @@ import org.springframework.webflow.expression.DefaultExpressionParserFactory;
*/
public abstract class AbstractMvcView implements View {
private static final Log logger = LogFactory.getLog(AbstractMvcView.class);
private static final MappingResultsCriteria PROPERTY_NOT_FOUND_ERROR = new PropertyNotFoundError();
private static final MappingResultsCriteria MAPPING_ERROR = new MappingError();
@@ -81,6 +89,12 @@ public abstract class AbstractMvcView implements View {
private String eventId;
private Set allowedBindFields;
private String fieldMarkerPrefix = "_";
private ConversionExecutor bindingTypeConverter;
/**
* Creates a new MVC view.
* @param view the Spring MVC view to render
@@ -105,6 +119,34 @@ public abstract class AbstractMvcView implements View {
*/
public void setFormatterRegistry(FormatterRegistry formatterRegistry) {
this.formatterRegistry = formatterRegistry;
bindingTypeConverter = new FormatterBackedMappingConversionExecutor(this.formatterRegistry);
}
/**
* Specify a prefix that can be used for parameters that mark potentially empty fields, having "prefix + field" as
* name. Such a marker parameter is checked by existence: You can send any value for it, for example "visible". This
* is particularly useful for HTML checkboxes and select options.
* <p>
* Default is "_", for "_FIELD" parameters (e.g. "_subscribeToNewsletter"). Set this to null if you want to turn off
* the empty field check completely.
* <p>
* HTML checkboxes only send a value when they're checked, so it is not possible to detect that a formerly checked
* box has just been unchecked, at least not with standard HTML means.
* <p>
* This auto-reset mechanism addresses this deficiency, provided that a marker parameter is sent for each checkbox
* field, like "_subscribeToNewsletter" for a "subscribeToNewsletter" field. As the marker parameter is sent in any
* case, the data binder can detect an empty field and automatically reset its value.
*/
public void setFieldMarkerPrefix(String fieldMarkerPrefix) {
this.fieldMarkerPrefix = fieldMarkerPrefix;
}
/**
* Register fields that should be allowed for binding. Default is all fields.
* @param allowedBindFields the set of field name strings to allow
*/
public void setAllowedBindFields(Set allowedBindFields) {
this.allowedBindFields = allowedBindFields;
}
public void render() throws IOException {
@@ -236,23 +278,91 @@ public abstract class AbstractMvcView implements View {
}
private MappingResults bind(Object model) {
if (logger.isDebugEnabled()) {
logger.debug("Setting up view->model mappings");
}
DefaultMapper mapper = new DefaultMapper();
addDefaultMappings(mapper, requestContext.getRequestParameters(), model);
return mapper.map(requestContext.getRequestParameters(), model);
ParameterMap requestParameters = requestContext.getRequestParameters();
addDefaultMappings(mapper, requestParameters.asMap().keySet(), model);
return mapper.map(requestParameters, model);
}
private void addDefaultMappings(DefaultMapper mapper, ParameterMap requestParameters, Object model) {
for (Iterator it = requestParameters.asMap().keySet().iterator(); it.hasNext();) {
String name = (String) it.next();
Expression source = new RequestParameterExpression(name);
Expression target = expressionParser.parseExpression(name, new FluentParserContext().evaluate(model
.getClass()));
DefaultMapping mapping = new DefaultMapping(source, target);
mapping.setTypeConverter(new FormatterBackedMappingConversionExecutor(formatterRegistry));
mapper.addMapping(mapping);
private void addDefaultMappings(DefaultMapper mapper, Set parameterNames, Object model) {
for (Iterator it = parameterNames.iterator(); it.hasNext();) {
String parameterName = (String) it.next();
if (fieldMarkerPrefix != null && parameterName.startsWith(fieldMarkerPrefix)) {
String field = parameterName.substring(fieldMarkerPrefix.length());
if (isAllowedField(field)) {
if (!parameterNames.contains(field)) {
addEmptyValueMapping(mapper, field, model);
}
} else {
if (logger.isDebugEnabled()) {
logger.debug("Will not map field marker parameter '" + parameterName + "'");
}
}
} else {
if (isAllowedField(parameterName)) {
addDefaultMapping(mapper, parameterName, model);
} else {
if (logger.isDebugEnabled()) {
logger.debug("Will not map parameter '" + parameterName + "'");
}
}
}
}
}
private boolean isAllowedField(String field) {
if (allowedBindFields == null) {
// always allow
return true;
} else {
return allowedBindFields.contains(field);
}
}
private void addEmptyValueMapping(DefaultMapper mapper, String field, Object model) {
ParserContext parserContext = new FluentParserContext().evaluate(model.getClass());
Expression target = expressionParser.parseExpression(field, parserContext);
try {
Class propertyType = target.getValueType(model);
Expression source = new StaticExpression(getEmptyValue(propertyType));
DefaultMapping mapping = new DefaultMapping(source, target);
if (logger.isDebugEnabled()) {
logger.debug("Adding empty parameter value mapping for field '" + field + "'");
}
mapper.addMapping(mapping);
} catch (EvaluationException e) {
}
}
private Object getEmptyValue(Class fieldType) {
if (fieldType != null && boolean.class.equals(fieldType) || Boolean.class.equals(fieldType)) {
// Special handling of boolean property.
return Boolean.FALSE;
} else if (fieldType != null && fieldType.isArray()) {
// Special handling of array property.
return Array.newInstance(fieldType.getComponentType(), 0);
} else {
// Default value: try null.
return null;
}
}
private void addDefaultMapping(DefaultMapper mapper, String parameter, Object model) {
Expression source = new RequestParameterExpression(parameter);
ParserContext parserContext = new FluentParserContext().evaluate(model.getClass());
Expression target = expressionParser.parseExpression(parameter, parserContext);
DefaultMapping mapping = new DefaultMapping(source, target);
mapping.setTypeConverter(bindingTypeConverter);
if (logger.isDebugEnabled()) {
logger.debug("Adding mapping for parameter '" + parameter + "'");
}
mapper.addMapping(mapping);
}
private boolean hasMappingErrors(MappingResults results) {
return results.hasErrorResults() && !onlyPropertyNotFoundErrorsPresent(results);
}

View File

@@ -3,6 +3,7 @@ package org.springframework.webflow.mvc.view;
import java.security.Principal;
import java.util.Calendar;
import java.util.Date;
import java.util.HashSet;
import java.util.Locale;
import java.util.Map;
@@ -184,6 +185,84 @@ public class MvcViewTests extends TestCase {
assertEquals("foo", bindBean.getBeanProperty().getName());
}
public void testResumeEventModelBindingAllowedFields() throws Exception {
MockRequestContext context = new MockRequestContext();
context.putRequestParameter("_eventId", "submit");
context.putRequestParameter("stringProperty", "foo");
context.putRequestParameter("integerProperty", "5");
context.putRequestParameter("dateProperty", "2007-01-01");
context.putRequestParameter("beanProperty.name", "foo");
BindBean bindBean = new BindBean();
StaticExpression modelObject = new StaticExpression(bindBean);
modelObject.setExpressionString("bindBean");
context.getCurrentState().getAttributes().put("model", modelObject);
context.getFlowScope().put("bindBean", bindBean);
context.getMockExternalContext().setNativeContext(new MockServletContext());
context.getMockExternalContext().setNativeRequest(new MockHttpServletRequest());
context.getMockExternalContext().setNativeResponse(new MockHttpServletResponse());
context.getMockFlowExecutionContext().setKey(new MockFlowExecutionKey("c1v1"));
org.springframework.web.servlet.View mvcView = new MockView();
AbstractMvcView view = new MockMvcView(mvcView, context);
view.setFormatterRegistry(formatterRegistry);
HashSet allowedBindFields = new HashSet();
allowedBindFields.add("stringProperty");
view.setAllowedBindFields(allowedBindFields);
view.processUserEvent();
assertTrue(view.hasFlowEvent());
assertEquals("submit", view.getFlowEvent().getId());
assertEquals("foo", bindBean.getStringProperty());
assertEquals(new Integer(3), bindBean.getIntegerProperty());
Calendar cal = Calendar.getInstance();
cal.clear();
cal.set(Calendar.YEAR, 2008);
assertEquals(cal.getTime(), bindBean.getDateProperty());
assertEquals(null, bindBean.getBeanProperty().getName());
}
public void testResumeEventModelBindingFieldMarker() throws Exception {
MockRequestContext context = new MockRequestContext();
context.putRequestParameter("_eventId", "submit");
context.putRequestParameter("_booleanProperty", "whatever");
BindBean bindBean = new BindBean();
StaticExpression modelObject = new StaticExpression(bindBean);
modelObject.setExpressionString("bindBean");
context.getCurrentState().getAttributes().put("model", modelObject);
context.getFlowScope().put("bindBean", bindBean);
context.getMockExternalContext().setNativeContext(new MockServletContext());
context.getMockExternalContext().setNativeRequest(new MockHttpServletRequest());
context.getMockExternalContext().setNativeResponse(new MockHttpServletResponse());
context.getMockFlowExecutionContext().setKey(new MockFlowExecutionKey("c1v1"));
org.springframework.web.servlet.View mvcView = new MockView();
AbstractMvcView view = new MockMvcView(mvcView, context);
view.setFormatterRegistry(formatterRegistry);
HashSet allowedBindFields = new HashSet();
allowedBindFields.add("booleanProperty");
view.setAllowedBindFields(allowedBindFields);
view.processUserEvent();
assertEquals(false, bindBean.getBooleanProperty());
}
public void testResumeEventModelBindingFieldMarkerFieldPresent() throws Exception {
MockRequestContext context = new MockRequestContext();
context.putRequestParameter("_eventId", "submit");
context.putRequestParameter("booleanProperty", "true");
context.putRequestParameter("_booleanProperty", "whatever");
BindBean bindBean = new BindBean();
StaticExpression modelObject = new StaticExpression(bindBean);
modelObject.setExpressionString("bindBean");
context.getCurrentState().getAttributes().put("model", modelObject);
context.getFlowScope().put("bindBean", bindBean);
context.getMockExternalContext().setNativeContext(new MockServletContext());
context.getMockExternalContext().setNativeRequest(new MockHttpServletRequest());
context.getMockExternalContext().setNativeResponse(new MockHttpServletResponse());
context.getMockFlowExecutionContext().setKey(new MockFlowExecutionKey("c1v1"));
org.springframework.web.servlet.View mvcView = new MockView();
AbstractMvcView view = new MockMvcView(mvcView, context);
view.setFormatterRegistry(formatterRegistry);
view.processUserEvent();
assertEquals(true, bindBean.getBooleanProperty());
}
private class MockMvcView extends AbstractMvcView {
public MockMvcView(View view, RequestContext context) {
@@ -214,6 +293,7 @@ public class MvcViewTests extends TestCase {
private String stringProperty;
private Integer integerProperty = new Integer(3);
private Date dateProperty;
private boolean booleanProperty = true;
private NestedBean beanProperty;
public BindBean() {
@@ -240,6 +320,14 @@ public class MvcViewTests extends TestCase {
this.integerProperty = integerProperty;
}
public boolean getBooleanProperty() {
return booleanProperty;
}
public void setBooleanProperty(boolean booleanProperty) {
this.booleanProperty = booleanProperty;
}
public Date getDateProperty() {
return dateProperty;
}