thymeleaf and spring boot form exception - java

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.

Related

Unable to get updated values in a dynamically created form using Spring Boot and Thymeleaf

I have created a dynamic form in Thymeleaf which populates feedbacks from all users in a table format on the UI. The form is first called when the GET Api of the controller gets hit. Relevant code for the same is given below :
allfeedbacks.html
<h2>Dynamic form</h2>
<form action="#" th:action="#{/updatefb}" th:object="${feedbacklist}"
method="post">
<table>
<tr>
<th>Message</th>
<th>Status</th>
<th>Comments</th>
</tr>
<tr th:each="feedback : ${feedbacklist.myfbList}">
<td th:text="${feedback.message}" th:field="${feedback.message}">The
first name</td>
<td><select>
<option value="Pending"
th:selected="${feedback.status == 'Pending'}">Pending</option>
<option value="In Process"
th:selected="${feedback.status == 'In Process'}">In
Process</option>
<option value="Done" th:selected="${feedback.status == 'Done'}">Done</option>
</select></td>
<td><input type="text" placeholder="Enter Comment Here"
name="comments" th:text="${feedback.comment}"
th:field="${feedback.comment}" /></td>
</tr>
</table>
<button type="submit">Submit</button>
</form>
Basically I have created two beans, one is the Feedback.java bean while the other is FeedbackList.java bean. Code for the same is given below :
Feedback.java
#Entity
#Table(name = "feedback")
public class Feedback implements Serializable {
private static final long serialVersionUID = -3009157732242241606L;
#Id
private String id;
public String getId() {
return id;
}
public String getMessage() {
return message;
}
public String getStatus() {
return status;
}
public String getComment() {
return comment;
}
#Column(name = "message")
private String message;
#Column(name = "status")
private String status;
#Column(name = "comment")
private String comment;
public Feedback() {
}
public Feedback(String message, String status) {
this.message = message;
this.status = status;
this.id = UUID.randomUUID().toString();
}
FeedbackList.java
public class FeedbackList {
ArrayList<Feedback> myfbList;
public ArrayList<Feedback> getMyfbList() {
return myfbList;
}
public void setMyfbList(ArrayList<Feedback> myfbList) {
this.myfbList = myfbList;
}
}
Relevant code from my Controller class is as follows :
#RequestMapping(value = "/getAll", method = RequestMethod.GET)
public String getAllFeedbacks(#Valid FeedbackList feedbacklist,
BindingResult bindingResult, Model model) {
ArrayList<Feedback> fbarray = new ArrayList<>();
for (Feedback fb : repository.findAll()) {
fbarray.add(fb);
}
feedbacklist.setMyfbList(fbarray);
model.addAttribute("feedback", new Feedback());
model.addAttribute("feedbacklist", feedbacklist);
return "allfeedbacks";
}
#RequestMapping(value = "/updatefb", method = RequestMethod.POST)
public String updatefbStatus(#Valid FeedbackList feedbacklist,
BindingResult
bindingResult, Model model) {
//feedbacklist is coming as NULL below
for (Feedback fb : feedbacklist.getMyfbList()) {
System.out.println(fb.getComment());
System.out.println(fb.getMessage());
System.out.println(fb.getStatus());
}
// Code to update the database with the new status and comment would go
// here
return "result";
}
The form is getting properly rendered on the UI when I fire the Get request, however, when I make some changes in the form and submit it ( POST ), feedbacklist is coming as NULL. Could anyone please guide me with this ?
To use a list inside a form with Thymeleaf is a little bit more tricky, you need to use an specific syntax, here i show you an example.
<tr th:each="feedback : ${feedbacklist.myfbList}">
<td th:field="*{myfbList[__${feedbackStat.index}__].message}">The
first name
</td>
...//Same for others fields
</tr>
In thymeleaf you have to use the Stat object to say the array position where you want to set the value, also as normal fields inside an object you have to use the '*' notation.

OneToMany relation with multiple properties in JSP form

I have this model:
Order.java
#Entity
#Table(name = "`order`")
public class Order {
private Long id;
#OneToMany(fetch = FetchType.EAGER, mappedBy = "order")
private List<OrderProduct> orderProducts;
#Override
public int hashCode() {
return new Long(id).hashCode();
}
#Override
public boolean equals(Object obj) {
if (obj == null) {
return false;
}
if (!(obj instanceof Order)) {
return false;
}
return this.id == ((Order) obj).getId();
}
/* getters & setters */
}
OrderProduct.java
#Entity
#Table(name = "order_product")
public class OrderProduct {
private Long id;
private String name;
private Long quantity;
#ManyToOne
#JoinColumn(name = "order_id")
private Order order;
/* getters & setters */
}
And this controller:
OrderController.java
#Controller
#SessionAttributes({"products"})
public class OrderController {
#Autowired
private OrderService orderService;
#Autowired
private ProductService productService;
#Autowired
private SecurityService securityService;
#Autowired
private UserService userService;
#RequestMapping(value = "/order/add", method = RequestMethod.GET)
public String addOrder(Model model) {
Order order = new Order();
order.setOrderProducts(new AutoPopulatingList<>(OrderProduct.class));
model.addAttribute("orderForm", order);
model.addAttribute("products", productService.findAll());
return "order/add";
}
#RequestMapping(value = "/order/add", method = RequestMethod.POST)
public String addOrder(#ModelAttribute("orderForm") Order orderForm, BindingResult bindingResult, Model model) {
orderForm.setUser(userService.findByUsername(securityService.findLoggedInUsername()));
for (OrderProduct orderProduct : orderForm.getOrderProducts()) {
orderProduct.setOrder(orderForm);
}
orderService.save(orderForm);
return "redirect:/order/view";
}
}
And I want to create a form where a user can choose few products, give a quantity of them and submit his order. I was trying to write JSP code like that:
<form:form method="POST" modelAttribute="orderForm">
<spring:bind path="orderProducts">
<tr>
<td>
<c:forEach items="${products}" var="product">
<form:checkbox path="orderProducts[${product.id}].name"
value="${product.name}"
label="${product.name}"/>
<form:input type="text" path="orderProducts[${product.id}].quantity" placeholder="Quantity"/>
</c:forEach>
</td>
</tr>
</spring:bind>
<button class="btn btn-lg btn-primary btn-block" type="submit">Submit</button>
</form:form>
...but unfortunately it's probably trying to create as many orderProducts as I have in products list and as a result the orderProducts, which a service try to save, doesn't have properly setted values and at the end there is created an order without the orderProducts. So my question is how can I properly send the orderProducts via JSP code to the controller which finally will create the right order with its items?
I am suspecting this ${product.id}. As this is an index value, it should be like
orderProducts[0].name orderProducts[0].quantity
orderProducts[1].name orderProducts[1].quantity
orderProducts[2].name orderProducts[2].quantity
I solved it by removing an orderProducts from an orderForm which don't have a quantity properly setted in my OrderController but probably it isn't the best solution.

Form data not binding with Spring Controller Annotation getting new object after submit

I am not getting form data in spring controller after submitting the form below is my code
#RequestMapping(value = "category/addCategory.htm" , method = RequestMethod.GET)
public String add(Model model) {
if (log.isDebugEnabled()){
log.debug("Invoking listCategory");
}
model.addAttribute("categoryView", new CategoryView());
return "editCategory";
}
#RequestMapping(value = "category/saveCategory.htm", method = RequestMethod.POST)
public String saveCategory(CategoryView categoryView, Model model, BindingResult result) {
Category category = prepareCategoryFromView(categoryView);
categoryService.save(category);
categoryView.setCategoryId(category.getCategoryId());
model.addAttribute("categoryView",categoryView);
return "editCategory";
}
prepareCategoryFromView is a method which is setting the actual values on Category it's hibernate entity, below categoryView
public class CategoryView {
private long categoryId;
private String image = "";
private int parentId;
private boolean top;
private int column = 1;
private int sortOrder = 1;
private boolean status;
private String description;
private String name;
.
.
other variable and setter and getters
}
and the form is
<sf:form method="post" enctype="multipart/form-data" id="form-category" cssClass="form-horizontal" modelAttribute="categoryView">
<sf:label path="name" cssClass="col-sm-2 control-label">Category Name</sf:label>
<sf:input path="name" id="name" name="name" cssClass="form-control" placeholder="Category Name" />
<sf:hidden path="categoryId" id="categoryId" name="categoryId" />
<sf:hidden path="languageId" id="languageId" name="languageId" />
<sf:label path="description" cssClass="col-sm-2 control-label">Category Name</sf:label>
<sf:textarea path="description" cssClass="form-control" placeholder="Description" id="description"/>
.
.
.
</sf:form>
In above form every time I am getting name and description is null (I think it's creating a new view object without given values in form)
Pls let me know, where I am wrong
Remove the enctype="multipart/form-data" from your form tag and try again (with the method arguments in the correct order). #ModelAttribute is not strictly required since your attribute name matches the class name.
I think #ModelAttribute annotation is missing here on CategoryView object.Because as per your form code it is the model attribute which will bind data to the bean in controller.
Attach it with your method argument like below,then you can check the data is binding to it or not.
#RequestMapping(value = "category/saveCategory.htm", method = RequestMethod.POST)
public String saveCategory(#ModelAttribute("categoryView") CategoryView categoryView, Model model, BindingResult result) {
Category category = prepareCategoryFromView(categoryView);
categoryService.save(category);
categoryView.setCategoryId(category.getCategoryId());
model.addAttribute("categoryView",categoryView);
return "editCategory";
}

Spring MVC Form Processing

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!

Spring Conversion Service, binding form to a nested object

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);
}
});
}

Categories