Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Spring Form Validation with Two Objects

Got a complicated problem with a Spring Boot application that I've been trying to solve for a while now and I'm hoping someone can help me. I've removed all the other parts of the project and tried to make it as simple as possible. If you go to localhost:8080, there'll be a form with two text boxes to enter two names into, and a Submit button. The first name will be stored in the Nominee object, the second in the Submitter object. When you click Submit, it will perform validation on the fields to make sure neither of them are empty. I'll post the code below and explain my problem at the end.

Application.java

@SpringBootApplication
@EnableJms
@EnableWebMvc
public class Application {

    public static void main(String[] args) throws Exception {
        // Launch the application
        SpringApplication.run(Application.class, args);
    }
}

WebController.java

@Controller
public class WebController extends WebMvcConfigurerAdapter {
    protected static final Logger LOG = LoggerFactory.getLogger(WebController.class);

    @InitBinder("nominee")
    protected void initNomineeBinder(WebDataBinder binder) {
        binder.setValidator(new NomineeValidator());
    }

    @InitBinder("submitter")
    protected void initSubmitterBinder(WebDataBinder binder) {
        binder.setValidator(new SubmitterValidator());
    }

    @Override
    public void addViewControllers(ViewControllerRegistry registry) {
        registry.addViewController("/success").setViewName("success");
    }

    @RequestMapping(value="/", method=RequestMethod.GET)
    public String showForm(Model model) {
        model.addAttribute("nominee", new Nominee());
        model.addAttribute("submitter", new Submitter());
        return "form";
    }

    @RequestMapping(value="/", method=RequestMethod.POST)
    public String checkPersonInfo(@ModelAttribute(value="nominee") @Valid Nominee nominee,
                                  @ModelAttribute(value="submitter") @Valid Submitter submitter,
                                  BindingResult bindingResult, @Valid Model model) {
        LOG.info("Nominee to string: " + nominee.toString());
        LOG.info("Submitter to string: " + submitter.toString());
        LOG.info("bindingResult to string: " + bindingResult.toString());
        if (bindingResult.hasErrors()) {
            return "form";
        }

        return "redirect:/success";
    }
}

Nominee.java

import lombok.Data;

@Data
public class Nominee {
    private String name;
}

NomineeValidatior.java

public class NomineeValidator implements Validator {

    public boolean supports(Class clazz) {
        return Nominee.class.equals(clazz);
    }

    public void validate(Object object, Errors errors) {
        ValidationUtils.rejectIfEmpty(errors, "name", "name", "This field is empty.");
    }
}

Submitter.java

import lombok.Data;

@Data
public class Submitter {
    private String sname;
}

SubmitterValidator.java

public class SubmitterValidator implements Validator {

    public boolean supports(Class clazz) {
        return Submitter.class.equals(clazz);
    }

    public void validate(Object object, Errors errors) {
        ValidationUtils.rejectIfEmpty(errors, "sname", "sname", "This field is empty.");
    }
}

form.html

<html>
<body>
<form role="form" th:action="@{/}" method="post">
    <h2>Nominee details</h2>
    <table>
        <tr>
            <td>Name:</td>
            <td>
                <input type="text" th:field="${nominee.name}"/>
            </td>
            <td><p th:if="${#fields.hasErrors('nominee.name')}" th:errors="${nominee.name}">Empty field</p></td>
        </tr>
    </table>
    <h2>Your details</h2>
    <table>
        <tr>
            <td>Your name:</td>
            <td>
                <input type="text" th:field="${submitter.sname}"/>
            </td>
            <td><p th:if="${#fields.hasErrors('submitter.sname')}" th:errors="${submitter.sname}">Empty field</p></td>
        </tr>
    </table>
    <div>
        <button type="submit">Submit nomination</button>
    </div>
</form>
</body>
</html>

success.html

<html><body>Form successfully submitted.</body></html>

If I leave the first text field blank (and fill or don't fill in the second text field), an error message appears on screen that reads:

Whitelabel Error Page
This application has no explicit mapping for /error, so you are seeing this as a fallback. Tue May 12 13:10:17 AEST 2015 There was an unexpected error (type=Bad Request, status=400). Validation failed for object='nominee'. Error count: 1

I don't know how to fix it so that leaving the first textbox blank won't cause a whitelabel error page. If I leave the second text field blank but fill in the first, it acts perfectly normal, so I'm not sure why it causes an error if I try it the other way around. Any help fixing this would be greatly appreciated.

Also, you may have noticed I had to use 'name' and 'sname' as my variables in Nominee and Submitter, if I set them both to 'name' then it doesn't work properly. If there's any way to edit it so they both can use 'name', I'd love to know how.

Edit: Found a solution. In the WebController, checkPersonInfo needs a separate BindingResult for each object being validated. The BindingResult needs to be in the method parameters immediately following each @Valid object.

So, in WebController.java, this:

@RequestMapping(value="/", method=RequestMethod.POST)
public String checkPersonInfo(@ModelAttribute(value="nominee") @Valid Nominee nominee,
                              @ModelAttribute(value="submitter") @Valid Submitter submitter,
                              BindingResult bindingResult, @Valid Model model) {
    LOG.info("Nominee to string: " + nominee.toString());
    LOG.info("Submitter to string: " + submitter.toString());
    LOG.info("bindingResult to string: " + bindingResult.toString());
    if (bindingResult.hasErrors()) {
        return "form";
    }

    return "redirect:/success";
}

Needs to become this:

@RequestMapping(value="/", method=RequestMethod.POST)
public String checkPersonInfo(@ModelAttribute(value="nominee") @Valid Nominee nominee,
                              BindingResult bindingResultNominee,
                              @ModelAttribute(value="submitter") @Valid Submitter submitter,
                              BindingResult bindingResultSubmitter) {
    LOG.info("Nominee to string: " + nominee.toString());
    LOG.info("Submitter to string: " + submitter.toString());
    if (bindingResultNominee.hasErrors() || bindingResultSubmitter.hasErrors()) {
        return "form";
    }

    return "redirect:/success";
}

(The model object was removed since it's never actually used anywhere, if you needed to validate it with @Valid then you'd add a third BindingResult object.)

like image 637
Jordan Avatar asked May 12 '15 03:05

Jordan


2 Answers

Found a solution. In the WebController, checkPersonInfo needs a separate BindingResult for each object being validated. The BindingResult needs to be in the method parameters immediately following each @Valid object.

So, in WebController.java, this:

    @RequestMapping(value="/", method=RequestMethod.POST)
public String checkPersonInfo(@ModelAttribute(value="nominee") @Valid Nominee nominee,
                              @ModelAttribute(value="submitter") @Valid Submitter submitter,
                              BindingResult bindingResult, @Valid Model model) {
    LOG.info("Nominee to string: " + nominee.toString());
    LOG.info("Submitter to string: " + submitter.toString());
    LOG.info("bindingResult to string: " + bindingResult.toString());
    if (bindingResult.hasErrors()) {
        return "form";
    }

    return "redirect:/success";
}

Needs to become this:

    @RequestMapping(value="/", method=RequestMethod.POST)
public String checkPersonInfo(@ModelAttribute(value="nominee") @Valid Nominee nominee,
                              BindingResult bindingResultNominee,
                              @ModelAttribute(value="submitter") @Valid Submitter submitter,
                              BindingResult bindingResultSubmitter) {
    LOG.info("Nominee to string: " + nominee.toString());
    LOG.info("Submitter to string: " + submitter.toString());
    if (bindingResultNominee.hasErrors() || bindingResultSubmitter.hasErrors()) {
        return "form";
    }

    return "redirect:/success";
}

(The model object was removed since it's never actually used anywhere, if you needed to validate it with @Valid then you'd add a third BindingResult object.)

like image 85
Jordan Avatar answered Nov 05 '22 00:11

Jordan


The usual case is to use Dto objects for that case. This means you create an object containing all relevant form fields and validate based on that.

For example:

@Data
public class MyDto {

    private Nominee nominee;

    private Submitter submitter;
}

@RequestMapping(value="/", method=RequestMethod.POST)
public String checkPersonInfo(@Valid MyDto dto, BindingResult bindingResult) {
    LOG.info("Nominee to string: " + dto.getNominee().toString());
    LOG.info("Submitter to string: " + dto.getSubmitter().toString());
    LOG.info("bindingResult to string: " + bindingResult.toString());
    if (bindingResult.hasErrors()) {
        return "form";
    }
    return "redirect:/success";
}

Additionally you can add something like that to your controller.

@ExceptionHandler(Throwable.class)
public ModelAndView processError(Throwable ex) {
    ex.toString();
    // do something and return a ModelAndView object as you like.
}
like image 25
mh-dev Avatar answered Nov 05 '22 00:11

mh-dev