How to accept input for a list of objects using checkboxes in a form - forms

I have a scenario with 2 entities:
1)Student:
#Entity
#Table(name = "student_registration")
public class Student
{
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
private int id;
#Column
private String firstName;
#Column
private String lastName;
#OneToMany(mappedBy = "student", cascade = CascadeType.ALL)
private List<Hobby> hobbies;
//constructors (default, all params), getters and setters
}
2)Hobbies:
#Entity
#Table(name = "hobby")
public class Hobby
{
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
private int id;
#ManyToOne(cascade = CascadeType.ALL)
#JoinColumn(name = "hobby_id")
private Student student;
#Column
private String name;
//contructors (default, Hobby(name, student), getters and setters)
}
I want to input each checkbox option from a group of checkboxes as a hobby with name equal to the value of the checkbox
Here is my JSP section for the above:
<div class="form">
<form:form action="success" modelAttribute="student" onsubmit="return validate()">
<div class="row">
<div class="label">First Name</div>
<div class="field">
<form:input path="firstName" />
</div>
</div>
<div class="row">
<div class="label">Last Name</div>
<div class="field">
<form:input path="lastName" />
</div>
</div>
<div class="row">
<div class="label">Hobbies</div>
<div class="field">
<form:checkbox path="hobbies" id="hobby1Checkbox" value="Hobby 1" />
<label class="sublabel" for="hobby1Checkbox">Hobby 1</label>
<form:checkbox path="hobbies" id="hobby2Checkbox" value="Hobby 2" />
<label class="sublabel" for="hobby2Checkbox">Hobby 2</label>
<form:checkbox path="hobbies" id="hobby3Checkbox" value="Hobby 3" />
<label class="sublabel" for="hobby3Checkbox">Hobby 3</label>
</div>
</div>
</form:form>
</div>
Here is my controller:
#Controller
public class StudentController {
#Autowired
StudentService stuServ;
#RequestMapping("/")
public String index() {
return "index";
}
#GetMapping("/registration")
public String registrationRedirect(ModelMap studentModel)
{
studentModel.addAttribute("student", new Student());
studentModel.addAttribute("hobby", new Hobby());
return "registration";
}
#PostMapping("/success")
public String sayHello(#ModelAttribute("registeredStudent") Student student) {
stuServ.addStudent(student);
return "success";
}
}
I understand that what I have done is incorrect, since I cannot map a String value from a checkbox to a list of objects (List)
However I am unable to find a way to achieve the same. I was looking into possible solutions using a Converter or a PropertyEditor but I am new to Spring MVC and I'm confused.
*Update:
I have observed that upon adding a parameterized constructor within the Hobby class with only the name as the argument, I am able to directly pass the value to the Hobby object using the checkboxes, since the String value of the checkboxes is initialized to the Hobby name via the aforementioned contructor.
This works if I am only to use the hobby name for my registration success view, however another obvious issue is that the student field of the Hobby class never gets initialized since only the contructor Hobby(name) is getting invoked, hence the relationship isn't bidirectional. Student field remains null in the Hobby class.
Kindly assist with appropriate ways to handle the situation. I am unable to think of ways with which I might let Spring know that which student am I assigning the Hobby selected by the checkbox to.
Edit with respect to my earlier provided answer:
Another issue arises if I create a repository for Hobby entity:
public interface HobbyRepository extends JpaRepository<Hobby, Integer>
{
boolean findByName(String hobby);
}
The checkbox String value initialization for hobbies does not work anymore. Earlier the constructor with name parameter for Hobby entity did the job but on addition of Hobby repository I am getting the following expected error:
There was an unexpected error (type=Bad Request, status=400).
Validation failed for object='registeredStudent'. Error count: 1
org.springframework.validation.BindException: org.springframework.validation.BeanPropertyBindingResult: 1 errors
Field error in object 'registeredStudent' on field 'hobbies': rejected value [Singing,Sketching,Swimming]; codes [typeMismatch.registeredStudent.hobbies,typeMismatch.hobbies,typeMismatch.java.util.List,typeMismatch]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [registeredStudent.hobbies,hobbies]; arguments []; default message [hobbies]]; default message [Failed to convert property value of type 'java.lang.String[]' to required type 'java.util.List' for property 'hobbies'; nested exception is org.springframework.core.convert.ConversionFailedException: Failed to convert from type [java.lang.String] to type [java.lang.Integer] for value 'Singing'; nested exception is java.lang.NumberFormatException: For input string: "Singing"]
Pretty much, the Hobby with name = the String value of the checkboxes isn't being able to initialize anymore. I have tried removing the Hobby repository and it works perfectly. Any further insight would be appreciated as the only possible solution for now appears to be adding an extra List<String> field in my Student POJO class and using the values from checkboxes passed in the field to initialize the Hobby objects contained in Student.hobbies

In my controller, the current student object passed as ModelAttribute can be assigned to each of the hobbies belonging to the current student object, thereby completing the bidirectional relationship, as follows:
#PostMapping("/success")
public String sayHello(#ModelAttribute("registeredStudent") Student student) {
for(Hobby h : student.getHobbies())
{
h.setStudent(student);
}
stuServ.addStudent(student);
return "success";
}
I had to exclude the mapped fields, i.e. hobbies in Student entity and student in Hobby entity from the hashcode and equals methods in the respective entities, in order to prevent StackOverFlow Error due to infinite nesting of the entities
This works as a solution, however there might be better ways to approach this problem.
Edit:
Another issue arises if I create a repository for Hobby entity:
public interface HobbyRepository extends JpaRepository<Hobby, Integer>
{
boolean findByName(String hobby);
}
The checkbox String value initialization for hobbies does not work anymore. Earlier the constructor with name parameter for Hobby entity did the job but on addition of Hobby repository I am getting the following expected error:
There was an unexpected error (type=Bad Request, status=400).
Validation failed for object='registeredStudent'. Error count: 1
org.springframework.validation.BindException: org.springframework.validation.BeanPropertyBindingResult: 1 errors
Field error in object 'registeredStudent' on field 'hobbies': rejected value [Singing,Sketching,Swimming]; codes [typeMismatch.registeredStudent.hobbies,typeMismatch.hobbies,typeMismatch.java.util.List,typeMismatch]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [registeredStudent.hobbies,hobbies]; arguments []; default message [hobbies]]; default message [Failed to convert property value of type 'java.lang.String[]' to required type 'java.util.List' for property 'hobbies'; nested exception is org.springframework.core.convert.ConversionFailedException: Failed to convert from type [java.lang.String] to type [java.lang.Integer] for value 'Singing'; nested exception is java.lang.NumberFormatException: For input string: "Singing"]
Pretty much, the Hobby with name = the String value of the checkboxes isn't being able to initialize anymore. I have tried removing the Hobby repository and it works perfectly. Any further insight would be appreciated as the only possible solution for now appears to be adding an extra List<String> field in my Student POJO class and using the values from checkboxes passed in the field to initialize the Hobby objects contained in Student.hobbies

Related

Spring boot with Thymeleaf th:object use an object with existing values without overriding them

I'm trying to use my Entity class User as a form object.
#Entity
#Table(name = "users")
public class User implements Serializable {
#Column(unique = true, nullable = false)
#Id
private int id;
#Column(name = "username")
private String username;
#Column(name = "created_at", nullable = false)
private Timestamp createdAt;
// Other variables, getters and setters...
}
The view:
<form th:action="#{/users/{id}/save(id=${user.id})}" th:method="POST" th:object="${user}">
<label for="username">Vartotojo vardas</label>
<input type="text" id="username" name="username" th:value="${user.username}" th:field="*{username}"/>
<button type="submit" class="btn btn-success" th:text="Saugoti"></button>
</form>
Now when I show the page containing that form I pass an object named "user" to use in th:value.
What I want to achieve? User contains a field called createdAt after submitting the form it becomes null, even if it was not null before(I printed out in my view before the form to confirm). I want the the form to not change values which I don't tell it to change.
A simple solution would be to add the createdAt field to your form as a hidden field with its value populated.
Maybe a better solution would be to use a data transfer object that contains only fields that you want to update in this particular form. That way you can be sure the fields you want to update will be updated, and the ones you want left alone will be alone.

Spring form path with multiple model attributes with the same property name

The problem is that I have a spring form and 2 #ModelAttribute params with the same properties in my controller. The 'commandName' parameter of the the form is set to one of my modelAttributes names. I was surprised that the maps the property not only to the model attribute specified with 'commandName', but also to the second one.
I haven't found the exact solution here, except the similar to mine: Spring-form multiple forms with same model atribute name properties
But in my case I can't see any 'strange things', I have one form, one Model attribute to bind this form, and one model attribute to have accsess to controller scoped #SessionAttribute.
I've also tried to use form's 'modelAttribute' parameter (Actually I can't see any difference between them), but it didn't help.
My code example:
view.jsp:
<form:form name="form" action="/myAction" method="POST" commandName="model1">
<form:input path="property"/>
....
<input type="submit" value="Submit"/>
</form:form>
Controller.java
#SessionAttributes("model2")
class Controller {
#RequestMapping(value = "/myAction", method = POST)
public String submitEditSite(final #ModelAttribute(value = "model1") Model1 model1,
final #ModelAttribute(value = "model2") Model2 model2) {
....
return "redirect:/home";
}
}
Model1.java Model2.java
class Model1 {
private String property;
}
class Model2 {
private String property;
}
Where am I wrong?
If I understand you correctly you want to prevent the setting of any property on model2, right?
Then this should do:
#InitBinder("model2")
public void initBinder(WebDataBinder binder) {
binder.setDisallowedFields("*");
}

Null fields after form submit in Spring

I've got a Product with a Rating rating attribute. I've got a product update form (updateStart method) which doesn't contain the rating field (since I don't want it to be editable).
The problem is that when I submit the form (with update method), the rating is automatically set to null.
So I tried to add the Rating to the form model in updateStart, retrieving it in the update method, but it keeps being rewritten as well.
I tried to set a #SessionAttributes("rating") annotation in the controller. This time the rating value is kept, but Spring creates a new entry in the database, cloned from the other rating object, and attaches it to the Product.
#Controller
#SessionAttributes("rating")
#RequestMapping("/products")
public class ProductsController {
#RequestMapping("/update_start")
public String updateStart(#RequestParam("id") Long id, Model model) throws BusinessException {
Product product = productService.findProductById(id);
System.out.println("RATING A START "+product.getRating().getAbsoluteRating());
List<Category> categories = productService.findAllCategories();
model.addAttribute("categories", categories);
model.addAttribute("product", product);
model.addAttribute("id", id);
model.addAttribute("rating",product.getRating());
return "products.updateform";
}
#RequestMapping(value="/update", method = RequestMethod.POST)
public String update(#ModelAttribute("rating") Rating rating, #ModelAttribute Product product, BindingResult bindingResult) throws BusinessException {
System.out.println("RATING A UPDATE "+rating.getAbsoluteRating());
validator.validate(product, bindingResult);
List<Image> images = imageService.getProductImages(product.getId());
product.setRating(rating);
productService.updateProduct(product,images,sellerid);
return "redirect:/products/viewsforsellers.do";
}
}
What can I do?
EDIT: I'd prefer to avoid placing a hidden input field with ratingId in my form.
In the form include a hidden input with the name and value specified for the Rating. The value should include
<form>
<input name="product.rating" value="${product.rating.id}"/>
<!-- Other fields -->
</form>
Now when the request comes over the wire it should include a Rating specified by id for the product.
#RequestMapping(value="/update", method = RequestMethod.POST)
public String update(#ModelAttribute Product product, BindingResult bindingResult) throws BusinessException {
//implementation
}
#ModelAttribute should attempt to bind this parameter to the Product however it is not aware of what a Rating is. This is where a Converter comes into play. A Converter is used during databinding to tell Spring MVC how to map a field of type String to a field of type Rating.
public class StringToRatingConverter implements Converter<String, Rating> {
public Rating convert(String source) {
//Use the source String to convert to rating
//Possibly via database call or enum conversion, pending ratings type and definition
//Ultimately the code needs to return the appropriate object of type Rating
return rating; //The above implementation will create the rating object.
}
}
The StringToRatingConverter must then be registered in the dispatcher configuration file.
<!-- Register Converters - Used for data binding-->
<bean id="conversionService" class="org.springframework.context.support.ConversionServiceFactoryBean">
<property name="converters">
<list>
<bean class="fully.qualified.path.to.StringToRatingConverter"/>
</list>
</property>
</bean>
The first time I encountered this scenario, I captured it in a post on my blog, which you may be helpful.
You should add "types" element to your #SessionAttributes("rating") annotation in order properties of attributes to be kept; e.g.
#SessionAttributes(types = Rating.class, names = "rating")

Spring Mvc/Jpa-OneToMany : How to display a list of class associated to another one

I've got a class Module with a OneToMany binding with a class Sequence.
My aim is to show the list of Modules, and by clicking on one of them, display the associated list of Sequences
But it doesn't work, I have a HTTP 500 error.
Here there is my controller :
#RequestMapping(value="formation", method = RequestMethod.GET)
public ModelAndView allModules() {
List<Module> allModules = moduleService.findAll();
return new ModelAndView("formation", "modules", allModules);
}
#RequestMapping(value="sequences/{module}", method = RequestMethod.GET)
public String displaySequences(#PathVariable ("module") Module module, Model model) {
List<Sequence> allSequences = sequenceService.findByModule(module);
model.addAttribute("sequences", allSequences);
return "sequences";
}
and the jsp which show the list of modules to return the list of sequences
<c:forEach items="${modules}" var="module">
<ul>
<li>${module.titre}
<br/>
</li>
</ul>
</c:forEach>
So, where does my error come from?
It works when I do that:
#RequestMapping(value="/sequences/{moduleId}", method = RequestMethod.GET)
public String displaySequences(#PathVariable ("moduleId") Long moduleId, Model model) {
Module module = moduleService.findById(moduleId);
model.addAttribute("module", module);
return "sequences";
}
and I change the link with :
<a href="sequences/${module}">${module.titre}
but I'd like to understand my error.
The reason why you weren't able to display sequences is Spring doesn't know how to parse this
/cmap-web/sequences/com.almerys.jpa.tomcatspring.Module#12b0f0ae
into Module instance.
You can read on this in Spring docs here in the section's 16.3.2.2 URI Template Patterns last paragraph. I paste it here for convenience.
A #PathVariable argument can be of any simple type such as int, long, Date, etc. Spring automatically converts to the appropriate type or throws a TypeMismatchException if it fails to do so. You can also register support for parsing additional data types. See Section 16.3.3.14, “Method Parameters And Type Conversion” and Section 16.3.3.15, “Customizing WebDataBinder initialization”.

Spring MVC <form:options> selected value

I got relation "Many to one" between my CPVCode adn OrderType:
public class CPVCode {
#Id
#GeneratedValue
private int id;
private String cpv_code;
private String description;
#ManyToOne
#JoinColumn(name="id_parent")
private OrderType orderType;
//getters na setters: ...
}
Everything works well, but I NEED to displays selected value in my form:
<form:select path="orderType" items="${orderTypes }" itemLabel="title" itemValue="id" ></form:select>
It seems to work almost good: It displays list of all OrderTypes ( by ${orderTypes} which returns array of that objects type), it saves proper values by Hibernate, BUT thereis no way to select current value of orderType after refreshing...
your passing a list to a selectbox, so it iterates over the list. You need to change which bean the selectbox references - a single value oderType from CPVcode.
And also possibly change the selectbox to a different html form element ?