4

We are using Spring MVC 3.0.6, but we are not using JSR 303 validation, only the Binding errors using BindingResult in our Controller methods that deal with our model form beans. I am going to try and simplify the examples below because the question isn't about how things are architected, as those decissions were made prior to my arrival. I am just trying to get things to work right within the parameters I have.

In this particular form I am working on I have a form bean that is a list of sub-beans, with the view allowing the user to add/remove a bunch of these sub-beans.

The form bean looks something like:

public class FormBean {
    private List<SubBean> subBeans;
    ...
}

And the sub bean:

public class SubBean {
    private Integer value1;
    private Date value2;
    private String value3;
}

In the view JSP we are doing something like:

<form:form modelAttribute="formBean">
    <spring:hasBindErrors name="formBean">
        <div class="error-box">
            <div class="error-txt">
                <form:errors path="*" cssClass="error" />
            </div>
        </div>
    </spring:hasBindErrors>

    <c:forEach items="${formBean.subBeans}" var="subBean" varStatus="subBeanStatus">
        ...
        <form:input path="subBeans[${subBeanStatus.index}].value1" />
        <form:input path="subBeans[${subBeanStatus.index}].value2" />        
        <form:input path="subBeans[${subBeanStatus.index}].value3" />
        ...
    </c:forEach>
    ...
</form:form>

The problem comes when I submit the form with an value that doesn't pass Binding-mustard. For instance, if I add an invalid int value for value1, I get an error message like:

Failed to convert property value of type java.lang.String to required type java.lang.Integer for property subBeans[0].value1; nested exception is java.lang.NumberFormatException: For input string: "sdfs"

I know with non-nested beans, you can simply add a message to the Resource Bunder in the form:

typeMismatch.beanName.fieldName="This is my custom error message!!!"

But how do you control the error message when you have a List, as I do?

CodeChimp
  • 8,016
  • 5
  • 41
  • 79

4 Answers4

3

I didn't like the default messages either, and customized my own BindingErrorProcessor.

Basically what I want, usually, is just the "Last Field" name -- I want to say there's an Invalid value for Date, or Invalid value for Staff, or whatever. I include the rejected field text as well, which the standard Spring error-processor doesn't supply to the message.

public class SimpleMessage_BindingErrorProcessor 
        extends DefaultBindingErrorProcessor 
        {

    @Override
    public void processPropertyAccessException(PropertyAccessException ex, BindingResult bindingResult) {
        // Create field error with the exceptions's code, e.g. "typeMismatch".
        String field = ex.getPropertyName();
        String[] codes = bindingResult.resolveMessageCodes(ex.getErrorCode(), field);

        Object rejectedValue = ex.getValue();
        if (rejectedValue != null && rejectedValue.getClass().isArray()) {
            rejectedValue = StringUtils.arrayToCommaDelimitedString(ObjectUtils.toObjectArray(rejectedValue));
        }
        Object[] arguments = getArgumentsForBindError( bindingResult.getObjectName(), field, rejectedValue);

        FieldError fieldError = new FieldError(
                bindingResult.getObjectName(), field, rejectedValue, true,
                codes, arguments, ex.getLocalizedMessage());
        bindingResult.addError( fieldError);
    }

    /**
     * Return FieldError arguments for a binding error on the given field.
     * <p>TW's implementation returns {0} simple field title, {1} rejected value, {2} FQ field resolvable as per Spring DefaultBindingErrorProcessor
     * (of type DefaultMessageSourceResolvable, with "objectName.field" and "field" as codes).
     * @param objectName the name of the target object
     * @param propPath the field that caused the binding error
     * @param rejectedValue the value that was rejected
     * @return the Object array that represents the FieldError arguments
     * @see org.springframework.validation.FieldError#getArguments
     * @see org.springframework.context.support.DefaultMessageSourceResolvable
     */
    protected Object[] getArgumentsForBindError (String objectName, String propPath, Object/*String*/ rejectedValue) {

        // just the Simple Name of Field;
        //      (last field in path).
        //
        String lastField = getLastField_Title( propPath);

        // create Resolvable for "Fully-Qualified" Field;
        //      -- Spring standard,  too specific/ would require defining hundreds of distinct messages;    we don't use these.
        //
        String[] codes = new String[] {objectName + Errors.NESTED_PATH_SEPARATOR + propPath, propPath};
        DefaultMessageSourceResolvable fqField_resolvable = new DefaultMessageSourceResolvable(codes, propPath);

        // return Args;     {0} simple name, {1} rejected text, {2} FQ complex name.
        return new Object[]{ 
                lastField, rejectedValue, fqField_resolvable
        };
    }

    /**
     * Return FieldError arguments for a binding error on the given field.
     * <p>TW's implementation returns {0} simple field title, {1} FQ field resolvable as per Spring DefaultBindingErrorProcessor
     * (of type DefaultMessageSourceResolvable, with "objectName.field" and "field" as codes).
     * @param objectName the name of the target object
     * @param propPath the field that caused the binding error
     * @return the Object array that represents the FieldError arguments
     * @see org.springframework.validation.FieldError#getArguments
     * @see org.springframework.context.support.DefaultMessageSourceResolvable
     */
    @Override
    protected Object[] getArgumentsForBindError (String objectName, String propPath) {

        // just the Simple Name of Field;
        //      (last field in path).
        //
        String lastField = getLastField_Title( propPath);

        // create Resolvable for "Fully-Qualified" Field;
        //      -- Spring standard,  too specific/ would require defining hundreds of distinct messages;    we don't use these.
        //
        String[] codes = new String[] {objectName + Errors.NESTED_PATH_SEPARATOR + propPath, propPath};
        DefaultMessageSourceResolvable fqField_resolvable = new DefaultMessageSourceResolvable(codes, propPath);

        // return Args;     {0} simple name, {2} FQ complex name.
        return new Object[]{ 
                lastField, fqField_resolvable
        };
    }

    protected String getLastField_Title (String propPath) {
        int index = propPath.lastIndexOf('.');
        String title = (index >= 0) ? propPath.substring(index+1) : propPath;
        return StrUtil.capitalize( title);
    }

}

This works well! Now all your messages.properties has to say is:

# Type Mismatch generally;
#   INCOMING 21/8/13 -- use {0} as 'Simple Name' of field,  when using SimpleMessage_BindingErrorProcessor;   {1} is 'resolvable' FQN of field.
#   
typeMismatch=Invalid value for {0}: "{1}"


# Method Invocation/ value conversion; 
#   INCOMING 21/8/13 -- only expected for certain 'Value Converting'/ self-parsing properties;  SPEC.
#   
methodInvocation.machine=Invalid value for {0}: "{1}"

This area wasn't very clear.. the whole Binding -> Error Processing -> Message Resolving system is fairly complicated, and (as far as I can see) stuck with the problem that the message codes are generally far too specific.

There's very little around on this (I didn't find anything directly relevant on Google), so I hope this helps people.

Thomas W
  • 13,940
  • 4
  • 58
  • 76
2

If you want to get the message provided by your resource bundle, you first need a registered messageSource instance:

<bean id="messageSource"
    class="org.springframework.context.support.ResourceBundleMessageSource">
    <property name="basenames" value="ValidationMessages"/>
</bean>

Then,

@Autowired
private MessageSource messageSource;

And to get your message from the resource bundle,

for (Object object : bindingResult.getAllErrors()) {
    if(object instanceof FieldError) {
        FieldError fieldError = (FieldError) object;

    // Use null for second parameter if you do not use i18n
    String message = messageSource.getMessage(fieldError, null);
    }
}

Your validator should look like:

errors.rejectValue("<FIELD_NAME>", "typeMismatch.beanName.fieldName", new Object [] {"123"}, null);

This will give you some idea about how you can go about achieving your goal.

slashdot
  • 766
  • 5
  • 11
  • 1
    I know how to do it in simple cases where the bean has a single field/value, but in my case my bean has a List of beans. What I need is something like: typeMismatch.beanName.subBeans.value, where "subBeans" is really a list of "SubBean", such that I am mapping the path in the View as "beanName.subBeans[0].value", "beanName.subBeans[1].value", ..., "beanName.subBeans[n].value". For now, I just made my sub-bean's field name unique enough that I could do "typeMismatch.uniqueFieldName" as the resource bundle name. – CodeChimp Jan 28 '13 at 20:17
2

The way I solved this was to add a message like 'typeMismatch.fieldName', basically specifying only the end field name and not the bean/list names. The pro is that it works, the con is that it sets the message for ALL fields on any beans with the same name. Because we are working on portals, and there are hundreds of tiny little apps deployed all in the same WAR, this could be a problem down the road. For now, it works.

CodeChimp
  • 8,016
  • 5
  • 41
  • 79
1

You should check the docs for the DefaultMessageCodesResolver class. There you can find the error codes you may use when working with fields of type List or Map.

aaguilera
  • 1,080
  • 10
  • 27
  • To add to this, if you are using WebFlow, you should look to the documentation for [WebFlowMessageCodesResolver](http://docs.spring.io/spring-webflow/docs/current/api/org/springframework/webflow/validation/WebFlowMessageCodesResolver.html). It works the same way, but the error code appears last instead of first. Example when binding date-of-birth to an array of users: `users.dateOfBirth.typeMismatch` – KFox112 May 30 '17 at 15:39