Bidirectional OneToMany-ManyToOne Relationship referencing unsaved transient instance (Spring MVC - Thymeleaf) - spring-boot

new here. I'm new to Spring and Thymeleaf, I'm trying to learn by following a video and I don't know why I get the following exception (org.hibernate.TransientPropertyValueException: object references an unsaved transient instance - save the transient instance before flushing : org.launchcode.codingevents.models.Event.eventCategory -> org.launchcode.codingevents.models.EventCategory) when I try to creat an Event giving it an EventCategory in the Thymeleaf form. I tried cascading from one side, then from the other and then from both, but it didn't work.
I'll be immensely grateful with whoever helps me out.
Here's my code.
#MappedSuperclass
public abstract class AbstractEntity {
#Id
#GeneratedValue
private int id;
public int getId() {
return id;
}
#Override
public int hashCode() {
return Objects.hash(id);
}
#Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null || getClass() != obj.getClass()) {
return false;
}
AbstractEntity entity = (AbstractEntity) obj;
return this.id == entity.id;
}
#Entity
public class Event extends AbstractEntity {
#NotBlank(message = "Name is required")
#Size(min = 3, max = 50, message = "Name must be between 3 and 50 characters")
private String name;
#Size(max = 500, message = "Description too long!")
private String description;
#NotBlank(message = "Email is required")
#Email(message = "Invalid email. Try again")
private String contactEmail;
#ManyToOne
#NotNull(message = "Category is required")
private EventCategory eventCategory;
public Event() {
}
public Event(String name, String description, String contactEmail, EventCategory eventCategory) {
this.name = name;
this.description = description;
this.contactEmail = contactEmail;
this.eventCategory = eventCategory;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
public String getContactEmail() {
return contactEmail;
}
public void setContactEmail(String contactEmail) {
this.contactEmail = contactEmail;
}
public EventCategory getEventCategory() {
return eventCategory;
}
public void setEventCategory(EventCategory eventCategory) {
this.eventCategory = eventCategory;
}
#Override
public String toString() {
return name;
}
#Entity
public class EventCategory extends AbstractEntity implements Serializable {
#Size(min = 3, message = "Name must be at least 3 characters long")
private String name;
#OneToMany(mappedBy = "eventCategory")
private final List<Event> events = new ArrayList<>();
public EventCategory() {
}
public EventCategory(#Size(min = 3, message = "Name must be at least 3 characters long") String name) {
this.name = name;
}
public List<Event> getEvents() {
return events;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
#Override
public String toString() {
return name;
#Controller
#RequestMapping("events")
public class EventController {
#Autowired
private EventRepository eventRepository;
#Autowired
private EventCategoryRepository eventCategoryRepository;
#GetMapping
public String displayAllEvents(#RequestParam(required = false) Integer categoryId, Model model) {
if (categoryId == null) {
model.addAttribute("title", "All Events");
model.addAttribute("events", eventRepository.findAll());
} else {
Optional<EventCategory> result = eventCategoryRepository.findById(categoryId);
if (!result.isPresent()) {
model.addAttribute("title", "Invalid Category Id: " + categoryId);
} else {
EventCategory category = result.get();
model.addAttribute("title", "Events in Category: " + category.getName());
model.addAttribute("events", category.getEvents());
}
}
return "events/index";
}
// Lives at /events/create
#GetMapping("create")
public String displayCreateEventForm(Model model) {
model.addAttribute("title", "Create Event");
model.addAttribute(new Event());
model.addAttribute("categories", eventCategoryRepository.findAll());
return "events/create";
}
// lives at /events/create
#PostMapping("create")
public String processCreateEventForm(#Valid #ModelAttribute("newEvent") Event newEvent, Errors errors, Model model) {
if (errors.hasErrors()) {
model.addAttribute("title", "Create Event");
return "events/create";
}
model.addAttribute("events", eventRepository.findAll());
eventRepository.save(newEvent);
return "redirect:";
}
// lives at /events/delete
#GetMapping("delete")
public String displayDeleteEventForm(Model model) {
model.addAttribute("title", "Delete Events");
model.addAttribute("events", eventRepository.findAll());
return "events/delete";
}
// lives at /events/delete
#PostMapping("delete")
public String processDeleteEventForm(#RequestParam(required = false) int[] eventIds) {
if (eventIds != null) {
for (int id : eventIds) {
eventRepository.deleteById(id);
}
}
return "redirect:";
}
}
Create Event
<nav th:replace="fragments :: navigation"></nav>
<form method="post" th:action="#{/events/create}" th:object="${event}">
<div class="form-group">
<label>Name
<input class="form-control" th:field="${event.name}">
</label>
<p class="error" th:errors="${event.name}"></p>
</div>
<div class="form-group">
<label>Description
<input class="form-control" th:field="${event.description}">
</label>
<p class="error" th:errors="${event.description}"></p>
</div>
<div class="form-group">
<label>Contact Email
<input class="form-control" th:field="${event.contactEmail}">
</label>
<p class="error" th:errors="${event.contactEmail}"></p>
</div>
<div class="form-group">
<label>Category
<select th:field="${event.eventCategory}">
<option th:each="eventCategory : ${categories}" th:value="${eventCategory.id}"
th:text="${eventCategory.name}">
</option>
</select>
<p class="error" th:errors="${event.eventCategory}"></p>
</label>
</div>
<div th:replace="fragments :: create-button"></div>
</form>

As per your code you are only trying to save Event entity and ignoring EventCategory.
You need to set Event to EventCategory as well as EventCategory to Event and make the cascade save.
First add cascade property in Event entity as below.
#ManyToOne(cascade = CascadeType.ALL)
#NotNull(message = "Category is required")
private EventCategory eventCategory;
Then in the Controller make the following changes.
#PostMapping("create")
public String processCreateEventForm(#Valid #ModelAttribute("newEvent") Event newEvent, Errors errors, Model model) {
if (errors.hasErrors()) {
model.addAttribute("title", "Create Event");
return "events/create";
}
model.addAttribute("events", eventRepository.findAll());
EventCategory eventCategory = newEvent.getEventCategory();
eventCategory.setEvent(newEvent);
eventRepository.save(newEvent);
return "redirect:";
}

Related

Spingboot Thymeleaf form passes null values into the controller

could someone help me figure out why it is passing null values instead of actual names? The form has a input textbox for last name and fistname but I would only get null values when I access "authorRequest.getLastName() or authorRequest.getFirstName().
Controller:
#Controller
public class AuthorController {
private BooktownService __authorService = BooktownServiceFactory.getInstance();
private Model mod;
private String msg = "";
first endpoint, return a collection of authors
#GetMapping("/booktown/list")
public List<Author> returnAuthors() {
return __authorService.getAuthors();
}
#GetMapping("/booktown/list")
public String returnAuthors(Model model) {
this.mod = model;
Author auth= new Author();
List<Author> auths = __authorService.getAuthors();
if(msg == ""){
msg = "Listing page for Authors";
}
mod.addAttribute("authors", auths);
mod.addAttribute("authorRequest", auth);
mod.addAttribute("msg",msg);
return "list_form";
}
// third endpoint, create an Author via POST
#PostMapping("/booktown/add")
public String createAuthor(#ModelAttribute("authorRequest") Author authorRequest) {
__authorService.createAuthor(authorRequest.getLastName(), authorRequest.getFirstName());
msg = "Added Author" + authorRequest.getFirstName() + " " + authorRequest.getLastName();
return "redirect:/booktown/list";
}
// Sixth endpoint: DELETE
#GetMapping("/booktown/delete")
public String deleteAuthor(#RequestParam Integer id) {
// returns a boolean
__authorService.deleteAuthor(id);
msg = "Deleted Author " + id;
System.out.println("deleted");
return "redirect:/booktown/list";
}
}
Html:
<form action="#" th:action="#{/booktown/add}" th:object="${authorRequest}" method="post">
<p>Last name: <input type="text" th:field="*{__lastName}" /></p>
<p>First name: <input type="text" th:field="*{__firstName}" /></p>
<p><input type="submit" value="Submit" /> <input type="reset" value="Reset" /></p>
</form>
Author.java:
package com.booktown.booktownservice;
public class Author {
public Author(){}
public Author(int id, String lname, String fname) {
__id = id;
__lastName = lname;
__firstName = fname;
}
public int getAuthorID() {
return __id;
}
public String getLastName() {
return __lastName;
}
public String getFirstName() {
return __firstName;
}
public int get__id() {
return __id;
}
public String get__lastName() {
return __lastName;
}
public String get__firstName() {
return __firstName;
}
private int __id;
private String __lastName;
private String __firstName;
}

org.springframework.beans.NotReadablePropertyException

In my web app, I want to build a registration form within which I wish to list all properties of a user's contact information that can be edited and saved. However, when I run my code, I get following error.
Any help will be greatly appreciated as I am new and learning as I go.
2019-01-03 17:09:58.705 ERROR 12223 --- [nio-8080-exec-5] o.a.c.c.C.[.[.[/].[dispatcherServlet] : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [An exception occurred processing JSP page /WEB-INF/views/enrollment/view.jsp at line 382
org.springframework.beans.NotReadablePropertyException: Invalid property 'contact' of bean class [com.intelo.model.Enrollment]: Bean property 'contact' is not readable or has an invalid getter method: Does the return type of the getter match the parameter type of the setter?
Here is my view.jsp
<form:form id="editRegistration" action="/enrollment/action" modelAttribute="enrollment" method="post" enctype="multipart/form-data">
<input type="hidden" name="enrollmentId" value="${enrollment.id}">
...
<c:forEach items="${contacts}" var="contact">
<div class="form-group col-md-6">
<label class="control-label">First Name</label>
<form:input class="form-control" path="contact.firstName" disabled="${allowedActions.edit? 'false' : 'true'}"/>
</div>
</c:forEach>
Here is my EnrollmentControleller:
#GetMapping("/enrollment/view")
public String viewEnrollment(#ModelAttribute("enrollment") Enrollment enrollment, Model model,
#ModelAttribute("user") Member user)
{
StateManager stateManager = StateManager.getStateManager(enrollment);
model.addAttribute("hasActions", stateManager.allowedActions(user).size() > 0);
model.addAttribute("allowedActions", stateManager.allowedActionMap(user));
model.addAttribute("isAdministrator", stateManager.getRoleInContext(user).hasAdministratorPrivilege());
model.addAttribute("pageTitle", "View Enrollment");
RegistrationDetails registrationDetails = enrollment.getRegistrationDetails();
model.addAttribute("studentInformation", registrationDetails.getStudentInformation());
model.addAttribute("healthInformation", registrationDetails.getHealthInformation());
model.addAttribute("emergencyContact", registrationDetails.getEmergencyContact());
model.addAttribute("familyInformation", registrationDetails.getFamilyInformation());
model.addAttribute("schoolHistory", registrationDetails.getSchoolHistory());
model.addAttribute("contacts", registrationDetails.getContacts());
model.addAttribute("siblings", registrationDetails.getSiblings());
model.addAttribute("preferredSite", registrationDetails.getPreferredSite());
return "enrollment/view";
}
Enrollment class:
#Entity
#Table(name="enrollments")
#OnDelete(action = OnDeleteAction.CASCADE)
public class Enrollment extends ModelObject {
#JsonProperty("firstName")
private String firstName = "";
#JsonProperty("middleName")
private String middleName = "";
#JsonProperty("lastName")
private String lastName = "";
#JsonProperty("birthDate")
#DateTimeFormat(pattern="MM/dd/uuuu")
private LocalDate birthDate = LocalDate.now();
#JsonProperty("studentGrade")
private String studentGrade = null;
#JsonProperty("registrationDate")
#JsonDeserialize(using = LocalDateDeserializer.class)
#JsonSerialize(using = LocalDateSerializer.class)
#DateTimeFormat(pattern="MM/dd/uuuu")
private LocalDate registrationDate = LocalDate.now();
#JsonIgnore
#OneToOne(fetch=FetchType.LAZY, mappedBy="enrollment", cascade={CascadeType.PERSIST, CascadeType.REFRESH, CascadeType.MERGE}, orphanRemoval=true)
#OnDelete(action = OnDeleteAction.CASCADE)
RegistrationDetails registrationDetails = null;
#JsonIgnore
#ManyToOne
#JoinColumn(name = "programId")
private Program program = null;
#JsonIgnore
#OrderBy("date")
#OneToMany (mappedBy = "enrollment", cascade = {CascadeType.PERSIST, CascadeType.REFRESH, CascadeType.MERGE})
#OnDelete(action = OnDeleteAction.CASCADE)
private List<AttendanceRecord> attendanceRecords = new ArrayList<AttendanceRecord>();
public Enrollment() {
super();
setType(this.getClass().getCanonicalName());
setState(State.DRAFT);
setRegistrationDetails(new RegistrationDetails());
}
public String getStudentGrade() {
return studentGrade;
}
public void setStudentGrade(String studentGrade) {
this.studentGrade = studentGrade;
}
public List<AttendanceRecord> getAttendance() {
return attendanceRecords;
}
public void addAttendanceRecord(AttendanceRecord attendanceRecord) {
attendanceRecords.add(attendanceRecord);
attendanceRecord.setEnrollment(this);
}
public void removeAttendanceRecord(AttendanceRecord attendanceRecord) {
attendanceRecords.remove(attendanceRecord);
attendanceRecord.setEnrollment(null);
}
public LocalDate getRegistrationDate() {
return registrationDate;
}
public void setRegistrationDate(LocalDate registrationDate) {
this.registrationDate = registrationDate;
}
public Program getProgram() {
return program;
}
public void setProgram(Program program) {
this.program = program;
setParent(program);
}
#Override
public ParentContext getParentContext() {
return program;
}
public RegistrationDetails getRegistrationDetails() {
return registrationDetails;
}
public void setRegistrationDetails(RegistrationDetails registrationDetails) {
if (this.registrationDetails != null)
registrationDetails.setEnrollment(null);
this.registrationDetails = registrationDetails;
registrationDetails.setEnrollment(this);
}
public String getFirstName() {
return firstName;
}
public void setFirstName(String firstName) {
this.firstName = firstName;
}
public String getLastName() {
return lastName;
}
public void setLastName(String lastName) {
this.lastName = lastName;
}
public String getMiddleName() {
return middleName;
}
public void setMiddleName(String middleName) {
this.middleName = middleName;
}
public LocalDate getBirthDate() {
return birthDate;
}
public void setBirthDate(LocalDate birthDate) {
this.birthDate = birthDate;
}
}
Here is my Contact class
#Embeddable
public class Contact {
String firstName = "";
String lastName = "";
String email = "";
String homePhone = "";
String dayPhone = "";
String cellPhone = "";
String relationship = "";
#Embedded
#AttributeOverrides({
#AttributeOverride(name="street", column=#Column(name="contact_street")),
#AttributeOverride(name="apartment", column=#Column(name="contact_apartment")),
#AttributeOverride(name="city", column=#Column(name="contact_city")),
#AttributeOverride(name="state", column=#Column(name="contact_state")),
#AttributeOverride(name="zipCode", column=#Column(name="contact_zipcode")),
#AttributeOverride(name="country", column=#Column(name="contact_country"))
})
Address address = new Address();
public String getFirstName() {
return firstName;
}
public void setFirstName(String firstName) {
this.firstName = firstName;
}
public String getLastName() {
return lastName;
}
public void setLastName(String lastName) {
this.lastName = lastName;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public String getHomePhone() {
return homePhone;
}
public void setHomePhone(String homePhone) {
this.homePhone = homePhone;
}
public String getDayPhone() {
return dayPhone;
}
public void setDayPhone(String dayPhone) {
this.dayPhone = dayPhone;
}
public String getCellPhone() {
return cellPhone;
}
public void setCellPhone(String cellPhone) {
this.cellPhone = cellPhone;
}
public String getRelationship() {
return relationship;
}
public void setRelationship(String relationship) {
this.relationship = relationship;
}
public Address getAddress() {
return address;
}
}

Null value returned from jsp form:options to controller

The value returned by the JSP page for the path constituency is NULL. I have opted for a drop down list to display all the possible constituencies that can be selected. I have elaborated on the problem below.
Here is the Person Class:
#Entity
#Table(name="person")
public class person{
#Id
#Column(name = "NIC")
private Integer NIC;
#Column(name= "firstname")
private String fname;
#Column(name= "lastname")
private String lname;
#ManyToOne
#JoinColumn(name= "constituency_id")
private constituency constituency;
#OneToOne
#JoinColumn(name="NIC")
private login log;
public person(){
this.NIC = 1234;
this.fname = "Undefined";
this.lname = "Undefined";
}
public person(String fname, String lname, Integer NIC) {
this.fname = fname;
this.lname = lname;
this.NIC = NIC;
}
public Integer getNIC() {
return this.NIC;
}
public void setNIC(Integer NIC) {
this.NIC = NIC;
}
public String getfname() {
System.out.println(this.fname);
return this.fname;
}
public void setfname(String name) {
this.fname = name;
}
public String getlname() {
return this.lname;
}
public void setlname(String lname) {
this.lname = lname;
}
public constituency getConstituency() {
return this.constituency;
}
public void setConstituency(constituency id) {
this.constituency = id;
}
public login getlogin() {
return this.log;
}
public void setlogin(login log) {
this.log = log;
}
#Override
public String toString() {
return this.lname + " : " + this.fname;
}
}
Here is the Constituency Class:
#Entity
#Table(name="constituency")
public class constituency {
#Column(name="address")
private String address;
#Column(name="name")
private String name;
#Id
#Column(name="constituency_id")
private Integer constituency_id;
#Column(name="noofvoters")
private Integer voters;
#OneToMany(mappedBy="constituency", cascade = {CascadeType.ALL})
private Set <person> persons;
public constituency(){
this.constituency_id = 0;
this.name = "Undefined";
this.address = "Undefined";
this.voters = 0;
}
public constituency(String name, String address, Integer voters, Integer id) {
this.name = name;
this.address = address;
this.voters = voters;
this.constituency_id = id;
}
public String getname() {
System.out.println(this.name);
return this.name;
}
public void setname(String name) {
this.name = name;
}
public String getaddress() {
System.out.println(this.address);
return this.address;
}
public void setaddress(String name) {
this.address = name;
}
public Integer getvoters() {
return this.voters;
}
public void setvoters(Integer voters) {
this.voters = voters;
}
public Integer getconstituency_id() {
return this.constituency_id;
}
public void setconstituency_id(Integer id) {
this.constituency_id = id;
}
#Override
public String toString() {
return this.name;
}
}
Here is the portion of the controller responsible for handling the operation:
#RequestMapping(value="/Add", method=RequestMethod.GET)
public String add(Model model){
List <constituency> constit = constituencyDAO.details();
model.addAttribute("message", "Add a person for voting");
model.addAttribute("per", new person());
model.addAttribute("constituency", constit);
return "Add";
}
#RequestMapping(value="/Add", method=RequestMethod.POST)
public ModelAndView addperson(#ModelAttribute("per") person per, BindingResult bind){
System.out.println("In controller");
System.out.println(per.getfname()+" First-name");
System.out.println(per.getConstituency()+" constituency");
return hello();
}
Finally, this is the portion of the JSP page with the form:select tag:
<div class = "form-group">
<label for = "element1" class="control-label col-xs-2">Constituency ID</label>
<div class="col-xs-10">
<form:select path="constituency" name="constituency">
<form:option value="NONE" label="--- Select ---" />
<form:options items="${constituency}" itemValue="constituency_id" itemLabel="name" />
</form:select>
<p>Constituency ID</p>
</div>
</div>
Now, the problem is that even though the JSP page is able to successfully display the constituency attribute in the form:options, when the "per" model attribute is retrieved in the controller, the value for constituency and only constituency is NULL.
Below is the output in Eclipse.
The Form:
The output in the RequestMethod.POST:

How to correctly bind checkbox to the object list in thymeleaf?

My domain model consist of an Employee and Certificate. One Employee can reference/have many certificates (one-to-many relationship). The full list of certificates could be get from the certificateService.
To assign some special certificate to the employee I used th:checkbox element from thymeleaf as follow:
<form action="#" th:action="#{/employee/add}" th:object="${employee}" method="post">
<table>
<tr>
<td>Name</td>
<td><input type="text" th:field="*{name}"></td>
</tr>
<tr>
<td>Certificate</td>
<td>
<th:block th:each="certificate , stat : ${certificates}">
<input type="checkbox" th:field="*{certificates}" name="certificates" th:value="${certificate.id]}"/>
<label th:text="${certificate.name}" ></label>
</th:block>
</td>
</tr>
<tr>
<td colspan="2"><input type="submit" value="Add"/></td>
</tr>
</table>
</form>
Now when I'm trying to submit the HTML form I always get following error:
400 - The request sent by the client was syntactically incorrect.
My question is: How to correctly bind checkbox elements to the object list with thymeleaf?
Controller
#RequestMapping(value = "/add" , method = RequestMethod.GET)
public String add(Model model) {
model.addAttribute("employee",new Employee());
model.addAttribute("certificates",certificateService.getList());
return "add";
}
#RequestMapping(value = "/add" , method = RequestMethod.POST)
public String addSave(#ModelAttribute("employee")Employee employee) {
System.out.println(employee);
return "list";
}
Employee Entity
#Entity
#Table(name = "employee")
public class Employee {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name = "ID")
private int id;
#Column(name = "Name")
private String name;
#ManyToMany(fetch = FetchType.EAGER)
#JoinTable(name = "emp_cert",
joinColumns = {#JoinColumn(name = "employee_id")},
inverseJoinColumns = {#JoinColumn(name = "certificate_id")})
private List<Certificate> certificates;
public Employee() {
if (certificates == null)
certificates = new ArrayList<>();
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public List<Certificate> getCertificates() {
return certificates;
}
public void setCertificates(List<Certificate> certificates) {
this.certificates = certificates;
}
#Override
public String toString() {
return "Employee [id=" + id + ", name=" + name + "certificates size = " + certificates.size() + " ]";
}
}
Certificate Entity
#Entity
#Table(name = "certificate")
public class Certificate {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name = "Id")
private int id;
#Column(name = "name")
private String name;
#ManyToMany(mappedBy = "certificates")
private List<Employee> employees;
public Certificate() {
if (employees == null)
employees = new ArrayList<>();
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public List<Employee> getEmployees() {
return employees;
}
public void setEmployees(List<Employee> employees) {
this.employees = employees;
}
#Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + id;
return result;
}
#Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
Certificate other = (Certificate) obj;
if (id != other.id)
return false;
return true;
}
}
I used a custom solution to solve this issue, i implemented it by sending an array of certificates id to controller and received it as requestParam . The require changes are defined below .
View
<tr>
<td>Certificate</td>
<td>
<th:block th:each="certificate : ${certificates}">
<input type="checkbox" name="cers" th:value="${certificate.id}"/>
<label th:text="${certificate.name}"></label>
</th:block>
</td>
</tr>
Controller
#RequestMapping(value = "/add" , method = RequestMethod.POST)
public String addSave(
#ModelAttribute("employee")Employee employee ,
#RequestParam(value = "cers" , required = false) int[] cers ,
BindingResult bindingResult , Model model) {
if(cers != null) {
Certificate certificate = null ;
for (int i = 0; i < cers.length; i++) {
if(certificateService.isFound(cers[i])) {
certificate = new Certificate();
certificate.setId(cers[i]);
employee.getCertificates().add(certificate);
}
}
for (int i = 0; i < employee.getCertificates().size(); i++) {
System.out.println(employee.getCertificates().get(i));
}
}
Can you check you Html Template syntax and change below line:
<input type="checkbox" th:field="*{certificates}" name="certificates" th:value="${certificate.id]}"/>
to:
<input type="checkbox" th:field="*{certificates}" name="certificates" th:value="${certificate.id}"/>

Spring MVC Pre Populate Checkboxes

First little background info. Got a fairly standard User Role relationship where the User can have many roles. I have roles defined as a set within the user class. Now I know that html forms have all the values as strings and trying to get values as my custom Role object does not work. I implemented an initbinder to convert the id's back into object so that I can retrieve the selected values off of my checkboxes, that part works.
But I can't seem to go back the other way. I retrieve a User from the database that already has roles and want to pre populate role checkboxes with all the roles that a user has. Based on this example :
Checkboxes example
They say that:
form:checkboxes items="${dynamic-list}" path="property-to-store"
For multiple checkboxes, as long as the “path” or “property” value is
equal to any of the “checkbox values – ${dynamic-list}“, the matched
checkbox will be checked automatically.
My interpretation of that is I should be able to feed it a Set of all the roles and define the path to be the roles from the User object and it should match them thus causing the check box to pre populate.
Every example out there seems to have the value of dynamic-list as a String[]. Well thats great and dandy but how does this work for custom objects that our defined as a Set? Can I still use this one line definition for checkboxes or do I need to do some kind of data binding heading into the view also?
Here is my user dto, user controller, custom form binder, and user edit page.
User DTO
#Entity
#Table
public class User extends BaseDto
{
#Column(updatable = false) #NotBlank
private String username;
#Column(name = "encrypted_password") #Size(min = 6, message = "password must be at least 6 characters") #Pattern(regexp = "^\\S*$", message = "invalid character detected")
private String password;
#Column(name = "first_name")
private String firstName;
#Column(name = "last_name")
private String lastName;
#Column #NotNull
private boolean enabled;
#Column #Email #NotBlank
private String email;
#Transient
private String confirmPassword;
#ManyToMany(targetEntity = Role.class, fetch = FetchType.EAGER, cascade = CascadeType.REFRESH) #JoinTable(name = "user_role", joinColumns = #JoinColumn(name = "user_id"),
inverseJoinColumns = #JoinColumn(name = "role_id"))
private Set<Role> roles;
public User()
{
}
public User(final String usernameIn, final String passwordIn, final String firstNameIn, final String lastNameIn, final String emailIn, final boolean enabledIn)
{
username = usernameIn;
password = passwordIn;
firstName = firstNameIn;
lastName = lastNameIn;
email = emailIn;
enabled = enabledIn;
}
public String getUsername()
{
return username;
}
public void setUsername(final String usernameIn)
{
username = usernameIn;
}
public String getPassword()
{
return password;
}
public void setPassword(final String passwordIn)
{
password = passwordIn;
}
public String getFirstName()
{
return firstName;
}
public void setFirstName(final String firstNameIn)
{
firstName = firstNameIn;
}
public String getLastName()
{
return lastName;
}
public void setLastName(final String lastNameIn)
{
lastName = lastNameIn;
}
public String getEmail()
{
return email;
}
public void setEmail(final String emailIn)
{
email = emailIn;
}
public String getConfirmPassword()
{
return confirmPassword;
}
public void setConfirmPassword(final String confirmPasswordIn)
{
confirmPassword = confirmPasswordIn;
}
public boolean isEnabled()
{
return enabled;
}
public void setEnabled(final boolean enabledIn)
{
enabled = enabledIn;
}
public Set<Role> getRoles()
{
return roles;
}
public void setRoles(final Set<Role> rolesIn)
{
roles = rolesIn;
}
}
User Controller
#Controller #RequestMapping("/user")
public class UserController
{
#Autowired private UserService userService;
#Autowired private UserDao userDao;
#Autowired private RoleDao roleDao;
#InitBinder
public void bindForm(final WebDataBinder binder)
{
binder.registerCustomEditor(Set.class, "roles", new CustomFormBinder<RoleDao>(roleDao, Set.class));
}
#RequestMapping(method = RequestMethod.GET)
public String index(final ModelMap modelMap)
{
return "/user/index";
}
#RequestMapping(value = "/create", method = RequestMethod.GET)
public String create(final ModelMap modelMap)
{
modelMap.addAttribute("userInstance", new User());
modelMap.addAttribute("validRoles", new HashSet<Role>(roleDao.findAll()));
return "/user/create";
}
#RequestMapping(value = "/save", method = RequestMethod.POST)
public String save(final ModelMap modelMap, #Valid #ModelAttribute("userInstance") final User user, final BindingResult bindingResult)
{
// TODO move to service validation
if (user.getPassword() == null || !user.getPassword().equals(user.getConfirmPassword()) )
{
bindingResult.addError(new FieldError("userInstance", "password", "password fields must match"));
bindingResult.addError(new FieldError("userInstance", "confirmPassword", "password fields must match"));
}
if (user.getRoles() == null || user.getRoles().isEmpty())
{
bindingResult.addError(new FieldError("userInstance", "roles", "Must select at least one role for a User"));
}
if (bindingResult.hasErrors())
{
modelMap.addAttribute("validRoles", new HashSet<Role>(roleDao.findAll()));
return "/user/create";
}
userService.save(user);
return "redirect:/user/list";
}
#RequestMapping(value = "/edit/{id}", method = RequestMethod.GET)
public String edit(#PathVariable final Integer id, final ModelMap modelMap)
{
final User user = userDao.find(id);
if (user != null)
{
modelMap.addAttribute("userInstance", user);
modelMap.addAttribute("validRoles", new HashSet<Role>(roleDao.findAll()));
return "/user/edit";
}
return "redirect:/user/list";
}
#RequestMapping(value = "/edit", method = RequestMethod.GET)
public String editCurrent(final ModelMap modelMap)
{
return edit(userService.getLoggedInUser().getId(), modelMap);
}
#RequestMapping(value = "/update", method = RequestMethod.POST)
public String update(#Valid #ModelAttribute("userInstance") final User user, final BindingResult bindingResult)
{
if (bindingResult.hasErrors())
{
return "/user/edit";
}
userService.save(user);
return "redirect:/user/list";
}
#ModelAttribute("userInstances")
#RequestMapping(value = "/list", method = RequestMethod.GET)
public List<User> list()
{
return userDao.findAll();
}
}
Custom Form Binder
public class CustomFormBinder<T extends GenericDao> extends CustomCollectionEditor
{
private final T dao;
private static final Logger LOG = LoggerFactory.getLogger(CustomFormBinder.class);
public CustomFormBinder(final T daoIn, final Class collectionType)
{
super(collectionType, true);
dao = daoIn;
}
#Override
protected Object convertElement(final Object element)
{
try
{
// forms should return the id as the itemValue
return dao.find(Integer.valueOf(element.toString()));
}
catch (NumberFormatException e)
{
LOG.warn("Unable to convert " + element + " to an integer");
return null;
}
}
}
User Edit View
<html>
<head>
<title>Create User</title>
</head>
<body>
<c:url value="/user/update" var="actionUrl"/>
<form:form method="post" commandName="userInstance" action="${actionUrl}">
<h1>Edit User ${userInstance.username}</h1>
<div>
<form:label path="username">Username:</form:label>
<form:input path="username" id="username" readonly="true"/>
</div>
<div>
<form:label path="password">Password:</form:label>
<form:input path="password" id="password" type="password" readonly="true"/>
<tag:errorlist path="userInstance.password" cssClass="formError"/>
</div>
<div>
<form:label path="firstName">First Name:</form:label>
<form:input path="firstName" id="firstName"/>
<tag:errorlist path="userInstance.firstName" cssClass="formError"/>
</div>
<div>
<form:label path="lastName">Last Name:</form:label>
<form:input path="lastName" id="lastName"/>
<tag:errorlist path="userInstance.lastName" cssClass="formError"/>
</div>
<div>
<form:label path="email">Email:</form:label>
<form:input path="email" id="email" size="30"/>
<tag:errorlist path="userInstance.email" cssClass="formError"/>
</div>
<div>
**<%--Want to Pre Populate these checkboxed--%>
<form:checkboxes title="Assigned Roles:" path="roles" id="roles" items="${validRoles}" itemLabel="displayName" itemValue="id" element="div"/>**
<tag:errorlist path="userInstance.roles" cssClass="formError"/>
</div>
<form:hidden path="enabled"/>
<form:hidden path="id"/>
<form:hidden path="version"/>
<div class="submit">
<input type="submit" value="Update"/>
Cancel
</div>
</form:form>
</body>
</html>
You need a correct implemented equals method for Role!
If this is not enough have a look at class oorg.springframework.web.servlet.tags.form.AbstractCheckedElementTag. The method void renderFromValue(Object item, Object value, TagWriter tagWriter) is where the the checked flag is set.

Resources