Wow, I finally discovered the problem. To your relief, it's not a problem with Spring Web Flow. The problem is that my Map has null values in one scenario (when bound by SWF), and real Long values when bound by SimpleFormController.
When binding to a Map type (and possibly other Collections as well), if the entry you are trying to bind to doesn't already have a value, then the type specific message code isn't added to the error code list. Let me walk you through it...
Basically these lines of code aren't executed (DefaultMessageCodesResolver.java: 104-106):
Code:
if (fieldType != null) {
codeList.add(errorCode + CODE_SEPARATOR + fieldType..getName());
}
Because these lines return null (BeanWrapperImpl.java: 1146-1155):
Code:
Object value = getPropertyValue(propertyName);
if (value != null) {
return value.getClass();
}
...
return null;
You can run the test case below and set break points on these lines to get a good idea of what's going on:
- 1. BindException [line: 221] - resolveMessageCodes(String, String)
2. BeanWrapperImpl [line: 1146] - getPropertyType(String)
3. DefaultMessageCodesResolver [line: 104] - resolveMessageCodes(String, String, String, Class)
What to look for when stepping:
1. "fieldType" is set to null when the map entry value (entries[FOOBAR]) is null, or returns the type of the current (pre-binding) entry value.
2. This is the code that determine the field type in #1.
3. This is the code that addes the code typeMismatch.java.lang.Long is the fieldType isn't null.
This seems to be a problem with BeanWrapperImpl's getPropertyType method. I think it's a valid use case to have a collection you want to bind values to, that does not yet have any values. You should get the same error codes regardless. It's not the existing values that count, it's the values you're trying to bind. Maybe if the fieldType is returned as null, then a secondary check should be performed to see what the value should be based on any custom editors for the named property?
Should I go ahead and create a JIRA ticket for this?
Here's the updated test case. It passes as expected, but shows that the message code (typeMismatch.java.lang.Long) is missing. Note the constructor to TestBean.
Code:
public class FormActionBindingTests extends TestCase {
public static class TestBean {
private Map<String, Long> entries = new HashMap<String, Long>();
public TestBean() {
// The first triggers the typeMismatch.java.lang.Long code, the second does not.
//entries.put("FOOBAR", new Long(0));
entries.put("FOOBAR", null);
}
public Map<String, Long> getEntries() {
return entries;
}
public void setEntries(Map<String, Long> entries) {
this.entries = entries;
}
}
public void testMessageCodesOnBindFailure() throws Exception {
MockHttpServletRequest request = new MockHttpServletRequest();
request.setMethod("POST");
request.addParameter("entries[FOOBAR]", "A");
MockHttpServletResponse response = new MockHttpServletResponse();
MockRequestContext context = new MockRequestContext();
context.setLastEvent(new ServletEvent(request, response));
context.setProperty("method", "bindAndValidate");
// use a for FormAction to do the binding
FormAction formAction = new FormAction() {
protected void initBinder(org.springframework.webflow.RequestContext context,
org.springframework.validation.DataBinder binder) {
binder.registerCustomEditor(Long.class, "entries", new CustomNumberEditor(Long.class, false));
};
};
formAction.setFormObjectClass(TestBean.class);
formAction.execute(context);
Errors formActionErrors = (Errors) context.getRequestScope().get(BindException.ERROR_KEY_PREFIX + "formObject");
assertNotNull(formActionErrors);
assertTrue(formActionErrors.hasErrors());
// use a SimpleFormController to do the binding
SimpleFormController simpleFormController = new SimpleFormController() {
protected void initBinder(javax.servlet.http.HttpServletRequest request,
org.springframework.web.bind.ServletRequestDataBinder binder) throws Exception {
binder.registerCustomEditor(Long.class, "entries", new CustomNumberEditor(Long.class, false));
};
};
simpleFormController.setCommandClass(TestBean.class);
simpleFormController.setCommandName("formObject");
ModelAndView modelAndView = simpleFormController.handleRequest(request, response);
Errors simpleFormControllerErrors = (Errors) modelAndView.getModel().get(BindException.ERROR_KEY_PREFIX + "formObject");
assertNotNull(simpleFormControllerErrors);
assertTrue(simpleFormControllerErrors.hasErrors());
assertNotSame(formActionErrors, simpleFormControllerErrors);
assertEquals(formActionErrors.getErrorCount(), simpleFormControllerErrors.getErrorCount());
assertEquals(formActionErrors.getGlobalErrorCount(), simpleFormControllerErrors.getGlobalErrorCount());
assertEquals(formActionErrors.getFieldErrorCount("entries[FOOBAR]"), simpleFormControllerErrors
.getFieldErrorCount("entries[FOOBAR]"));
assertEquals(1, formActionErrors.getFieldErrorCount("entries[FOOBAR]"));
assertEquals(formActionErrors.getFieldError("entries[FOOBAR]").getCodes().length, simpleFormControllerErrors.getFieldError(
"entries[FOOBAR]").getCodes().length);
System.out.println(formActionErrors.getFieldError("entries[FOOBAR]"));
System.out.println(simpleFormControllerErrors.getFieldError("entries[FOOBAR]"));
}
}