POSTing data with many to one relationship using Thymeleaf - java

I have a simple model class Product which exhibits a many to one relationship with ProductCategory:
Product class:
#Entity
#Table(name="product")
public class Product {
#Id
#GeneratedValue(strategy=GenerationType.IDENTITY)
#Column(name="id")
private Long id;
#Column(name="name")
private String name;
#Column(name="description")
private String description;
#ManyToOne(fetch=FetchType.LAZY)
#JoinColumn(name="category_id")
private ProductCategory category;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getPdfUrl() {
return pdfUrl;
}
public void setPdfUrl(String pdfUrl) {
this.pdfUrl = pdfUrl;
}
public ProductCategory getCategory() {
return category;
}
public void setCategoryId(ProductCategory category) {
this.category = category;
}
}
ProductCategory class
#Entity
#Table(name="product_category",uniqueConstraints={#UniqueConstraint(columnNames="name")})
public class ProductCategory {
#Id
#GeneratedValue(strategy=GenerationType.IDENTITY)
#Column(name="id")
private Long id;
#Column(name="name")
private String name;
#OneToMany(fetch=FetchType.LAZY, mappedBy="category")
private Set<Product> products = new HashSet<Product>(0);
// getters() & setters()
}
I am using Spring boot with Thymeleaf to create the necessary forms for the usual CRUD operations.
Here is the essential portion of my html page which I use to add a new Product object into the database.
<form action="#" th:action="/product/save" th:object="${newProduct}" method="POST">
<input type="text" th:field="*{name}" />
<input type="text" th:field="*{description}" />
<select th:field="*{category}">
<option th:each="category: ${productCategories}" th:value="${category}" th:text="${category.name}" />
</select>
<button type="submit">Submit</button>
</form>
The problem is, when I try and insert the resulting Product object from the controller (I know I haven't shown it here, mostly because I don't think that is actually the cause of the problem), there is a
MySQLIntegrityConstraintViolationException: Column 'category_id' cannot be null
I have tried changing the value of the option to ${category.id}, but even that doesn't fix it.
In a nutshell
How do I actually pass a complex object as a POST parameter into a controller using Thymeleaf?
Update
Contrary to my first thoughts, this might actually be related to my Controller, so here is my ProductController:
#RequestMapping(value="/product/save", method=RequestMethod.POST)
public String saveProduct(#Valid #ModelAttribute("newProduct") Product product, ModelMap model) {
productRepo.save(product);
model.addAttribute("productCategories", productCategoryRepo.findAll());
return "admin-home";
}
#RequestMapping(value="/product/save")
public String addProduct(ModelMap model) {
model.addAttribute("newProduct", new Product());
model.addAttribute("productCategories", productCategoryRepo.findAll());
return "add-product";
}

Note that I have changed the form method to POST.
From thymeleafs perspective I can assure the below code should work.
<form method="POST" th:action="#{/product/save}" th:object="${newProduct}">
....
<select th:field="*{category}" class="form-control">
<option th:each="category: ${productCategories}" th:value="${category.id}" th:text="${category.name}"></option>
</select>
Provided that your controller looks like this.
#RequestMapping(value = "/product/save")
public String create(Model model) {
model.addAttribute("productCategories", productCategoryService.findAll());
model.addAttribute("newproduct", new Product()); //or try to fetch an existing object
return '<your view path>';
}
#RequestMapping(value = "/product/save", method = RequestMethod.POST)
public String create(Model model, #Valid #ModelAttribute("newProduct") Product newProduct, BindingResult result) {
if(result.hasErrors()){
//error handling
....
}else {
//or calling the repository to save the newProduct
productService.save(newProduct);
....
}
}
Update
Your models should have proper getters and setters with the correct names. For example, for the property category You should have,
public ProductCategory getCategory(){
return category;
}
public void setCategory(productCategory category){
this.category = category;
}
NOTE - I have not compiled this code, I got it extracted from my current working project and replace the names with your class names

Related

Cannot solve the problem javax.el.PropertyNotFoundException: Property [content] not found on type [java.lang.String]

I've read the same problem solutions, but it didn't helped me.
there is a part of my Bean class with good written getter:
#Entity
#Table(name = "notes")
public class Note {
#Id
#GeneratedValue (strategy = GenerationType.IDENTITY)
#Column (name = "id")
private int id;
#Column (name = "content")
private String content;
public Note() {
}
public Note(String title, String content, GregorianCalendar date, boolean done) {
this.title = title;
this.content = content;
this.date = date;
this.done = done;
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getContent() {
return content;
}
public void setContent(String content) {
this.content = content;
}
}
Using debug mode i can see, that i've got my ArrayList of notes from dataBase. It means, that connection is good. there is a code from servlet:
public static final String OUTPUT_LIST = "List For Pager";
// other code, not nessesary for showing
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
Integer pageNumber = (Integer) req.getAttribute(PAGE_NUMBER);
if(pageNumber==null) pageNumber = 1;
ArrayList<Note> result = new ArrayList<Note>();
ArrayList<Note> notes = DaoDriver.getActualNotesHandler().getNotesList();
//iteration method for filling result
fillListForPage(pageNumber,notes,result);
req.setAttribute(OUTPUT_LIST,result);
RequestDispatcher requestDispatcher = req.getRequestDispatcher("/index_test.jsp");
requestDispatcher.forward(req,resp);
There is a place, where i call my list from jsp:
<c:forEach var="note" items="${MainServlet.OUTPUT_LIST}">
<div class="row" padding="5" >
<div class="card-deck">
<div class="card">
<div class="card-header">Title1</div>
<div class="card-body"><p>${note.content}</p></div>
<div class="card-footer">
<input type="checkbox" class="Done">
<label>Done</label>
<button>Edit Note</button>
</div>
</div>
</c:forEach>
i have additional problem here, that can crash my application. I have the same situation, like in this question:
JPA Cannot resolve column/IntelliJ
but i have my data associated and quick fix doesn't resolve this problem.
what is wrong with my code?
UPD: I've fixed this problem by changing 2 strings:
req.setAttribute("list",result);
and
<c:forEach var="note" items="${list}">
And that's why i have new question: why can't i use the staic final string value (constant) from MainServlet.class for the key of request's property?
Try changing ${MainServlet.OUTPUT_LIST} to ${requestScope.OUTPUT_LIST}
<c:forEach var="note" items="${requestScope.OUTPUT_LIST}">

org.springframework.beans.NotReadablePropertyException:

From controller I am sending BinderPlaceOrder object like this
model.addAttribute("addNewBinderPlaceOrder", new BinderPlaceOrder());
My Thymeleaf page is,
<form class="addBinderPlaceOrderForm" role="form" action="#" th:action="#{/binderPlaceOrder/new-binderPlaceOrder}" th:object="${addNewBinderPlaceOrder}" method="post">
<div class="form-group">
<label>Book</label>
<select class="form-control" th:field="*{binderOrderItemDetails.book}">
<option th:if="${book} == null" value=" " >Select Book</option>
<option th:each="book : ${allBook}"
th:value="${book.id}"
th:text="${book.name}">
</option>
</select>
</div>
<form>
Here is the controller,
#Controller
#RequestMapping("/binderPlaceOrder")
public class BinderPlaceOrderController{
#Autowired
BinderPlaceOrderService binderPlaceOrderService;
#Autowired
BookService bookService;
#RequestMapping(value="/new-binderPlaceOrder", method = RequestMethod.GET)
public String newBinderPlaceOrder(Model model){
model.addAttribute("addNewBinderPlaceOrder", new BinderPlaceOrder());
model.addAttribute("allBook", bookService.getAllBooks();
model.addAttribute("addNewBinderOrderItemDetails", new BinderOrderItemDetails());
return "user/binderPlaceOrder/new";
}
}
During run time I get the below error
org.springframework.beans.NotReadablePropertyException: Invalid property 'binderOrderItemDetails.book' of bean class [PublisherInventory.model.user.BinderPlaceOrder]: Bean property 'binderOrderItemDetails.book' is not readable or has an invalid getter method: Does the return type of the getter match the parameter type of the setter?
Below are the classes.
Please note, every class has their own Id property along with other properties. For readability those are omitted.
Here is BinderPlaceOrder Class,
#Entity
public class BinderPlaceOrder implements Comparator<BinderPlaceOrder> {
#OneToMany(mappedBy = "binderPlaceOrder", fetch = FetchType.LAZY, cascade = CascadeType.ALL)
private List<BinderOrderItemDetails> binderOrderItemDetails;
public List<BinderOrderItemDetails> getBinderOrderItemDetails() {
return binderOrderItemDetails;
}
public void setBinderOrderItemDetails(List<BinderOrderItemDetails> binderOrderItemDetails) {
this.binderOrderItemDetails = binderOrderItemDetails;
}
}
Here is BinderOrderItemDetails class
#Entity
#Indexed
public class BinderOrderItemDetails {
#OneToOne(fetch = FetchType.EAGER,cascade = CascadeType.ALL)
#JoinColumn(name="bookId")
private Book book;
#ManyToOne(fetch = FetchType.EAGER, cascade = CascadeType.ALL)
#JoinColumn(name="binderPlaceOrderId")
private BinderPlaceOrder binderPlaceOrder;
public Book getBook() { return book; }
public void setBook(Book book) { this.book = book; }
public BinderPlaceOrder getBinderPlaceOrder() { return binderPlaceOrder; }
public void setBinderPlaceOrder(BinderPlaceOrder binderPlaceOrder) {
this.binderPlaceOrder = binderPlaceOrder; }
}
Here is the Book Class
#Entity
#Indexed
public class Book implements Comparator<Book> {
#Column(name = "name")
#NotNull
#NotBlank
private String name;
public String getName() { return name; }
public void setName(String name) { this.name = name;}
}
Can you please tell me of what I am doing wrong or how it can be solved?
Thanks in advance.
As #JBNizet pointed out, your BinderPlaceOrder has
public List<BinderOrderItemDetails> getBinderOrderItemDetails() {
return binderOrderItemDetails;
}
which is a list, hence
th:field="*{binderOrderItemDetails.book}"
would not be correct,
but say you want to insert the first element,
th:field="${binderOrderItemDetails[0].book}"

How to remove object from Autopopulate list in spring

I am new to spring + hibernate. When I add a customer and its destinations (one to many relationship), everything is fine. But when I update the customer's destination, all previous destinations remain in the database with a null customer foreign key.
Suppose I insert 4 destinations a, b, c, d. After updating the customer, I insert x, y. Then it stores total 6 destinations: a, b, c, d with null references and x, y with customer references.
Here is my code:
1). Customer Entity
Has one-to-many relationship with destination and relationship is unidirectional.
#Entity
#Table(name="customers")
#Proxy(lazy=false)
public class CustomerEntity {
#Id
#Column(name="id")
#GeneratedValue
private Integer id;
private String description;
private String panNo;
private String cstNo;
private String vatNo;
#OneToMany(fetch = FetchType.EAGER,cascade = CascadeType.ALL)
#JoinColumn(name = "customer_id", referencedColumnName = "id")
public List<DestinationsEntity> destination = new AutoPopulatingList<DestinationsEntity>(DestinationsEntity.class);
//getter and setters
}
2). Destination Entity
#Entity
#Table(name = "destinations")
#Proxy(lazy = false)
public class DestinationsEntity {
#Id
#Column(name = "id")
#GeneratedValue
private Integer id;
#Column(name="destination")
private String destination;
// getter and setter
}
1). AddCustomer.jsp
This code for adding more destinations in Autopopulate list
<div id="destination_container">
<div><textarea row="3" col="5" class="destination_address" name= "destination[${0}].destination" placeholder="Please enter address"></textarea></div>
</div>
<script type="text/javascript">
$(document).ready(function(){
var index = 1;
/*
* Add more destination
*/
$('#add_more_destination').click(function(){
$('#destination_container').append('<div><textarea row="3" col="5" class="destination_address" name= "destination[${"'+index+'"}].destination" placeholder="Please enter address"></textarea><span class="remove_dest">*</span></div>');
index++;
});
});
</script>
2). updateCustomer.jsp
All destinations added by customer is show here and he/she can be change destinations(like before inserted pune, mumbai , banglore) now updating destinations( delhi, punjab)
<c:set var="index" scope="page" value="${fn:length(destinationss)}"/>
<c:forEach items="${destinationss}" var="dest" varStatus="i">
<div>
<textarea class="destination_address" name= "destination[${i.index}].destination" placeholder="Please enter address">${dest.destination}</textarea><span class="remove_dest">*</span>
</div>
</c:forEach>
<button type ="button" id="add_more_destination">Add More Destinations</button>
<script type="text/javascript">
$(document).ready(function(){
/*
* Add a destination
*/
var index = ${index};
$('#add_more_destination').click(function(){
$('#destination_container').append('<div><textarea row="3" col="5" class="destination_address" name=destination["'+index+'"].destination placeholder="Please enter address"></textarea><span class="remove_dest">*</span></div>');
alert(index);
index++;
});
</script>
Controller
#RequestMapping(value = "/addCustomerForm", method = RequestMethod.GET)
public String addCustomerForm(ModelMap map) {
return "master/addCustomer";
}
#RequestMapping(value = "/addCustomer", method = RequestMethod.POST)
public String addCustomer(#ModelAttribute(value = "customer") CustomerEntity customer,BindingResult result, HttpServletRequest request) {
customerService.addCustomer(customer);
return "redirect:/customer";
}
Update Customer
This is new thing I tried last night. Problem is solved partially.
#ModelAttribute
public void updateOperation(HttpServletRequest request, ModelMap map) {
if(null !=request.getParameter("id"))
map.addAttribute("customer1", customerService.findOne(Integer.parseInt(request.getParameter("id"))));
}
#RequestMapping(value = "/updateCustomerForm/{customerId}", method = RequestMethod.GET)
public String updateCustomerForm(#PathVariable("customerId") Integer customerId, ModelMap map, HttpServletRequest request) {
CustomerEntity customerEntity = customerService.findOne(customerId);
map.addAttribute("customer", customerEntity);
map.addAttribute("destinationss",customerEntity.getDestination());
}
#RequestMapping(value = "/updateCustomer", method = RequestMethod.POST)
public String updateCustomer(#ModelAttribute(value = "customer1")CustomerEntity customer1,BindingResult result, HttpServletRequest request,HttpServletResponse response) {
customerService.updateCustomer(customer1);
return "redirect:/customer";
}
}
1). CustomerServiceImpl
public class CustomerServiceImpl implements CustomerService{
#Autowired
private CustomerDao customerDao;
#Override
#Transactional
public void addCustomer(CustomerEntity customer) {
customerDao.addCustomer(customer);
}
#Override
#Transactional
public CustomerEntity findOne(Integer id){
return customerDao.findOne(id);
}
#Override
#Transactional
public void updateCustomer(CustomerEntity customerEntity){
if (null != customerEntity) {
customerDao.updateCustomer(customerEntity);
}
}
}
2).CustomerDaoImpl
public class CustomerDaoImpl implements CustomerDao{
#Autowired
private SessionFactory sessionFactory;
#Override
#Transactional
public void addCustomer(CustomerEntity customer){
this.sessionFactory.getCurrentSession().save(customer);
}
#Override
public CustomerEntity findOne(Integer id){
return (CustomerEntity) sessionFactory.getCurrentSession().load(CustomerEntity.class, id);
}
#Override
#Transactional
public void updateCustomer(CustomerEntity customerEntity){
if (null != customerEntity) {
this.sessionFactory.getCurrentSession().update(customerEntity);
}
}
}
The issue is Spring will give you new Customer entity, so I guess the Destination entities in this Customer is empty initially. So in your update operation you are just adding some new Destination entities and then adding them to customer as per your code.
So in this case, the customer entity is having only the new Destination objects where as the already existing Destination entities which were mapped earlier are not present in your Customer entity.
To fix the issue, first get the Customer entity from database, then this entity will have the set of Destination objects. Now to this Customer you can add new Destination objects and also update the existing Destination objects if needed then ask Hibernate to do the update operation. In this case Hibernate can see your earlier destination objects and also the new destination objects and based on that it will run the insert & update queries.
The code looks something like this:
// First get the customer object from database:
Customer customer = (Customer) this.sessionFactory.getCurrentSession().get(Customer.class, customerId);
// Now add your destination objects, if you want you can update the existing destination entires here.
for (int i = 0; i < destinationAddrs.length; i++) {
DestinationsEntity destination = new DestinationsEntity();
destination.setDestination(destinationAddrs[i]);
customer.getDestinationEntity().add(destination);
}
// Then do the update operation
this.sessionFactory.getCurrentSession().update(customer);

how do I locate cause of errors in BindingResult

In a spring mvc web application using hibernate in eclipse and tomcat server, I changed a couple of text fields to drop down lists in a jsp, so that a person's gender and race can each be selected from its own drop down menu. I was careful to change other levels of the application, including setting up joined tables for gender and race in the underlying database, and changing code in the model and repository levels. The application compiles, and the jsp loads with the correct selected values for the selected person in each dropdown list, but clicking the submit/update button causes a BindingResult.hasErrors() problem which does not help me localize the cause of the problem.
Can someone help me find the cause of the failure to process the update?
Here is the processUpdatePatientForm() method that is called in the controller class. Note that it triggers the System.out.println() which shows that BindingResult.hasErrors() and returns the jsp:
#RequestMapping(value = "/patients/{patientId}/edit", method = RequestMethod.PUT)
public String processUpdatePatientForm(#Valid Patient patient, BindingResult result, SessionStatus status) {
if (result.hasErrors()) {
System.out.println(":::::::::::::::: in PatientController.processUpdatePatientForm() result.hasErrors() ");
List<ObjectError> errors = result.getAllErrors();
for(int i=0;i<result.getErrorCount();i++){System.out.println("]]]]]]] error "+i+" is: "+errors.get(i).toString());}
return "patients/createOrUpdatePatientForm";}
else {
this.clinicService.savePatient(patient);
status.setComplete();
return "redirect:/patients?patientID=" + patient.getId();
}
}
When the jsp is returned, the following error messages are included:
//This is printed out in my jsp below the Sex drop down list:
Failed to convert property value of type java.lang.String to required type org.springframework.samples.knowledgemanager.model.Gender for property sex; nested exception is java.lang.IllegalStateException: Cannot convert value of type [java.lang.String] to required type [org.springframework.samples.knowledgemanager.model.Gender] for property sex: no matching editors or conversion strategy found
//This is printed out in my jsp below the Race drop down list:
Failed to convert property value of type java.lang.String to required type org.springframework.samples.knowledgemanager.model.Race for property race; nested exception is java.lang.IllegalStateException: Cannot convert value of type [java.lang.String] to required type [org.springframework.samples.knowledgemanager.model.Race] for property race: no matching editors or conversion strategy found
The following is all that is printed in the eclipse console:
Hibernate: select gender0_.id as id1_2_, gender0_.name as name2_2_ from gender gender0_ order by gender0_.name
Hibernate: select race0_.id as id1_7_, race0_.name as name2_7_ from race race0_ order by race0_.name
:::::::::::::::: in PatientController.processUpdatePatientForm() result.hasErrors()
]]]]]]] error 0 is: Field error in object 'patient' on field 'race': rejected value [Hispanic]; codes [typeMismatch.patient.race,typeMismatch.race,typeMismatch.org.springframework.samples.knowledgemanager.model.Race,typeMismatch]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [patient.race,race]; arguments []; default message [race]]; default message [Failed to convert property value of type 'java.lang.String' to required type 'org.springframework.samples.knowledgemanager.model.Race' for property 'race'; nested exception is java.lang.IllegalStateException: Cannot convert value of type [java.lang.String] to required type [org.springframework.samples.knowledgemanager.model.Race] for property 'race': no matching editors or conversion strategy found]
]]]]]]] error 1 is: Field error in object 'patient' on field 'sex': rejected value [Male]; codes [typeMismatch.patient.sex,typeMismatch.sex,typeMismatch.org.springframework.samples.knowledgemanager.model.Gender,typeMismatch]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [patient.sex,sex]; arguments []; default message [sex]]; default message [Failed to convert property value of type 'java.lang.String' to required type 'org.springframework.samples.knowledgemanager.model.Gender' for property 'sex'; nested exception is java.lang.IllegalStateException: Cannot convert value of type [java.lang.String] to required type [org.springframework.samples.knowledgemanager.model.Gender] for property 'sex': no matching editors or conversion strategy found]
Note that the values [Hispanic] and [Male] are shown in the error message as triggering the error. The problem might be that the name property of Gender and Race is being passed to Spring MVC, when the id property should be passed instead. But how do I fix this in the code?
Can someone help me get to the bottom of this? The first step would be how can I get a more useful error message which locates the location in my code where the problem is being triggered.
EDIT:
Per Sotirios's request, the following is my form in the jsp:
<form:form modelAttribute="patient" method="${method}" class="form-horizontal" id="add-patient-form">
<petclinic:inputField label="First Name" name="firstName"/>
<petclinic:inputField label="Middle Initial" name="middleInitial"/>
<petclinic:inputField label="Last Name" name="lastName"/>
<div class="control-group">
<petclinic:selectField label="Sex" name="sex" names="${genders}" size="5"/>
</div>
<petclinic:inputField label="Date of Birth" name="dateOfBirth"/>
<div class="control-group">
<petclinic:selectField label="Race" name="race" names="${races}" size="5"/>
</div>
<div class="form-actions">
<c:choose>
<c:when test="${patient['new']}">
<button type="submit">Add Patient</button>
</c:when>
<c:otherwise>
<button type="submit">Update Patient</button>
</c:otherwise>
</c:choose>
</div>
</form:form>
And the Patient.java class is:
#Entity
#Table(name = "patients")
public class Patient extends BaseEntity {
#OneToMany(cascade = CascadeType.ALL, mappedBy = "patient", fetch=FetchType.EAGER)
private Set<Document> documents;
#OneToMany(cascade = CascadeType.ALL, mappedBy = "patient", fetch=FetchType.EAGER)
private Set<Address> addresses;
#OneToMany(cascade = CascadeType.ALL, mappedBy = "patient", fetch=FetchType.EAGER)
private Set<PhoneNumber> phonenumbers;
#Column(name = "first_name")
#NotEmpty
protected String firstName;
#Column(name = "middle_initial")
protected String middleInitial;
#Column(name = "last_name")
#NotEmpty
protected String lastName;
#ManyToOne
#JoinColumn(name = "sex_id")
protected Gender sex;
#Column(name = "date_of_birth")
#Type(type = "org.jadira.usertype.dateandtime.joda.PersistentDateTime")
#DateTimeFormat(pattern = "yyyy/MM/dd")
protected DateTime dateOfBirth;
#ManyToOne
#JoinColumn(name = "race_id")
protected Race race;
////////////// Document methods
protected void setDocumentsInternal(Set<Document> documents) {this.documents = documents;}
public Set<Document> getFaxes() {
Set<Document> faxes = new HashSet<Document>();
for (Document doc : getDocumentsInternal()) {if (doc.getType().getName().equals("ScannedFaxes")) {faxes.add(doc);}}
return faxes;
}
public Set<Document> getForms() {
Set<Document> forms = new HashSet<Document>();
for (Document doc : getDocumentsInternal()) {if (doc.getType().getName().equals("ScannedPatientForms")) {forms.add(doc);}}
return forms;
}
protected Set<Document> getDocumentsInternal() {
if (this.documents == null) {this.documents = new HashSet<Document>();}
return this.documents;
}
public List<Document> getDocuments() {
List<Document> sortedDocuments = new ArrayList<Document>(getDocumentsInternal());
PropertyComparator.sort(sortedDocuments, new MutableSortDefinition("name", true, true));
return Collections.unmodifiableList(sortedDocuments);
}
public void addDocument(Document doc) {
getDocumentsInternal().add(doc);
doc.setPatient(this);
}
public Document getDocument(String name) {return getDocument(name, false);}
/** Return the Document with the given name, or null if none found for this Patient.
* #param name to test
* #return true if document name is already in use
*/
public Document getDocument(String name, boolean ignoreNew) {
name = name.toLowerCase();
for (Document doc : getDocumentsInternal()) {
if (!ignoreNew || !doc.isNew()) {
String compName = doc.getName();
compName = compName.toLowerCase();
if (compName.equals(name)) {
return doc;
}
}
}
return null;
}
//////////// Address methods
protected void setAddressesInternal(Set<Address> addresses) {this.addresses = addresses;}
protected Set<Address> getAddressesInternal() {
if (this.addresses == null) {this.addresses = new HashSet<Address>();}
return this.addresses;
}
public List<Address> getAddresses() {
List<Address> sortedAddresses = new ArrayList<Address>(getAddressesInternal());
PropertyComparator.sort(sortedAddresses, new MutableSortDefinition("address", true, true));
return Collections.unmodifiableList(sortedAddresses);
}
public void addAddress(Address addr) {
getAddressesInternal().add(addr);
addr.setPatient(this);
}
public Address getAddress(String address) {return getAddress(address, false);}
/** Return the Address with the given name, or null if none found for this Patient.
* #param name to test
* #return true if document name is already in use
*/
public Address getAddress(String addr, boolean ignoreNew) {
addr = addr.toLowerCase();
for (Address address1 : getAddressesInternal()) {
if (!ignoreNew || !address1.isNew()) {
String compName = address1.getAddress();
compName = compName.toLowerCase();
if (compName.equals(addr)) {
return address1;
}
}
}
return null;
}
//////////// PhoneNumber methods
protected void setPhoneNumbersInternal(Set<PhoneNumber> phonenumbers) {this.phonenumbers = phonenumbers;}
protected Set<PhoneNumber> getPhoneNumbersInternal() {
if (this.phonenumbers == null) {this.phonenumbers = new HashSet<PhoneNumber>();}
return this.phonenumbers;
}
public List<PhoneNumber> getPhoneNumbers() {
List<PhoneNumber> sortedPhoneNumbers = new ArrayList<PhoneNumber>(getPhoneNumbersInternal());
PropertyComparator.sort(sortedPhoneNumbers, new MutableSortDefinition("phonenumber", true, true));
return Collections.unmodifiableList(sortedPhoneNumbers);
}
public void addPhoneNumber(PhoneNumber pn) {
getPhoneNumbersInternal().add(pn);
pn.setPatient(this);
}
public PhoneNumber getPhoneNumber(String pn) {return getPhoneNumber(pn, false);}
/** Return the PhoneNumber with the given name, or null if none found for this Patient.
* #param name to test
* #return true if phone number is already in use
*/
public PhoneNumber getPhoneNumber(String pn, boolean ignoreNew) {
pn = pn.toLowerCase();
for (PhoneNumber number : getPhoneNumbersInternal()) {
if (!ignoreNew || !number.isNew()) {
String compName = number.getPhonenumber();
compName = compName.toLowerCase();
if (compName.equals(pn)) {
return number;
}
}
}
return null;
}
public String getFirstName(){return this.firstName;}
public void setFirstName(String firstName){this.firstName = firstName;}
public String getMiddleInitial() {return this.middleInitial;}
public void setMiddleInitial(String middleinitial) {this.middleInitial = middleinitial;}
public String getLastName() {return this.lastName;}
public void setLastName(String lastName) {this.lastName = lastName;}
public Gender getSex() {return this.sex;}
public void setSex(Gender sex) {this.sex = sex;}
public void setDateOfBirth(DateTime birthDate){this.dateOfBirth = birthDate;}
public DateTime getDateOfBirth(){return this.dateOfBirth;}
public Race getRace() {return this.race;}
public void setRace(Race race) {this.race = race;}
#Override
public String toString() {
return new ToStringCreator(this)
.append("id", this.getId())
.append("new", this.isNew())
.append("lastName", this.getLastName())
.append("firstName", this.getFirstName())
.append("middleinitial", this.getMiddleInitial())
.append("dateofbirth", this.dateOfBirth)
.toString();
}
}
SECOND EDIT:
Per Alexey's comment, the following is the method in the controller class which has always had the #InitBinder annotation. It is identical to a method in the controller of a similar module which works:
#InitBinder
public void setAllowedFields(WebDataBinder dataBinder) {dataBinder.setDisallowedFields("id");}
THIRD EDIT:
PatientController.java:
#Controller
#SessionAttributes(types = Patient.class)
public class PatientController {
private final ClinicService clinicService;
#Autowired
public PatientController(ClinicService clinicService) {this.clinicService = clinicService;}
#ModelAttribute("genders")
public Collection<Gender> populateGenders() {return this.clinicService.findGenders();}
#ModelAttribute("races")
public Collection<Race> populateRaces() {return this.clinicService.findRaces();}
#InitBinder
public void setAllowedFields(WebDataBinder dataBinder) {dataBinder.setDisallowedFields("id");}
#RequestMapping(value = "/patients/new", method = RequestMethod.GET)
public String initCreationForm(Map<String, Object> model) {
Patient patient = new Patient();
model.put("patient", patient);
return "patients/createOrUpdatePatientForm";
}
#RequestMapping(value = "/patients/new", method = RequestMethod.POST)
public String processCreationForm(#Valid Patient patient, BindingResult result, SessionStatus status) {
if (result.hasErrors()) {return "patients/createOrUpdatePatientForm";}
else {
this.clinicService.savePatient(patient);
status.setComplete();
return "redirect:/patients?patientID=" + patient.getId();
}
}
#RequestMapping(value = "/patients", method = RequestMethod.GET)
public String processFindForm(#RequestParam("patientID") String patientId, Patient patient, BindingResult result, Map<String, Object> model) {
Collection<Patient> results = this.clinicService.findPatientByLastName("");
model.put("selections", results);
int patntId = Integer.parseInt(patientId);
Patient sel_patient = this.clinicService.findPatientById(patntId);//I added this
model.put("sel_patient",sel_patient);
return "patients/patientsList";
}
#RequestMapping(value = "/patients/{patientId}/edit", method = RequestMethod.GET)
public String initUpdatePatientForm(#PathVariable("patientId") int patientId, Model model) {
Patient patient = this.clinicService.findPatientById(patientId);
model.addAttribute(patient);
return "patients/createOrUpdatePatientForm";
}
#RequestMapping(value = "/patients/{patientId}/edit", method = RequestMethod.PUT)
public String processUpdatePatientForm(#Valid Patient patient, BindingResult result, SessionStatus status) {
if (result.hasErrors()) {
System.out.println(":::::::::::::::: in PatientController.processUpdatePatientForm() result.hasErrors() ");
List<ObjectError> errors = result.getAllErrors();
for(int i=0;i<result.getErrorCount();i++){System.out.println("]]]]]]] error "+i+" is: "+errors.get(i).toString());}
return "patients/createOrUpdatePatientForm";}
else {
this.clinicService.savePatient(patient);
status.setComplete();
return "redirect:/patients?patientID=" + patient.getId();
}
}
}
FOURTH EDIT:
Gender.java
#Entity
#Table(name = "gender")
public class Gender extends NamedEntity {}
NamedEntity.java:
#MappedSuperclass
public class NamedEntity extends BaseEntity {
#Column(name = "name")
private String name;
public void setName(String name) {this.name = name;}
public String getName() {return this.name;}
#Override
public String toString() {return this.getName();}
}
BaseEntity.java:
#MappedSuperclass
public class BaseEntity {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
protected Integer id;
public void setId(Integer id) {this.id = id;}
public Integer getId() {return id;}
public boolean isNew() {return (this.id == null);}
}
You need to add a converter or a proper editor. I prefer the first one. Refer to section 6.5. on this page for the details.
Your converter would have to get the Entity with the given name from the database and return it. The code would be something like this:
class StringToGender implements Converter<String, Gender> {
#Autowired
private GenderRepository repository;
public Gender convert(String name) {
return repository.getGenderByName(name);
}
}
And in your application context xml (if you use xml):
<bean id="conversionService" class="org.springframework.format.support.FormattingConversionServiceFactoryBean">
<property name="converters">
<set>
<bean class="org.example.StringToGender"/>
</set>
</property>

Spring + Hibernate: Many-to-Many relationships and web forms

I have been wrestling with how to implement a form that creates many-to-many relations in a web application I am building with Spring 3 and Hibernate 4. I am trying to build a simple blog tool with a tagging system. I have created a model BlogPost that has a many-to-many relationship with the model Tags. When I create a new BlogPost object, the web form input for tags is a single-lined text input. I'd like to be able to split this text string by whitespace and use it to create Tag objects. Alternatively, when editing an existing BlogPost, I'd like to be able to take the Set of Tag objects associated with the BlogPost and convert it to a String that is used as the value of the input element. My problem is in converting between the text input and the referenced set of Tag objects using my form.
What is the best practice for binding/fetching/updating many-to-many relationships with web forms? Is there an easy way to do this that I am unaware of?
UPDATE
I decided, as suggested in the answer below, to manually handle the object conversion between the String tag values in the form and the Set<Tag> object required for the object model. Here is the final working code:
editBlogPost.jsp
...
<div class="form-group">
<label class="control-label col-lg-2" for="tagInput">Tags</label>
<div class="col-lg-7">
<input id="tagInput" name="tagString" type="text" class="form-control" maxlength="100" value="${tagString}" />
</div>
<form:errors path="tags" cssClass="help-inline spring-form-error" element="span" />
</div>
....
BlogController.java
#Controller
#SessionAttributes("blogPost")
public class BlogController {
#Autowired
private BlogService blogService;
#Autowired
private TagService tagService;
#ModelAttribute("blogPost")
public BlogPost getBlogPost(){
return new BlogPost();
}
//List Blog Posts
#RequestMapping(value="/admin/blog", method=RequestMethod.GET)
public String blogAdmin(ModelMap map, SessionStatus status){
status.setComplete();
List<BlogPost> postList = blogService.getAllBlogPosts();
map.addAttribute("postList", postList);
return "admin/blogPostList";
}
//Add new blog post
#RequestMapping(value="/admin/blog/new", method=RequestMethod.GET)
public String newPost(ModelMap map){
BlogPost blogPost = new BlogPost();
map.addAttribute("blogPost", blogPost);
return "admin/editBlogPost";
}
//Save new post
#RequestMapping(value="/admin/blog/new", method=RequestMethod.POST)
public String addPost(#Valid #ModelAttribute BlogPost blogPost,
BindingResult result,
#RequestParam("tagString") String tagString,
Model model,
SessionStatus status)
{
if (result.hasErrors()){
return "admin/editBlogPost";
}
else {
Set<Tag> tagSet = new HashSet();
for (String tag: tagString.split(" ")){
if (tag.equals("") || tag == null){
//pass
}
else {
//Check to see if the tag exists
Tag tagObj = tagService.getTagByName(tag);
//If not, add it
if (tagObj == null){
tagObj = new Tag();
tagObj.setTagName(tag);
tagService.saveTag(tagObj);
}
tagSet.add(tagObj);
}
}
blogPost.setPostDate(Calendar.getInstance());
blogPost.setTags(tagSet);
blogService.saveBlogPost(blogPost);
status.setComplete();
return "redirect:/admin/blog";
}
}
//Edit existing blog post
#Transactional
#RequestMapping(value="/admin/blog/{id}", method=RequestMethod.GET)
public String editPost(ModelMap map, #PathVariable("id") Integer postId){
BlogPost blogPost = blogService.getBlogPostById(postId);
map.addAttribute("blogPost", blogPost);
Hibernate.initialize(blogPost.getTags());
Set<Tag> tags = blogPost.getTags();
String tagString = "";
for (Tag tag: tags){
tagString = tagString + " " + tag.getTagName();
}
tagString = tagString.trim();
map.addAttribute("tagString", tagString);
return "admin/editBlogPost";
}
//Update post
#RequestMapping(value="/admin/blog/{id}", method=RequestMethod.POST)
public String savePostChanges(#Valid #ModelAttribute BlogPost blogPost, BindingResult result, #RequestParam("tagString") String tagString, Model model, SessionStatus status){
if (result.hasErrors()){
return "admin/editBlogPost";
}
else {
Set<Tag> tagSet = new HashSet();
for (String tag: tagString.split(" ")){
if (tag.equals("") || tag == null){
//pass
}
else {
//Check to see if the tag exists
Tag tagObj = tagService.getTagByName(tag);
//If not, add it
if (tagObj == null){
tagObj = new Tag();
tagObj.setTagName(tag);
tagService.saveTag(tagObj);
}
tagSet.add(tagObj);
}
}
blogPost.setTags(tagSet);
blogPost.setPostDate(Calendar.getInstance());
blogService.updateBlogPost(blogPost);
status.setComplete();
return "redirect:/admin/blog";
}
}
//Delete blog post
#RequestMapping(value="/admin/delete/blog/{id}", method=RequestMethod.POST)
public #ResponseBody String deleteBlogPost(#PathVariable("id") Integer id, SessionStatus status){
blogService.deleteBlogPost(id);
status.setComplete();
return "The item was deleted succesfully";
}
#RequestMapping(value="/admin/blog/cancel", method=RequestMethod.GET)
public String cancelBlogEdit(SessionStatus status){
status.setComplete();
return "redirect:/admin/blog";
}
}
BlogPost.java
#Entity
#Table(name="BLOG_POST")
public class BlogPost implements Serializable {
#Id
#GeneratedValue(strategy=GenerationType.AUTO)
#Column(name="POST_ID")
private Integer postId;
#NotNull
#NotEmpty
#Size(min=1, max=200)
#Column(name="TITLE")
private String title;
...
#ManyToMany(fetch = FetchType.LAZY, cascade = {CascadeType.ALL})
#JoinTable(name="BLOG_POST_TAGS",
joinColumns={#JoinColumn(name="POST_ID")},
inverseJoinColumns={#JoinColumn(name="TAG_ID")})
private Set<Tag> tags = new HashSet<Tag>();
...
public Set<Tag> getTags() {
return tags;
}
public void setTags(Set<Tag> tags) {
this.tags = tags;
}
}
Tag.java
#Entity
#Table(name="TAG")
public class Tag implements Serializable {
#Id
#GeneratedValue(strategy=GenerationType.AUTO)
#Column(name="TAG_ID")
private Integer tagId;
#NotNull
#NotEmpty
#Size(min=1, max=20)
#Column(name="TAG_NAME")
private String tagName;
#ManyToMany(fetch = FetchType.LAZY, mappedBy="tags")
private Set<BlogPost> blogPosts = new HashSet<BlogPost>();
public Integer getTagId() {
return tagId;
}
public void setTagId(Integer tagId) {
this.tagId = tagId;
}
public String getTagName() {
return tagName;
}
public void setTagName(String tag) {
this.tagName = tag;
}
public Set<BlogPost> getBlogPosts() {
return blogPosts;
}
public void setBlogPosts(Set<BlogPost> blogPosts) {
this.blogPosts = blogPosts;
}
}
If you choose to encode your Tags in a String as the transfer data model between client and server you might make your life a little harder if you want to improve your UX later on.
I would consider having Set<Tag> as its own model element and I would do the transformation directly in the front-end using JavaScript on a JSON model.
Since I would like to have auto completion for my tagging, I would pass all existing Tags as part of the /admin/blog/new model with the ability to mark which tags belong to the blog post (e.g. as a Map<Tag, Boolean> or two Sets) - most likely with a JSON mapping. I would modify this model using JavaScript in the frontend (perhaps utilizing some jquery plugins that provides some nice autocomplete features) and rely on default JSON Mapping (Jackson) for the back conversion.
So my model would have at least two elements: the blog post and all the tags (some who are marked as "assigned to this BlogPost". I would use a TagService to ensure existence of all relevant tags, query them with where name in (<all assigned tag names>) and set my BlogPost.setTags(assignedTags).
In addition I would want to have some cleanup function to remove unused Tags from the DB. If I would want to make it easier for the server, I would have another model element with the removed removed tags (so I can check whether this was the last BlogPost that used this Tag).
This should work in your form:
<div class="form-check">
<input class="form-check-input" type="checkbox" value="1"
name="categories"> <label class="form-check-label"
for="categories"> Cat 1 </label>
<input class="form-check-input"
type="checkbox" value="2" name="categories"> <label
class="form-check-label" for="categories"> Cat 2 </label>
</div>

Categories