How to receive complex objects in spring 3.2 mvc?
In the simple example below, I have two model classes, with a many to one relationship. When adding a new Employee object, I would like to use a html select to select it's department.
When I post to add a new Employee, I get the following error:
Failed to convert property value of type java.lang.String to required type hu.pikk.model.Department for property department; nested exception is java.lang.IllegalStateException: Cannot convert value of type [java.lang.String] to required type [hu.pikk.model.Department] for property department: no matching editors or conversion strategy found
How should I implement the editor or conversion strategy? Are there best practices, or pitfalls, one should watch for?
I've read the spring mvc documentation, and some articles and stackoverflow questions, but to be honest, I find them a little bit confusing and many times too short,too off-handed.
Models:
#Entity
public class Employee {
#Id
#GeneratedValue
private int employeeId;
#NotEmpty
private String name;
#ManyToOne
#JoinColumn(name="department_id")
private Department department;
//getters,setters
}
#Entity
public class Department {
#Id
#GeneratedValue
private int departmentId;
#Column
private String departmentName;
#OneToMany
#JoinColumn(name = "department_id")
private List<Employee> employees;
//getters,setters
}
In my controller class:
#RequestMapping(value = "/add", method = RequestMethod.GET)
private String addNew(ModelMap model) {
Employee newEmployee = new Employee();
model.addAttribute("employee", newEmployee);
model.addAttribute("departments", departmentDao.getAllDepartments());
return "employee/add";
}
#RequestMapping(value = "/add", method = RequestMethod.POST)
private String addNewHandle(#Valid Employee employee, BindingResult bindingResult, ModelMap model, RedirectAttributes redirectAttributes) {
if (bindingResult.hasErrors()) {
model.addAttribute("departments", departmentDao.getAllDepartments());
return "employee/add";
}
employeeDao.persist(employee);
redirectAttributes.addFlashAttribute("added_employee", employee.getName());
redirectAttributes.addFlashAttribute("message", "employee added...");
return "redirect:list";
}
In add.jsp:
<f:form commandName="employee" action="${pageContext.request.contextPath}/employee/add" method="POST">
<table>
<tr>
<td><f:label path="name">Name:</f:label></td>
<td><f:input path="name" /></td>
<td><f:errors path="name" class="error" /></td>
</tr>
<tr>
<td><f:label path="department">Department:</f:label></td>
<td><f:select path="department">
<f:option value="${null}" label="NO DEPARTMENT" />
<f:options items="${departments}" itemLabel="departmentName" itemValue="departmentId" />
</f:select></td>
<td><f:errors path="department" class="error" /></td>
</tr>
</table>
<input type="submit" value="Add Employee">
</f:form>
The problem is, when the controller receives the POST, it doesn't know how to convert the id string to a Department object.
I've found that there are basically three ways to solve this problem:
Not using spring form jstl, but a simple html select with a custom name, and in the Controller reading it with #RequestParam, accessing the database, and populating it.
Implementing the Converter interface, and registering it as a bean.
Implementing the PropertyEditor interface. Doing this is easier by extending the PropertyEditorSupport class.
I was going with the third option. (Later when I will have some time, I will edit this answer with the first two options explored.)
2. Implementing the Converter<String, Department> interface
#Component
public class DepartmentConverter implements Converter<String,Department>{
#Autowired
private DepartmentDao departmentDao;
#Override
public Department convert(String id){
Department department = null;
try {
Integer id = Integer.parseInt(text);
department = departmentDao.getById(id);
System.out.println("Department name:" + department.getDepartmentName());
} catch (NumberFormatException ex) {
System.out.println("Department will be null");
}
return department;
}
}
In the spring beans configuration file:
<mvc:annotation-driven conversion-service="conversionService"/>
<bean id="conversionService"
class="org.springframework.context.support.ConversionServiceFactoryBean">
<property name="converters">
<list>
<bean class="package.DepartmentConverter"/>
</list>
</property>
</bean>
3. Extending the PropertyEditorSupport class
public class SimpleDepartmentEditor extends PropertyEditorSupport {
private DepartmentDao departmentDao;
public SimpleDepartmentEditor(DepartmentDao departmentDao){
this.departmentDao = departmentDao;
}
#Override
public void setAsText(String text) throws IllegalArgumentException {
Department department = null;
try {
Integer id = Integer.parseInt(text);
department = departmentDao.getById(id);
System.out.println("Department name:" + department.getDepartmentName());
} catch (NumberFormatException ex) {
System.out.println("Department will be null");
}
setValue(department);
}
}
Inside the Controller, I needed to add an #InitBinder:
#InitBinder
protected void initBinder(HttpServletRequest request, ServletRequestDataBinder binder) {
binder.registerCustomEditor(Department.class, new SimpleDepartmentEditor(departmentDao));
}
For my opinion you've encounterd next situation here. When Spring trying to deserialize Employee entity which was received in addNewHandle it found property department of type of String but in target entity it has Department type, then, because you have no conversion regsieterd for such kind of conversion, it fails. So, to solve this issues you can implement own converter (Converter) that gets String and returns Department and regsiter it in conversionService or you can implement own JSON deserializing strategy by overriding Jackson JsonDeserializer and annotating department property with #JsonDeserialize(using=<your Jacson custom deserializer>.class). Hope this helps.
Related
I'm trying to add data to my database and reload the same page using spring boot and thymeleaf but when I save data I face this error
org.springframework.beans.TypeMismatchException: Failed to convert value of type 'java.lang.String' to required type 'org.closure.gcp.entities.QuestionEntity'; nested exception
is org.springframework.core.convert.ConversionFailedException: Failed to convert from type [java.lang.String] to type [java.lang.Integer] for value 'adsf'; nested exception is java.lang.NumberFormatException: For input string: "adsf"
controller code :
#Controller
#RequestMapping(path = "/Questions")
public class QuestionView {
#Autowired
QuestionRepo questionRepo;
#RequestMapping(path = "/")
public String index(#ModelAttribute("question") QuestionEntity question, Model model)
{
List<QuestionEntity> list = questionRepo.findAll();
model.addAttribute("questions", list);
return "Questions";
}
#RequestMapping(value="/add", method=RequestMethod.POST)
public String addQuestion(Model model,#ModelAttribute("question") QuestionEntity question) {
questionRepo.save((QuestionEntity)model.getAttribute("question"));
List<QuestionEntity> list = questionRepo.findAll();
model.addAttribute("questions", list);
return "Questions";
}
}
thymeleaf page :
<html>
<header>
<title>Questions</title>
</header>
<body>
<h2>hello questions</h2>
<hr>
<tr th:each="q: ${questions}">
<td th:text="${q.question}"></td>
<br>
<td th:text="${q.question_type}"></td>
<hr>
</tr>
<!-- <form th:action="#{/add}" th:object="${question}" method="post"> -->
<form action="./add" th:object="${question}" method="POST">
<input type="text" th:field="*{question}" />
<br >
<input type="text" th:field="*{question_type}" />
<br >
<input type="submit" value="save" >
</form>
</body>
</html>
#Entity
#Table(name="question")
public class QuestionEntity {
#Id
#GeneratedValue(strategy = GenerationType.TABLE)
private Integer id;
#Column(nullable=false)
private String question;
#Column(nullable=false)
private String question_type;
#ManyToOne(optional = true)
private InterestEntity interest;
#ManyToOne(optional = true)
private LevelEntity level;
#Column(nullable = true)
private String sup_file;
#Column(nullable = false)
private int pionts;
#ManyToMany
private List<ContestEntity> contest;
#OneToMany(mappedBy ="question")
private List<AnswerEntity> answers;
// getters and setters
}
notice when I try to open another page in "/add" it works
I found this to solve
I just made a model class and use it instead of entity
and I used just one method to handle index and add requests
#Controller
#RequestMapping(path = "/Questions")
public class QuestionView {
#Autowired
QuestionRepo questionRepo;
#RequestMapping(path = {"/",""},method = {RequestMethod.POST,RequestMethod.GET})
public String index(#ModelAttribute("question") QuestionModel question, Model model,HttpServletRequest request)
{
if(request.getMethod().equals("POST"))
{
questionRepo.save(new QuestionEntity().question(question.getQuestion()).question_type(question.getQuestion_type()));
}
List<QuestionEntity> list = questionRepo.findAll();
model.addAttribute("questions", list);
return "Questions";
}
}
It is better to have 2 separate methods, one for GET and one for POST and to use redirect after the POST (see https://en.wikipedia.org/wiki/Post/Redirect/Get). This is how I would code this based on your separate QuestionModel class:
#Controller
#RequestMapping(path = "/Questions")
public class QuestionView {
#Autowired
QuestionRepo questionRepo;
#GetMapping
public String index(Model model)
{
List<QuestionEntity> list = questionRepo.findAll();
model.addAttribute("questions", list);
model.addAttribute("question", new QuestionModel());
return "Questions";
}
#PostMapping("/add")
public String addQuestion(#Valid #ModelAttribute("question") QuestionModel question, BindingResult bindingResult, Model model) {
if(bindingResult.hasErrors()) {
return "Questions";
}
questionRepo.save(new QuestionEntity().question(question.getQuestion()).question_type(question.getQuestion_type()));
return "redirect:/Questions";
}
}
Main points:
Use separate methods for GET and POST
Add the #Valid annotation to the #ModelAttribute in the POST method so any validation annotations on QuestionModel are checked (Because you probably want to make sure the question has at least some text in it for example).
Use BindingResult as parameter to check if there are validation errors.
Use "redirect:" to force a new GET after the POST to help avoid double submissions if a user would refresh the browser.
please help me.
I'm working validate form. The fields of class student as "name","address","email" display message when i click submit form but my problem is the fields that class student contains relationship as class major is not display message. I tried to put #Valid annotations and i get the same a result.
I get an errors: Failed to convert property value of type java.lang.String to required type com.springmvc.entities.Major for property major; nested exception is java.lang.IllegalStateException: Cannot convert value of type java.lang.String to required type com.springmvc.entities.Major for property major: no matching editors or conversion strategy found.
Can someone help me or give me solutions ? I'm so grateful !
I sincerely apologize if my English is not good
#Entity(name="student")
public class Student{
#Id
#GeneratedValue(strategy=GenerationType.IDENTITY)
int id;
#NotNull(message="Your name must not null !")
String name;
#NotNull(message="Your address must not null !")
String address;
#NotNull(message="Your email must not null !")
String email;
#NotNull(message="Select a major !")
#Valid
#OneToOne(fetch=FetchType.EAGER)
#JoinColumn(name="idMajor")
private Major major;
//getter - setter ...
}
#Entity(name="major")
public Class Major{
#Id
#GeneratedValue(strategy=GenerationType.IDENTITY)
int idMajor;
String major;
//getter - setter ...
}
//My controller
#Controller
public class StudentController{
#Autowired
StudentService studentService;
#InitBinder
public void InitBinder(WebDataBinder binder){
StringTrimmerEditor stringTrimmerEditor = new StringTrimmerEditor(true);
binder.registerCustomEditor(String.class, stringTrimmerEditor);
}
#ModelAttribute("studentForm")
public Student studentForm()
{
return new Student();
}
#RequestMapping(value="/saveStudent",method= RequestMethod.POST)
public String SaveStudent(#Valid #ModelAttribute("studentForm") Student
student,BindingResult bindingResult, ModelMap model) {
if (bindingResult.hasErrors())
{
return "page-student";
}
else {
model.addAttribute("msg", "Save success!");
studentService.SaveStudent(student);
return "page-student;
}
}
}
//My View (page-student.jsp)
<form:form action="saveStudent" enctype="multipart/form-data" method="post"
modelAttribute="studentForm" >
<p>Name:<form:input path="name"/></p>
<form:errors path="name" cssClass="error" /> // Validate ok !
<p>Address:<form:input path="address"/></p>
<form:errors path="address" cssClass="error" /> // Validate ok !
<p>Email:<form:input path="email"/></p>
<form:errors path="email" cssClass="error" /> // Validate ok !
<p>Major:<form:select path="major">
<form:option value="0">-- Select --</form:option>
<c:forEach var="major" items="${major}">
<form:option value="${major.getIdMajor()}">
${major.getMajor()}
</form:option>
</c:forEach>
</form:select></p>
<form:errors path="major" cssClass="error" />
// I get an errors as I mentioned in my description above.
<form:button type="submit" >Submit</form:button>
</form:form>
I use Spring, jsp and Hibernate in my project. I have two entities Employee and Department. Dapartment is a part of Employee and they have relationship one-to-many
#Entity
#Table(name = "employee")
public class Employee {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
#OneToOne
#JoinColumn(name = "dep_id")
private Department department;
I create controller, DAO and jsp pages for view.
My problem: I want to update data of Employee in my jsp page. Before this I add Employee and list of departments in model
In controller:
model.addAttribute("employee", employeeDao.find(id));
model.addAttribute("departments", departmentDao.list());
In JSP:
<form method="post">
<select value="${employee.department}">
<c:forEach items="${departments}" var ="dep">
<option value="${dep}">${dep.name}</option>
</c:forEach>
</form>
In controller (post request)
#RequestMapping(value = "/{id}", method = RequestMethod.POST)
public String updateEmployee(#PathVariable("id") long id, Employee employee) {
employee.setId(id);
employeeDao.update(employee);
return "redirect:/employees";
}
but value employee.department=null Why?
Of course, on jsp page in "select" tag I can create variable dep I mean:
<select name ="dep">
<option value="${dep.id}">${dep.name}</option>
</select>
and then in controller using id of department i will be able to get department from database and update Employee. Is it right way?
You have not posted your full Controller however if we assume the initial get request looks like:
#RequestMapping(value = "/{id}", method = RequestMethod.GET)
public ModelAndView loadForEdit(#PathVariable("id") long id)
model.addAttribute("employee", employeeDao.find(id));
model.addAttribute("departments", departmentDao.list());
return new ModelAndView(...);
}
Then on loadForEdit() we load an Employee and Departments for edit and set them in the model for rendering the edit page.
Now, on submit, the POST method updateEmployee(...) knows nothing about this previous Employee and Department and therefore the framework is simply passing in a new instance to updateEmployee(...).
If you refactor along the lines of the following, then on the call to both the GET and POST handlers, the method annotated with #ModelAttribute(value = "employee") will execute. In the first case it will be added to the model, as previously, and in the second case the Employee will be retrieved, the fields bound to the updated values amd will then be passed to your POST handler.
#RequestMapping(value = "/{id}", method = RequestMethod.Get)
public String loadForEdit(){
return "nextView";
}
#RequestMapping(method = RequestMethod.POST)
public String updateEmployee(value = "/{id}", #ModelAttribute("employee") Employee employee)
{
return "redirect:/employees";
}
#ModelAttribute(value = "employee")
public Employee getEmployee(#PathVariable("id") long id)
{
return employeeDao.find(id);
}
#ModelAttribute(value = "departments")
public List<Department> getDepartments()
{
return departmentDao.list());
}
There is a lot of flexibility on how you can handle this:
See 'Using #ModelAttribute on a method argument' in:
http://docs.spring.io/spring/docs/current/spring-framework-reference/html/mvc.html
#RequestMapping(value="/owners/{ownerId}/pets/{petId}/edit", method =
> RequestMethod.POST) public String processSubmit(#ModelAttribute Pet
> pet) { }
Given the above example where can the Pet instance come from?
There are several options:
It may already be in the model due to use of #SessionAttributes — see
the section called “Using #SessionAttributes to store model attributes
in the HTTP session between requests”.
It may already be in the model
due to an #ModelAttribute method in the same controller — as explained
in the previous section. [My suggestion]
It may be retrieved based on a URI template
variable and type converter (explained in more detail below).
It may be instantiated using its default constructor. [current position]
Sory, I forgot to publish the decision to solve. Thanks to all who helped me to find right solution.
In controller:
#RequestMapping(value = "/{id}", method = RequestMethod.GET)
public String getEmployee(#PathVariable("id") long id, Model model) {
model.addAttribute("employee", employeeDao.find(id));
model.addAttribute("departments", departmentDao.list());
return "employees/view";
}
Then we need to edit view. On JSP-page:
<%# taglib prefix="sf" uri="http://www.springframework.org/tags/form"%>
<sf:form modelAttribute="employee" method="post">
<sf:select path="department" id="select-departments" >
<c:forEach items="${departments}" var="dep" varStatus="status">
<option value="${dep.id}">${dep.name}</option>
</c:forEach>
</sf:select>
</sf:form>
We also need to create department's editor:
public class DepartmentEditor extends PropertyEditorSupport {
private DepartmentDao departmentDao;
public DepartmentEditor(DepartmentDao departmentDao) {
this.departmentDao = departmentDao;
}
#Override
public void setAsText(String text) throws IllegalArgumentException {
long id = Long.parseLong(text);
Department department = departmentDao.find(id);
setValue(department);
}
}
And at the and we need to add some code in controller:
#InitBinder
protected void initBinder(WebDataBinder binder) {
binder.registerCustomEditor(Department.class, new DepartmentEditor(departmentDao));
}
#RequestMapping(value = "/{id}", method = RequestMethod.POST)
public String updateEmployee(#PathVariable("id") long id, #ModelAttribute Employee employee) {
employee.setId(id);
employeeDao.update(employee);
return "redirect:/employees";
}
First of all: I'm a beginner in Spring and this is my first try to implement an web application with Spring MVC.
Here is what I've done yet:
Entities:
#Entity
#Table(name = "coins")
public class Coin
{
#Id
#GeneratedValue
private Integer id;
#OneToOne
private Country country;
private double value;
private int year;
}
#Entity
#Table(name = "countries")
public class Country
{
#Id
#GeneratedValue
private Integer id;
private String name;
}
Controller:
#Controller
public class CoinViewController {
#Autowired
private CoinService service;
#Autowired
private CountryService countryService;
#ModelAttribute("countries")
public List<Country> frequencies() {
return countryService.get();
}
#RequestMapping(value = "/coins/add", method = RequestMethod.GET)
public String addCoin(Model model) {
model.addAttribute("coin", new Coin());
return "coins/add";
}
#RequestMapping(value = "/coins/add", method = RequestMethod.POST)
public String addCoinResult(#ModelAttribute("coin") Coin coin, BindingResult result) {
// TODO: POST HANDLING
return "/coins/add";
}
}
JSP:
<form:form action="add" method="POST" modelAttribute="coin">
<div class="form-group">
<label for="country">Country:</label>
<form:select path="country" class="form-control" >
<form:option value="" label="-- Choose one--" />
<form:options items="${countries}" itemValue="id" itemLabel="name" />
</form:select>
</div>
<div class="form-group">
<label for="value">Value:</label>
<form:input path="value" class="form-control" />
</div>
<div class="form-group">
<label for="year">Year:</label>
<form:input path="year" class="form-control" />
</div>
<button type="submit" value="submit" class="btn btn-default">Erstellen</button>
</form:form>
But when I try to save the input from the JSP I always get this:
Field error in object 'coin' on field 'country': rejected value [1];
codes
[typeMismatch.coin.country,typeMismatch.country,typeMismatch.Country,typeMismatch];
arguments
[org.springframework.context.support.DefaultMessageSourceResolvable:
codes [coin.country,country]; arguments []; default message
[country]]; default message [Failed to convert property value of type
'java.lang.String' to required type 'Country' for property 'country';
nested exception is java.lang.IllegalStateException: Cannot convert
value of type [java.lang.String] to required type [Country] for
property 'country': no matching editors or conversion strategy found]
So my questions are:
What should I use Editor / Converter?
How do I register one of them in my Controller?
You can register a custom editor into initBinder of your controller class:
#Controller
public class CoinViewController {
#Autowired
private CountryEditor countryEditor;
#InitBinder
protected void initBinder(final WebDataBinder binder, final Locale locale) {
binder.registerCustomEditor(Country.class, countryEditor);
}
......
}
(locale parameter is not needed in this case, but it can be useful if you need locale to make conversion - for example if you are working with dates)
and you can define your CountryEditor like the following:
#Component
public class CountryEditor extends PropertyEditorSupport {
#Autowired
private CountryService countryService;
#Override
public void setAsText(final String text) throws IllegalArgumentException {
try{
final Country country = countryService.findById(Long.parseLong(text));
setValue(cliente);
}catch(Exception e){
setValue(country);
// or handle your exception
}
}
}
I let spring handle injection of my editors with #Component annotation. So if you like to do in that way remember to enable package scan for that class!
Hope this help!
I am having difficulties binding the spring form value to a backing object.
The following are the related parts of the code.
This is from page.jsp
<form:form method="post" commandName="building" action="addBuilding">
<div>
<div>
<form:label path="buildingName">Building Name:</form:label>
<form:input path="buildingName" />
<form:errors path="buildingName"></form:errors>
</div>
<div>
<form:label path="buildingType">Building Type:</form:label>
<form:select path="buildingType">
<form:option value="none">--Select One--</form:option>
<form:options items="${buildingTypeList}" itemValue="id" itemLabel="typeName"/>
</form:select>
<form:errors path="buildingType"></form:errors>
</div>
</div>
</form:form>
Model classes I want to bind are as the following. I add these for the sake of completeness
#Entity
#Table(name="tablename")
class Building {
#Column
private buildingName;
#ManyToOne(fetch = FetchType.EAGER)
#JoinColumn(name = "buildingType", referencedColumnName = "id", nullable = false)
private BuildingType buildingType;
//other fields, getters and setters etc.
}
#Entity
#Table(name="tablename")
class BuildingType {
#Id
#Column
private int id;
#Column
private String typeName;
//getters, setters
}
At this point I can see the building type name in the combo-box just fine (in a GET request). The problem happens when I post the form. Itemvalue from combo-box is int and I want to bind it to the buildingType field in the Building model. The code will explain it better I guess. Related controller functions:
#RequestMapping(value = "addBuilding", method = RequestMethod.GET)
public String addBuildingPage(Model model) {
Building building = new Building();
model.addAttribute("building", building);
List<BuildingType> buildingTypeList = buildingTypeDao.findAll();
model.addAttribute("buildingTypeList", buildingTypeList);
return "addBuilding";
}
#RequestMapping(value = "addBuilding", method = RequestMethod.POST)
public String submitNewBuilding(#ModelAttribute(value = "building") #Valid Building building,
BindingResult result, Model model) {
if (result.hasErrors()) {
return "addBuilding";
}
model.addAttribute("building", building);
return "addBuilding";
}
I get a cannot cast int to BuildingType exception, after some search I followed the blog post written here. So I decided to write a custom Formatter and use ConversionService.
This is the formatter class
#Component
public class BuildingTypeFormatter implements Formatter<BuildingType> {
#Autowired
private BuildingTypeDao buildingTypeDao;
#Override
public String print(BuildingType buildingType, Locale arg1) {
return buildingType.getName();
}
#Override
public BuildingTypeDBO parse(String id, Locale arg1) throws ParseException {
return buildingTypeDao.findOne(Long.parseLong(id));
}
}
And this is the spring configuration class. (I don't use xml configuration.)
#EnableWebMvc
#Configuration
#ComponentScan({ "my.packages" })
public class MvcConfig extends WebMvcConfigurerAdapter {
#Autowired
private BuildingTypeDBOFormatter formatter;
public MvcConfig() {
super();
}
#Bean(name = "conversionService")
public FormattingConversionServiceFactoryBean conversionService() {
FormattingConversionServiceFactoryBean bean = new FormattingConversionServiceFactoryBean();
Set<Formatter<?>> formatters = new HashSet<Formatter<?>>();
formatters.add(formatter);
bean.setFormatters(formatters);
return bean;
}
I think I need to register conversion service as explained in the blog post. Using and init binder in my controller like this.
#Autowired
ConversionService conversionService;
#InitBinder
public void initBinder(WebDataBinder binder) {
binder.setConversionService(conversionService);
}
The problem is I get the following exception when using setConversionException. And when I debug it I see that binder is initialized with a default conversionService.
java.lang.IllegalStateException: DataBinder is already initialized with ConversionService
at org.springframework.util.Assert.state(Assert.java:385)
at org.springframework.validation.DataBinder.setConversionService(DataBinder.java:562)
at my.package.controller.MyController.initBinder(MyController.java:138)
I came across with many answers suggesting setConversionService but it just doesn't work, how can I fix this? (PS: Sorry for the long post, but I think there may be couple of ways to fix this, so I preferred to post the whole thing.)
You can try add custom property editor in your controller
#InitBinder
public void initBinder(ServletRequestDataBinder binder) {
binder.registerCustomEditor(BuildingType.class, "buildingType", new PropertyEditorSupport() {
public void setAsText(String text) {
Long buildingTypeId = Long.parseLong(text);
BuildingType buildingType = (BuildingType) buildingTypeDao.findOne(buildingTypeId);
setValue(buildingType);
}
});
}