I am developing a web application in Java using spring.
This application includes Ajax calls in javascript which requests html code that is then inserted into the html document.
In order to process a thymeleaf template into a String i'm using TemplateEngine process(..) method.
I encountered an error when the thymeleaf template contains a form.
My sample code:
form.html:
<form th:object="${customer}" xmlns:th="http://www.w3.org/1999/xhtml">
<label>Name</label>
<input type="text" th:field="*{name}" />
</form>
AjaxController.java:
package project;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.thymeleaf.TemplateEngine;
import org.thymeleaf.context.Context;
#Controller
public class AjaxController {
#Autowired
private TemplateEngine templateEngine;
private ObjectMapper objectMapper = new ObjectMapper();
#ResponseBody
#GetMapping(value="/form1")
public String form1() throws JsonProcessingException {
Customer customer = new Customer("Burger King");
Context templateContext = new Context();
templateContext.setVariable("customer", customer);
AjaxResponse response = new AjaxResponse();
response.html = templateEngine.process("form", templateContext);
response.additionalData = "ab123";
return objectMapper.writeValueAsString(response);
}
#GetMapping(value="/form2")
public String form2(Model model) throws JsonProcessingException {
Customer customer = new Customer("Burger King");
model.addAttribute("customer", customer);
return "form";
}
class Customer {
private String name;
public Customer(String name) {
this.name = name;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
class AjaxResponse {
public String html;
public String additionalData;
}
}
form1 is the one crashing, I'm trying to return the html code parsed by the thymeleaf template and also include additional data in this json response.
It crashes on the line templateEngine.process("form", templateContext);
form1 works when replacing form.html with:
Customer name is: [[${customer.name}]]
Which leads me to conclude that it is the form tag and th:object which causes this to crash.
form2 works just as expected, but without any way to manipulate the thymeleaf return value. It proves that the thymeleaf template itself is valid.
The whole error output is a bit too massive to paste in here but:
org.thymeleaf.exceptions.TemplateInputException: An error happened during template parsing (template: "class path resource [templates/form.html]")
Caused by: org.thymeleaf.exceptions.TemplateProcessingException: Cannot process attribute '{th:field,data-th-field}': no associated BindStatus could be found for the intended form binding operations. This can be due to the lack of a proper management of the Spring RequestContext, which is usually done through the ThymeleafView or ThymeleafReactiveView (template: "form" - line 3, col 21)
My question is: Is this a bug in the spring framework? or if not then what am i doing wrong?
Update 1:
Replacing th:field with th:value makes it work, seems that th:field inside a form when using TemplateEngine .process is what produces the error.
Update 2:
Okay so after a lot of detective work i've figured out a sort of hack to make this work temporarily. The problem is that thymeleaf requires IThymeleafRequestContext to process a template with a form, When TemplateEngine .process runs then this will not be created. It is possible to inject this into your model like following:
#Autowired
ServletContext servletContext;
private String renderToString(HttpServletRequest request, HttpServletResponse response, String viewName, Map<String, Object> parameters) {
Context templateContext = new Context();
templateContext.setVariables(parameters);
RequestContext requestContext = new RequestContext(request, response, servletContext, parameters);
SpringWebMvcThymeleafRequestContext thymeleafRequestContext = new SpringWebMvcThymeleafRequestContext(requestContext, request);
templateContext.setVariable("thymeleafRequestContext", thymeleafRequestContext);
return templateEngine.process(viewName, templateContext);
}
and now you use this method like this:
#ResponseBody
#GetMapping(value="/form1")
public String form1(HttpServletRequest request, HttpServletResponse response) throws JsonProcessingException {
Customer customer = new Customer("Burger King");
BindingAwareModelMap bindingMap = new BindingAwareModelMap();
bindingMap.addAttribute("customer", customer);
String html = renderToString(request, response, "form", bindingMap);
AjaxResponse resp = new AjaxResponse();
resp.html = html;
resp.additionalData = "ab123";
String json = objectMapper.writeValueAsString(resp);
return json;
}
I will not put down this as an answer as i don't see any reason of this being intended to be used this way. I'm in communication with the spring people to get a real fix for this.
Welcome to SO.
Remove xmlns:th="http://www.w3.org/1999/xhtml" from the form tag. This is not proper syntax. This would belong in the html tag.
You can find plenty of clear examples in the docs.
It seems you're trying to manually render an HTML template, outside of the web request context, return it serialized as an AJAX response - but still expect form binding to work. This is the key problem here.
Using th:field in a template means that you're expecting form binding from the HTTP request. In your code snippet, you're providing an empty, non-web context and still expect form binding to happen.
Since Thymeleaf can be used in various contexts (like rendering an email template before sending a newsletter, rendering a document in a batch application), we can't enforce a web context in all cases.
When rendering views the way Spring Framework expects things (by returning the view name as the return value of the controller handler), Spring will use and configure Thymeleaf accordingly.
Your answer is technically valid because it solves your problem, but it comes from the convoluted constraint of rendering a template and wrap that into a json String, and still expect HTTP binding.
Related
I am a beginner so please don't be mean.
I have got an html page index.html
And I want the method MainController::getListEmployee to be called.
In this method, I put a System.err to see if the method is called. And I see nothing.
Controller code
package com.cgi.listeemployes.controller;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import com.cgi.listeemployes.model.User;
import com.cgi.listeemployes.repository.UserRepository;
#Controller // This means that this class is a Controller
#RequestMapping(path="/") // This means URL's start with /demo (after Application path)
public class MainController {
#Autowired // This means to get the bean called userRepository
// Which is auto-generated by Spring, we will use it to handle the data
private UserRepository userRepository;
#GetMapping(path="/index.html")
public #ResponseBody Iterable<User> getListEmployee() {
// This returns a JSON or XML with the users
System.err.println("getting ");
return userRepository.findAll();
}
#PostMapping(path="/add") // Map ONLY POST Requests
public #ResponseBody String addNewUser (#RequestParam String name
, #RequestParam String email) {
// #ResponseBody means the returned String is the response, not a view name
// #RequestParam means it is a parameter from the GET or POST request
User n = new User();
n.setName(name);
n.setEmail(email);
userRepository.save(n);
return "Saved";
}
#GetMapping(path="/all")
public #ResponseBody Iterable<User> getAllUsers() {
// This returns a JSON or XML with the users
return userRepository.findAll();
}
}
thanks for your help
When you want to return a html, just return a string with the name of the html file, it could be "Index" (without the .html).
In your #GetMapping(path="/index.html"), you are returning an object instead a html.
If you want to load data from database and render it at your html, then add the attribute "Model model" in your parameters, like this:
#GetMapping(path="/index.html")
public String getListEmployee(Model model) {
List<User> users = userRepository.findAll();
model.addAttribute("yourUsers", users); // this gonna inject the list of users in your html
System.err.println("getting ");
return "Index"
}
Then in your html, you can get the users with ${yourUsers} and do whatever you want.
I saw your project, it is missing the template engine. Template engine is what gonna get the data of your backend and show in your front/html. I added the Thymeleaf template engine into your pom.xml, and it worked. Here is the thymeleaf dependency:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
To work with thymeleaf, you have to put all your html into a new folder called "templates" in the "resources", same level of "static". You cannot use html in the static folder, this folder should have only css, javascripts and assets.
I need to submit a JS-formed Array of Objects to a Spring MVC Controller. All the property names match.
#PostMapping("/addAdmin")
public void addAdmin(#RequestParam List<UserRolesGUIBean> userRolesGUIBeans)
{
// ...
}
JS:
var entries = [];
//...
// entries is an array of objects of the form {id: "..", role: ".."}
// verified to be correct before submission
$.ajax({
type : "post",
dataType : "json",
url : 'addAdmin',
data : JSON.stringify(entries)
})
Bean
public class UserRolesGUIBean implements Serializable {
private String id;
private String role;
// + Constructors (Empty + Full), Getters and setters
}
Error:
Required List parameter 'userRolesGUIBeans' is not present]
Also tried this with ModelAttribute and an ArrayList,
PostMapping("/addAdmin")
public void addAdmin(#ModelAttribute ArrayList<UserRolesGUIBean> userRolesGUIBeans) {
Now there are no errors, but the list is empty, no data was received.
Tried everything -- arrays vs. lists, JSON.stringify(data) or a data object with data {"entries" : entries}, RequestBody doesn't work and gives UTF Errors; and RequestParam as above doesn't work either.
This is way too complicated for a simple task.
You are trying to send a JSON object by using a post. You should use #RequestBody annotation.
Try to change your method in this way:
#PostMapping("/addAdmin")
public void addAdmin(#RequestBody List<UserRolesGUIBean> userRolesGUIBeans)
{
// ...
}
In this way Spring will intercept the Json and transform it in List of wished objects
SOLUTION:
1) In theory, if I was doing Form Submission (like $('#myForm').submit()), I could use #ModelAttribute to automatically bind my form to the bean. That's what #ModelAttribute does -- it's used for Form Submission. I don't have a real form; only my own custom values.
I could still "fake" a Form Submit by creating a Dynamic Form "on the fly," but I couldn't get the Arrayed-Field Form Submission (e.. obj[] with [] notation in the HTML Name) to map to a #ModelAttribute List<Bean>, so I disregarded this unusual Form Submit approach.
2) The real approach that worked is to just submit a custom JSON string which is my own. Not related to any form submission, so can't use #ModelAttribute. Instead, this is the #RequestBody approach. Then I have to parse the JSON RequestBody String myself -- and here we have to use Jackson, Java JSON, or GSON to parse the JSON Array.
In my case,
JS:
$.ajax({
type : "post",
dataType : 'json',
url : 'addAdmin',
data : JSON.stringify(entries)
})
Controller (note it takes a custom String only). Then it uses Jackson to parse the string manually, because Spring won't do it in this case. (Spring will only auto-parse if you're using #ModelAttribute form binding.)
#PostMapping("/addAdmin")
public boolean addAdmin(#RequestBody String json) throws Exception {
String decodedJson = java.net.URLDecoder.decode(json, "UTF-8");
ObjectMapper jacksonObjectMapper = new ObjectMapper(); // This is Jackson
List<UserRolesGUIBean> userRolesGUIBeans = jacksonObjectMapper.readValue(
decodedJson, new TypeReference<List<UserRolesGUIBean>>(){});
// Now I have my list of beans populated.
}
As promised, please go to https://start.spring.io/ and create a new project with a single depdendency for spring-boot-starter-web.
After that, you can create the following bean in your project.
import java.util.List;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
#RestController
public class DemoController {
#PostMapping("/request-body")
public void getRequestBody(#RequestBody List<Person> list) {
for (Person person : list) {
System.out.println(person.name);
}
}
public static class Person {
private String name;
private String phoneNo;
/**
* #return the name
*/
public String getName() {
return name;
}
/**
* #param name the name to set
*/
public void setName(String name) {
this.name = name;
}
/**
* #return the phoneNo
*/
public String getPhoneNo() {
return phoneNo;
}
/**
* #param phoneNo the phoneNo to set
*/
public void setPhoneNo(String phoneNo) {
this.phoneNo = phoneNo;
}
}
}
Nothing special, just take in a list of Person and print out the names. You can right click and run the project directly from the IDE.
You can open Postman and make a POST request as following.
This is what gets printed in the console.
If it works with Postman, you can make it work in JS. You just haven't figured out how. Instead of settling with that "workaround" you found, I think you should find out the proper way to submit a request in JS. In addition, some understanding of the Spring framework would help too. Otherwise, you will just keep randomly trying stuff like #ModelAttribute without getting anywhere.
I have written a book catalog in Spring.
It collects books (pdf, epub, mobi, ebook) from a directory, collects some metadata from them, stores them in a DB and then puts them in a List that is made available to my views:
#Slf4j
#Controller
public class BookCatalogController {
// == Fields ==
private final BookService bookService;
#Autowired
public BookCatalogController(BookService bookService){this.bookService = bookService; }
// == Model attributes ==
#ModelAttribute
public List<Book> bookData(){ return bookService.getBooksFromMemory(); }
public static final File bookDirectory= new File("D:\\edu_repo\\ebooks_test\\");
.
.
.
// Catalog Simple View
#GetMapping(Mappings.CATALOG_SIMPLE)
public String catalogSimple(Model model){
log.info("catalogSimple method called");
// This is adding the entire BookManager book list into the model.
model.addAttribute(AttributeNames.BOOK_DATA, bookData());
return ViewNames.CATALOG_SIMPLE;
}
// Catalog Detail View
#GetMapping(Mappings.CATALOG_DETAIL)
public String catalogDetail(Model model){
log.info("catalogDetail method called");
// This is adding the entire BookManager book list into the model.
model.addAttribute(AttributeNames.BOOK_DATA, bookData());
return ViewNames.CATALOG_DETAIL;
}
.
.
.
#GetMapping(Mappings.LOAD_BOOKS)
public void loadBooks(Model model) {
bookService.loadBooksFromDirectory(bookDirectory);
}
}
Obviously I'm not using #GetMapping(Mappings.LOAD_BOOKS) properly as you can see in the error below:
The error:
There was an unexpected error (type=Internal Server Error, status=500).
Error resolving template [load-books], template might not exist or might not be accessible by any of the configured Template Resolvers
org.thymeleaf.exceptions.TemplateInputException: Error resolving template [load-books], template might not exist or might not be accessible by any of the configured Template Resolvers
How does one invoke a method like I am doing but without Spring trying to redirect the user to another view?
I'm not expecting the page to update at all since I'm not returning a View!
When you click a link in your browser with a load-books anchor, your browser sends it to the server and waits for result, which causes your page to be reloaded. Once the request to a load-books endpoint reached to the server, Spring MVC handles this and starting to looking up an appropriate controller with its method. It founds public void loadBooks(Model model) in your case. When Spring MVC invokes the method, it expects to obtain a view name to resolve and return back to your browser.
Since you haven't provided a View or String as a return type, Spring MVC used the endpoint's path as a view name (I'm not seeing your Mappings.LOAD_BOOKS constant, but it supposed to be load-books).
If you're not going to return any view back to the browser, you can annotate the method like that:
#GetMapping(Mappings.LOAD_BOOKS)
#ResponseBody
public void loadBooks(Model model) {
which tells Spring to treat void as a response body.
But it's not preventing a page refreshing, you'll just see an empty page after clicking the link. In order to fix this you can redirect a user to another page by returning the following string (without ResponseBody annotation on the method)
return "redirect:/path-to-redirect";
When Spring MVC sees this prefix it redirects you to another controller, but user going to notice that too.
If you really don't want to see a blank page for a moment, you'll have to use some JavaScript to perform AJAX request to the server when button is clicked.
Actually, it seems that you want to preload some files in a service by a given path. If it's all you want to do, you can use Spring's runners like that:
#Component
class Preloader implements ApplicationRunner {
private final BookCatalogService bookService;
#Autowired
public Preloader(BookCatalogService service) {
this.bookService = service;
}
#Override
public void run(ApplicationArguments args) throws Exception {
bookService.loadBooksFromDirectory(BookCatalogController.bookDirectory);
}
}
Spring automatically calls all registered runners when application is ready, so your code will be executed without having a user to visit load-books endpoint.
When I have the following model with JSR-303 (validation framework) annotations:
public enum Gender {
MALE, FEMALE
}
public class Profile {
private Gender gender;
#NotNull
private String name;
...
}
and the following JSON data:
{ "gender":"INVALID_INPUT" }
In my REST controller, I want to handle both the binding errors (invalid enum value for gender property) and validation errors (name property cannot be null).
The following controller method does NOT work:
#RequestMapping(method = RequestMethod.POST)
public Profile insert(#Validated #RequestBody Profile profile, BindingResult result) {
...
}
This gives com.fasterxml.jackson.databind.exc.InvalidFormatException serialization error before binding or validation takes place.
After some fiddling, I came up with this custom code which does what I want:
#RequestMapping(method = RequestMethod.POST)
public Profile insert(#RequestBody Map values) throws BindException {
Profile profile = new Profile();
DataBinder binder = new DataBinder(profile);
binder.bind(new MutablePropertyValues(values));
// validator is instance of LocalValidatorFactoryBean class
binder.setValidator(validator);
binder.validate();
// throws BindException if there are binding/validation
// errors, exception is handled using #ControllerAdvice.
binder.close();
// No binding/validation errors, profile is populated
// with request values.
...
}
Basically what this code does, is serialize to a generic map instead of model and then use custom code to bind to model and check for errors.
I have the following questions:
Is custom code the way to go here or is there a more standard way of doing this in Spring Boot?
How does the #Validated annotation work? How can I make my own custom annotation that works like #Validated to encapsulate my custom binding code?
This is the code what i have used in one of my project for validating REST api in spring boot,this is not same as you demanded,but is identical.. check if this helps
#RequestMapping(value = "/person/{id}",method = RequestMethod.PUT)
#ResponseBody
public Object updatePerson(#PathVariable Long id,#Valid Person p,BindingResult bindingResult){
if (bindingResult.hasErrors()) {
List<FieldError> errors = bindingResult.getFieldErrors();
List<String> message = new ArrayList<>();
error.setCode(-2);
for (FieldError e : errors){
message.add("#" + e.getField().toUpperCase() + ":" + e.getDefaultMessage());
}
error.setMessage("Update Failed");
error.setCause(message.toString());
return error;
}
else
{
Person person = personRepository.findOne(id);
person = p;
personRepository.save(person);
success.setMessage("Updated Successfully");
success.setCode(2);
return success;
}
Success.java
public class Success {
int code;
String message;
public int getCode() {
return code;
}
public void setCode(int code) {
this.code = code;
}
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
}
Error.java
public class Error {
int code;
String message;
String cause;
public int getCode() {
return code;
}
public void setCode(int code) {
this.code = code;
}
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
public String getCause() {
return cause;
}
public void setCause(String cause) {
this.cause = cause;
}
}
You can also have a look here : Spring REST Validation
Usually when Spring MVC fails to read the http messages (e.g. request body), it will throw an instance of HttpMessageNotReadableException exception. So, if spring could not bind to your model, it should throw that exception. Also, if you do NOT define a BindingResult after each to-be-validated model in your method parameters, in case of a validation error, spring will throw a MethodArgumentNotValidException exception. With all this, you can create ControllerAdvice that catches these two exceptions and handles them in your desirable way.
#ControllerAdvice(annotations = {RestController.class})
public class UncaughtExceptionsControllerAdvice {
#ExceptionHandler({MethodArgumentNotValidException.class, HttpMessageNotReadableException.class})
public ResponseEntity handleBindingErrors(Exception ex) {
// do whatever you want with the exceptions
}
}
You can't get BindException with #RequestBody. Not in the controller with an Errors method parameter as documented here:
Errors, BindingResult For access to errors from validation and data
binding for a command object (that is, a #ModelAttribute argument) or
errors from the validation of a #RequestBody or #RequestPart
arguments. You must declare an Errors, or BindingResult argument
immediately after the validated method argument.
It states that for #ModelAttribute you get binding AND validation errors and for your #RequestBody you get validation errors only.
https://docs.spring.io/spring/docs/current/spring-framework-reference/web.html#mvc-ann-methods
And it was discussed here:
https://github.com/spring-projects/spring-framework/issues/11406?jql=text%2520~%2520%2522RequestBody%2520binding%2522
For me it still does not make sense from a user point of view. It is often very important to get the BindExceptions to show the user a proper error message. The argument is, you should do client side validation anyway. But this is not true if a developer is using the API directly.
And imagine your client side validation is based on an API request. You want to check if a given date is valid based on a saved calendar. You send the date and time to the backend and it just fails.
You can modify the exception you get with an ExceptionHAndler reacting on HttpMessageNotReadableException, but with this exception I do not have proper access to which field was throwing the error as with a BindException. I need to parse the exception message to get access to it.
So I do not see any solution, which is kind of bad because with #ModelAttribute it is so easy to get binding AND validation errors.
I've given up on this; it is just not possible to get the binding errors using #RequestBody without a lot of custom code. This is different from controllers binding to plain JavaBeans arguments because #RequestBody uses Jackson to bind instead of the Spring databinder.
See https://jira.spring.io/browse/SPR-6740?jql=text%20~%20%22RequestBody%20binding%22
One of the main blocker for solving this problem is the default eagerly-failing nature of the jackson data binder; one would have to somehow convince it to continue parsing instead of just stumble at first error. One would also have to collect these parsing errors in order to ultimately convert them to BindingResult entries. Basically one would have to catch, suppress and collect parsing exceptions, convert them to BindingResult entries then add these entries to the right #Controller method BindingResult argument.
The catch & suppress part could be done by:
custom jackson deserializers which would simply delegate to the default related ones but would also catch, suppress and collect their parsing exceptions
using AOP (aspectj version) one could simply intercept the default deserializers parsing exceptions, suppress and collect them
using other means, e.g. appropriate BeanDeserializerModifier, one could also catch, suppress and collect the parsing exceptions; this might be the easiest approach but requires some knowledge about this jackson specific customization support
The collecting part could use a ThreadLocal variable to store all necessary exceptions related details. The conversion to BindingResult entries and the addition to the right BindingResult argument could be pretty easily accomplished by an AOP interceptor on #Controller methods (any type of AOP, Spring variant including).
What's the gain
By this approach one gets the data binding errors (in addition to the validation ones) into the BindingResult argument the same way as would expect for getting them when using an e.g. #ModelAttribute. It will also work with multiple levels of embedded objects - the solution presented in the question won't play nice with that.
Solution Details (custom jackson deserializers approach)
I created a small project proving the solution (run the test class) while here I'll just highlight the main parts:
/**
* The logic for copying the gathered binding errors
* into the #Controller method BindingResult argument.
*
* This is the most "complicated" part of the project.
*/
#Aspect
#Component
public class BindingErrorsHandler {
#Before("#within(org.springframework.web.bind.annotation.RestController)")
public void logBefore(JoinPoint joinPoint) {
// copy the binding errors gathered by the custom
// jackson deserializers or by other means
Arrays.stream(joinPoint.getArgs())
.filter(o -> o instanceof BindingResult)
.map(o -> (BindingResult) o)
.forEach(errors -> {
JsonParsingFeedBack.ERRORS.get().forEach((k, v) -> {
errors.addError(new FieldError(errors.getObjectName(), k, v, true, null, null, null));
});
});
// errors copied, clean the ThreadLocal
JsonParsingFeedBack.ERRORS.remove();
}
}
/**
* The deserialization logic is in fact the one provided by jackson,
* I only added the logic for gathering the binding errors.
*/
public class CustomIntegerDeserializer extends StdDeserializer<Integer> {
/**
* Jackson based deserialization logic.
*/
#Override
public Integer deserialize(JsonParser p, DeserializationContext ctxt) throws IOException, JsonProcessingException {
try {
return wrapperInstance.deserialize(p, ctxt);
} catch (InvalidFormatException ex) {
gatherBindingErrors(p, ctxt);
}
return null;
}
// ... gatherBindingErrors(p, ctxt), mandatory constructors ...
}
/**
* A simple classic #Controller used for testing the solution.
*/
#RestController
#RequestMapping("/errormixtest")
#Slf4j
public class MixBindingAndValidationErrorsController {
#PostMapping(consumes = MediaType.APPLICATION_JSON_UTF8_VALUE)
public Level1 post(#Valid #RequestBody Level1 level1, BindingResult errors) {
// at the end I show some BindingResult logging for a #RequestBody e.g.:
// {"nr11":"x","nr12":1,"level2":{"nr21":"xx","nr22":1,"level3":{"nr31":"xxx","nr32":1}}}
// ... your whatever logic here ...
With these you'll get in BindingResult something like this:
Field error in object 'level1' on field 'nr12': rejected value [1]; codes [Min.level1.nr12,Min.nr12,Min.java.lang.Integer,Min]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [level1.nr12,nr12]; arguments []; default message [nr12],5]; default message [must be greater than or equal to 5]
Field error in object 'level1' on field 'nr11': rejected value [x]; codes []; arguments []; default message [null]
Field error in object 'level1' on field 'level2.level3.nr31': rejected value [xxx]; codes []; arguments []; default message [null]
Field error in object 'level1' on field 'level2.nr22': rejected value [1]; codes [Min.level1.level2.nr22,Min.level2.nr22,Min.nr22,Min.java.lang.Integer,Min]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [level1.level2.nr22,level2.nr22]; arguments []; default message [level2.nr22],5]; default message [must be greater than or equal to 5]
where the 1th line is determined by a validation error (setting 1 as the value for a #Min(5) private Integer nr12;) while the 2nd is determined by a binding one (setting "x" as value for a #JsonDeserialize(using = CustomIntegerDeserializer.class) private Integer nr11;). 3rd line tests binding errors with embedded objects: level1 contains a level2 which contains a level3 object property.
Note how other approaches could simply replace the usage of custom jackson deserializers while keeping the rest of the solution (AOP, JsonParsingFeedBack).
enter code here
public class User {
#NotNull
#Size(min=3,max=50,message="min 2 and max 20 characters are alllowed !!")
private String name;
#Email
private String email;
#Pattern(regexp="[7-9][0-9]{9}",message="invalid mobile number")
#Size(max=10,message="digits should be 10")
private String phone;
#Override
public String toString() {
return "User [name=" + name + ", email=" + email + ", phone=" + phone + "]";
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public String getPhone() {
return phone;
}
public void setPhone(String phone) {
this.phone = phone;
}
}
Controller.java
#Controller
public class User_Controller {
#RequestMapping("/")
public String showForm(User u,Model m)
{
m.addAttribute("user",new User());
m.addAttribute("title","Validation Form");
return "register";
}
#PostMapping("/")
public String register(#Valid User user,BindingResult bindingResult ,Model m)
{
if(bindingResult.hasErrors())
{
return "register";
}
else {
m.addAttribute("message", "Registration successfully... ");
return "register";
}
}
}
register.html
<div class="container">
<div class="alert alert-success" role="alert" th:text="${message}">
</div>
<h1 class="text-center">Validation Form </h1>
<form action="/" th:action="#{/}" th:object="${user}" method="post">
<div class="mb-3">
<label for="exampleInputEmail1" class="form-label">Name</label>
<input type="text" class="form-control" id="exampleInputEmail1" aria-
describedby="emailHelp" th:field="*{name}">
<br>
<p th:if="${#fields.hasErrors('name')}" th:errors="*{name}" class="alert alert-
danger"></p>
</div>
<div class="mb-3">
<label for="exampleInputPassword1" class="form-label">Email</label>
<input type="email" class="form-control" id="exampleInputPassword1" th:field="*
{email}">
<br>
<p th:if="${#fields.hasErrors('email')}" th:errors="*{email}" class="alert alert-
danger"></p>
</div>
<div class="mb-3">
<label for="exampleInputPassword1" class="form-label">Phone</label>
<input type="text" class="form-control" id="exampleInputPassword1" th:field="*
{phone}">
<p th:if="${#fields.hasErrors('phone')}" th:errors="*{phone}" class="alert alert-
danger"></p>
<br>
</div>
<button type="submit" class="btn btn-primary">Submit</button>
</form>
</div>
According to this post https://blog.codecentric.de/en/2017/11/dynamic-validation-spring-boot-validation/ - you can add an extra parameter "Errors" to your controller method - eg.
#RequestMapping(method = RequestMethod.POST)
public Profile insert(#Validated #RequestBody Profile profile, Errors errors) {
...
}
to then get validation errors, if any, in that.
I think I should answer your questions in reverse order.
For your second question,
The #Validate annotation throws MethodArgumentNotValidException if there is an error during field validation. The object of this annotation contains two methods, getBindingResult(),getAllErrors() which gives details of validation error. You may create your custom annotation with AspectJ (AOP). But that's not needed here. Because your situation can be solved using the ExceptionHandler of SpringBoot.
Now your first question,
Please go through section 5 of this link Link. Actually it covers whole bean validation in spring boot. Your problem can be solved by section 5. Basic knowledge on general exception handling in spring boot may be good to understand it better. For that, I can share the query link on google for this topic ExceptionHandling.Please go through the first few results of it.
I'm using HibernateValidator 4.3.1. Validations are performed as intended throughout the entire application.
I have registered some custom editors to perform validation globally such as for ensuring numeric values (double, int etc) in a text-field, for ensuring valid dates regarding the Joda-Time API etc.
In this type of validation, I'm allowing null/empty values by setting the allowEmpty parameter to false as usual to validate it separately especially for displaying separate user friendly error messages when such fields are left blank.
Therefore, in addition to validating with HibernateValidator and custom editors, I'm trying to use the following validation strategy. Again, this kind of validation is only for those fields which are registered for custom editors are when left blank.
The following is the class that implements the org.springframework.validation.Validator interface.
package test;
import org.springframework.stereotype.Component;
import org.springframework.validation.Errors;
import org.springframework.validation.ValidationUtils;
import org.springframework.validation.Validator;
import validatorbeans.TempBean;
#Component
public final class TempValidator implements Validator {
#Override
public boolean supports(Class<?> clazz) {
System.out.println("supports() invoked.");
return TempBean.class.isAssignableFrom(clazz);
}
#Override
public void validate(Object target, Errors errors) {
TempBean tempBean = (TempBean) target;
System.out.println("startDate = " + tempBean.getStartDate() + " validate() invoked.");
System.out.println("doubleValue = " + tempBean.getDoubleValue() + " validate() invoked.");
System.out.println("stringValue = " + tempBean.getStringValue() + " validate() invoked.");
ValidationUtils.rejectIfEmptyOrWhitespace(errors, "startDate", "java.util.date.nullOrEmpty.error");
ValidationUtils.rejectIfEmptyOrWhitespace(errors, "doubleValue", "java.lang.double.nullOrEmpty.error");
}
}
The class is designated with the #Component annotation so that it can be auto-wired to a specific Spring controller class. The debugging statements display exactly based on the input provided by a user.
The following is the controller class.
package controller;
import customizeValidation.CustomizeValidation;
import java.util.Map;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.validation.Valid;
import javax.validation.groups.Default;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.validation.BindingResult;
import org.springframework.validation.DataBinder;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import test.TempValidator;
import validatorbeans.TempBean;
#Controller
public final class TempController {
#Autowired
private TempService tempService;
private TempValidator tempValidator;
public TempValidator getTempValidator() {
return tempValidator;
}
#Autowired
public void setTempValidator(TempValidator tempValidator) {
this.tempValidator = tempValidator;
}
#RequestMapping(method = {RequestMethod.GET}, value = {"admin_side/Temp"})
public String showForm(#ModelAttribute("tempBean") #Valid TempBean tempBean, BindingResult error, Map model, HttpServletRequest request, HttpServletResponse response) {
return "admin_side/Temp";
}
#RequestMapping(method = {RequestMethod.POST}, value = {"admin_side/Temp"})
public String onSubmit(#ModelAttribute("tempBean") #Valid TempBean tempBean, BindingResult errors, Map model, HttpServletRequest request, HttpServletResponse response) {
//tempValidator.supports(TempBean.class);
//tempValidator.validate(tempBean, errors);
DataBinder dataBinder = new DataBinder(tempBean);
dataBinder.setValidator(tempValidator);
dataBinder.validate();
//errors=dataBinder.getBindingResult();
if (CustomizeValidation.isValid(errors, tempBean, TempBean.ValidationGroup.class, Default.class) && !errors.hasErrors()) {
System.out.println("Validated");
}
return "admin_side/Temp";
}
}
I'm invoking the validator from the Spring controller class itself (which I indeed want) by
DataBinder dataBinder = new DataBinder(tempBean);
dataBinder.setValidator(tempValidator);
dataBinder.validate();
The validator is called but the validation which is expected is not performed.
If only I invoke the validator manually using the following statement (which is commented out above),
tempValidator.validate(tempBean, errors);
then validation is performed. So I don't believe my validator is correctly working. Why does it fail to work with DataBinder?
In my application-context.xml file, this bean is simply configured as follows.
<bean id="tempValidator" class="test.TempValidator"/>
This many packages as below including the test package which the TempValidator class is enclosed within are auto-detected.
<context:component-scan base-package="controller spring.databinder validatorbeans validatorcommands test" use-default-filters="false">
<context:include-filter expression="org.springframework.stereotype.Controller" type="annotation"/>
<context:include-filter expression="org.springframework.web.bind.annotation.ControllerAdvice" type="annotation"/>
</context:component-scan>
I have even tried to put
<bean id="validator" class="org.springframework.validation.beanvalidation.LocalValidatorFactoryBean"/>
In my dispatcher-servlet.xml file.
What am I overlooking here?
If I understand well what you try to achieve - distinguish between blank fields and incorrect values entered - you can use MUCH MORE SIMPLER approach:
public class MyBean {
#NotNull
#DateTimeFormat(pattern="dd.MM.yyyy HH:mm")
private DateTime date;
#NotNull
#Max(value=5)
private Integer max;
#NotNull
#Size(max=20)
private String name;
// getters, setters ...
}
Controller mapping:
public void submitForm(#ModelAttribute #Valid MyBean myBean, BindingResult result) {
if (result.hasErrors){
// do something}
else{
// do something else
}
}
Validation messages:
NotNull=Required field.
NotNull.date=Date is required field.
NotNull.max=Max is required field.
Size=Must be between {2} and {1} letters.
Max=Must be lower than {1}.
typeMismatch.java.lang.Integer=Must be number.
typeMismatch.org.joda.time.DateTime=Required format dd.mm.yyyy HH:mm
Spring configuration:
#Configuration
public class BaseValidatorConfig {
#Bean
public LocalValidatorFactoryBean getValidator() {
LocalValidatorFactoryBean lvfb = new LocalValidatorFactoryBean();
lvfb.setValidationMessageSource(getValidationMessageSource());
return lvfb;
}
protected MessageSource getValidationMessageSource() {// return you validation messages ...}
}
I can provide more details and explanation, if needed.
I don't know why the approach as mentioned in the question didn't work. I didn't make it work but walking through this document, I found another approach that worked for me as per my requirements.
I set the validator inside a method which was designated by the #InitBinder annotation.
From docs
The Validator instance invoked when a #Valid method argument is
encountered may be configured in two ways. First, you may call
binder.setValidator(Validator) within a #Controller's #InitBinder
callback. This allows you to configure a Validator instance per
#Controller class:
Specifically, in my requirements, the validation should only be performed while updating or inserting data into the database i.e when an associated submit button for those operations is pressed (there is a common button for both of these tasks (insert and update) in my application whose name is btnSubmit).
The validation should be muted in any other case (for example, when the delete button is pressed). To meet this requirement, I have registered the validator as follows.
#InitBinder
protected void initBinder(WebDataBinder binder, WebRequest webRequest) {
if (webRequest.getParameter("btnSubmit") != null) {
binder.setValidator(new TempValidator());
} else {
binder.setValidator(null);
}
}
In this situation, the validator - TempValidator would only be set when the submit button whose name attribute is btnSubmit is clicked by the client.
There is no need for xml configuration anywhere as well as auto-wiring.
The exemplary controller class now looks like the following.
#Controller
public final class TempController {
#Autowired
private TempService tempService;
#InitBinder
protected void initBinder(WebDataBinder binder, WebRequest webRequest) {
if (webRequest.getParameter("btnSubmit") != null) {
binder.setValidator(new TempValidator());
} else {
binder.setValidator(null);
}
}
//Removed the #Valid annotation before TempBean, since validation is unnecessary on page load.
#RequestMapping(method = {RequestMethod.GET}, value = {"admin_side/Temp"})
public String showForm(#ModelAttribute("tempBean") TempBean tempBean, BindingResult error, Map model, HttpServletRequest request, HttpServletResponse response) {
return "admin_side/Temp";
}
#RequestMapping(method = {RequestMethod.POST}, value = {"admin_side/Temp"})
public String onSubmit(#ModelAttribute("tempBean") #Valid TempBean tempBean, BindingResult errors, Map model, HttpServletRequest request, HttpServletResponse response) {
if (CustomizeValidation.isValid(errors, tempBean, TempBean.ValidationGroup.class, Default.class) && !errors.hasErrors()) {
System.out.println("Validated");
}
return "admin_side/Temp";
}
}
The WebRequest paramenter in the initBinder() method is not meant for handling the entire Http request as obvious. It's just for using general purpose request metadata.
Javadocs about WebRequest.
Generic interface for a web request. Mainly intended for generic web
request interceptors, giving them access to general request metadata,
not for actual handling of the request.
If there is something wrong that I might be following, then kindly clarify it or add another answer.