Spring form with thymeleaf not binding right, throwing exception: Parameter is null - spring

I have two views, one to book appointments and one to show them in a calendar view. After the booking was successful, there's a confirmation shown. The confirmation then forwards to the calendar view. I want to pass the booking infos as parameters to the calendar view so it can display the new booking accordingly in the calendar, but a null exception is thrown in that step.
I copied the template of a working form. I inspected the web request, all the necessary data is there, I think it's just not binding right.
data class EventAppointmentSearchRequest (val startDateTime: LocalDateTime, val endDateTime: LocalDateTime, val rooms: List<Room>)
/**
* Gets called when confirming a booking to add it to the DB.
*/
#PostMapping("/roomBookingConfirmation")
fun roomBookingConfirmation(model: Model, #ModelAttribute roomBookingRequest: RoomBookingRequest): String {
makeBooking(roomBookingRequest)
val date = roomBookingRequest.datetimeFrom
val start = roomBookingRequest.datetimeFrom.minusDays(date.dayOfWeek.value.toLong())
val end = roomBookingRequest.datetimeFrom.plusDays(7 - date.dayOfWeek.value.toLong())
model.addAttribute("eventAppointmentSearchRequest", EventAppointmentSearchRequest(
startDateTime = start,
endDateTime = end,
rooms = listOf(roomRepository.findByRoomName(roomBookingRequest.roomNr))
))
return "roomBookingConfirmation"
}
/**
* Displays the appointments in the calendar view according to the request
*/
#PostMapping("/calendarView")
fun calendarView(model: Model, #ModelAttribute eventAppointmentSearchRequest: EventAppointmentSearchRequest): String {
// THIS THROWS THE EXCEPTION: java.lang.IllegalArgumentException: Parameter specified as non-null is null: method ...requests.EventAppointmentSearchRequest.<init>, parameter startDateTime
...
}
<!-- /*#thymesVar id="eventAppointmentSearchRequest" type="de.tudarmstadt.pvw.tulpe.soonToBeArtifactory.requests.EventAppointmentSearchRequest"*/ -->
<form th:action="#{/calendarView}" method="post" th:object="${eventAppointmentSearchRequest}" id="forwardToCalendar" style="grid-column: span 4">
<H1 th:text="#{roomBooking.bookingConfirmed}">
Booking confirmed.
</H1>
<div class="links">
<a href="#" th:text="#{roomBooking.nowRedirecting}" onclick="forwardToCalendar()">Redirecting to
calendarView in </a> <b id="secondsLeft">7</b>
<input type="hidden" th:field="${eventAppointmentSearchRequest.startDateTime}" th:name="startDateTime" th:value="${eventAppointmentSearchRequest.startDateTime}">
<input type="hidden" th:field="${eventAppointmentSearchRequest.endDateTime}" th:name="endDateTime" th:value="${eventAppointmentSearchRequest.endDateTime}">
<input type="hidden" th:field="${eventAppointmentSearchRequest.rooms}" name="rooms[]" th:each="room: ${eventAppointmentSearchRequest.rooms}" th:value="${room.RoomId}">
</div>
...
</form>
I expect the form to just be bound correctly, I can see all the necessary data to use the constructor of EventAppointmentSearchRequest in the web inspector of my browser. Actual output is this error message:
java.lang.IllegalArgumentException: Parameter specified as non-null is null: method ...requests.EventAppointmentSearchRequest., parameter startDateTime

Something like this should suffice for the two date hidden inputs:
<input type="hidden" th:field="*{endDateTime}">
As for the third, th:field takes precedence over name and value attributes, if you look closely into the generated HTML, you'll see that the value is identical for each room hidden input, and it's the toString() on the list of Rooms. This is clearly wrong, and you need to specify each attribute of Room that you want to submit, have a look at the following article: https://www.baeldung.com/thymeleaf-list
I don't see the point in transmitting the details of the booking 2 more times between the client and server, I'd just pass a booking ID to the calendar page and have it load all the details...

Related

retain model values in case of error and showing same thymeleaf template

As per my understanding, model attributes are associated with every request and they can not survive multiple requests, until we add them as flashAttributes.
I have a simple controller method which shows a couple of options to user to select from. However, those options are being attached to thymeleaf template using model attributes.
<div class="input-group mb-3" th:each="ingredient : ${recipes.ingredients}">
<div class="input-group-prepend">
<div class="input-group-text">
<input aria-label="Checkbox for following text input" name="ingredient"
th:value="${ingredient.name}" type="checkbox">
</div>
<input aria-label="Text input with checkbox" class="form-control" disabled
th:value="${ingredient.name + ' ' + ingredient.price + 'Rs.'}"
type="text">
</div>
assume "recipes" as model attribute here, which was injected to modelMap inside the controller.
when bean validation fails, below line exectutes.
if (errors.hasErrors()) return "selectItem";
and selectItem template is re-rendered, but whatever model attributes I have set inside previous controller vanishes.
I have solved this using a #ModelAttribute method inside the same controller to set model attributes for every HTTP requests for the specific controller(until it is not in controllerAdvice for global effect).
I am being confused if I am on right way || is there any elegant way to achieve this.
Setting Model attribute for every request is kind of overhead, when I want them to be available for handful of request mappings.
When you say:
selectItem template is re-rendered, but whatever model attributes I
have set inside previous controller vanishes.
You mean that when the page reloads due to validation errors, your model attributes are no longer existing and Thymeleaf probably returns an error, because it cannot find them, correct?
If this is the case, then you have to manually prepare the same model attributes within the if statement (i.e. adding them to your MapModel):
if (errors.hasErrors()) {
map.addAttribute("recipes", recipes);
return "selectItem";
}
Alternatively, if you need this model attribute also on other pages in your controller, you can reduce code duplication by declaring a method with the ModelAttribute annotation, which will add this attribute to all models in your controller:
#ModelAttribute("recipes")
public Recipes loadRecipes() {
// get list of Recipes
return list;
}

how to display validation errors from another form submit

Imagine a form with a single field where the user provides their email. On POST, the controller action method sends a confirmation code to that email and displays a second form in which the user is supposed to enter the confirmation code they received. If the code does not match I then return the original view and I would like to display an error message next to the email ("email was not confirmed").
Sample code below:
1st view (asking the email)
<div>
Please provide the following information to sign-up:
<form:form modelAttribute="newAccountInfo" action="signup-submit.do" method="POST">
<div><form:label path="email">Email:</form:label>
<form:input type="text" path="email"/><form:errors path="email" cssClass="error" element="div"/>
</div>
...
1st view controller method
#RequestMapping(path="/signup-submit", method=RequestMethod.POST)
public String signupSubmit(HttpServletRequest request
, #ModelAttribute("newAccountInfo") #Valid NewAccountInfo newAccountInfo
, BindingResult result
, Model model) {
String confirmCode = generateRandomSecret();
// send confirmCode by email to newAccountInfo.email (omitted)
model.addAttribute("emailConfirmation" , new EmailConfirmation());
request.getSession().setAttribute("newAccountInfo", newAccountInfo);
request.getSession(false).setAttribute("email-code", confirmCode);
return View.SIGNUP_EMAIL_CONFIRMATION.name;
}
2nd view (asking the confirmation code)
<div>
Enter the confirmation code that was sent to your email:
<form:form modelAttribute="emailConfirmation" action="signup-email-confirmation-submit.do" method="POST">
<form:label path="code">Confirmation code:</form:label>
<form:input type="text" path="code"/>
<input type="submit" value="Submit" />
</form:form>
2nd view controller method
#RequestMapping(path="/signup-email-confirmation-submit", method=RequestMethod.POST)
public String signupEmailConfirmationSubmit(
#ModelAttribute("emailConfirmation") EmailConfirmation emailConfirmation
, BindingResult result
, Model model) {
if (emailConfirmation.getCode().equals(request.getSession(false).getAttribute("email-code")))
return View.SIGNUP_SUCCESS.name;
else {
model.addAttribute("newAccountInfo", request.getSession(false).getAttribute("newAccountInfo"));
request.getSession(false).invalidate();
// TODO - what should I do here ?
return View.SIGNUP.name;
}
Assuming the confirmation code was not correctly entered, what should I do in the second view controller method so that when the first view is displayed (for the second time), there is a field validation error message next to the email with description "email was not confirmed" ?
In the line marked with the TODO comment I 've tried the following:
result.rejectValue("email", null, "email was not confirmed");
… but that results in the following exception:
org.springframework.beans.NotReadablePropertyException: Invalid property 'email' of bean class [EmailConfirmation]: Bean property 'email' is not readable or has an invalid getter method: Does the return type of the getter match the parameter type of the setter?
(which makes sense as the email is not a field of EmailConfirmation).
However, the below also fails (silently, without an exception, the 1st view is displayed, I just don't see the validation error message):
result.addError(new FieldError("newAccountInfo", "email", "email could not be confirmed"));
In the end, the only way I could get this to work was to add a custom model property by adding the following (in the TODO line of the second view controller method always):
model.addAttribute("emailConfirmationError", true);
… and then modifying the 1st view as follows:
<div><form:label path="email">Email:</form:label>
<form:input type="text" path="email"/>
<form:errors path="email" cssClass="error" element="div"/>
<c:if test="${not empty emailConfirmationError}">
<span class="error">The email could not be confirmed</span>
</c:if>
</div>
The above succeeds but feels like a hack as I am not using the validation machinery of Spring MVC.
My questions are:
what is the idiomatic way to achieve the above
are there any other flawed mental models or misunderstandings present in the above code?

How to get checked checkbox value from html page to spring mvc controller

im using spring mvc framework with thymeleaf template engine
the problem is , i have 1 page with multiple check box iterated sing thymeleaf th:each iterator.When i clicked multiple check boxes i want to pass check box values to the controller method..
html content
<table>
<tr th:each="q : ${questions}">
<h3 th:text="${q.questionPattern.questionPattern}"></h3>
<div>
<p >
<input type="checkbox" class="ads_Checkbox" th:text="${q.questionName}" th:value="${q.id}" name="id"/>
</p>
</div>
</tr>
</table>
*Controller*
#RequestMapping(value = Array("/saveAssessment"), params = Array({ "save" }))
def save(#RequestParam set: String, id:Long): String = {
var userAccount: UserAccount = secService.getLoggedUserAccount
println(userAccount)
var questionSetQuestion:QuestionSetQuestion=new QuestionSetQuestion
var questionSet: QuestionSet = new QuestionSet
questionSet.setUser(userAccount)
questionSet.setSetName(set)
questionSet.setCreatedDate(new java.sql.Date(new java.util.Date().getTime))
questionSetService.addQuestionSet(questionSet)
var list2: List[Question] = questionService.findAllQuestion
var limit=list2.size
var qustn:Question=null
var a = 1;
for( a <- 1 to limit ){
println( a );
qustn= questionService.findQuestionById(a)
questionSetQuestion.setQuestion(qustn)
questionSetQuestion.setQuestionSet(questionSet)
questionSetQuestion.setCreatedDate(new java.sql.Date(new java.util.Date().getTime))
questionSetQuestionService.addQuestionSetQuestion(questionSetQuestion) } "redirect:/teacher/Assessment.html" }
I think you pretty much have it. With a checkbox, you can only send one piece of information back with the form...that being the value. So if you are trying to determine which checkboxes are checked when the user clicks the submit button, then I would have the checkboxes all use one name...like "id" (exactly like you have). Value is the actual id of the question (again like you have). Once submitted, "id" will be a String array which includes all the values of the checkboxes that were checked.
So your controller method needs to take param called "ids" mapped to parameter "id" which is a string[]. Now for each id, you can call questionService.findQuestionById.
(I'm not a Groovy guru so no code example sry :)
I have used JSTL with JSP and thymeleaf was something new. I read the THYMELEAF documentation.
There is a section which explains multi valued check boxes.
<input type="checkbox"
class="ads_Checkbox"
th:text="${q.questionName}"
th:value="${q.id}" name="id"/>
In the above code we are not binding the value to the field of the command object. Instead try doing this
<input type="checkbox"
class="ads_Checkbox"
th:text="${q.questionName}"
th:field="*{selectedQuestions}"
th:value="${q.id}" />
here the selectedQuestions is an array object present in the spring command object.

how to do binding in Spring annotated request parameter?

i have a controller that is using annotation for request mapping and requestParam.
the controller is working fine. However when submitting a command object with array, spring will crap out saying array index out of bound. i am guessing there is something wrong with binding but don't know how to fix it.
to be more specific, in eclipse i would set debugger at the beginning of the controller, and when submitting the form (by hitting a input submit button) eclipse debugger will not trigger and i will see array index out of bound error in console.
the controller is something like this:
#RequestMapping(value = {"/internal/pcsearch.dex", "/external/pcsearch.dex"},
method = {RequestMethod.POST, RequestMethod.GET})
public ModelAndView executeProductCatalogSearch(
HttpServletRequest request,
#RequestParam(value = "cat" ,required = false) String cat,
#RequestParam(value = "brand" ,required = false) String brand,
#ModelAttribute("command") ProductCatalogCommand cmd
){
[edit]
and the jsp is like:
<form name="pForm"
id="pForm"
action="<c:url value="psearch.dex"><c:param name="cat" value="${cat}"/></c:url>"
method="POST"
style="display:inline;">
...
...
<c:forEach var="model" items="${models}" varStatus="modelLinkStatus">
<script>
var modelImg<c:out value="${modelLinkStatus.index}"/>Src = '<c:out value="${model.altModelImage}"/>';
</script>
<spring:bind path="command.models[${modelLinkStatus.index}].modelSkusDisplayed">
<input type="hidden" name="<c:out value="${status.expression}"/>" id="<c:out value="${status.expression}"/>" value="<c:out value="${status.value}"/>"/>
</spring:bind>
<spring:bind path="command.updateCartButton">
<input type="submit" value="<spring:message code="orderEntryMessages.ecatalog.button.addToCart" text="Add to Cart" htmlEscape="yes" />" name="<c:out value="${status.expression}"/>" id="<c:out value="${status.expression}"/>" class="sub_buttons"/>
</spring:bind>
...
and the command object declare the model array as:
private List<ModelLink> models = new ArrayList<ModelLink>();
where modelLink is a custom ds.
the first foreach tag handle the the model command object and the 2nd part is the submit button i clicked on.
i think you should use AutoPopulatingList as models to bind list to view and controller. for example please refer link. This might resolve your problem of index.

play framework: how to repopulate form on validation-failure when using datamodel?

I'm building some crude CMS-like functionality (to get acquinted with Play Framework). For this test-case I've build 2 pages, 1 for listing tags and 1 for creating/editing/saving tags.
The flow is like this (routes-file):
#list tags
GET /tags Application.listTags
#view/edit existing tag
GET /tag/{<(?!new$)(.+)>name} Application.showTag
#new tag
GET /tag/new Application.showTag
the create/view/edit page displays a form which gets it's values from a tagDTO.
The normal flow works without problems, but when the form gives validation-errors (e.g: the tag-name must exist) I want to display the page again, repopulating the form with the edited values.
For this (following the Play Framework conventions) I could use the 'flash'-object which contains these last values, but the form is already bound to the tagDTO (which is null on redirect) instead of the 'flash'-object.
First the code:
Application.java
.....
public static void showTag(String name) {
TagDTO tagDTO = TagDTO.buildDTOFromModelOrNew(name);
render(tagDTO);
}
/**
* Save tag and redirect to Show
*
* #param name
* #param displayname
* #param isnew
*/
public static void saveTag(
#Required(message="Name is required") String name,
String displayname,
boolean isnew)
{
checkAuthenticity();
if(validation.hasErrors()) {
params.flash();
validation.keep();
showTag(null);
}
//fetch tagDTO based on backend or create new if not exist
TagDTO tag = TagDTO.buildDTOFromModelOrNew(name);
// Append / Overwrite values
tag.displayname = displayname;
tag.name = name;
//save result to model
TagDTO.buildAndSaveModelFromDTO(tag);
flash.success("Thanks for " + (isnew?"creating":"updating") + " tag " + tag.name);
//redirect to show
showTag(tag.name);
}
And ShowTag.html
#{extends 'main.html' /}
#{if flash.success}
<p class="success">${flash.success}</p>
#{/if}
#{ifErrors}
<p class="errors">Oops...</p>
#{/ifErrors}
#{form #Application.saveTag()}
#{authenticityToken /}
<p>
<label for="name">Name: </label>
<input type="text" name="name" id="name" value="${tagDTO.name}" />
<span class="error">#{error 'name' /}</span>
</p>
<p>
<label for="displayname">Displayname: </label>
<input type="text" name="displayname" id="displayname" value="${tagDTO.displayname}" />
<span class="error">#{error 'displayname' /}</span>
</p>
<p>
<input type="hidden" name="isnew" value="${tagDTO.isnew}" />
<input type="submit" value="Submit your comment" />
</p>
#{/form}
Now I could think of some ways to make it work, but none really elegant:
bind the form to the flash-object (or params-object) and populate the flas/params- object from the tagDTO
on validation-failure, refetch the tagDTO (not avail anymore so DB-call necessary) and overwrite values in tagDTO with values available in flash-object, bind form to tagDTO.
like 2, but using some sort of cache to quickly fetch tagDTO (so no need for db-call)
Some general mechanism to (de)serialize tagDTO from/to the session.
In short, I don't like any of them really.
What would you consider to be a best practice in this situation? Or is there any functionality in the Play Framework that I'm missing?
This is where the explicit render calls comes handy. Retain the form values from previous submission and give it back (if validation fails) as follows,
checkAuthenticity();
if(validation.hasErrors()) {
render("#showTag", name, displayname, isnew);
}
This will avoid the extra redirect (307 in case of Play!) that would have happened if you had called 'action from another action'.
Render the form again and avoid the redirect is a solution. I think it's OK if a user press F5 he will get the error again. But I think you should create a reload/cancel button, so the user can dismiss all the information.
To have always the correct URL you can do the following in the routes.conf:
GET /tag/create TagController.create
POST /tag/create TagController.insert
The flash solution has the disadvantage that your cookie can get really big.

Resources