I've tried to create an object with another object inside, with a form but the Object picked from a dropdown list gets converted into a String when returned from the Thymeleaf form.
Those are the entities in my project, with an 1:n relatioship between them:
Entity User
//imports
#Entity
#Table(name = "USERS")
public class User {
#Id
#GeneratedValue
#Column( name ="USER_ID")
private int id;
#Column( name ="username")
private String username;
#Column( name ="password")
private String password;
#Column( name ="email")
private String email;
#OneToMany(fetch= FetchType.LAZY, mappedBy="user", cascade = CascadeType.ALL)
private List<Post> posts;
//setter & getters & toString
}
Entity Post
//imports
#Entity
#Table(name="POSTS")
public class Post {
#Id
#GeneratedValue
#Column(name="POST_ID")
private int id;
#Column(name="tittle")
private String tittle;
#Column(name="text")
private String text;
#ManyToOne
#JoinColumn(name="USER_ID",referencedColumnName="USER_ID")
private User user;
//getters & setters & toString
To create a new Post:
//In Controller
#RequestMapping(value = "/posts/new")
public String newPost(Model model) {
model.addAttribute("post", new Post());
model.addAttribute("users", userService.list());
return "addPost";
}
Which returns the template that has this form:
<form th:action="#{/savePost}" th:object="${post}" method="post">
<tr>
<td><input type="hidden" th:field="${post.id}" /></td>
</tr>
<tr>
<td>Titulo</td>
<td>Texto</td>
<td>Usuario</td>
</tr>
<tr>
<td><input type="text" th:field="${post.tittle}"
th:value="${post.tittle}" /></td>
<td><input type="text" th:field="${post.text}"
th:value="${post.text}" /></td>
<td><select th:field="${post.user}">
<option th:each="user : ${users}" th:text="${user.username}"
th:value="${user.id}"></option>
</select></td>
</tr>
<tr>
<td colspan="3"><input class="btn btn-primary" type="submit"
value="GUARDAR"></td>
</tr>
</form>
The action attribute in the form calls:
#PostMapping("/savePost")
//#RequestMapping(value = "users/save",method = RequestMethod.POST)
public String savePost(#ModelAttribute Post post) {
postService.add(post);
return "redirect:/posts";
}
At this point, I try to create a Post and select a User from the dropdown but when attempting to save it gives me this error:
Field error in object 'post' on field 'user': rejected value [16]; codes [typeMismatch.post.user,typeMismatch.user,typeMismatch.com.julian.bootmvchibernate.model.User,typeMismatch]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [post.user,user]; arguments []; default message [user]]; default message [Failed to convert property value of type 'java.lang.String' to required type 'com.julian.bootmvchibernate.model.User' for property 'user'; nested exception is org.springframework.core.convert.ConversionFailedException: Failed to convert from type [java.lang.String] to type [#javax.persistence.ManyToOne #javax.persistence.JoinColumn com.julian.bootmvchibernate.model.User] for value '16'; nested exception is java.lang.NullPointerException]
So I tried to implement a Formatter like so:
public class UserFormatter implements Formatter<User> {
#Autowired
#Qualifier("userService")
public GeneralService<User> userService;
#Override
public String print(User object, Locale locale) {
return (object != null ? object.getUsername() : "");
}
#Override
public User parse(String text, Locale locale) throws ParseException {
final Integer userId = Integer.parseInt(text);
return userService.get(userId);
}
}
Registering it:
#SpringBootApplication
public class BootmvchibernateApplication implements WebMvcConfigurer{
#SuppressWarnings("unchecked")
#Override
public void addFormatters(FormatterRegistry registry) {
registry.addFormatter(new UserFormatter());
}
public static void main(String[] args) {
SpringApplication.run(BootmvchibernateApplication.class, args);
}
}
But when this approach is tried the next error is found, this just uppon accessing the /post/new direcction (the template addPost doesn't work):
An error happened during template parsing (template: "class path resource [templates/addPost2.html]")
org.thymeleaf.exceptions.TemplateInputException: An error happened during template parsing (template: "class path resource [templates/addPost2.html]")
.....
Caused by: org.attoparser.ParseException: Error during execution of processor 'org.thymeleaf.spring5.processor.SpringOptionFieldTagProcessor' (template: "addPost2" - line 43, col 8)
.....
Caused by: org.springframework.core.convert.ConversionFailedException: Failed to convert from type [java.lang.String] to type [#javax.persistence.ManyToOne #javax.persistence.JoinColumn com.julian.bootmvchibernate.model.User] for value '2'; nested exception is java.lang.NullPointerException
at org.springframework.core.convert.support.ConversionUtils.invokeConverter(ConversionUtils.java:47)
If any more information is needed, tell me and I post it.
EDIT: this is the repository if someone is interested.
github.com/JulianBautistaVelez/JPA_Spring_Excercise
The problem is in your addPost.html except that everything is ok now.
<table>
<form th:action="#{/posts/new/mod}" th:object="${post}"
method="POST">
<tr>
<td><input type="hidden" th:field="${post.id}" /></td>
</tr>
<tr>
<td>Titulo</td>
<td>Texto</td>
<td>Usuario</td>
</tr>
<tr>
<td><input type="text" th:field="${post.tittle}"
th:value="${post.tittle}" /></td>
<td><input type="text" th:field="${post.text}"
th:value="${newPost.text}" /></td>
<!-- <td><select th:field="${newPost.user}">
<option th:each="user : ${users}" th:text="${user.username}"
th:value="${user.id}"></option>
</select></td> -->
<td><select th:field="*{user}" class="form-control">
<option th:each="user: ${users}"
th:value="${user.id}" th:text="${user.username}"></option>
</select></td>
</tr>
<tr>
<td colspan="3"><input class="btn btn-primary" type="submit"
value="GUARDAR"></td>
</tr>
</form>
</table>
I changed newPost as post , because you are adding in here as post model.
#RequestMapping(value = "/posts/new")
public String newPost(Model model) {
logger.info("-- en NEW Usuario");
model.addAttribute("post", new Post());
model.addAttribute("users", userService.list());
logger.info("-- -- -- LISTA DE USUARIOS -- -- --");
System.out.println(userService.list());
return "addPost";
}
Related
I am trying to return the data as a response body in java spring boot when a button is click in an html page.I have a list of countries displayed in my page with an edit button assigned to each. I want to find the data that was clicked by id so I have defined the method in my controller class. That's when the edit button is click, it should take the id of the country in the clicked row and display the information based on that id. When I test the api in Postman, it returns the data correctly but when I called the same api in my html page, it's giving me this error.
org.thymeleaf.exceptions.TemplateProcessingException: Exception evaluating SpringEL expression: "/findById/{id=${country.id}}" (template: "country" - line 555, col 26)
at org.thymeleaf.spring5.expression.SPELVariableExpressionEvaluator.evaluate(SPELVariableExpressionEvaluator.java:292) ~[thymeleaf-spring5-3.0.14.RELEASE.jar:3.0.14.RELEASE]
Caused by: org.springframework.expression.spel.SpelParseException: Expression [/findById/{id=${country.id}}] #0: EL1070E: Problem parsing left operand
This is my data class
Entity
#Data
#NoArgsConstructor
#AllArgsConstructor
public class Country {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
private String code;
private String capital;
private String description;
private String nationality;
private String continent;
}
My Controller class
#Controller
public class CountryController {
#Autowired
private CountryService countryService;
#GetMapping("/countries")
public String getCountry(Model model){
List<Country> countryList = countryService.getAllCountry();
model.addAttribute("countries",countryList);
return "country";
}
#PostMapping("/countries/addNew")
public String saveInfo(Country country){
countryService.saveCountryInfo(country);
return "redirect:/countries";
}
#GetMapping("/findById/{id}")
#ResponseBody
public ResponseEntity<Country> getCountryById(#PathVariable("id") Long countryId){ //Bind PathVariable id to id
return ResponseEntity.ok(countryService.getCountryById(countryId)) ;
}
#GetMapping("/country/code/{code}")
public Country getCountryCode(#PathVariable("code") String code){
return countryService.getCountryByCode(code);
}
}
My Service class
#Service
public class CountryService {
#Autowired
private CountryRepository countryRepository;
public List<Country> getAllCountry() {
return countryRepository.findAll();
}
public void saveCountryInfo(Country country){
countryRepository.save(country);
}
public Country getCountryById(Long id){
return countryRepository.findById(id).get();
}
public Country getCountryByCode(String code){
return countryRepository.findByCode(code);
}
}
My Repository class
#Repository
public interface CountryRepository extends JpaRepository<Country,Long> {
public Country findByCode(String code);
}
Here is the html code
<section class="section dashboard">
<div class="row">
<!-- Left side columns -->
<div class="row">
<div class="col-lg-9 col-md-12">
<div class="panel panel-default">
<div class="panel-heading">
<!-- Image background -->
<button type="button" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#addModal" data-whatever="#mdo">Add A Country</button>
<h1>List of Country</h1>
<table class="table">
<thead>
<tr>
<th>Id</th>
<th>Code</th>
<th>Capital</th>
<th>Description</th>
<th>Nationality</th>
<th>Continent</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
<tr th:each="country:${countries}">
<td th:text="${country.id}"></td>
<td th:text="${country.code}">Code</td>
<td th:text="${country.capital}">Capital</td>
<td th:text="${country.description}">Description</td>
<td th:text="${country.nationality}">Nationality</td>
<td th:text="${country.continent}">Continent</td>
<td>
<div class="btn-group">
<a th:href="${/findById/{id=${country.id}}}" class="btn btn-primary" id="editButton" data-bs-toggle="modal" data-bs-target="#editModal">Edit</a>
</div>
</td>
</tr>
</tbody>
</table>
</div><!-- End of Image background -->
</div><!-- End Left side columns -->
</div>
</div>
</div>
</section>
I am having issues indexing my for each objects. I have a list that I pull from the repository, that is called MonthlyAcct. I want to iterate over the list in the thymeleaf html file and show each property of the MonthlyAcct object displayed as an editable input field inside a table. I keep getting errors that Indexing into type 'monthAcct' is not supported, or currently, the error is: "Neither BindingResult nor plain target object for bean name 'monthAcct[0]' available as request attribute."
This is definitely an issue with how I've set up th:field, as if I switch it out to th:name, it shows up and does not throw errors. Do I have to make this into a form to get the th:field to work?
I have used this same style/tactic in other areas of my project and it works, so I am not sure why this time this type of set up is not working. Any ideas? I also have a different form on this page, that updates details for a client class, can this be causing any issues?
Just for reference I have tried * and $ with the th:each statement, and have tried both symbols with the th:field as well. Both throw the above mentioned error.
<table class="table table-striped" data-toggle="table" data-show-toggle="true" data-classes="table-no-bordered" data-striped="true" data-search="true" data-show-columns="true" >
<thead>
<th>year</th>
<th>January</th>
</thead>
<tbody>
<tr th:each="acct, stat : ${monthAcct}">
<td th:text="${acct.year}"></td>
<td>
<input type="number" class="text-left form-control" th:field="${monthAcct[__${stat.index}__].janAmt}"/>
</td>
</tr>
</tbody>
</table>
In the controller:
#RequestMapping(value="/accounting/client/{id}")
public String accountingDetails(#PathVariable("id")Client client, MonthlyAccountingTracker monthlyAccountingTracker, Model model) {
List<MonthlyAccountingTracker> monthAcct = monthlyAccountingTrackerRepository.findByClient(client);
model.addAttribute("client",clientRepository.findById(client.getId()));
model.addAttribute("monthAcct",monthAcct);
return "accounting";
}
#DynamicUpdate
#Entity
#Table(name="MonthlyMinAcctTracker")
#EntityListeners(AuditingEntityListener.class)
public class MonthlyAccountingTracker {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name = "id")
private Long id;
#ManyToOne
#JoinColumn(name="client")
private Client client;
private BigDecimal year;
private BigDecimal janAmt;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public Client getClient() {
return client;
}
public void setClient(Client client) {
this.client = client;
}
public BigDecimal getJanAmt() {
return janAmt;
}
public void setJanAmt(BigDecimal janAmt) {
this.janAmt = janAmt;
}
}
I get my monthAcct list from the repository:
public interface MonthlyAccountingTrackerRepository extends CrudRepository<MonthlyAccountingTracker,Long>, JpaSpecificationExecutor {
MonthlyAccountingTracker save(MonthlyAccountingTracker entity);
MonthlyAccountingTracker findById(Long id);
List<MonthlyAccountingTracker> findByClient(Client client);
void delete(MonthlyAccountingTracker entity);
List<MonthlyAccountingTracker> findAll();
}
*{monthAcct} should be ${monthAcct} as you are setting the value in modelAndView or in your case model. The monthAcct is not a field of the th:object.
You can't use a List as a form backing object. You need another object that wraps your array (and to use that object as your backing object). For example:
Java:
// Backing object
class BackingObject {
List<MonthlyAccountingTracker> accounts;
public BackingObject(List<MonthlyAccountingTracker> accounts) {
this.accounts = accounts;
}
// Put in getters/setters...
}
// Controller
#RequestMapping(value="/accounting/client/{id}")
public String accountingDetails(#PathVariable("id")Client client, MonthlyAccountingTracker monthlyAccountingTracker, Model model) {
model.addAttribute("client",clientRepository.findById(client.getId()));
model.addAttribute("form", new BackingObject(monthlyAccountingTrackerRepository.findByClient(client)));
return "accounting";
}
Form:
<form th:object="${form}">
.
.
.
<tr th:each="acct, stat : *{accounts}">
<td th:text="${acct.year}"></td>
<td>
<input type="number" class="text-left form-control" th:field="*{accounts[__${stat.index}__].janAmt}"/>
</td>
</tr>
.
.
.
</form>
You should use an * instead of $ sign for monthAcct field:
<input type="number" class="text-left form-control" th:field="*{monthAcct[__${stat.index}__].janAmt}"/>
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 am populate a List of String as radio button in the JSP. I add the List to the ModelMap but still the below Exception occurred. What am i missing please?
Attribute 'items' must be an array, a Collection or a Map:
java.lang.IllegalArgumentException: Attribute 'items' must be an
array, a Collection or a Map
#Controller
public class EmployeeController {
#Autowired
private EmployeeManager employeeManager;
#RequestMapping(value = {"/"}, method = RequestMethod.GET)
public String homePage(ModelMap map) {
map.addAttribute("employee", new Employee());
populateDepartments(map);
return "addEmployee";
}
private void populateDepartments(ModelMap map){
List<String> departments = new ArrayList<String>();
departments.add("Dept 1");
departments.add("Dept 2");
map.addAttribute("departments",departments);
}
}
addEmployee.jsp:
<form:form method="post" action="add" commandName="employee">
<table>
<tr>
<td><form:label path="name">Name</td>
<td><form:input path="name" /></td>
<td>Address</td>
<td><form:input path="address" /></td>
<td>Departments</td>
<td><form:radiobuttons path="empDepartment" items="${departments}"/></td>
</tr>
<tr>
<td colspan="2">
<input type="submit" value="Add"/>
</td>
</tr>
</table>
</form:form>
The entity
#Entity
public class Employee {
#Id
#GeneratedValue(strategy=GenerationType.AUTO)
private Integer id;
private String name;
private String address;
#Transient
private String empDepartment;
}
I have a table cards which is related to some other master tables (divisions and units) with one to one relation.
On UI I am showing drop downs to select the division and unit values for card, this is the function to create form in CardController.java
#RequestMapping(value = "/addCardForm", method = RequestMethod.GET)
public String addCardForm(ModelMap map)
{
map.addAttribute("divisions", divisionService.getAllDivisions());
map.addAttribute("units", unitService.getAllUnits());
return "admin/addCard";
}
addCard.jsp :
<form:form method="post" action="addCard">
<table cellspacing="10" id="card-table">
<tr>
<td><label for="division" class="control-label">Division : </label></td>
<td><select name="division" class="selectpicker">
<option>Select</option>
<c:forEach items="${divisions}" var="division">
<option value="${division.id}">${division.name}</option>
</c:forEach>
</select></td>
</tr>
<tr>
<td><label for="unit" class="control-label">Unit : </label></td>
<td><select name="unit" class="selectpicker">
<option>Select</option>
<c:forEach items="${units}" var="unit">
<option value="${unit.id}">${unit.name}</option>
</c:forEach>
</select></td>
</tr>
<tr>
<td><button type="submit" class="btn btn-primary">Submit</button></td>
<td></td>
</tr>
</table>
</form>
Drop downs are populated with the data but on submitting form its not setting division or unit to the cardEntity object they are set to null, this is the addCard function in controller :
#RequestMapping(value = "/addCard", method = RequestMethod.POST)
public String addCard(#ModelAttribute(value="card") CardEntity card, BindingResult result)
{
cardService.addCard(card);
//card.getDivision(); -- this is null
return "redirect:/card";
}
There are other fields which are added in the card except drop downs.
CardEntity.java
#Entity
#Table(name="cards")
#Proxy(lazy=false)
public class CardEntity {
#Id
#Column(name="id")
#GeneratedValue(strategy = GenerationType.AUTO)
private Integer id;
#OneToOne
#JoinColumn(name="division_id", referencedColumnName="id")
private DivisionEntity division;
#OneToOne
#JoinColumn(name="unit_of_qty_id", referencedColumnName="id")
private UnitEntity units;
public DivisionEntity getDivision() {
return division;
}
public void setDivision(DivisionEntity division) {
this.division = division;
}
public UnitEntity getUnits() {
return units;
}
public void setUnits(UnitEntity units) {
this.units = units;
}
}
Edit :
I have to set divisionEntity and unitEntity manually :
#RequestMapping(value = "/addCard", method = RequestMethod.POST)
public String addCard(HttpServletRequest request, #ModelAttribute(value="card") CardEntity card, BindingResult result)
{
card.setDivision(divisionService.findOne(Integer.parseInt(request.getParameter("division"))));
card.setUnit(unitService.findOne(Integer.parseInt(request.getParameter("unit"))));
cardService.addCard(card);
return "redirect:/card";
}
So I guess the problem is divisionEntity and unitEntity objects are not set in card after submitting the form.
You can do this too:
<tr>
<td><label for="division.id" class="control-label">Division : </label></td>
<td><select name="division.id" class="selectpicker">
<option>Select</option>
<c:forEach items="${divisions}" var="division">
<option value="${division.id}">${division.name}</option>
</c:forEach>
</select></td>
</tr>
<tr>
<td><label for="unit.id" class="control-label">Unit : </label></td>
<td><select name="unit.id" class="selectpicker">
<option>Select</option>
<c:forEach items="${units}" var="unit">
<option value="${unit.id}">${unit.name}</option>
</c:forEach>
</select></td>
</tr>
The rest is same as your original. But:
Make sure you have a default constrcutor in Division class;
The CardEntity should have getUnit() and setUnit(), not getUnits() and setUnits().
Hope it help.