I experience a weired error where the form data is bound to a completely wrong object upon submission.
I am using spring with thymeleaf and have the following form:
<form method="post" th:action="#{/backend/user/create}"
th:object="${userInCreation}" id="userCreateForm">
<input th:field="*{firstName}" />Create user</button>
</form>
The object I want to bind to is
public class UserInCreation implements Serializable {
private String firstName;
public UserInCreation() {}
public String getFirstName() {
return firstName;
}
public void setFirstName(String firstName) {
this.firstName = firstName;
}
}
Binding happens in the controller
#Controller
#RequestMapping("/backend/user")
public class UserController {
#RequestMapping(value = "/create", method = RequestMethod.GET)
public String createUserForm(UserInCreation userInCreation) {
return "backend/user/create";
}
#RequestMapping(value = "/create", method = RequestMethod.POST)
public String createUser(#Valid UserInCreation userInCreation, BindingResult result, Model model) {
return "backend/user/index";
}
}
This works fine, despite a big issue: The data I type into the firstName field is also bound to the Spring-Security Principal, which I make available as a ModelAttribute:
#ModelAttribute("currentAuthor")
public User getCurrentAuthor(Principal principal, HttpSession session) {
User author = (User) ((Authentication) principal).getPrincipal();
return author;
}
The class User also has a field firstName, and this is changed. So when I type "Some name" into the form and submit, suddenly the first name of the principal will be "Some name". Any thoughts?
I found out what the problem is. Actually, I had a function of the type
#ModelAttribute("foo")
public Foo xyz(#ModelAttribute("bar") Bar bar) {
...
}
This is against the specification, but it seemed to work at first. But it also seems to mess up the data binding system completely.
Related
I created a custom validation to <form:select> that populate country list.
Customer.jsp
Country:
<form:select path="country" items="${countries}" />
<form:errors path="country" cssClass="error"/>
FomeController.java
#RequestMapping(value = "/customer", method = RequestMethod.POST)
public String prosCustomer(Model model,
#Valid #ModelAttribute("defaultcustomer") Customer customer,
BindingResult result
) {
CustomerValidator vali = new CustomerValidator();
vali.validate(customer, result);
if (result.hasErrors()) {
return "form/customer";
} else {
...
}
}
CustomValidator.java
public class CustomerValidator implements Validator {
#Override
public boolean supports(Class<?> type) {
return Customer.class.equals(type);
}
#Override
public void validate(Object target, Errors errors) {
Customer customer = (Customer) target;
int countyid=Integer.parseInt(customer.getCountry().getCountry());
if (countyid==0) {
errors.rejectValue("country", "This value is cannot be empty");
}
}
}
Customer.java
private Country country;
Validation is working perfectly fine. But the problem is that the validation method has attached another message too.
Please tell me how to correct this message.
Can you try changing the implementation of Validator in controller as explained in https://stackoverflow.com/a/53371025/10232467
So you controller method can be like
#Autowired
CustomerValidator customerValidator;
#InitBinder("defaultcustomer")
protected void initDefaultCustomerBinder(WebDataBinder binder) {
binder.addValidators(customerValidator);
}
#PostMapping("/customer")
public String prosCustomer(#Validated Customer defaultcustomer, BindingResult bindingResult) {
// if error
if (bindingResult.hasErrors()) {
return "form/customer";
}
// if no error
return "redirect:/sucess";
}
Additionally the form model name in jsp should be defined as "defaultcustomer"
EDIT :
I missed the nested Country object in Customer class. In validator replace
errors.rejectValue("country", "This value is cannot be empty");
with
errors.rejectValue("defaultcustomer.country", "This value is cannot be empty");
Also found that Customer class should be modified as
#Valid
private Country country;
I have a user entity that has many attributes (some fields not shown here):
#Entity
public class User {
#OneToOne(cascade = ALL, orphanRemoval = true)
private File avatar; // File is a custom class I have created
#NotEmpty
#NaturalId
private String name;
#Size(min = 6)
private String password;
#Enumerated(EnumType.STRING)
private Role role;
}
In my thymeleaf template I have a form that submits username, password and avatar (MultipartFile). Now in my controller instead of these parameters...
#PostMapping("/register")
public String register(#RequestParam String username,
#RequestParam String password,
#RequestParam MultipartFile avatar) { ...
...I want to use #ModelAttribute #Valid User user. My problem is that:
password first should be encrypted then passed to the user entity,
bytes[] from MultipartFile should be extracted then stored in user entity (as a custom File object),
some other fields such as Role should be set manually in the service class.
How can I take advantage of #ModelAttribute?
Instead of trying to shoehorn everything into your User class, write a UserDto or UserForm which you can convert from/to a User. The UserForm would be specialized for the web and converted to a User later on.
The conversions you are talking about should be done in your controller (as that is ideally only a conversion layer before actually talking to your business services).
public class UserForm {
private MultipartFile avatar;
#NotEmpty
private String username;
#Size(min = 6)
#NotEmpty
private String password;
public UserForm() {}
public UserForm(String username) {
this.username = username;
}
static UserForm of(User user) {
return new UserForm(user.getUsername());
}
// getters/setters omitted for brevity
}
Then in your controller do what you intended to do (something like this):
#PostMapping("/register")
public String register(#ModelAttribute("user") UserForm userForm, BindingResult bindingResult) {
if (!bindingResult.hasErrors()) {
User user = new User();
user.setName(userForm.getUsername());
user.setPassword(encrypt(userForm.getPassword());
user.setAvataor(createFile(userForm.getAvatar());
userService.register(user);
return "success";
} else {
return "register";
}
}
This way you have a specialized object to fix your web based use cases, whilst keeping your actual User object clean.
Maybe you can just use a setter to make all these actions. When Spring is mapping data to fields, and you have setters in the entity, it will use them to pass data. You can preprocess data in this way and set final values to fields.
#PostMapping("/register")
public String register(#ModelAttribute User user, Model model) { // remember if You have another name for parameter and backing bean You should type this one #ModelAttribute(name="userFromTemplate") User user
encryptPassword(user.getPassword); //remember that is sample code, You can do it however You want to
extractBytes(user.getAvatar); //same here
user.setRole(manuallySetRole);
model.addAttribute("user", user);
return "success"; // here u can redirect to ur another html which will contain info about ur user
} else
return "redirect:sorry";
}
encryptPassword(String password) { ... }
same for the rest methods
Here i give You sample code how to use #ModelAttribute in your example. If You have questions feel free to comment.
What is the proper way to handle editing objects in Spring MVC. Let's say I have user object:
public class User {
private Integer id;
private String firstName;
private String lastName;
//Lets assume here are next 10 fields...
//getters and setters
}
Now in my controller I have GET and POST for url: user/edit/{id}
#RequestMapping(value = "/user/edit/{user_id}", method = RequestMethod.GET)
public String editUser(#PathVariable Long user_id, Model model) {
model.addAttribute("userForm", userService.getUserByID(user_id));
return "/panels/user/editUser";
}
#RequestMapping(value = "/user/edit/{user_id}", method = RequestMethod.POST)
public String editUser(#Valid #ModelAttribute("userForm") User userForm,
BindingResult result, #PathVariable String user_id, Model model) {
if(result.hasErrors()) {
User user = userService.getById(user_id);
user.updateFields(userForm);
}
userService.update(user);
}
Now the question is do I really need to get my user from database in POST method and update every field one by one in some update method or is there better way for that?
I am thinking about using #PathVariable for User and get User from database with converter and then in some way inject parameters from POST method into that object automatically. Something like this:
#RequestMapping(value = "/user/edit/{user}", method = RequestMethod.POST)
public String editUser(#Valid #PathVariable("user") User userForm,
BindingResult result, Model model)
But when I try this I got error with BindingResults:
java.lang.IllegalStateException: An Errors/BindingResult argument is expected to be declared immediately after the model attribute, the #RequestBody or the #RequestPart arguments
Is there any easy way to create controller to handle objects editing or do I need to copy fields which could change one by one??
By the way, I can't use SessionAttributes because it causes problems for multiple tabs.
I believe you are sending "userForm" as a model attribute. If so try with following pattern,
#RequestMapping(value = "/user/edit/{user_id}", method = RequestMethod.POST)
public String editUser(#PathVariable String user_id, #Valid #ModelAttribute("userForm") User userForm,
BindingResult result, Model model)
Thanks
You keep user id in a input hidden inside your edit form.
#RequestMapping(value = "/user/edit", method = RequestMethod.POST)
public String editUser(#Valid #ModelAttribute("userForm") User userForm,
BindingResult result,Model model){
if(result.hasErrors()){
User user = userService.getById(userForm.getId());
user.updateFields(userForm);
}
userService.update(user);
return "redirect:.......";
}
I have a User Modal
public class RegisterUser {
#Size(min = 2, max = 30)
private String fname;
#Size(min = 2, max = 30)
private String lname;
#NotEmpty
#Size(min = 6, max = 15)
private String password;
....
#NotEmpty
private String publicProfile;
... getters and setters
}
1) I want to use this modal during registration action (fname, lname, password etc but without publicProfile field)
2) I want to use this modal during myprofile action (all fields except password)
My action for register:
#RequestMapping(value = "/register", method = RequestMethod.POST)
public String submitRegisterForm(
#Valid RegisterUser registerUser,
BindingResult result,
Model m) {
....
}
Here I don't intend to provide 'publicprofile' on jsp and therefore do not want to validate this field although my Modal has #NotEmpty annotation
My action for myprofile
#RequestMapping(value = "/myprofile", method = RequestMethod.POST)
public String submitMyprofileForm(
#Valid RegisterUser registerUser,
BindingResult result,
Model m) {
....
}
Here I don't intend to provide 'password' field on jsp and therefore do not want to validate this field although my Modal has #NotEmpty and #Size(min = 6, max = 15) annotation
My question is how can I achieve this ?
Is there any way where I can say in this modal for this action validate only mentioned fields?
Thanks in advance
Manisha
You can use Validation Groups (for different scenarios) and Spring's #Validated annotation to specify which group you want to use
I don't know if this is possible with Bean Validation, but you can set up different implementations of Spring's Validation Interface for different request parameters.
#RequestMapping(value = "/register", method = RequestMethod.POST)
public String submitRegisterForm(#Valid RegisterUser registerUser, ...
and
#RequestMapping(value = "/register", method = RequestMethod.POST)
public String submitMyprofileForm(#Valid RegisterUser registerUserProfile, ...
And then you can use #InitBinder to connect different Validators to your request params. You would add these methods to your controller. Just omit the validation you dont want in the second Validator.
#InitBinder("registerUser")
protected void initUserBinder(WebDataBinder binder) {
binder.setValidator(new RegisterUserValidator());
}
#InitBinder("registerUserProfile")
protected void initUserBinderProfile(WebDataBinder binder) {
binder.setValidator(new RegisterUserProfileValidator());
}
Then you would need to do the annotation stuff manually. You could also use inheritance for your Validators, because they are exactly the same, except the one additional field validation for registration forms.
public class RegisterUserValidator implements Validator {
public boolean supports(Class clazz) {
return RegisterUser.class.equals(clazz);
}
public void validate(Object obj, Errors e) {
ValidationUtils.rejectIfEmpty(e, "publicProfile", "empty");
RegisterUser r = (RegisterUser) obj;
if (r.getFname().length() < 2) {
e.rejectValue("fname", "min");
} else if (r.getFname().length() > 30) {
e.rejectValue("fname", "max");
}
// ...
}
}
I have a Contact object that I put in the request, this object is modified in the
form and then get the modified object. I would like the object that is back is the same object that you send, you keep the value of the attributes that were not in the form.
class Contact{
private String name; // this attributes will be modified
private String lastName;
private Long id;
private Date created; // this atributes will not be modified
// getters and setters ....
}
#RequestMapping(value = "/{id}/edit", method = RequestMethod.GET)
public String updateContact(#PathVariable("id") Long id, Model model) {
Contact c = contactDao.get(id);
model.addAttribute("contact", c);
return "contact/form";
}
#RequestMapping(value = "/{id}/edit", method = RequestMethod.POST)
public String update(#PathVariable("id") Long id, #Valid #ModelAttribute Contact contact, BindingResult result, Model model) {
// The contact I get here I want to keep the original attributes of the
// object sent, and have the changes in the fields shown on the form. is that possible?
return "redirect:/contact";
}
<form:form action="${pageContext.servletContext.contextPath}/tags/create" commandName="contact">
<form:input path="name"/>
<form:errors path="name" cssClass="formError"/>
<form:input path="lastName"/>
</form:form>
I do not want to use hidden fields to maintain the value of the attributes that will not be changing
If you only want some of the fields to be handled in a form, make a new class - ContactDTO that contains only them, and then manually (or through reflection) copy them to the original Contact object (which you load by id from the DB)
I found the solution to the problem by stating the contact object as an object that lives in the session
#Controller
#RequestMapping("/contact")
#SessionAttributes("contact")
public class ContactController {
....
....
#RequestMapping(value = "/{id}/edit", method = RequestMethod.GET)
public String updateContact(#PathVariable("id") Long id, Model model) {
Contact c = contactDao.get(id);
model.addAttribute("contact", c);
return "contact/form";
}
#RequestMapping(value = "/{id}/edit", method = RequestMethod.POST)
public String update(#PathVariable("id") Long id, #Valid #ModelAttribute Contact contact, BindingResult result, Model model) {
contactDao.update(contact);
return "redirect:/contact";
}
}
What is your persistence framework? is it JPA or Hibernate? If so, annotate the field with #Column(updatable=false)