简体   繁体   中英

Spring validation, how to have PropertyEditor generate specific error message

I'm using Spring for form input and validation. The form controller's command contains the model that's being edited. Some of the model's attributes are a custom type. For example, Person's social security number is a custom SSN type.

public class Person {
    public String getName() {...}
    public void setName(String name) {...}
    public SSN getSocialSecurtyNumber() {...}
    public void setSocialSecurtyNumber(SSN ssn) {...}
}

and wrapping Person in a Spring form edit command:

public class EditPersonCommand {
    public Person getPerson() {...}
    public void setPerson(Person person) {...}
}

Since Spring doesn't know how to convert text to a SSN, I register a customer editor with the form controller's binder:

public class EditPersonController extends SimpleFormController {
    protected void initBinder(HttpServletRequest req, ServletRequestDataBinder binder) {
        super.initBinder(req, binder);
        binder.registerCustomEditor(SSN.class, "person.ssn", new SsnEditor());
    }
}

and SsnEditor is just a custom java.beans.PropertyEditor that can convert text to a SSN object:

public class SsnEditor extends PropertyEditorSupport {
    public String getAsText() {...} // converts SSN to text
    public void setAsText(String str) {
        // converts text to SSN
        // throws IllegalArgumentException for invalid text
    }
}

If setAsText encounters text that is invalid and can't be converted to a SSN, then it throws IllegalArgumentException (per PropertyEditor setAsText 's specification). The issue I'm having is that the text to object conversion (via PropertyEditor.setAsText() ) takes place before my Spring validator is called. When setAsText throws IllegalArgumentException , Spring simply displays the generic error message defined in errors.properties . What I want is a specific error message that depends on the exact reason why the entered SSN is invalid. PropertyEditor.setAsText() would determine the reason. I've tried embedded the error reason text in IllegalArgumentException 's text, but Spring just treats it as a generic error.

Is there a solution to this? To repeat, what I want is the specific error message generated by the PropertyEditor to surface to the error message on the Spring form. The only alternative I can think of is to store the SSN as text in the command and perform validation in the validator. The text to SSN object conversion would take place in the form's onSubmit . This is less desirable as my form (and model) has many properties and I don't want to have to create and maintain a command that has each and every model attribute as a text field.

The above is just an example, my actual code isn't Person/SSN, so there's no need to reply with "why not store SSN as text..."

You're trying to do validation in a binder. That's not the binder's purpose. A binder is supposed to bind request parameters to your backing object, nothing more. A property editor converts Strings to objects and vice versa - it is not designed to do anything else.

In other words, you need to consider separation of concerns - you're trying to shoehorn functionality into an object that was never meant to do anything more than convert a string into an object and vice versa.

You might consider breaking up your SSN object into multiple, validateable fields that are easily bound (String objects, basic objects like Dates, etc). This way you can use a validator after binding to verify that the SSN is correct, or you can set an error directly. With a property editor, you throw an IllegalArgumentException, Spring converts it to a type mismatch error because that's what it is - the string doesn't match the type that is expected. That's all that it is. A validator, on the other hand, can do this. You can use the spring bind tag to bind to nested fields, as long as the SSN instance is populated - it must be initialized with new() first. For instance:

<spring:bind path="ssn.firstNestedField">...</spring:bind>

If you truly want to persist on this path, however, have your property editor keep a list of errors - if it is to throw an IllegalArgumentException, add it to the list and then throw the IllegalArgumentException (catch and rethrow if needed). Because you can construct your property editor in the same thread as the binding, it will be threadsafe if you simply override the property editor default behavior - you need to find the hook it uses to do binding, and override it - do the same property editor registration you're doing now (except in the same method, so that you can keep the reference to your editor) and then at the end of the binding, you can register errors by retrieving the list from your editor if you provide a public accessor. Once the list is retrieved you can process it and add your errors accordingly.

As said:

What I want is the specific error message generated by the PropertyEditor to surface to the error message on the Spring form

Behind the scenes, Spring MVC uses a BindingErrorProcessor strategy for processing missing field errors, and for translating a PropertyAccessException to a FieldError . So if you want to override default Spring MVC BindingErrorProcessor strategy, you must provide a BindingErrorProcessor strategy according to:

public class CustomBindingErrorProcessor implements DefaultBindingErrorProcessor {

    public void processMissingFieldError(String missingField, BindException errors) {
        super.processMissingFieldError(missingField, errors);
    }

    public void processPropertyAccessException(PropertyAccessException accessException, BindException errors) {
        if(accessException.getCause() instanceof IllegalArgumentException)
            errors.rejectValue(accessException.getPropertyChangeEvent().getPropertyName(), "<SOME_SPECIFIC_CODE_IF_YOU_WANT>", accessException.getCause().getMessage());
        else
            defaultSpringBindingErrorProcessor.processPropertyAccessException(accessException, errors);
    }

}

In order to test, Let's do the following

protected void initBinder(HttpServletRequest request, ServletRequestDataBinder binder) {
    binder.registerCustomEditor(SSN.class, new PropertyEditorSupport() {

        public String getAsText() {
            if(getValue() == null)
                return null;

            return ((SSN) getValue()).toString();
        }

        public void setAsText(String value) throws IllegalArgumentException {
            if(StringUtils.isBlank(value))
                return;

            boolean somethingGoesWrong = true;
            if(somethingGoesWrong)
                throw new IllegalArgumentException("Something goes wrong!");
        }

    });
}

Now our Test class

public class PersonControllerTest {

    private PersonController personController;
    private MockHttpServletRequest request;

    @BeforeMethod
    public void setUp() {
        personController = new PersonController();
        personController.setCommandName("command");
        personController.setCommandClass(Person.class);
        personController.setBindingErrorProcessor(new CustomBindingErrorProcessor());

        request = new MockHttpServletRequest();
        request.setMethod("POST");
        request.addParameter("ssn", "somethingGoesWrong");
    }

    @Test
    public void done() {
        ModelAndView mav = personController.handleRequest(request, new MockHttpServletResponse());

        BindingResult bindingResult = (BindingResult) mav.getModel().get(BindingResult.MODEL_KEY_PREFIX + "command");

        FieldError fieldError = bindingResult.getFieldError("ssn");

        Assert.assertEquals(fieldError.getMessage(), "Something goes wrong!");
    }

}

regards,

As a follow up to @Arthur Ronald's answer, this is how I ended up implementing this:

On the controller:

setBindingErrorProcessor(new CustomBindingErrorProcessor());

And then the binding error processor class:

public class CustomBindingErrorProcessor extends DefaultBindingErrorProcessor {

    public void processPropertyAccessException(PropertyAccessException accessException, 
                                               BindingResult bindingResult) {

        if(accessException.getCause() instanceof IllegalArgumentException){

            String fieldName = accessException.getPropertyChangeEvent().getPropertyName();
            String exceptionError = accessException.getCause().getMessage();

            FieldError fieldError = new FieldError(fieldName,
                                                   "BINDING_ERROR", 
                                                   fieldName + ": " + exceptionError);

            bindingResult.addError(fieldError);
        }else{
            super.processPropertyAccessException(accessException, bindingResult);
        }

    }

}        

So the processor method's signature takes a BindingResult instead of a BindException on this version.

I believe you could just try to put this in your message source:

typeMismatch.person.ssn=Wrong SSN format

This sounds similar to an issue I had with NumberFormatExceptions when the value for an integer property could not be bound if, say, a String was entered in the form. The error message on the form was a generic message for that exception.

The solution was to add my own message resource bundle to my application context and add my own error message for type mismatches on that property. Perhaps you can do something similar for IllegalArgumentExceptions on a specific field.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM