I have an application written in Spring 3.0 hooked up using Hibernate to a database. I have a controller to an update form. Whenever the form is submitted, I expect the object that is shown to be updated however a new object is created with a new ID value. I've looked over the "petclinic" sample and i can't see how it is different.
POJO
public class Person
{
private int id;
#NotNull
private String name;
//getter/setter for id
//getter/setter for name
}
Controller
public class PersonUpdateController
{
//injected
private PersonService personService;
#RequestMapping(value="/person/{personId}/form", method=RequestMethod.POST)
public String updateForm(ModelMap modelMap, #PathVariable personId)
{
Person person = personService.getById(personId);
modelMap.addAttribute(person);
return "person/update";
}
#RequestMapping(value="/person/{personId}", method=RequestMethod.POST)
public String update(ModelMap modelMap, #Valid Person person, BindingResult bindingResult)
{
if(bindingResult.hasErrors())
{
modelMap.addAttribute(person);
return "person/update";
}
personService.save(person);
return "redirect:person/" + person.getId() + "/success";
}
}
JSP
<spring:url value="/person/${person.id}" var="action_url" />
<spring:form action="${action_url}" modelAttribute="person" method="POST">
<spring:input name="name" path="name" />
<input type="submit" value="Save" />
</spring:form>
PersonService Implementation
public class HibernatePersonService
implements PersonService
{
//injected
private SessionFactory sessionFactory;
//other methods
public void save(Person person)
{
Session session = sessionFactory.getCurrentSession();
session.saveOrUpdate(person);
}
}
Spring MVC doesn't do any magic with HTML forms. Since your form contains only one field, you get only one field populated in update method. So, you have two options:
Pass id as a hidden field in the form: <spring:hidden path = "id" />. Note that in this case you need to check possible consequences for security (what happens if malicious person changes the id).
Store Person in the session so that data from the form is used to update the stored object (note that it may cause interference if several instances of the form is opened in one session). That's how it's done in Petclinic:
-
#SessionAttributes("person")
public class PersonUpdateController {
...
#RequestMapping(value="/person/{personId}", method=RequestMethod.POST)
public String update(ModelMap modelMap, #Valid Person person,
BindingResult bindingResult, SessionStatus status)
{
...
personService.save(person);
status.setComplete(); // Removes person from the session after successful submit
...
}
#InitBinder
public void setAllowedFields(WebDataBinder dataBinder) {
dataBinder.setDisallowedFields("id"); // For security
}
}
Related
In Spring I know I can bind thymeleaf form inputs to a command object using #ModelAttribute in the controller, but is there an equivalent in Micronaut? I can't see it documented anywhere
Mapping form data in Micronaut is straight forward.
Given a form containing two input fields
<input name="name"/>
<input name="location"/>
you can handle them on the backend either
Binding them to parameters
#Controller("/form")
public class FormController {
#Post
public HttpResponse processForm(String name, String location) {
return HttpResponse.created();
}
}
or
Bind form data to POJO
public class MyForm {
private String name;
private String location;
// getter/setter omitted
}
#Controller("/form")
public class FormController {
#Post
public HttpResponse processForm(#Body MyForm) {
return HttpResponse.created();
}
}
In the following piece of code I just want to create a new user and link it to the selected groups.
Everything works fine when the user and group are valid. The problem comes when the bindingresult has errors. The controller detects such error (all fine so far) and returns the same view (I want to keep the data entered by the user) but the list of groups is empty (I have discovered that, after showing again the view, userform.groups is null).
Has anyone a clue about what the problem could be?
UserForm
#Component
public class UserForm {
#Valid
private User user;
#Valid
private Collection<Group> allGroups;
// Setters and getters
}
UserController
#Controller
public class UserController {
#Autowired
UserGroupService userGroupService;
#Autowired
BCryptPasswordEncoder passwordEncoder;
#InitBinder
public void initBinder (WebDataBinder binder) {
binder.registerCustomEditor(Set.class, "userform.user.groups", new GroupListEditor(userGroupService));
}
#RequestMapping(value = "/admin/users/CreateUser", method = RequestMethod.GET)
public ModelAndView createUsetGet () {
ModelAndView mav = new ModelAndView("/admin/users/CreateUser");
UserForm userForm = new UserForm();
userForm.setUser(new User());
userForm.setGroups(userGroupService.getAllEnabledGroups());
mav.addObject("userform", userForm);
return mav;
}
#RequestMapping(value = "/admin/users/CreateUser", method = RequestMethod.POST)
public String createUserPost (#Valid #ModelAttribute("userform") UserForm userForm, BindingResult result) {
if (result.hasErrors() == true) {
return "/admin/users/CreateUser";
}
userForm.getUser().setPassword(passwordEncoder.encode(userForm.getUser().getPassword()));
userGroupService.saveUser(userForm.getUser());
return "redirect:/admin/users/ViewUsers";
}
}
CreateUser.jsp (Only piece regarding the groups)
<form:form modelAttribute="userform" method="post">
Username:
<form:input path="user.loginName"/>
<!-- More fields -->
<form:select path="user.groups" multiple="true">
<form:options items="${userform.groups}" itemValue="id" itemLabel="name" />
</form:select>
<button type="submit">Create</button>
</form:form>
Any help is appreciated!
The object gets recreated and values are bound to the resulting object. Which means no group objects.
Also those shouldn't be in the object at all. To solve use a #ModelAttribute annotated method, which will be invoked for each request handling method and create an object and fill the list of groups.
#ModelAttribute
public void init(Model model) {
UserForm userForm = new UserForm();
userForm.setUser(new User());
model.addAttribute("userform", userForm);
model.addAtrribute("groups", userGroupService.getAllEnabledGroups());
}
#RequestMapping(value = "/admin/users/CreateUser", method = RequestMethod.GET)
public String createUsetGet () {
return "/admin/users/CreateUser";
}
#RequestMapping(value = "/admin/users/CreateUser", method = RequestMethod.POST)
public String createUserPost (#Valid #ModelAttribute("userform") UserForm userForm, BindingResult result) {
if (result.hasErrors() == true) {
return "/admin/users/CreateUser";
}
userForm.getUser().setPassword(passwordEncoder.encode(userForm.getUser().getPassword()));
userGroupService.saveUser(userForm.getUser());
return "redirect:/admin/users/ViewUsers";
}
Ofcourse your jsp has to change slightly also.
<form:select path="user.groups" multiple="true">
<form:options items="${groups}" itemValue="id" itemLabel="name" />
</form:select>
There is one drawback of using this approach now the userGroupService.getAllEnabledGroups() is called for each incoming request. This might not be needed. You could store those in the session using the #SessionAttributes annotation on the class.
#Controller
#SessionAttributes("groups")
public class UserController {
#Autowired
UserGroupService userGroupService;
#Autowired
BCryptPasswordEncoder passwordEncoder;
#InitBinder
public void initBinder (WebDataBinder binder) {
binder.registerCustomEditor(Set.class, "userform.user.groups", new GroupListEditor(userGroupService));
}
#ModelAttribute("groups")
public List<Group> groups() {
return userGroupService.getAllEnabledGroups();
}
#ModelAttribute("userform")
public UserForm userform() {
UserForm userForm = new UserForm();
userForm.setUser(new User());
return userForm;
}
#RequestMapping(value = "/admin/users/CreateUser", method = RequestMethod.GET)
public String createUsetGet () {
return "/admin/users/CreateUser";
}
#RequestMapping(value = "/admin/users/CreateUser", method = RequestMethod.POST)
public String createUserPost (#Valid #ModelAttribute("userform") UserForm userForm, BindingResult result, SessionStatus status) {
if (result.hasErrors() == true) {
return "/admin/users/CreateUser";
}
userForm.getUser().setPassword(passwordEncoder.encode(userForm.getUser().getPassword()));
userGroupService.saveUser(userForm.getUser());
status.setComplete();
return "redirect:/admin/users/ViewUsers";
}
}
You will then need, on success, to tell the SessionStatus that you are finished. If you don't do this your session might pollute.
It's because the information about the validation errors is lost after redirect.
You can solve this using RedirectAttributes. Check this tutorial.
I have a controller where works fine, It can register and update an entity, just how the following about to create the forms to save and update an entity respectively
#RequestMapping(value="/registrar.htm", method=RequestMethod.GET)
public String crearRegistrarFormulario(Model model){
…
}
#RequestMapping(value="/{id}/actualizar.htm", method=RequestMethod.GET)
public String crearActualizarFormulario(#PathVariable("id") String id, Model model){
…
}
Until here I have no problems.
My problem is about the #InitBinder
I need work with the same entity Deportista (Sportsman), one special setting for save and update. For example
#InitBinder
public void registrarInitBinder(WebDataBinder binder) { // register or save
logger.info(">>>>>>>> registrarInitBinder >>>>>>>>>>>>>");
…
CustomDateEditor customDateEditor = new CustomDateEditor(...
…
}
#InitBinder
public void actualizarInitBinder(WebDataBinder binder) { // update
logger.info(">>>>>>>> actualizarInitBinder >>>>>>>>>>>>>");
…
CustomDateEditor customDateEditor = new CustomDateEditor(...
…
binder.setDisallowedFields(…) //I need this only for update
}
I have read the following:
How to define multiple initBinders
Addiing multiple validators using initBinder
The links mentioned work around different entities, for example User and Customer, it through the #InitBinder's value attribute, but I need work with the same entity, how I can configure the #InitBinder's value to indicate Spring use or discriminate each #InitBinder? one for save and update respectively.
Thanks
Edit: From the Serge Ballesta's answer, the following is mandatory too:
#Controller
#RequestMapping(value="/deportista")
#SessionAttributes(value={"deportistaRegistrar", "deportistaActualizar"})
public class DeportistaController {
…
#RequestMapping(value="/registrar.htm", method=RequestMethod.GET)
public String crearRegistrarFormulario(Model model){
Deportista deportista = new Deportista();
model.addAttribute("deportistaRegistrar", deportista);
return "deportista.formulario.registro";
}
#RequestMapping(value="/{id}/actualizar.htm", method=RequestMethod.GET)
public String crearActualizarFormulario(#PathVariable("id") String id, Model model){
Deportista deportista = this.fakeMultipleRepository.findDeportista(id);
model.addAttribute("deportistaActualizar", deportista);
return "deportista.formulario.actualizacion";
}
It to let work his answer:
// registrarInitBinder will be used here
#RequestMapping(value="/registrar.htm", method=RequestMethod.POST)
public String doCrearRegistrarFormulario(#ModelAttribute("deportistaRegistrar") XXX value,
BindingResult result, Model model){
…
}
// actualizarInitBinder will be used here
#RequestMapping(value="/{id}/actualizar.htm", method=RequestMethod.POST)
public String crearActualizarFormulario(#PathVariable("id") String id,
#ModelAttribute("deportistaActualizar") XXX value, BindingResult result, Model model){
…
}
According to the javadoc page for #InitBinder, you can use multiple init-binders in one single controllers, and specialize them with the name of the model attribute variable for which they will be applied. Example :
#InitBinder("saveValue")
public void registrarInitBinder(WebDataBinder binder) { // register or save
logger.info(">>>>>>>> registrarInitBinder >>>>>>>>>>>>>");
…
CustomDateEditor customDateEditor = new CustomDateEditor(...
…
}
#InitBinder("updateValue")
public void actualizarInitBinder(WebDataBinder binder) { // update
logger.info(">>>>>>>> actualizarInitBinder >>>>>>>>>>>>>");
…
CustomDateEditor customDateEditor = new CustomDateEditor(...
…
binder.setDisallowedFields(…) //I need this only for update
}
and then (XXX is the type of the form object that will be processed by the submit)
// registrarInitBinder will be used here
#RequestMapping(value="/registrar.htm", method=RequestMethod.POST)
public String doCrearRegistrarFormulario(#ModelAttribute("saveValue") XXX value,
BindingResult result, Model model){
…
}
// actualizarInitBinder will be used here
#RequestMapping(value="/{id}/actualizar.htm", method=RequestMethod.POST)
public String crearActualizarFormulario(#PathVariable("id") String id,
#ModelAttribute("updateValue") XXX value, BindingResult result, Model model){
…
}
I have a spring mvc controller like the following
#RequestMapping(value="/new", method=RequestMethod.POST)
public String createBooking(#Valid Booking booking, BindingResult bindingResult, Model model, Principal principal)
{
if(bindingResult.hasErrors()) {
return "booking/edit";
}
//Store Booking in db...
...
The problem is the Booking object i get from the POST is constructed by Spring, but one of the properties required by the validator cannot be populated, as the property is not present in the form. So my question is is there a way for me to intercept the Booking before it gets processed by the #Valid tag handler to add this required property?
Cheers!
NFV
There are 2 ways to modify the model attribute object before the #Valid will trigger:
Remove #Valid and autowire the validator and manually trigger the validator:
class MyController {
private final Validator validator;
class MyController(Validator validator) {
this.validator = validator;
}
#PostMapping("/new")
public String createBooking(Booking booking, BindingResult bindingResult, Model model, Principal principal) {
// edit booking here
validator.validate(booking, result)
// original method body here
}
}
Decorate the default validator and pre-process the booking object inside the decorated validator.
class MyController {
#InitBinder
public void initBinder(WebDataBinder binder) {
binder.setValidator(new PreProcessBookingValidator(binder.getValidator()));
}
#PostMapping("/new")
public String createBooking(#Valid Booking booking, BindingResult bindingResult, Model model, Principal principal) {
...
}
private static class PreProcessBookingValidator implements Validator {
private final Validator validator;
public PreProcessBookingValidator(Validator validator) {
this.validator = validator;
}
#Override
public boolean supports(#Nonnull Class<?> clazz) {
return validator.supports(clazz);
}
#Override
public void validate(#Nonnull Object target, #Nonnull Errors errors) {
if (target instanceof Booking) {
Booking booking = (Booking) target;
// manipulate booking here
}
validator.validate(target, errors);
}
}
}
(This second tip is what I picked up from https://github.com/spring-projects/spring-framework/issues/11103)
I'm not sure I understand the question, but it sounds like you want to set a field in the command object to a value before your command is bound by the form submission. If so, you can add a method in your controller as follows...
#ModelAttribute
public Booking getBooking() {
Booking booking = new Booking();
booking.setMyRequiredProperty("some value");
return booking;
}
Hope that helps
I'm new to Tomcat and Spring Web. I'm trying to use Spring's form validation features by following this tutorial. Everything seems to run smoothly except for one thing... my form doesn't do any validation and I can always get to the success page when I send the form no matter which data I provide.
Am I using the constraints correctly? I want to enforce that the user fills in their first name and that the first name be at least two characters long.
package net.devmanuals.form;
import javax.validation.constraints.Size;
import org.hibernate.validator.constraints.NotEmpty;
public class RegistrationForm {
#NotEmpty(message = "You surely have a name, don't you?")
#Size(min = 2, message = "I'm pretty sure that your name consists of more than one letter.")
private String firstName;
public void setFirstName(String firstName) {
this.firstName = firstName;
}
public String getFirstName() {
return this.firstName;
}
}
Form code:
<form:form method="post" commandName="regform">
<p><form:input path="firstName" /> <form:errors path="firstName" /></p>
<p><input type="submit" /></p>
</form:form>
The controller:
#Controller
#RequestMapping("/register")
public class RegistrationController {
#RequestMapping(method = RequestMethod.GET)
public String showRegForm(Map model) {
RegistrationForm regForm = new RegistrationForm();
model.put("regform", regForm);
return "regform";
}
#RequestMapping(method = RequestMethod.POST)
public String validateForm(#Valid RegistrationForm regForm, BindingResult result, Map model) {
if (result.hasErrors()) {
return "regform";
}
model.put("regform", regForm);
return "regsuccess";
}
}
Am I applying the constraints incorrectly?
In addition to adding <mvc:annotation-driven/> to your config, you need to make sure the JSR-303 jar is on your classpath. From the docs:
[AnnotationDrivenBeanDefinitionParser] ... configures the validator if specified, otherwise defaults to a fresh Validator instance created by the default LocalValidatorFactoryBean if the JSR-303 API is present on the classpath.