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.)
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.)
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.
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With