Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Spring MVC Controller, how to keep BindingResult errors, while emptying the form values

Tags:

spring-mvc

I have a web form, using Spring MVC Controller. The form is validated by Spring. When there are validation errors, Spring shows the same form, pre-filled in with the values entered by the user, and the validation errors.

For security reasons, I don't want the form the be pre-filled with the values entered by the user, but I do need to show the validation errors.

How can I do this?

I've achieved this behaviour by looking at the Spring MVC source code and seeing how the BINDING_RESULT_KEY is built. Here it is the source code.

However, this is a hack, and it might stop working on a new version of Spring MVC.

How do I achieve this properly?

package com.nespresso.ecommerce.naw.controller;

import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BeanPropertyBindingResult;
import org.springframework.validation.BindingResult;
import org.springframework.validation.Errors;
import org.springframework.validation.ObjectError;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;

import javax.validation.Valid;


@Controller
@RequestMapping("/my_form")
public class MyFormController extends FrontEndController {
    final String MY_FORM_OBJECT_NAME = "myForm";
    final String BINDING_RESULT_KEY = BindingResult.MODEL_KEY_PREFIX + "myForm";

    @RequestMapping(method = RequestMethod.POST)
    public String post(@Valid @ModelAttribute("myForm") MyForm myForm, Errors errors, Model model) {
        if (errors.hasErrors()) {
            emptyMyFormWhileKeepingFormErrors(model);
            return "my_form";
        }

        return "redirect:/confirmation";
    }

    @ModelAttribute("myForm")
    public MyForm myForm() {
        return new MyForm();
    }

    private void emptyMyFormWhileKeepingFormErrors(Model model) {
        BeanPropertyBindingResult bindingResult = (BeanPropertyBindingResult) model.asMap().get(BINDING_RESULT_KEY);
        if (bindingResult == null) {
            return;
        }
        MyForm emptyForm = myForm();

        // set the empty form, so the form is not pre-filled with the previous values.
        // However, this clears the validation errors also
        model.addAttribute(MY_FORM_OBJECT_NAME, emptyForm);

        // re-attach the validation errors, and empty the rejectedValue
        BeanPropertyBindingResult updatedBindingResult = new BeanPropertyBindingResult(emptyForm, MY_FORM_OBJECT_NAME);
        for (ObjectError oe : bindingResult.getAllErrors()) {
            if (!(oe instanceof FieldError)) {
                updatedBindingResult.addError(oe);
            } else {
                FieldError fieldError = (FieldError) oe;

                String rejectedValue = null;   // that's the point, create a copy of the FieldError, emptying the rejectedValue;

                FieldError updatedFieldError = new FieldError(
                        MY_FORM_OBJECT_NAME,
                        fieldError.getField(),
                        rejectedValue,
                        fieldError.isBindingFailure(),
                        fieldError.getCodes(),
                        fieldError.getArguments(),
                        fieldError.getDefaultMessage());
                updatedBindingResult.addError(updatedFieldError);
            }
        }

        model.addAttribute(BINDING_RESULT_KEY, updatedBindingResult);
    }
}
like image 345
David Portabella Avatar asked Aug 15 '14 12:08

David Portabella


2 Answers

The simplest solution I can think of is to not use the <form:*> tags. Instead use a standard <input> tag.

For example:

<form:form modelAttribute="userForm" method="post">
    Username: <form:input path="username" /><br>

    <form:errors path="password" element="div" />
    Password: <input path="password" ><br>

    <input type="submit" value="Save" />
</form:form>

In the above example the password field will remain blank at all times while still showing the validation error.

like image 44
Bart Avatar answered Sep 25 '22 11:09

Bart


Edit : add another solution

You can have an empty model object while keeping previous errors simply by copying fresh values (from a newly initialized object) into your model attribute via org.springframework.beans.BeanUtils:

@RequestMapping(method = RequestMethod.POST)
public String post(@Valid @ModelAttribute("myForm") MyForm myForm, Errors errors, Model model) {
    if (errors.hasErrors()) {
        MyForm emptyForm = new MyForm();
        BeanUtils.copyProperties(emptyForm, myForm);
        return "my_form";
    }

    return "redirect:/confirmation";
}

That way, you can still show previous errors, and the form is empty.

There is still a caveat : if works fine when I use <input> fields, but not with <form:input> spring enhanced fields that do use the rejected value from errors.

If you prefere to keep using <form:input> fields, you will have to create another BindingResult object and initialize it with current errors, simply setting the rejected values to null. You have even the possibility to reset all fields or only the fields in error :

@RequestMapping(value = "/form", method=RequestMethod.POST)
public String update(@Valid @ModelAttribute Form form, BindingResult result, Model model) {
    if (result.hasErrors()) {
        // uncomment line below to reset all fields - by default only offending ones are
        //form = new Form();
        BeanPropertyBindingResult result2 = new BeanPropertyBindingResult(form, result.getObjectName());
        for(ObjectError error: result.getGlobalErrors()) {
            result2.addError(error);
        }
        for (FieldError error: result.getFieldErrors()) {
            result2.addError(new FieldError(error.getObjectName(), error.getField(), null, error.isBindingFailure(), error.getCodes(), error.getArguments(), error.getDefaultMessage()));
        }
        model.addAllAttributes(result2.getModel());
        return "my_form";
    }

    return "redirect:/confirmation";
}
like image 117
Serge Ballesta Avatar answered Sep 22 '22 11:09

Serge Ballesta