How to display list errors in thymeleaf? - java

Here is a sample skeleton of DTO object.
public class MyDTO
{
List<Student> students=new ArrayList<>();
}
public class Student
{
String name;
Integer age;
// setter and getter methods
}
Now, the user has a chance to enter a lot of students into the list and any student detail might contain an error. The possible errors are student age being greater than 25, and name containing special characters etc.
For example, students[2].name has a special character and student[4].age > 25, then they are errors. Now, I would like to display the error below those fields and also highlight the corresponding fields.
<form th:field="${myDTO}">
<input type="text" th:field="*{students[0].name}" th:errorclass="fieldError"/>
<span class="error" th:if="${#fields.hasErrors('students[0].name')}" th:errors="*{students[0].name}"></span>
<input type="number" th:field="*{students[0].age}" min="15" max="25" th:errorclass="fieldError"/>
<span class="error" th:if="${#fields.hasErrors('students[0].age')}" th:errors="*{students[0].age}"></span>
</form>
I am confused on what to put in th:field attribute? When I write as above, such type of error is the result
Neither BindingResult nor plain target object for bean name
'students+'['+0+']'' available as request attribute.
In my validators, I have such type of code..
int idx=0;
for(Student st: students)
{
errors.pushNestedPath("students["+idx+"]");
ValidationUtils.invokeValidator(studentValidator, st, errors);
errors.popNestedPath();
idx++;
}
and in the StudentValidator class..
#Override
public void validate(Object obj, Errors errors) {
Student s=(Student) obj;
if(containsSpecialCharacters(s.name))
{
errors.rejectValue("name","name.containsSpecialCharacters",null,null);
}
if(s.age>25 || s.age<15)
{
errors.rejectValue("age","age.invalid",null,null);
}
}
Now, my problems are
How do I show those errors, highlight the corresponding fields?
What to put in the th:field tag?
Next, the student records are added dynamically, that is the student rows doesn't exist previously, by clicking on Add student button, the user will be able to add the student. Now, even the th:field must also be updated. How to do that, because it is related to thymeleaf template processing which is done previously but not after the page is loaded?
Hope you will reply as soon as possible.

your validation seems to be right, but maybe you need to pass a BindingResult as parameter in your controller so that the error can be retrieved in your view layer.
#PostMapping("/students")
public String saveStudent(#Valid Student, BindingResult bindingResult, RedirectAttributes redirAttrs) {
if (bindingResult.hasErrors()) {
// Show errors here
bindingResult.getAllErrors().stream().forEach(System.out::println);
return "student-edit";
} else {
Long id = releaseLogService.save(student).getId();
redirAttrs.addFlashAttribute("message", "Success");
return "redirect:/student/edit/" + id;
}
}

Related

Sping MVC, Thymeleaf, POST request, how to pass list of objects to controller

A "teacher" can have several assigned "subjects" to teach:
public class Teacher {
private int id;
private String firstName;
private String lastName;
...
private List<Subject> subjects;
}
In HTML view user can select one ore more subjects for the teacher and send POST request:
<select class="form-control" id="subjects" name="subjects" size="5" multiple required>
<option th:each="subject: ${allSubjects}"
th:value="${subject.id}"
th:text="${subject.name}"
th:selected="${teacher.subjects.contains(subject)}">
</option>
</select>
Controller to process this request:
#PostMapping("/update")
public String update(#ModelAttribute("teacher") Teacher teacher) {
logger.debug("Received update data: {}", teacher);
teacherService.update(teacher);
return "redirect:/teachers";
}
Here is the POST request that is being passed:
I expect Spring to take subject.id`s and inject them into teacher as list of subjects.
But I get exception:
BindException
org.springframework.validation.BeanPropertyBindingResult:
1 errors Field error in object 'teacher' on field 'subjects': rejected value [2,4]; codes [typeMismatch.teacher.subjects,typeMismatch.subjects,typeMismatch.java.util.List,typeMismatch];
arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [teacher.subjects,subjects];
arguments []; default message [subjects]]; default message [Failed to convert property value of type 'java.lang.String[]' to required type 'java.util.List' for property 'subjects'; nested exception is java.lang.IllegalStateException: Cannot convert value of type 'java.lang.String' to required type 'ua.com.foxminded.university.model.Subject' for property 'subjects[0]': no matching editors or conversion strategy found]
I carefully read first 50 google results for my question, code should work, but it doesn't. I must be missing something.
First of all wrong annotation - u dont use #ModelAttribute inside of the post but rather #RequestBody which is implicit in spring controllers for post.
Other than that what you are sending is not a teacher entity but rather teacherDTO which wont have all the fields (collections) that teacher entity that you showed has. That means you should be receiving a different class (TeacherDTO) and then convert it correctly to teacher entity which you then update in the database
i suppose this is an update form, so it take an object teacher as an input. try this
<form th:object="${teacher}" method="post">
<select class="form-control" th:field="*{subjects}" id="subjects" name="subjects" size="5" multiple required>
<option th:each="subject: ${allSubjects}"
th:value="${subject.id}"
th:text="${subject.name}"
th:selected="*{subjects.contains(subject)}">
</option>
</select>
</form>
Finally I found the solution while working on another part of my project.
I tried to pass Lecture object to controller, Lecture contains List of student groups. Each group has 2 fields: id and name.
First we implement a formatter
public class GroupFormatter implements Formatter<Group> {
#Override
public Group parse(String text, Locale locale) throws ParseException {
Group group=new Group();
if (text != null) {
String[] parts = text.split(",");
group.setId(Integer.parseInt(parts[0]));
if(parts.length>1) {
group.setName(parts[1]);
}
}
return group;
}
#Override
public String print(Group group, Locale locale) {
return group.toString();
}
}
Register formatter in MVCConfig
#Override
public void addFormatters(FormatterRegistry registry) {
GroupFormatter groupFormatter=new GroupFormatter();
registry.addFormatter(groupFormatter);
}
And we get the correct format from POST request:
groups=[4:VM-08, 5:QWE-123]

Spring. Passing string values from View to int/Integer parameters in Model

I am learning spring and I've encountered some strange behavior I can't explain.
Here I have simple class:
public class Customer {
private Integer id;
public void setId(Integer id) {
this.id = id;
}
Simple controller:
#Controller
#RequestMapping("/customer")
public class CustomerController {
#InitBinder
public void initBinder(WebDataBinder dataBinder){
StringTrimmerEditor editor = new StringTrimmerEditor(true);
dataBinder.registerCustomEditor(String.class,editor);
}
#RequestMapping("/showForm")
public String customerPage(Model model){
model.addAttribute("customer",new Customer());
return "customerPage";
}
#RequestMapping("/showConfirmation")
public String customerConfirmation(#Valid #ModelAttribute("customer") Customer customer, BindingResult result){
System.out.println(customer.getLastName());
if(result.hasErrors())
return "customerPage";
else
return "customerConfirmation";
}
}
And here's a part of customerPage.jsp:
<body>
<form:form action="showConfirmation" modelAttribute="customer">
Id: <form:input path="id"/>
<form:errors path="id"/>
<input type="submit" value="Submit">
</form:form>
</body>
I run it, leave id field empty and press Submit button.
If my Customer.id property is Integer, then I get following sequence of actions: initBinder method triggers and sets my empty string to null. Then the setterMethod triggers, sets id to null, all good.
But if Customer.id is int, then in the browser I get this: NumberFormatException: For input string: "".
The question is why input string in exception is just empty? Isn't initBinder should have set input string to null by this moment? I thought that the sequence always looks like this: #InitBinder methods→setter methods→validating settled values.
result→errors→source→cause→stackTrace:
"java.base/java.lang.NumberFormatException.forInputString(NumberFormatException.java:68)"
"java.base/java.lang.Integer.parseInt(Integer.java:668)"
"java.base/java.lang.Integer.valueOf(Integer.java:989)"
"org.springframework.util.NumberUtils.parseNumber(NumberUtils.java:211)"
"org.springframework.beans.propertyeditors.CustomNumberEditor.setAsText(CustomNumberEditor.java:115)"
"org.springframework.beans.TypeConverterDelegate.doConvertTextValue(TypeConverterDelegate.java:429)"
"org.springframework.beans.TypeConverterDelegate.doConvertValue(TypeConverterDelegate.java:402)"
"org.springframework.beans.TypeConverterDelegate.convertIfNecessary(TypeConverterDelegate.java:155)"
"org.springframework.beans.AbstractNestablePropertyAccessor.convertIfNecessary(AbstractNestablePropertyAccessor.java:585)"
"org.springframework.beans.AbstractNestablePropertyAccessor.convertForProperty(AbstractNestablePropertyAccessor.java:604)"
"org.springframework.beans.AbstractNestablePropertyAccessor.processLocalProperty(AbstractNestablePropertyAccessor.java:453)"
"org.springframework.beans.AbstractNestablePropertyAccessor.setPropertyValue(AbstractNestablePropertyAccessor.java:278)"
"org.springframework.beans.AbstractNestablePropertyAccessor.setPropertyValue(AbstractNestablePropertyAccessor.java:266)"
"org.springframework.beans.AbstractPropertyAccessor.setPropertyValues(AbstractPropertyAccessor.java:97)"
"org.springframework.validation.DataBinder.applyPropertyValues(DataBinder.java:848)"
"org.springframework.validation.DataBinder.doBind(DataBinder.java:744)"
"org.springframework.web.bind.WebDataBinder.doBind(WebDataBinder.java:197)"
"org.springframework.web.bind.ServletRequestDataBinder.bind(ServletRequestDataBinder.java:107)"
"org.springframework.web.servlet.mvc.method.annotation.ServletModelAttributeMethodProcessor.bindRequestParameters(ServletModelAttributeMethodProcessor.java:158)"
"org.springframework.web.method.annotation.ModelAttributeMethodProcessor.resolveArgument(ModelAttributeMethodProcessor.java:160)"
"org.springframework.web.method.support.HandlerMethodArgumentResolverComposite.resolveArgument(HandlerMethodArgumentResolverComposite.java:121)"
"org.springframework.web.method.support.InvocableHandlerMethod.getMethodArgumentValues(InvocableHandlerMethod.java:167)"
"org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:134)"
"org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:106)"
"org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandlerMethod(RequestMappingHandlerAdapter.java:888)"
"org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:793)"
"org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:87)"
"org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:1040)"
"org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:943)"
"org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:1006)"
"org.springframework.web.servlet.FrameworkServlet.doPost(FrameworkServlet.java:909)"
"javax.servlet.http.HttpServlet.service(HttpServlet.java:660)"
"org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:883)"
"javax.servlet.http.HttpServlet.service(HttpServlet.java:741)"
"org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:231)"
"org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)"
"org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:53)"
"org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)"
"org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)"
"org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:202)"
"org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:96)"
"org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:526)"
"org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:139)"
"org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:92)"
"org.apache.catalina.valves.AbstractAccessLogValve.invoke(AbstractAccessLogValve.java:678)"
"org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:74)"
"org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:343)"
"org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:408)"
"org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:66)"
"org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:861)"
"org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1579)"
"org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:49)"
"java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1128)"
"java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:628)"
"org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61)"
"java.base/java.lang.Thread.run(Thread.java:830)"
Looking at the code for the setAsText in CustomNumberEditor at the time of writing:
#Override
public void setAsText(String text) throws IllegalArgumentException {
if (this.allowEmpty && !StringUtils.hasText(text)) {
// Treat empty String as null value.
setValue(null);
}
else if (this.numberFormat != null) {
// Use given NumberFormat for parsing text.
setValue(NumberUtils.parseNumber(text, this.numberClass, this.numberFormat));
}
else {
// Use default valueOf methods for parsing text.
setValue(NumberUtils.parseNumber(text, this.numberClass));
}
}
We see that if the allowEmpty property is true it will set the value as null when the text is null or an empty string. If the value is false it will just attempt to parse the number value from the String regardless, resulting in the error. Without digging further into the code it is reasonable that the allowEmpty flag is set to false presumably because an int is a primitive and can not be null. This may not seem like desirable behaviour but when an int is unassigned it will default to zero. You would prefer an exception to be thrown than an int set to 0 as this will just obscure errors.

Input in array Thymeleaf

I need choose values from one array and assign it to other array. Using Spring Thymeleaf. No idea how retrieve these choosed values.
My classes:
#Entity
public class Collaborator {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
#NotNull
#Size (min=3, max=32)
private String name;
#NotNull
#ManyToOne (cascade = CascadeType.ALL)
private Role role;
public Collaborator() {}...
#Entity
public class Role {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
#NotNull
#Size(min = 3, max = 99)
private String name;
public Role() {}....
My controllers:
#RequestMapping("/project_collaborators/{projectId}")
public String projectCollaborators(#PathVariable Long projectId, Model model) {
Project project = mProjectService.findById(projectId);
List<Collaborator> allCollaborators = mCollaboratorService.findAll();
List<Collaborator> assignments = new ArrayList<>();
if (project.getRolesNeeded()!=null) {
for (int i=0;i<project.getRolesNeeded().size();i++) {
assignments.add(new Collaborator("Unassigned", project.getRolesNeeded().get(i)));
assignments.get(i).setId((long) 0);
}
}
model.addAttribute("assignments", assignments);
model.addAttribute("allCollaborators", allCollaborators);
model.addAttribute("project", project);
return "project_collaborators";
}
#RequestMapping(value = "/project_collaborators/{projectId}", method = RequestMethod.POST)
public String projectCollaboratorsPost(#ModelAttribute Project project, #PathVariable Long projectId, Model model) {
Project p = mProjectService.findById(projectId);
//mProjectService.save(project);
return "redirect:/project_detail/{projectId}";
}
And template:
<form th:action="#{'/project_collaborators/' + ${project.id}}" method="post" th:object="${project}">
<label th:text="'Edit Collaborators: ' + ${project.name}">Edit Collaborators: Website Project</label>
<ul class="checkbox-list">
<li th:each="a : ${assignments}">
<span th:text="${a.role.name}" class="primary">Developer</span>
<div class="custom-select">
<span class="dropdown-arrow"></span>
<select th:field="${a.id}">
<option th:each="collaborator : ${allCollaborators}" th:value="${collaborator.id}" th:text="${collaborator.name}">Michael Pemulis</option>
</select>
</div>
</li>
</ul>
<div class="actions">
<input type="submit" value="Save" class="button"/>
Cancel
</div>
</form>
As you can see I want to let user choose for each role (roleNeeded) any collaborator from (allCollaborators) and keep that assigns in List (assignments).
And I get error message:
ava.lang.IllegalStateException: Neither BindingResult nor plain target
object for bean name 'a' available as request attribute
So question is: how to solve it, assign values from one array to another in template and retrieve that values in my controller.
The cause of the exception
The IllegalStateException you are getting is because th:field="${a.id}" in your select element must be related to the form th:object="${project}" element; the th:field attribute must refer to an actual field in the project instance (also you need to write th:field="*{fieldName}"). That should fix the exception you are getting, but will not solve your entire problem as the second part of it is related to how to make the values get into your controller, which I will explain next.
Sending the values to your controller
To get the values into your controller you will need to make a few changes. As I don't really know the code of your Project class, I will change a few things so you will be able to figure it out how to adapt this simple example to your particular case.
First, I understand you want to make a relation like the following in your form:
Role1 => CollaboratorA
Role2 => CollaboratorB
Your controller needs to receive a list and in order to receive this information we need two classes:
The class which will be storing the individual element data, mapping a role id with the collaborator id:
public class RoleCollaborator {
private Long roleId;
private Long collaboratorId;
public Long getRoleId() {
return roleId;
}
public void setRoleId(Long roleId) {
this.roleId = roleId;
}
public Long getCollaboratorId() {
return collaboratorId;
}
public void setCollaboratorId(Long collaboratorId) {
this.collaboratorId = collaboratorId;
}
}
A wrapper class to store a list of individual mappings:
public class RolesCollaborators {
private List<RoleCollaborator> rolesCollaborators;
public List<RoleCollaborator> getRolesCollaborators() {
return rolesCollaborators;
}
public void setRolesCollaborators(List<RoleCollaborator> rolesCollaborators) {
this.rolesCollaborators = rolesCollaborators;
}
}
The next thing to do is change your controllers where you have two methods, one that handles GET requests and another one which handles the POST requests and so, receives your form data.
In the GET one:
public String projectCollaborators(#PathVariable Long projectId, Model model) {
(... your code ...)
model.addAttribute("project", project);
// Add the next line to add the "rolesCollaborators" instance
model.addAttribute("rolesCollaborators", new RolesCollaborators());
return "project_collaborators";
}
As you can see, we added a line that will be used by the thymeleaf template. Right now is a wrapper of an empty list of roles and collaborators, but you can add values if you need to edit existing mappings instead of adding new ones.
In the POST one:
// We changed the #ModelAttribute from Project to RolesCollaborators
public String projectCollaboratorsPost(#ModelAttribute RolesCollaborators rolesCollaborators, #PathVariable Long projectId, Model model) {
(... your code ...)
}
At this point, your controller is prepared to receive the information sent from your form, that we also need to modify.
<form th:action="#{'/project_collaborators/' + ${project.id}}" method="post" th:object="${rolesCollaborators}">
<label th:text="'Edit Collaborators: ' + ${project.name}">Edit Collaborators: Website Project</label>
<ul class="checkbox-list">
<li th:each="a, stat : ${assignments}">
<span th:text="${a.role.name}" class="primary">Developer</span>
<div class="custom-select">
<input type="hidden" th:id="rolesCollaborators[__${stat.index}__].roleId" th:name="rolesCollaborators[__${stat.index}__].roleId" th:value="${a.role.id}" />
<span class="dropdown-arrow"></span>
<select th:field="*{rolesCollaborators[__${stat.index}__].collaboratorId}">
<option th:each="collaborator : ${allCollaborators}" th:value="${collaborator.id}" th:text="${collaborator.name}">Michael Pemulis</option>
</select>
</div>
</li>
</ul>
<div class="actions">
<input type="submit" value="Save" class="button"/>
Cancel
</div>
</form>
Here there are a few changes:
As I pointed out in The cause of the exception you need to change th:object="${project}" to th:object="${rolesCollaborators}", as rolesCollaborator is the instance name from where you will receive the values from your GET controller method and where you will be sending the values to your POST controller method.
I added a hidden input; this input will store the role id that will be send in association to the collaborator id the user picks from the interface using the select element. Take a look at the syntax used.
I changed the th:field value of your select element to refer to a field in the rollesCollaborators object we use in th:object="${rolesCollaborators}" form attribute. This will set the value of the collaborator in the RoleCollaborator element of the RolesCollaborators wrapped list.
With these changes your code will work. Of course, you can improve it with some other modifications, but I tried to not introduce more modifications to focus on your problem.

What is the "correct" or "safe" way to update a persisted object with Spring MVC 3 + Hibernate?

Given a very simple object:
class User {
private Integer id;
private String name;
public User() {}
public Integer getId() { return id; }
public String getName() { return name; }
public void setName(String name) { this.name = name; }
}
and a very simple controller action:
#RequestMapping(value="/edit/{id}/**", method=RequestMethod.POST)
public String editFromForm(#PathVariable("id") Integer id, #Valid User user, BindingResult bindingResult, Model model) {
// If we have errors, don't save
if(bindingResult.hasErrors()) {
// Put what they did in the model and send it back
model.addAttribute(user);
return "users/edit";
} else {
userDAO.save(user);
}
// Show them the updated page on success
return "redirect:/users/" + user.getId() + "/" + user.getName();
}
and a very simple form:
<sf:form method="POST" modelAttribute="user">
<label for="user_name">Name:</label>
<sf:input path="name" id="user_name" />
<input type="submit" value="save" /><sf:errors path="name" cssClass="error" />
</sf:form>
How should I be updating the entity in the database? Currently (since saveOrUpdate() is the actual hibernate call behind my DAO's save() method, a new object is persisted instead of updating the existing one because the id field is not being set on the object created from the form submission.
A couple of possible solutions have come to me, but I am not sure which is best in terms of keeping things clean and also secure (so that a malicious user cannot just fire in edits to whatever object Id's they wish).
Insert the id from the URL parameter into the object coming from the model binder
Have a hidden id field in the form and let the model binder attach the id
In both of those cases, there is no check in place to make sure that the object is still the same one, such as a checksum of some sort. How do others deal with this? Are there any clear example that walk through this issue?
Another issue that comes up is that I'd rather not need a setId() method since Hibernate is managing all of the id's. From what I have been able to determine, the Spring MVC model binder can only bind a field if it has the expected getter and setter. Is there some other way to apply the new state, such as getting the current User from the db by the URL's id parameter and then applying the new state to it, but without having to explicitly code all of the field copies?
I am sure there is a fairly simple, straightforward way to handle this, but my heat-addled brain does not seem to be able to come up with a suitable solution.
I'm fairly new to Spring + Hibernate so forgive me if this is one of those mundane, highly covered topics, but I have not been able to find any clear example that covers my very simple situation. If this has been covered sufficiently elsewhere, please point me in the right direction.
A couple of possible solutions have come to me, but I am not sure which is best in terms of keeping things clean and also secure (so that a malicious user cannot just fire in edits to whatever object Id's they wish).
Neither of the two approaches you mention will really handle a user who is attempting to edit objects that the user is not authorized to. At the end of the day, the user submitting the form needs to tell you which object they are submitting data for - whether it is in the URL parameter or in a hidden form parameter. I would say which of the two you choose is a matter of style and personal preference, really.
But what you need to be doing regardless of choice is to verify that the currently-logged-in user is authorized to change the object when processing the form submission. This would mean you need to check that this user is entitled to edit the current object ID, using whatever logic comprises "is allowed to do this" for your application.
Still have the same User POJO
class User {
private Integer id;
private String name;
public User() {}
public Integer getId() { return id; }
public String getName() { return name; }
public void setName(String name) { this.name = name; }
}
#RequestMapping("/create/userCreate.do")
public String createUser(Model model) {
User user = new User();
model.addAttribute(user);
return "userCreate";
}
Here I don't send the Id or any parameter, just pass the User form model attribute
#RequestMapping(value="/edit/userEdit.do", method=RequestMethod.POST)
public String editFromForm(#ModelAttribute("user") #Valid User user, BindingResult bindingResult, Model model) {
// If we have errors, don't save
if(bindingResult.hasErrors()) {
// Put what they did in the model and send it back
model.addAttribute(user);
return "users/edit";
} else {
userDAO.saveOrUpdate(user);
}
// Show them the updated page on success
return "redirect:/users/" + user.getId() + "/" + user.getName();
}
Here, the User object is bind user name and id. If there is no Id then the object will be saved and if the user object already is having Id then it will be updated. You don't have to pass id or parameter and make it visible on the address bar. Sometimes I don't like the idea of showing parameters to the users. I hope that helps
<%#taglib uri="http://www.springframework.org/tags" prefix="spring"%>
<%#taglib uri="http://www.springframework.org/tags/form" prefix="form"%>
<%#taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c"%>
<head>
<title> User Edit
</title>
</head>
<body>
<form:form method="post" action="userEdit.do" commandName="user">
<form:hidden path="id" />
<form:label path="username">User name:</form:label>
<form:input path="username" type="text" maxlength="20" />
<form:errors path="username" />
</form:form>
</body>
</html>
Im a bit late, but maybe it will be useful to someone.
Today I solved the same problem. I think your problem was there:
} else {
userDAO.save(user);
}
If it was hibernate session save, then new id was generated each time it was called. I used
session.saveOrUpdate(user)
instead of
session.save(user)
I agree 100% on what #matt b says. I have worked with similar products where at the end of the day you don't have any way of knowing if the user is editing the right object. Your only option is to make sure the user is permitted to do so. If you have a user who is editing their profile then obviously they shouldn't be editting any other person.
Now the easiest way to do what you are trying to do is actually niether. YOu don't want to use the url or hidden attributes. YOu should be able to use #ModelAttribute to do this. Here is an example
#RequestMapping(method = RequestMethod.GET)
public ModelAndView setupForm(#RequestParam("petId") int petId) {
User user = ...
model.addAttribute("user", user);
return model;
}
Then you should be able to do
#RequestMapping(method = RequestMethod.POST)
public ModelAndView submit(#ModelAttribute("user") User user) {
...
}
There is no setters or hidden fields. All the loading and binding is handled via Spring. More documentation here.

Spring form not submitting

I am using Spring SimpleFormController for my forms and for some reason it won't go to the onSubmit method
Here's my code:
public class CreateProjectController extends SimpleFormController {
ProjectDao projectDao;
public CreateProjectController() {
setCommandClass(Project.class);
setCommandName("Project");
setSessionForm(true);
}
#Override
protected Object formBackingObject(HttpServletRequest request)
throws Exception {
String id = request.getParameter("id");
Project project = projectDao.getProjectByOutsideId(id);
System.out.println("#formbacking object method");
System.out.println("the success view is "+getSuccessView());
return project;
}
#Override
protected ModelAndView onSubmit(Object command) throws Exception {
Project project = (Project) command;
System.out.println("this is the project title: "+project.getTitle());
System.out.println("the success view is "+getSuccessView());
projectDao.insert(project);
return new ModelAndView(getSuccessView());
}
I know because it prints "#formbacking object method" string but not the "the success view is..." string and the :"this is the pr..." string. I see "#formback.." string in the console but not the last two whenever I hit submit. I don't know where the problem is.
This is my jsp
<form:form method="POST" commandName="Project">
Name: <form:input path="title"/><br/>
Description: <form:input path="description"/><br/>
Link: <form:input path="url" disabled="true"/><br/>
Tags: <form:input path="tags"/><br/>
Assessors <form:input path="assessors"/><br/><br/>
<input type="submit" value="submit"/>
</form:form>
I am running on Google App Engine btw. Maybe the problem is there?
UPDATE: The problem seems to be with the formBackingObject method. When I removed it, the form now goes to the onSubmit when I click submit.
But I'd like to have values from of the command class from the database in my forms.
Another piece of code that doesn't work:
#Override
protected Object formBackingObject(HttpServletRequest request)
throws Exception {
String id = request.getParameter("id");
Project projectFromConsumer = projectDao.getProjectByOutsideId(id);
Project project = new Project();
String title = projectFromConsumer.getTitle();
project.setTitle(title);
project.setUrl("projectUrl");
return project;
}
but this does work:
#Override
protected Object formBackingObject(HttpServletRequest request)
throws Exception {
String id = request.getParameter("id");
Project projectFromConsumer = projectDao.getProjectByOutsideId(id);
Project project = new Project();
String title = projectFromConsumer.getTitle();
project.setTitle("projectTitle");
project.setUrl("projectUrl");
return project;
}
Now I am really confused. haha.
I was thinking along the same lines as axtavt. You are only going to have an id request parameter on updates, so you should add some code for creation forms:
FYI, formBackingObject requires a non-null object to be returned. To save some memory, you can have a final constant member variable that is the default return value. Your code satisfies this though since you're transferring objects, but I don't get why you're transferring data (creating an extra object) when you're not using a DTO. You could simply do this:
private final static Project PROJECT_INSTANCE = new Project();
static {
PROJECT_INSTANCE.setTitle("defaultProjectTitle");
}
#Override
protected Project formBackingObject(HttpServletRequest request) throws Exception {
String id = request.getParameter("id");
if(id == null || id.trim().length() == 0 || !id.matches("\\d+")) {
return PROJECT_INSTANCE;
}
return projectDao.getProjectByOutsideId(id);
}
You don't need a hidden id input field. You would use formBackingObject() for initializing the form input fields for updating (by navigating to page.jsp?id=111).
Look at the String id = request.getParameter("id");. There is no such field in your form, so probably you get an error there during submit process, maybe, getProjectByOutsideId returns null.
P.S. It's strange that your formBackingObject is executing when you press submit, it shouldn't if you really set setSessionForm(true).
Try turning the spring debugging up. It provides a lot of information, which can be helpful. Do this by editing the log4j.properties file.
log4j.logger.org.springframework=DEBUG
Have you added logging to make sure the formBackingObject is returning something?
System.out.println("#formbacking object method is returning: " + project);
It will make sure something is being returned. In general the formBackingObject should always return something.
EDIT:
Id is not being passed during submission in the snippet. Maybe it is during the load, e.g. /page.do?id=4, but it doesn't appear in the form.
Add <form:hidden path="id"/> to your form during on submit. Otherwise the id will not be a parameter and the getProjectByOutsideId will fail.

Categories