I am new to Thymeleaf and stuck with a strange issue. Let me tell you what works first. I have two simple class
public class Country {
private long countryid;
private String name;
}
And
public class Person {
private String name;
private long countryId;
}
In the addPerson page I want to select the country from a dropdown. I have manually created a list of countries ( from spring controller) and then my addPerson.html is designed as
<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<title></title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
</head>
<body>
<h1>Add Person</h1>
<form action="#" th:action="#{/addPerson}" th:object="${person}"
method="POST">
<p>
Name: <input type="text" th:field="*{name}" />
</p>
<select th:field="*{countryId}" class="form-control">
<option th:each="country: ${countryList}"
th:value="${country.countryid}" th:text="${country.name}"></option>
</select>
<p>
<input type="submit" value="Submit" /> <input type="reset"
value="Reset" />
</p>
</form>
</body>
</html>
When I select a country from dropdown, I get the countryid and everything works fine.
Now I want to change my Person class as below
public class Person {
private String name;
private Country country;
}
So, instead of the countryid, I want to have the country object itself. Keeping everything else same, I have changed my addPerson.html to
<select th:field="*{country}" class="form-control">
<option th:each="country: ${countryList}"
th:value="${country}" th:text="${country.name}"></option>
</select>
Now I can see the dropdown, but upon submitting, I am getting an error
There was an unexpected error (type=Bad Request, status=400).
Validation failed for object='person'. Error count: 1
In short : It works with property of the object, what I need to do to work with the entire object itself?
Kindly help.
UPDATE 1: the controller method signature
#GetMapping("/addPerson")
public String addPerson(Model model) {
Country country1 = new Country();
country1.setCountryid(1);
country1.setName("A");
Country country2 = new Country();
country2.setCountryid(2);
country2.setName("B");
List<Country> countryList = new ArrayList<Country>();
countryList.add(country1);
countryList.add(country1);
model.addAttribute("countryList", countryList);
model.addAttribute("person", new Person());
return "addPerson";
}
#PostMapping("/addPerson")
public void processAddPerson(#Valid #ModelAttribute("person") Person person) {
System.out.println(person.getName());
}
UPDATE 2
Upon debugging, I found out that in the second case, on submit, the control is not going to setCountry method of Person class at all!
Add BindingResult Object to your method.
public String processAddPerson( #Valid Person person,BindingResult bindingResult,Model model)
Ensure that the BindingResult has to be immediately after the object annotated with #Valid.
Related
I am working on a quiz app. I want to the user to be able to create a quiz with 10 questions (although I am open to this being variable length). If I create more than 1 question with #ModelAttribute, instead of getting more than 1 QuestionAnswerInfo objects, I get one with each field separated by commas. This does not seem to be a List, but simply Strings separated by commas.
I want each question to come in and be handled separately. What is the best way to approach this?
This is the best answer I have found but I can't seem to make sense of it in my context: Send multiple objects of same class from jsp to spring controller
Models
(Abstract Entity is just ID generation)
#Entity
public class Quiz extends AbstractEntity {
public String name;
#OneToMany (cascade = CascadeType.ALL)
#JoinColumn(name = "quiz_question_foreign_id", referencedColumnName = "id")
private List<QuestionAnswerInfo> questions = new ArrayList<>();
public Quiz(){}
public Quiz(String name, ArrayList<QuestionAnswerInfo> questions) {
super();
this.name = name;
this.questions = questions;
}
//Getters and Setters
}
#Entity
public class QuestionAnswerInfo extends AbstractEntity{
private String question;
private String answer;
private String questionType;
private String additionalAnswerInfo;
public QuestionAnswerInfo (){}
public QuestionAnswerInfo(String question, String answer, String questionType, String additionalAnswerInfo) {
super();
this.question = question;
this.answer = answer;
this.questionType = questionType;
this.additionalAnswerInfo = additionalAnswerInfo;
}
//Getters and Setters
Controller
#Controller
public class QuizController {
//Repositories
#RequestMapping("create")
public String displayCreateNewQuiz(Model model) {
model.addAttribute(new Quiz());
model.addAttribute("questions", new QuestionAnswerInfo());
return "create";
}
#PostMapping("create")
public String processCreateNewQuiz(#ModelAttribute Quiz newQuiz, #ModelAttribute QuestionAnswerInfo questions,
Model model) {
newQuiz.getQuestions().add(questions);
quizRepository.save(newQuiz);
return "index";
}
View
<!DOCTYPE html>
<html lang="en" xmlns:th="https://www.thymeleaf.org/">
<head>
<meta charset="UTF-8"/>
<title>Title</title>
</head>
<body>
<h1>Create a Quiz</h1>
<form method="post">
<div>
<label th:for="name">Quiz Name</label>
<input th:field="${quiz.name}"/>
</div>
<br>
<p>Question 1</p>
<div>
<label th:for="question">Add a Question</label>
<input th:field="${questions.question}"/>
</div>
<div>
<label th:for="answer">Add an Answer</label>
<input th:field="${questions.answer}"/>
</div>
<div>
<label th:for="questionType">Question Type</label>
<input th:field="${questions.questionType}"/>
</div>
<div>
<label th:for="additionalAnswerInfo">Add Additional Information</label>
<input th:field="${questions.additionalAnswerInfo}"/>
</div>
<p>Question 2</p>
<div>
<label th:for="question">Add a Question</label>
<input th:field="${questions.question}"/>
</div>
<div>
<label th:for="answer">Add an Answer</label>
<input th:field="${questions.answer}"/>
</div>
<div>
<label th:for="questionType">Question Type</label>
<input th:field="${questions.questionType}"/>
</div>
<div>
<label th:for="additionalAnswerInfo">Add Additional Information</label>
<input th:field="${questions.additionalAnswerInfo}"/>
</div>
<input type="submit" value="Create Quiz"/>
</form>
</body>
</html>
I will eventually have a similar problem with collecting User answers when they take the quiz.
you need to use js to handle multiple questions so that in the request there will be list of questions in the request body.
I am using Eclipse on Window. When running the program, an error occurred
An error happened during template parsing (template: "class path resource [templates/add.html]").
Error during execution of processor 'org.thymeleaf.spring5.processor.SpringInputGeneralFieldTagProcessor' (template: "add" - line 10, col 40).
Invalid property 'id' of bean class [com.example.demo.models.Book]: Bean property 'id' is not readable or has an invalid getter method: Does the return type of the getter match the parameter type of the setter?
Class Entity
#Table(name = "book")
#Entity
#NoArgsConstructor
#Getter
#Setter
#AllArgsConstructor
public class Book implements Serializable{
#Id
private Long id;
#Column
private String name;
#Column
private String image;
}
Class Controller
#Controller
public class BookController {
#Autowired
private BookService bookService;
#GetMapping("/")
public String add(ModelMap model) {
model.addAttribute("book", new Book());
return "add";
}
#PostMapping("/save")
public String save(ModelMap model, Book book) {
bookService.save(book);
model.addAttribute(book);
return list(model);
}
#GetMapping("/list")
public String list(ModelMap model) {
List<Book> list = (List<Book>) bookService.findAll();
model.addAttribute("books", list);
return "list";
}
}
Class Template
<!DOCTYPE html >
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="ISO-8859-1">
<title>Insert Book</title>
</head>
<body>
<form th:action="#{/save}" th:object="${book}" method="post">
<div th:if="${id == null}">
<input type="text" placeholder="Id" th:field="*{id}" /><br />
</div>
<input type="text" placeholder="Name" th:field="*{name}" /><br /> <input
type="text" th:field="*{image}" placeholder="Image" /><br /> <input
type="submit" value="Confirm"> <a th:href="#{/list}">List</a>
</form>
</body>
</html>
I added and installed Lombok and it didn't get any error but getter / setter error when running the program, so try to replace it manually and run successfully. I think it was Lombok's fault, but nothing was found.
.
The error is on the code below:
<div th:if="${id == null}">
You can fix that by replacing it with:
<div th:if="${book.id == null}">
You might be mislead by the error, but it is not related with Lombok.
I am following Spring in Action 5 and have problem with creating Taco model after pressing submit button. This is my design Taco controller class:
#GetMapping
public String showDesignForm(Model model){
List<Ingredient> ingredients = new ArrayList<>();
ingredientRepository.findAll().forEach(i -> ingredients.add(i));
Type[] types = Ingredient.Type.values();
for (Type type : types){
model.addAttribute(type.toString().toLowerCase(),
filterByType(ingredients, type));
}
return "welcomePage";
}
#ModelAttribute(name = "taco")
public Taco taco(){
return new Taco();
}
#PostMapping
public String processDesign(#Valid Taco taco, Errors errors, #ModelAttribute Order order){
if(errors.hasErrors()) {
return "welcomePage";
}
Taco saved = tacoRepository.save(taco);
order.addDesign(saved);
return "redirect:/orders/current";
}
And the error message which I catch:
org.springframework.validation.BeanPropertyBindingResult: 1 errors
Field error in object 'taco' on field 'ingredients': rejected value [CARN]; codes [typeMismatch.taco.ingredients,typeMismatch.ingredients,typeMismatch.java.util.List,typeMismatch]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [taco.ingredients,ingredients]; arguments []; default message [ingredients]]; default message [Failed to convert property value of type 'java.lang.String' to required type 'java.util.List' for property 'ingredients'; nested exception is java.lang.IllegalStateException: Cannot convert value of type 'java.lang.String' to required type 'org.server.models.Ingredient' for property 'ingredients[0]': no matching editors or conversion strategy found]
Taco entity looks like:
#Data
#Entity
public class Taco {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private int id;
private Date createdAt;
#NotNull
#Size(min = 3, message="Name must be at least 3 characters long")
private String name;
#ManyToMany(targetEntity = Ingredient.class)
#Size(min=1, message="You must choose at least 1 ingredient")
private List<Ingredient> ingredients = new ArrayList<>();
#PrePersist
void createdAt(){
this.createdAt = new Date();
}
}
And my entity with Ingredients:
#Data
#RequiredArgsConstructor
#NoArgsConstructor(access = AccessLevel.PRIVATE, force = true)
#Entity
public class Ingredient {
#Id
private final String id;
private final String name;
#Enumerated(EnumType.STRING)
private final Type type;
public static enum Type{
WRAP, PROTEIN, VEGGIES, CHEESE, SAUCE
}
}
This is html page which must create new Taco object with picked ingredients:
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Testing Firs Page</title>
</head>
<body>
<h1>Design your taco!</h1>
<img th:src="#{/images/taco.jpg}" alt="myImage"/>
<form method="POST" th:object="${taco}">
<span class="validationError"
th:if="${#fields.hasErrors('ingredients')}"
th:errors="*{ingredients}">Ingredient Error</span>
<div class="grid">
<div class="ingredient-group" id="wraps">
<h3>Designate your wrap:</h3>
<div th:each="ingredient : ${wrap}">
<input name="ingredients" type="checkbox" th:value="${ingredient.id}" />
<span th:text="${ingredient.name}">INGREDIENT</span><br/>
</div>
</div>
<div class="ingredient-group" id="proteins">
<h3>Pick your protein:</h3>
<div th:each="ingredient : ${protein}">
<input name="ingredients" type="checkbox" th:value="${ingredient.id}" />
<span th:text="${ingredient.name}">INGREDIENT</span><br/>
</div>
</div>
<div class="ingredient-group" id="cheeses">
<h3>Choose your cheese:</h3>
<div th:each="ingredient : ${cheese}">
<input name="ingredients" type="checkbox" th:value="${ingredient.id}" />
<span th:text="${ingredient.name}">INGREDIENT</span><br/>
</div>
</div>
<div class="ingredient-group" id="veggies">
<h3>Determine your veggies:</h3>
<div th:each="ingredient : ${veggies}">
<input name="ingredients" type="checkbox" th:value="${ingredient.id}" />
<span th:text="${ingredient.name}">INGREDIENT</span><br/>
</div>
</div>
<div class="ingredient-group" id="sauces">
<h3>Select your sauce:</h3>
<div th:each="ingredient : ${sauce}">
<input name="ingredients" type="checkbox" th:value="${ingredient.id}" />
<span th:text="${ingredient.name}">INGREDIENT</span><br/>
</div>
</div>
</div>
<div>
<h3>Name your taco creation:</h3>
<input type="text" th:field="*{name}"/>
<span class="validationError"
th:if="${#fields.hasErrors('name')}"
th:errors="*{name}">Name Error</span>
<br/>
<button>Submit your taco</button>
</div>
</form>
</body>
</html>
How can I fix it? Thanks for advance.
In Spring in Action, you should add IngredientByIdConverter class. this class is convert Ingredient to String.
#Component
public class IngredientByIdConverter
implements Converter<String, Ingredient> {
private IngredientRepository ingredientRepo;
#Autowired
public IngredientByIdConverter(IngredientRepository ingredientRepo) {
this.ingredientRepo = ingredientRepo;
}
#Override
public Ingredient convert(String id) {
return ingredientRepo.findById(id);
}
}
https://github.com/habuma/spring-in-action-5-samples/blob/ff98b2ec36eeb627e4547713c8acbbd26a0eaa33/ch03/tacos-jdbc/src/main/java/tacos/web/IngredientByIdConverter.java
package tacos.web;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.convert.converter.Converter;
import org.springframework.stereotype.Component;
import tacos.Ingredient;
import tacos.data.IngredientRepository;
#Component
public class IngredientByIdConverter implements Converter<String, Ingredient> {
private IngredientRepository ingredientRepo;
#Autowired
public IngredientByIdConverter(IngredientRepository ingredientRepo) {
this.ingredientRepo = ingredientRepo;
}
#Override
public Ingredient convert(String id) {
return ingredientRepo.findById(id);
}
}
The error is:
Cannot convert value of type java.lang.String to required type org.server.models.Ingredient for property ingredients[0]
You didn't share the code for Taco or Ingredient or the payload of the POST request, so we cannot say for sure what you need to change.
However, if you add a constructor to Ingredient that takes a String argument, I believe Spring will use that.
How you create an Ingredient object from a String value will of course depend on what the strings content is, so that's entirely up to you to figure out. If you need help with that, create a new question, and include the relevant information, such as the code of your POJO classes and the content of the POST request.
How can I validate a composition relationship in Thymeleaf/Spring Boot. I have a simple FundTrf class which "has a" Data class. Problem is when I validate form inputs, FundTrf class related fields are getting validated, but the Data class related fields are not getting validated. Is there additional biding needs to be done between these classes. Below is what I have tried.
<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<title>HNB CEFT | Test Bed</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
</head>
<body>
<h1>Form</h1>
<form action="#" th:action="#{/ceft/fundTrf}" th:object="${fundTrf}" method="post">
<table>
<tr><td>Version </td><td><input type="text" th:field="*{version}" /></td>
<td th:if="${#fields.hasErrors('version')}" th:errors="*{version}">Version Error</td>
</tr>
<tr><td>Bank Code </td><td><input type="text" th:field="*{data.dest_bank_code}" /></td>
<td th:if="${#fields.hasErrors('data.dest_bank_code')}" th:errors="*{data.dest_bank_code}">Bank Code Error</td>
</tr>
<tr><td>Amount </td><td><input type="text" th:field="*{data.amount}" /></td>
<td th:if="${#fields.hasErrors('data.amount')}" th:errors="*{data.amount}">Amount Error</td>
</tr>
</table>
<p><input type="submit" value="Submit" /> <input type="reset" value="Reset" /></p>
</form>
</body>
</html>
Below is my controller class.
#Controller
public class Hello implements WebMvcConfigurer{
#GetMapping("/ceft/welcome")
public String welcomeForm(Model model) {
model.addAttribute("fundTrf", new FundTrf());
return "welcome";
}
#PostMapping("/ceft/fundTrf")
public String ceftTransaction(#ModelAttribute #Valid FundTrf fundTrf, BindingResult bindingResult) {
if (bindingResult.hasErrors()) {
return "welcome";
} else {
return "result";
}
}
}
Below is my FundTrf class
public class FundTrf {
#NotEmpty
private String version;
private Data data;
..Getters and Setters
}
And this is the Data class.
public class Data {
#NotEmpty
private String reqId;
#NotEmpty
private String frm_hnb_account;
#NotEmpty
private String dest_bank_account;
#NotEmpty
private String benificiary_name;
#NotEmpty
private String dest_bank_code;
#NotEmpty
#Size(min = 2, max = 30)
private String amount;
..Getters and Setters
}
The issue is when I submit the form with empty values the message "Version must not be empty" is coming up, but Amount validation is not working. What am I doing wrong here?
You have to set #Valid on the object Data in order for your Data properties to be also validated.
public class FundTrf {
#NotEmpty
private String version;
#Valid //ADDED VALID HERE
private Data data;
..Getters and Setters
}
The javadoc for javax.validation.Valid says:
Marks a property, method parameter or method return type for
validation cascading. Constraints defined on the object and its
properties are be validated when the property, method parameter or
method return type is validated. This behavior is applied recursively.
I need to check the availability with the username, it means, each username must be unique and I need to show a message "This username already is in use" when already exists an identical username. It can be automatic (Generate a query and then show a message) or create a button to verify the availability and then it show a message. I don't have a clear idea about this simple procedure. What I need to do in this case?
The source code is like so:
Person class
#Entity
#Table(name="person")
public class Person {
#Id
#GeneratedValue(strategy=GenerationType.SEQUENCE)
#Column(name="id")
private int id;
#Column(name="firtsname")
private String first_name;
#Column(name="lastname")
private String last_name;
#Column(name="username", unique=true)
private String user_name;
#Column(name="password")
private String password;
// Getters and Setters
}
PersonDao>>PersonDaoImpl
#Repository("personDao")
public class PersonDaoImpl implements PersonDao {
#Autowired
private SessionFactory sessionFactory;
#Override
public void createPerson(Person person) {
sessionFactory.getCurrentSession().saveOrUpdate(person);
}
#Override
public Person updatePerson(int id) {
return (Person) sessionFactory.getCurrentSession().get(Person.class, id);
}
#Override
public void deletePerson(int id) {
sessionFactory.getCurrentSession().createQuery("DELETE FROM Person WHERE id="+id).executeUpdate();
}
#Override
#SuppressWarnings("unchecked")
public List<Person> listAllPersons() {
return (List<Person>) sessionFactory.getCurrentSession().createCriteria(Person.class).list();
}
}
Registration form (registration.jsp):
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>Insert title here</title>
</head>
<body>
<form:form method="POST" action="/userform/saveUser.html">
<p>
<form:input type="hidden" readonly="true" path="id" />
</p>
<p>
<form:label path="first_name" >First name:</form:label>
<form:input path="first_name" />
</p>
<p>
<form:label path="last_name">Last name:</form:label>
<form:input path="last_name" />
</p>
<p>
<form:label path="user_name">Username:</form:label>
<form:input path="user_name" />
</p>
<p>
<form:label path="password">Password:</form:label>
<form:input path="password" />
</p>
<p>
<button type="reset" value="Reset">Reset</button>
<button type="submit" value="Save">Save</button>
</p>
</form:form>
</body>
</html>
Thanks a lot!
Just try the insert and handle the failure. Any other approach is vulnerable to timing-window problems.
SELECT id FROM table_name WHERE id = "user_key"
if it returns selected username then you know you already got it. And once you know it, your layout-function can show message "this username is taken". It also would be nice if you make the id cell unique in your database. Thats all.
PS: If your id cell is unique then your can make INSERT and catch exception. Thats second solution.