JSP Input value To Java Method -> HttpServletRequest gives NULL value - java

Hello Guys.
I have simple Storage page whichs display all Products from DB.
I set to each of them with Unique name to change the Amount of product.
When i want to catch this value in Java method its returns me null.
Can you help me what i need to do to corretly catching value's in this text inputs ?
Controller :
#Controller
public class StoragePageController extends HttpServlet {
#GET
#RequestMapping(value = "/storage/subamount/{id}")
public String substractTheAmountValue(#PathVariable("id") int id, Model model, HttpServletRequest request) {
String amount_req = request.getParameter("amount_sub_" + id);
System.out.println(amount_req);
return null;
}
}
JSP fragment :
<c:set var="licznik" value="${recordStartCounter }" />
<div align="center">
<table width="1000" border="0" cellpadding="6" cellspacing="2">
<c:forEach var="u" items="${productList }">
<c:set var="licznik" value="${licznik+1}" />
<tr onmouseover="changeTrBg(this)" onmouseout="defaultTrBg(this)">
<td align="right"><c:out value="${licznik }" /></td>
<td align="left"><c:out value="${u.description }" /></td>
<td align="left"><c:out value="${u.amount }" /></td>
<td align="center"><input type="text" name="amount_sub_${licznik}" id="amount_sub_${licznik}"></td>
<td align="center"><input type="button" value="Substract the value" onclick="window.location.href='${pageContext.request.contextPath}/storage/subamount/${licznik}'"/></td>
</tr>
</c:forEach>
</table>

You should be having your API controller like the one given below given that your UI is posting the data to your API / Controller (assuming you are using the latest version of Spring Boot). You have a #Get mapping which does not accept request payload in the body.
#RestController
public class StoragePageController {
#PostMapping(value = "/storage/subamount/{id}", produces = {"application/json"})
public String substractTheAmountValue(#PathVariable("id") int id, Model model) {
String amount_req = id;
System.out.println(amount_req);
return null;
}
}

Related

Pass an instance of an object in each loop from thymeleaf html file to Spring controller

I have a problem with passing an instance of "threads" in each loop from thymeleaf to Spring controller using a submit button. I'm trying to solve this by using the annotation #ModelAttribute, but one more instance of MessageThread is creating.
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:th="http://www.thymeleaf.org" xmlns:form="http://www.w3.org/1999/html">
<head>
<meta charset="UTF-8">
<title>Форум</title>
</head>
<body>
<form method="post">
<input type="text" name="header">
<input type="text" name="text">
<button name="newThread" type="submit">Создать тред</button>
</form>
<table>
<thead>
<tr>
<th> Тред </th>
<th> ОП пост </th>
</tr>
</thead>
<tbody>
<tr th:if="${threads.isEmpty()}">
<td colspan="2"> Нет доступных тредов </td>
</tr>
<div>
<th:block th:each="thread : ${threads}">
<td th:type="id"><span th:text="${thread.getId()}"></span></td>
<td><span th:text="${thread.getHeader()}"> Title </span></td>
<td><span th:text="${thread.getText()}"> Title </span></td>
<form th:object="${thread}" th:method="post">
<td><button name="inThread" type="submit">В тред</button></td>
</form>
</th:block>
</div>
</tbody>
</table>
</body>
</html>
I can't find a way to pass an instance of "threads" from thymeleaf. All that I want is to press Submit button and pass the ${thread} to "toThread" method.
My controller:
#Controller
public class ThreadController {
private final MessageService messageService;
#Autowired
public ThreadController(MessageService messageService) {
this.messageService = messageService;
}
#GetMapping("/")
public String showThreads(Model model)
{
model.addAttribute("threads", messageService.getThreads());
return "threads-view";
}
#PostMapping(value = "/", params = "newThread")
public String addThread(Model model,
#RequestParam("header") String header,
#RequestParam("text") String text)
{
model.addAttribute("threads", messageService.getThreads());
messageService.addThread(header, text);
return "redirect:/";
}
#PostMapping(value = "/", params = "inThread")
public String toThread(#ModelAttribute("thread") MessageThread thread) {
System.out.println(thread.getId() + " " + thread.getHeader());
return "redirect:/thread:";
}
}
MessageThread class:
package com.project.imageboard.model;
import java.util.ArrayList;
import java.util.List;
public class MessageThread {
private String header;
private String text;
private int id;
private List<Message> messages = new ArrayList<>();
public MessageThread(String header, String text) {
this.header = header;
messages.add(new Message(text));
this.text = text;
this.id = messages.get(0).getId();
}
public int getId() {
return id;
}
public String getText() {
return text;
}
public String getHeader() {
return header;
}
public List<Message> getMessages() {
return messages;
}
public void insertMessage(Message message){
messages.add(message);
}
}
I would be grateful for any help.
your controller seems to be ok, you are mapping a post request to "/" and expecting to receive a MessageThread object which has to be built from the content of the request, the problem is in the template: you are not sending the data in the request from the client to the server, so spring has no way to assign the correct values to a new instance of MessageThread.
If we take this block from your template:
<th:block th:each="thread : ${threads}">
<td th:type="id"><span th:text="${thread.getId()}"></span></td>
<td><span th:text="${thread.getHeader()}"> Title </span></td>
<td><span th:text="${thread.getText()}"> Title </span></td>
<form th:object="${thread}" th:method="post">
<td><button name="inThread" type="submit">В тред</button></td>
</form>
</th:block>
1) The form tag is missing the action attribute, try adding something like th:action="#{/}" to target your request mapping on the server side.
2) You are not sending the actual content of the thread object to the server, for this you have to add input tags with the name of the imput matching the name of the field in the MessageThread object you want to populate. Something like the following:
<input type="hidden" th:field="*{header}" />
<input type="hidden" th:field="*{text}" />
In this example th:field is creating the name and value attributes for the input tags so you don't need to do it manually.
To sum up, there is no way to pass an actual "instance" of an object from the html running in the client to the java app running on the server, you can only send data using HTTP and parse that data on the server. The ModelAttribute annotation is instructing Spring to inspect an object (MessageThread in this case) and find in the data sent through the request the matching values to populate the object.
Hope this helps.

Spring Thymeleaf - How to map to a URL that requires an ID that is passed from HTML to the controller?

In my application the user clicks a link to edit an exam of their choice.
When they click the link it should open the editExam.html and the URL contains the ID of the exam they selected.
When I click the link the URL is correct (it contains the exam ID) however it doesn't display the editExam.html page it just displays my main page (the default localhost page)
allSubjects.html (the page where the user selects which exam to edit)
<h4> exams:</h4>
<div th:each="exam : ${subject.exam}">
<h4 th:text="${exam.examTitle}"/>
<a th:href="#{/editExam.html(id=${exam.examId})}">Edit Exam</a>
editExam.html (this is the page I can't display)
<form action="#"th:action="#{/editExam.html{examId}}" th:object="${exam}" method="put">
<table>
<tr>
<td> Exam Title:</td>
<td><input type="text" th:field="*{examTitle}" th:text="${exam.examTitle}"/></td>
<!-- <td th:if="${#fields.hasErrors('examTitlee')}" th:errors="*{examTitle}">error message</td> -->
</tr>
<tr>
<td> Exam grade worth </td>
<td><input th:field="*{examGradeWorth}" /></td>
<!-- <td th:if="${#fields.hasErrors('examGradeWorth')}" th:errors="*{examGradeWorth}">error message</td> -->
</tr>
<tr>
<td>examGradeAchieved</td>
<td><input th:field="*{examGradeAchieved}"/></td>
</tr>
<tr>
<td><button type="submit">Submit post</button></td>
</tr>
</table>
</div>
</form>
My controller:
//Update an exam
#RequestMapping(value = "/editExam.html{examId}", method = { RequestMethod.POST, RequestMethod.PUT})
public String editExam(#ModelAttribute("exam") #PathVariable(value = "examId")Long examId, #RequestBody Exam exam,Model model, BindingResult result) {
examRepository.findOne(examId);
model.addAttribute("examTitle", exam.getExamTitle());
model.addAttribute("examGradeWorth", exam.getExamGradeWorth());
model.addAttribute("examGradeAchieved", exam.getExamGradeAchieved());
exam.setExamTitle(exam.getExamTitle());
exam.setExamGradeWorth(exam.getExamGradeWorth());
exam.setExamGradeAchieved(exam.getExamGradeAchieved());
examRepository.save(exam);
return "editExam";
}
This code was displaying the editExam page before I added "RequestMethod.POST" to the controller.
Try to change
<a th:href="#{/editExam.html(id=${exam.examId})}">Edit Exam</a>
to
<a th:href="#{/exam/{id}(id=${exam.examId})}">Edit Exam</a>
and in your Controller
#RequestMapping(value = "/editExam.html{examId}", method = { RequestMethod.POST, RequestMethod.PUT})
to
#RequestMapping(value = "/exam/{examId}", method = { RequestMethod.POST, RequestMethod.PUT})
This url does not look correct. It should be /editExam.html?id=1, without a /.
I noticed that you are mixing request params and path variables. In your controller you expect #PathVariable(value = "examId") but in the view you are specifying request parameter instead of path variable.
Check this post: #RequestParam vs #PathVariable

Spring MVC controller returning null modelAttribute when #Valid fails

I've been scratching my head for a long time on this one. I'm working on a web app that has over 20 controllers and i'm having this strange issue with only one of them. No matter how long I compare with a working one i cannot seem to find any differences. Basically, I am able to add an Interface entity no problem as long as it does not fail validation. If i try to save say a completely empty object (or leave one of the required fields blank for example) I get this error :
java.lang.IllegalStateException: Neither BindingResult nor plain target object for bean name 'intface' available as request attribute
Here is the relevant code :
View :
<form:form method="POST" modelAttribute="intface" class="form-style-7">
<form:input type="hidden" path="id" id="id"/>
<table>
<tr>
<td><label for="name">Name: </label> </td>
<td><form:input path="name" id="name"/></td>
<td><form:errors path="name" cssClass="error"/></td>
</tr>
<tr>
<td><label for="type">Type: </label> </td>
<td><form:input path="type" id="type"/></td>
<td><form:errors path="type" cssClass="error"/></td>
</tr>
<tr>
<td><label for="ip">IP: </label> </td>
<td><form:input path="ip" id="ip"/></td>
<td><form:errors path="ip" cssClass="error"/></td>
</tr>
<tr>
<td><label for="port">Port: </label> </td>
<td><form:input path="port" id="port"/></td>
<td><form:errors path="port" cssClass="error"/></td>
</tr>
<tr>
<td><label for="test">Test: </label> </td>
<td><form:checkbox path="test" id="test"/></td>
<td><form:errors path="test" cssClass="error"/></td>
</tr>
<tr>
<td colspan="3">
<c:choose>
<c:when test="${edit}">
<input type="submit" value="Update Interface"/>
</c:when>
<c:otherwise>
<input type="submit" value="Add Interface"/>
</c:otherwise>
</c:choose>
</td>
</tr>
</table>
</form:form>
Controller :
#RequestMapping(value = {"/newInterface"}, method = RequestMethod.POST)
public String saveInterface(#Valid Interface intface, BindingResult result, ModelMap model)
{
if (result.hasErrors())
{
return "interfaceDataAccess";
}
interfaceService.saveInterface(intface);
return "redirect:/interfaces/list";
}
Entity :
#RequestMapping(value = {"/newInterface"}, method = RequestMethod.POST)
public String saveInterface(#Valid Interface intface, BindingResult result, ModelMap model)
{
if (result.hasErrors())
{
return "interfaceDataAccess";
}
interfaceService.saveInterface(intface);
return "redirect:/interfaces/list";
}
You will notice this #Unique annotation which is something i built. Essentially it allows me to check if one of the values entered by the user already exists and that works just fine on my other entities. I tried removing it completely and i still get this exception as soon as validation fails. I literally have over 20 controllers that work the exact same way, line for line, and they work fine. This completely baffles me and i'm sure it's something really stupid. I can provide the MVC code for a working entity for comparison if needed.
Thanks
* EDIT *
Here is some working code for my heightUnit class:
Controller :
#RequestMapping(value = {"/newHeightUnit"}, method = RequestMethod.GET)
public String newHeightUnit(ModelMap model)
{
HeightUnit heightUnit = new HeightUnit();
model.addAttribute("heightUnit", heightUnit);
model.addAttribute("edit", false);
return "heightUnitDataAccess";
}
#RequestMapping(value = {"/newHeightUnit"}, method = RequestMethod.POST)
public String saveHeightUnit(#Valid HeightUnit heightUnit, BindingResult result, ModelMap model)
{
if (result.hasErrors())
{
return "heightUnitDataAccess";
}
heightUnitService.saveHeightUnit(heightUnit);
return "redirect:/heightUnits/list";
}
View :
<form:form method="POST" modelAttribute="heightUnit" class="form-style-7">
<form:input type="hidden" path="id" id="id"/>
<ul>
<li>
<label for="name">Name: </label>
<form:input path="name" id="name"/>
<span><form:errors path="name" cssClass="error"/></span>
</li>
<c:choose>
<c:when test="${edit}">
<input type="submit" value="Update Height Unit"/>
</c:when>
<c:otherwise>
<input type="submit" value="Add Height Unit"/>
</c:otherwise>
</c:choose>
</ul>
</form:form>
Entity :
private int id;
private String name;
#Id
#GeneratedValue
public int getId()
{
return id;
}
public void setId(int id)
{
this.id = id;
}
#Size(min = 1, max = 15, message = "Name must be between 1 and 15 characters long")
#Unique(entityType = "heightUnit", field = "name", message = "The name needs to be unique!")
#Column(nullable = false, unique = true, length = 15)
public String getName()
{
return name;
}
public void setName(String name)
{
this.name = name;
}
#Override
public boolean equals(Object obj)
{
if (obj instanceof HeightUnit)
{
HeightUnit heightUnit = (HeightUnit)obj;
if (id == heightUnit.getId())
{
return true;
}
}
return false;
}
#Override
public int hashCode()
{
int hash = 7;
hash = 37 * hash + this.id;
hash = 37 * hash + Objects.hashCode(this.name);
return hash;
}
This works perfectly and i'm getting the errors back on the form when attempting to break a validation rule.
Your issue is that you are heading to the form page and it's looking for a pre-instantiated model that aIt's not available. What you have to do is to add the Interface object as an attribute of the model:
model.addAttribute("intface", new Interface());
Remember to add it to the #RequestMapping annoted method that takes you to the form page, something like this:
#RequestMapping(value = "/", method = RequestMethod.GET)
public String displayNewInterfaceLogin(Model model) {
model.addAttribute("intface", new Interface());
return "newInterface";
}
i FINALLY figured this out. For whatever the crap reason, i had to change the signature of my controller's saveInterface method to this :
public String saveInterface(#Valid #ModelAttribute("intface") Interface intface, BindingResult result, ModelMap model)
I have NO IDEA why only THIS entity complains about this when the CRUD operations of all my entity types are built the exact same way but there you go!
Hope this can help someone eventually!

How to bind an object list with thymeleaf?

I am having a lot of difficulty with POSTing back a form to the controller, which should contain simply an arraylist of objects that the user may edit.
The form loads up correctly, but when it's posted, it never seems to actually post anything.
Here is my form:
<form action="#" th:action="#{/query/submitQuery}" th:object="${clientList}" method="post">
<table class="table table-bordered table-hover table-striped">
<thead>
<tr>
<th>Select</th>
<th>Client ID</th>
<th>IP Addresss</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr th:each="currentClient, stat : ${clientList}">
<td><input type="checkbox" th:checked="${currentClient.selected}" /></td>
<td th:text="${currentClient.getClientID()}" ></td>
<td th:text="${currentClient.getIpAddress()}"></td>
<td th:text="${currentClient.getDescription()}" ></td>
</tr>
</tbody>
</table>
<button type="submit" value="submit" class="btn btn-success">Submit</button>
</form>
Above works fine, it loads up the list correctly. However, when I POST, it returns a empty object (of size 0). I believe this is due to the lack of th:field, but anyway here is controller POST method:
...
private List<ClientWithSelection> allClientsWithSelection = new ArrayList<ClientWithSelection>();
//GET method
...
model.addAttribute("clientList", allClientsWithSelection)
....
//POST method
#RequestMapping(value="/submitQuery", method = RequestMethod.POST)
public String processQuery(#ModelAttribute(value="clientList") ArrayList clientList, Model model){
//clientList== 0 in size
...
}
I have tried adding a th:field but regardless of what I do, it causes an exception.
I've tried:
...
<tr th:each="currentClient, stat : ${clientList}">
<td><input type="checkbox" th:checked="${currentClient.selected}" th:field="*{}" /></td>
<td th th:field="*{currentClient.selected}" ></td>
...
I cannot access currentClient (compile error), I can't even select clientList, it gives me options like get(), add(), clearAll() etc, so it things it should have an array, however, I cannot pass in an array.
I've also tried using something like th:field=${}, this causes runtime exception
I've tried
th:field = "*{clientList[__currentClient.clientID__]}"
but also compile error.
Any ideas?
UPDATE 1:
Tobias suggested that I need to wrap my list in a wraapper. So that's what I did:
ClientWithSelectionWrapper:
public class ClientWithSelectionListWrapper {
private ArrayList<ClientWithSelection> clientList;
public List<ClientWithSelection> getClientList(){
return clientList;
}
public void setClientList(ArrayList<ClientWithSelection> clients){
this.clientList = clients;
}
}
My page:
<form action="#" th:action="#{/query/submitQuery}" th:object="${wrapper}" method="post">
....
<tr th:each="currentClient, stat : ${wrapper.clientList}">
<td th:text="${stat}"></td>
<td>
<input type="checkbox"
th:name="|clientList[${stat.index}]|"
th:value="${currentClient.getClientID()}"
th:checked="${currentClient.selected}" />
</td>
<td th:text="${currentClient.getClientID()}" ></td>
<td th:text="${currentClient.getIpAddress()}"></td>
<td th:text="${currentClient.getDescription()}" ></td>
</tr>
Above loads fine:
Then my controller:
#RequestMapping(value="/submitQuery", method = RequestMethod.POST)
public String processQuery(#ModelAttribute ClientWithSelectionListWrapper wrapper, Model model){
...
}
The page loads correctly, the data is displayed as expected. If I post the form without any selection I get this:
org.springframework.expression.spel.SpelEvaluationException: EL1007E:(pos 0): Property or field 'clientList' cannot be found on null
Not sure why it's complaining
(In the GET Method it has: model.addAttribute("wrapper", wrapper);)
If I then make a selection, i.e. tick the first entry:
There was an unexpected error (type=Bad Request, status=400).
Validation failed for object='clientWithSelectionListWrapper'. Error count: 1
I'm guessing my POST controller is not getting the clientWithSelectionListWrapper. Not sure why, since I have set the wrapper object to be posted back via the th:object="wrapper" in the FORM header.
UPDATE 2:
I've made some progress! Finally the submitted form is being picked up by the POST method in controller. However, all the properties appear to be null, except for whether the item has been ticked or not. I've made various changes, this is how it is looking:
<form action="#" th:action="#{/query/submitQuery}" th:object="${wrapper}" method="post">
....
<tr th:each="currentClient, stat : ${clientList}">
<td th:text="${stat}"></td>
<td>
<input type="checkbox"
th:name="|clientList[${stat.index}]|"
th:value="${currentClient.getClientID()}"
th:checked="${currentClient.selected}"
th:field="*{clientList[__${stat.index}__].selected}">
</td>
<td th:text="${currentClient.getClientID()}"
th:field="*{clientList[__${stat.index}__].clientID}"
th:value="${currentClient.getClientID()}"
></td>
<td th:text="${currentClient.getIpAddress()}"
th:field="*{clientList[__${stat.index}__].ipAddress}"
th:value="${currentClient.getIpAddress()}"
></td>
<td th:text="${currentClient.getDescription()}"
th:field="*{clientList[__${stat.index}__].description}"
th:value="${currentClient.getDescription()}"
></td>
</tr>
I also added a default param-less constructor to my wrapper class and added a bindingResult param to POST method (not sure if needed).
public String processQuery(#ModelAttribute ClientWithSelectionListWrapper wrapper, BindingResult bindingResult, Model model)
So when an object is being posted, this is how it is looking:
Of course, the systemInfo is supposed to be null (at this stage), but the clientID is always 0, and ipAddress/Description always null. The selected boolean is correct though for all properties. I'm sure I've made a mistake on one of the properties somewhere. Back to investigation.
UPDATE 3:
Ok I've managed to fill up all the values correctly! But I had to change my td to include an <input /> which is not what I wanted... Nonetheless, the values are populating correctly, suggesting spring looks for an input tag perhaps for data mapping?
Here is an example of how I changed the clientID table data:
<td>
<input type="text" readonly="readonly"
th:name="|clientList[${stat.index}]|"
th:value="${currentClient.getClientID()}"
th:field="*{clientList[__${stat.index}__].clientID}"
/>
</td>
Now I need to figure out how to display it as plain data, ideally without any presence of an input box...
You need a wrapper object to hold the submited data, like this one:
public class ClientForm {
private ArrayList<String> clientList;
public ArrayList<String> getClientList() {
return clientList;
}
public void setClientList(ArrayList<String> clientList) {
this.clientList = clientList;
}
}
and use it as the #ModelAttribute in your processQuery method:
#RequestMapping(value="/submitQuery", method = RequestMethod.POST)
public String processQuery(#ModelAttribute ClientForm form, Model model){
System.out.println(form.getClientList());
}
Moreover, the input element needs a name and a value. If you directly build the html, then take into account that the name must be clientList[i], where i is the position of the item in the list:
<tr th:each="currentClient, stat : ${clientList}">
<td><input type="checkbox"
th:name="|clientList[${stat.index}]|"
th:value="${currentClient.getClientID()}"
th:checked="${currentClient.selected}" />
</td>
<td th:text="${currentClient.getClientID()}" ></td>
<td th:text="${currentClient.getIpAddress()}"></td>
<td th:text="${currentClient.getDescription()}" ></td>
</tr>
Note that clientList can contain null at
intermediate positions. Per example, if posted data is:
clientList[1] = 'B'
clientList[3] = 'D'
the resulting ArrayList will be: [null, B, null, D]
UPDATE 1:
In my exmple above, ClientForm is a wrapper for List<String>. But in your case ClientWithSelectionListWrapper contains ArrayList<ClientWithSelection>. Therefor clientList[1] should be clientList[1].clientID and so on with the other properties you want to sent back:
<tr th:each="currentClient, stat : ${wrapper.clientList}">
<td><input type="checkbox" th:name="|clientList[${stat.index}].clientID|"
th:value="${currentClient.getClientID()}" th:checked="${currentClient.selected}" /></td>
<td th:text="${currentClient.getClientID()}"></td>
<td th:text="${currentClient.getIpAddress()}"></td>
<td th:text="${currentClient.getDescription()}"></td>
</tr>
I've built a little demo, so you can test it:
Application.java
#SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
ClientWithSelection.java
public class ClientWithSelection {
private Boolean selected;
private String clientID;
private String ipAddress;
private String description;
public ClientWithSelection() {
}
public ClientWithSelection(Boolean selected, String clientID, String ipAddress, String description) {
super();
this.selected = selected;
this.clientID = clientID;
this.ipAddress = ipAddress;
this.description = description;
}
/* Getters and setters ... */
}
ClientWithSelectionListWrapper.java
public class ClientWithSelectionListWrapper {
private ArrayList<ClientWithSelection> clientList;
public ArrayList<ClientWithSelection> getClientList() {
return clientList;
}
public void setClientList(ArrayList<ClientWithSelection> clients) {
this.clientList = clients;
}
}
TestController.java
#Controller
class TestController {
private ArrayList<ClientWithSelection> allClientsWithSelection = new ArrayList<ClientWithSelection>();
public TestController() {
/* Dummy data */
allClientsWithSelection.add(new ClientWithSelection(false, "1", "192.168.0.10", "Client A"));
allClientsWithSelection.add(new ClientWithSelection(false, "2", "192.168.0.11", "Client B"));
allClientsWithSelection.add(new ClientWithSelection(false, "3", "192.168.0.12", "Client C"));
allClientsWithSelection.add(new ClientWithSelection(false, "4", "192.168.0.13", "Client D"));
}
#RequestMapping("/")
String index(Model model) {
ClientWithSelectionListWrapper wrapper = new ClientWithSelectionListWrapper();
wrapper.setClientList(allClientsWithSelection);
model.addAttribute("wrapper", wrapper);
return "test";
}
#RequestMapping(value = "/query/submitQuery", method = RequestMethod.POST)
public String processQuery(#ModelAttribute ClientWithSelectionListWrapper wrapper, Model model) {
System.out.println(wrapper.getClientList() != null ? wrapper.getClientList().size() : "null list");
System.out.println("--");
model.addAttribute("wrapper", wrapper);
return "test";
}
}
test.html
<!DOCTYPE html>
<html>
<head></head>
<body>
<form action="#" th:action="#{/query/submitQuery}" th:object="${wrapper}" method="post">
<table class="table table-bordered table-hover table-striped">
<thead>
<tr>
<th>Select</th>
<th>Client ID</th>
<th>IP Addresss</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr th:each="currentClient, stat : ${wrapper.clientList}">
<td><input type="checkbox" th:name="|clientList[${stat.index}].clientID|"
th:value="${currentClient.getClientID()}" th:checked="${currentClient.selected}" /></td>
<td th:text="${currentClient.getClientID()}"></td>
<td th:text="${currentClient.getIpAddress()}"></td>
<td th:text="${currentClient.getDescription()}"></td>
</tr>
</tbody>
</table>
<button type="submit" value="submit" class="btn btn-success">Submit</button>
</form>
</body>
</html>
UPDATE 1.B:
Below is the same example using th:field and sending back all other attributes as hidden values.
<tbody>
<tr th:each="currentClient, stat : *{clientList}">
<td>
<input type="checkbox" th:field="*{clientList[__${stat.index}__].selected}" />
<input type="hidden" th:field="*{clientList[__${stat.index}__].clientID}" />
<input type="hidden" th:field="*{clientList[__${stat.index}__].ipAddress}" />
<input type="hidden" th:field="*{clientList[__${stat.index}__].description}" />
</td>
<td th:text="${currentClient.getClientID()}"></td>
<td th:text="${currentClient.getIpAddress()}"></td>
<td th:text="${currentClient.getDescription()}"></td>
</tr>
</tbody>
When you want to select objects in thymeleaf, you dont actually need to create a wrapper for the purpose of storing a boolean select field. Using dynamic fields as per the thymeleaf guide with syntax th:field="*{rows[__${rowStat.index}__].variety}" is good for when you want to access an already existing set of objects in a collection. Its not really designed for doing selections by using wrapper objects IMO as it creates unnecessary boilerplate code and is sort of a hack.
Consider this simple example, a Person can select Drinks they like. Note: Constructors, Getters and setters are omitted for clarity. Also, these objects are normally stored in a database but I am using in memory arrays to explain the concept.
public class Person {
private Long id;
private List<Drink> drinks;
}
public class Drink {
private Long id;
private String name;
}
Spring controllers
The main thing here is that we are storing the Person in the Model so we can bind it to the form within th:object.
Secondly, the selectableDrinks are the drinks a person can select on the UI.
#GetMapping("/drinks")
public String getDrinks(Model model) {
Person person = new Person(30L);
// ud normally get these from the database.
List<Drink> selectableDrinks = Arrays.asList(
new Drink(1L, "coke"),
new Drink(2L, "fanta"),
new Drink(3L, "sprite")
);
model.addAttribute("person", person);
model.addAttribute("selectableDrinks", selectableDrinks);
return "templates/drinks";
}
#PostMapping("/drinks")
public String postDrinks(#ModelAttribute("person") Person person) {
// person.drinks will contain only the selected drinks
System.out.println(person);
return "templates/drinks";
}
Template code
Pay close attention to the li loop and how selectableDrinks is used to get all possible drinks that can be selected.
The checkbox th:field really expands to person.drinks since th:object is bound to Person and *{drinks} simply is the shortcut to referring to a property on the Person object. You can think of this as just telling spring/thymeleaf that any selected Drinks are going to be put into the ArrayList at location person.drinks.
<!DOCTYPE html>
<html lang="en" xmlns="http://www.w3.org/1999/xhtml"
xmlns:th="http://www.thymeleaf.org"
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" >
<body>
<div class="ui top attached segment">
<div class="ui top attached label">Drink demo</div>
<form class="ui form" th:action="#{/drinks}" method="post" th:object="${person}">
<ul>
<li th:each="drink : ${selectableDrinks}">
<div class="ui checkbox">
<input type="checkbox" th:field="*{drinks}" th:value="${drink.id}">
<label th:text="${drink.name}"></label>
</div>
</li>
</ul>
<div class="field">
<button class="ui button" type="submit">Submit</button>
</div>
</form>
</div>
</body>
</html>
Any way...the secret sauce is using th:value=${drinks.id}. This relies on spring converters. When the form is posted, spring will try recreate a Person and to do this it needs to know how to convert any selected drink.id strings into the actual Drink type. Note: If you did th:value${drinks} the value key in the checkbox html would be the toString() representation of a Drink which is not what you want, hence need to use the id!. If you are following along, all you need to do is create your own converter if one isn't already created.
Without a converter you will receive an error like
Failed to convert property value of type 'java.lang.String' to required type 'java.util.List' for property 'drinks'
You can turn on logging in application.properties to see the errors in detail.
logging.level.org.springframework.web=TRACE
This just means spring doesn't know how to convert a string id representing a drink.id into a Drink. The below is an example of a Converter that fixes this issue. Normally you would inject a repository in get access the database.
#Component
public class DrinkConverter implements Converter<String, Drink> {
#Override
public Drink convert(String id) {
System.out.println("Trying to convert id=" + id + " into a drink");
int parsedId = Integer.parseInt(id);
List<Drink> selectableDrinks = Arrays.asList(
new Drink(1L, "coke"),
new Drink(2L, "fanta"),
new Drink(3L, "sprite")
);
int index = parsedId - 1;
return selectableDrinks.get(index);
}
}
If an entity has a corresponding spring data repository, spring automatically creates the converters and will handle fetching the entity when an id is provided (string id seems to be fine too so spring does some additional conversions there by the looks). This is really cool but can be confusing to understand at first.

Using #Valid is throwing exceptions & not working in basic Spring 3.0 MVC program

I am learning Spring MVC using Spring In Action 3rd Action, I have implemented the basic program which shows the user registration form and once we submit the form, it will be validated using #Valid.
Here is my Spring Controller:
#Controller
#RequestMapping("/spitter")
public class SpitterController {
private final SpitterService spitterService;
#Inject
public SpitterController(SpitterService spitterService) {
this.spitterService = spitterService;
}
#RequestMapping(method = RequestMethod.GET, params = "new")
public String createSpitterProfile(Model model) {
Spittle spittle = new Spittle();
model.addAttribute(spittle);
model.addAttribute(new Spitter());
return "spittles/edit";
}
#RequestMapping(method = RequestMethod.POST)
public String addSpitterFromForm(#Valid Spitter spitter,
BindingResult bindingResult) {
if (bindingResult.hasErrors()) {
return "spittles/edit";
}
spitterService.saveSpitter(spitter);
return "redirect:/spitter/" + spitter.getUsername();
}
}
Here is my Spitter class file:
package com.habuma.spitter.domain;
import java.util.List;
import javax.validation.constraints.Pattern;
import javax.validation.constraints.Size;
public class Spitter {
private Long id;
#Size(min = 3, max = 20, message = "User name must be between 3 and 20 characters long.")
#Pattern(regexp = "^[a-zA-Z0-9]+$", message = "Username must be alphanumeric with no spaces")
private String username;
#Size(min = 6, max = 20, message = "The password must be atleast 6 characters long.")
private String password;
#Size(min = 3, max = 50, message = "Your full name must be between 3 and 50 characters long.")
private String fullName;
#Pattern(regexp = "[A-Za-z0-9._%+-]+#[A-Za-z0-9.-]+\\.[A-Za-z]{2,4}", message = "Invalid email address.")
private String email;
private List<Spittle> spittles;
private boolean updateByEmail;
......Setters & Getters.....
#Override
public boolean equals(Object obj) {
Spitter other = (Spitter) obj;
return other.fullName.equals(fullName)
&& other.username.equals(username)
&& other.password.equals(password);
}
#Override
public int hashCode() {
return super.hashCode();
}
}
This is my edit.jsp file which is shown to the user for registration:
<%# taglib prefix="sf" uri="http://www.springframework.org/tags/form"%>
<div>
<h2>Create a free Spitter account</h2>
<sf:form method="POST" modelAttribute="spitter" enctype="multipart/form-data">
<fieldset>
<table cellspacing="0">
<tr>
<th><label for="user_full_name">Fullname:</label></th>
<td><sf:input path="fullName" size="15" id="user_full_name" />
<sf:errors path="fullName" cssClass="error" /></td>
</tr>
<tr>
<th><label for="user_screen_name">Username:</label></th>
<td><sf:input path="username" size="15" maxlength="15"
id="user_screen_name" /> <small id="username_msg">No spaces,please.</small>
<sf:errors path="username" cssClass="error" /></td>
</tr>
<tr>
<th><label for="user_password">Password:</label></th>
<td><sf:password path="password" size="30" showPassword="true"
id="user_password" /> <small>6
characters or more (betricky!)</small> <sf:errors path="password"
cssClass="error" /></td>
</tr>
<tr>
<th><label for="user_email">EmailAddress:</label></th>
<td><sf:input path="email" size="30" id="user_email" /> <small>In
case you forget something</small> <sf:errors path="email"
cssClass="error" /></td>
</tr>
<tr>
<th></th>
<td><sf:checkbox path="updateByEmail"
id="user_send_email_newsletter" /> <label
for="user_send_email_newsletter">Send me email updates!</label></td>
</tr>
<tr>
<th><label for="image">Profile image:</label></th>
<td><input name="image" type="file" />
</tr>
<tr>
<th></th>
<td><input name="commit" type="submit"
value="I accept.Createmyaccount." /></td>
</tr>
</table>
</fieldset>
</sf:form>
</div>
To load the form I am accessing the URL as : http://localhost:8081/SpringInAction3/spitter?new, once the form is loaded I am just submitting the form without entering any details so that I can check if my form is getting validated or not. But I am getting below exception:
java.lang.NullPointerException
com.habuma.spitter.domain.Spitter.equals(Spitter.java:87)
org.hibernate.validator.engine.resolver.SingleThreadCachedTraversableResolver$TraversableHolder.equals(SingleThreadCachedTraversableResolver.java:138)
java.util.HashMap.get(HashMap.java:305)
org.hibernate.validator.engine.resolver.SingleThreadCachedTraversableResolver.isReachable(SingleThreadCachedTraversableResolver.java:45)
org.hibernate.validator.engine.ValidatorImpl.isValidationRequired(ValidatorImpl.java:757)
org.hibernate.validator.engine.ValidatorImpl.validateConstraint(ValidatorImpl.java:324)
org.hibernate.validator.engine.ValidatorImpl.validateConstraintsForRedefinedDefaultGroup(ValidatorImpl.java:273)
org.hibernate.validator.engine.ValidatorImpl.validateConstraintsForCurrentGroup(ValidatorImpl.java:256)
org.hibernate.validator.engine.ValidatorImpl.validateInContext(ValidatorImpl.java:210)
org.hibernate.validator.engine.ValidatorImpl.validate(ValidatorImpl.java:119)
org.springframework.validation.beanvalidation.SpringValidatorAdapter.validate(SpringValidatorAdapter.java:106)
org.springframework.validation.DataBinder.validate(DataBinder.java:760)
I am getting NullPointerException in my equals method of Splitter class. Please let me know where I am doing mistake?
Edit:
When I tried to print the values of the fields in my Spitter object I am getting null for all the fields so that is causing the NullPointerException.
This time I removed the equals and hashCode methods from my Spitter class, now when I am submitting the form, the validation is not happening and the page is going to http://localhost:8081/SpringInAction3/spitter/null without showing any errors.
Why the validation is not happening in this case? Also if I just follow the steps in that book, I am getting NullPointerException which is not expected. Please let me know where I am doing mistake?
As per this SO post : #Valid (jsr 303) not working in Spring mvc 3.0, I also have the tag <mvc:annotation-driven/> in my configuration file.
I see one mistake so far. The sf:form attribute enctype is set as multipart/form-data but that is only used on file uploads, so i guess spring mvc is using the MultipartResolver instead of Data binding mechanism that binds form data to form backing objects , try changing it to application/x-www-form-urlencoded, which is the default and correct type for your case.

Categories

Resources