Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Custom Binding Error Message with Collections of Beans in Spring MVC

Tags:

spring-mvc

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?

like image 543
CodeChimp Avatar asked Feb 17 '23 18:02

CodeChimp


1 Answers

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.

like image 96
Thomas W Avatar answered May 01 '23 11:05

Thomas W