Model property is set to null by thymeleaf - java

I have been dealing with the following issue:
I have a simple model class called User.
public class User{
private Long id;
private String name;
...}
And this is my the controller code:
#RequestMapping ( value = "/users", params = { "id" } )
public String showEditForm( final Model model, final HttpServletRequest req )
{
User edit = this.userRepository.findOne( Long.valueOf( req.getParameter( "id" ) ) );
model.addAttribute( "user", edit );
return "edit-user";
}
#RequestMapping ( value = "/users", method = RequestMethod.POST, params = {"edit"
})
public String editUser( #Valid #ModelAttribute ( "user" ) final User user,
final BindingResult bindingResult, final Model model )
{
if ( bindingResult.hasErrors() )
{
return "edit-user";
}
return "redirect:/users";
}
The following is the code snippet to displaying all the users:
<div class="userlist" th:unless="${#lists.isEmpty(users)}">
<h2 th:text="#{list.user.title}"></h2>
<table>
<thead>
<tr>
<th th:text="#{name}" />
<th th:text="#{details}" />
</tr>
</thead>
<tbody>
<tr th:each="u : ${users}">
<td th:text="${u.name}" />
<td><a th:href="#{/users(id=${tc.id})}" th:text="#{edit}"></a></td>
</tr>
</tbody>
</table>
</div>
And eventually the submit form:
<form action="#" th:action="#{/users}" th:object="${user}"
method="post">
<fieldset>
<ul th:if="${#fields.hasErrors('*')}" class="errorlist">
<li th:each="err : ${#fields.errors('*')}" th:text="${err}"></li>
</ul>
<div>
<input type="text" th:field="*{id}"/>
</div>
<div>
<label for="name"> <span th:text="#{name}"></span>
</label> <input type="text" th:field="*{name}"
th:class=" ${#fields.hasErrors('name')}? 'fieldError'" />
</div>
<div class="submit">
<button type="submit" th:text="#{update}" name="edit"></button>
</div>
</fieldset>
</form>
And now the problem description:
As long as the 'id'-field is present within the submit form, everything works fine. If I remove the 'id'-field from the submit form though, because the id property isn't meant to be modified, the workflow doesn't work anymore. In fact, the id is null in the editUser() method. I assume that Thymeleaf does set the value of the id property to null if it is not present within the submit form. But I'm not sure about this. And I think there must be some solution to this problem other than having to let the id-property reside in the submit form.
I hope anyone can help here out.
Thanks.
Edmond

That has nothing to do with Thymeleaf, but how binding works. Spring will only bind attributes to the model object which are present as parameter in the request. If you remove id that isn't present and as such cannot be bound (what should it be bound to?).
A solution to this is to either specify the id as a hidden form, so that it is present. Or in between requests store your object in the session (using #SessionAttributes on the controller) that way the earlier retrieved object is going to be used for binding.
#Controller
#SessionAttributes("user")
public class UserController {
#ModelAttribute("user")
public User initUser(#RequestParam("id") Long id) {
return this.userRepository.findOne(id);
}
RequestMapping ( value = "/users", params = { "id" } )
public String showEditForm() {
return "edit-user";
}
#RequestMapping ( value = "/users", method = RequestMethod.POST, params = {"edit"})
public String editUser( #Valid #ModelAttribute ( "user" ) final User user, final BindingResult bindingResult, SessionStatus status;) {
if ( bindingResult.hasErrors() ) {
return "edit-user";
}
status.setComplete(); // Indicate we are done,so #SessionAttributes can be cleaned up
return "redirect:/users";
}
Something like that should preserve the User between sessions, and the SessionStatus can then be used to trigger the cleanup of the session attributes.

Id field should be present in form otherwise how controller would know which user should be updated.
If this field is meaningless for client (that is it's some generated unique id) it should be done hidden. If it may be of some interest for client this field can be done read-only.

Related

Spring Boot Forms; hiding the path value and showing placeholder

I am having a small issue with Spring Boot forms displaying the information of the path value instead of the placeholder once you get to the editProfile.jsp. I want the input field to look like this;
Edit Profile Page instead of this Wrong Edit Profile. I do not want my users to have to click, select and delete the auto completed value. I want it to show the placeholder only and allow them to overwrite what is shown with ease.
This is the editProfile.jsp
<%--#elvariable id="editProfile" type=""--%>
<form:form method="POST" modelAttribute="editProfile">
<div class="MyForm form-group">
<h1>Edit Profile</h1>
<form:input type="email" class="MyInput" id="email" path="email" placeholder="${editProfile.email}" />
<form:button type="submit" class="from-control">Submit</form:button>
</div>
<div>
<img src="images/reg1.png" alt="picture">
</div>
</form:form>
</body>
</html>
This is the code specified in the Controller
#RequestMapping(value = "edit/{email}", method = RequestMethod.GET)
public String getEditUserData(#PathVariable("email") String email, Model model) {
AccountEntity accountInstance = accountRepo.findByEmail(email);
model.addAttribute("editProfile", accountInstance);
return "editProfile";
}
#RequestMapping(value = "edit/{email}", method = RequestMethod.POST)
public String enterEditUserData(#ModelAttribute("login") AccountEntity accountForm, #PathVariable("email") String email, Model model ) {
AccountEntity accountInstance = accountRepo.findByEmail(email);
accountInstance.setEmail(accountForm.getEmail());
accountRepo.save(accountInstance);
return "redirect:/login";
}
I have figured it out; You have to add a model of a new Entity, so the path variable does not fill in with the instance of the specific path value. Here is the new code, and compare it to the one I sent above.
#RequestMapping(value = "edit/{email}", method = RequestMethod.GET)
public String getEditUserData(#PathVariable("email") String email, Model model) {
AccountEntity accountInstance = accountRepo.findByEmail(email);
model.addAttribute("editProfile2", new AccountEntity());
model.addAttribute("editProfile1", accountInstance);
return "editProfile";
}
<%--#elvariable id="editProfile" type=""--%>
<%--#elvariable id="editProfile2" type=""--%>
<form:form method="POST" modelAttribute="editProfile2">
<div class="grid form-group">
<h1>Edit Profile</h1>
<form:input type="email" class="MyInput" id="email" path="email" placeholder='${editProfile1.email}' />
<form:button type="submit" class="from-control">Submit</form:button>
</div>

hibernate updating entity attributes to null which are not in form submit

I am building a MVC application using thymeleaf and Spring and Hibernate. My question here is more about hibernate than spring.
This is what i have so far.
A UI
<form role="form" th:action="#{/user/{userId}/official(userId=${userId})}" th:object="${user}" method="post">
<!-- first form group -->
<div class="form-group">
<label class="control-label col-xs-2">First Name</label>
<div class="col-xs-2">
<input type="text" class="form-control" th:field="*{firstName}" placeholder="First Name" />
</div>
<label class="control-label col-xs-2">Last Name</label>
<div class="col-xs-3">
<input type="text" class="form-control" th:field="*{lastName}" placeholder="Last Name" />
<!-- first form group end -->
</div>
<br/><br/>
<!-- third form group -->
<div class="form-group">
<label class="control-label col-xs-2">Email Address</label>
<div class="col-xs-2">
<input type="text" class="form-control" th:field="*{emailAddress}" placeholder="Email Address" />
</div>
</div>
<div class="form-group">
<div class="col-xs-2">
<input type="submit" value="Update" class="btn btn-primary" />
</form>
Controller :
#Controller
public class UserController {
#Autowired
private IUserService userServiceImpl;
#RequestMapping(value = "/user/{userId}/official", method = RequestMethod.GET)
public String getUserOfficialInfo(#PathVariable("userId") Integer userId, Model model) throws ServiceBusinessException {
UserBO userBO = userServiceImpl.findUserByUserId(userId);
model.addAttribute("user", userBO);
model.addAttribute("userId", userId);
model.addAttribute("genders", EnumSet.allOf(Gender.class));
return "official";
}
#RequestMapping(value = "/user/{userId}/official", method = RequestMethod.POST)
public String updateUserOfficialInfo(#PathVariable("userId") String userId, #ModelAttribute("user") UserBO user,BindingResult result, Model model) throws ServiceBusinessException {
userServiceImpl.updateUser(user);
UserBO userBO = userServiceImpl.findUserByUserId(Integer.parseInt(userId));
model.addAttribute("user", userBO);
model.addAttribute("userId", userId);
model.addAttribute("genders", EnumSet.allOf(Gender.class));
return "official";
}
}
DAO :
#Override
public void updateUser(UserEntity user) throws DaoException {
entityManager.merge(user);
}
The GET method in the controller, gets the user object to the view. But on the VIew i am just displaying few of those attributes of a user object in the form.
On the form Submit, the POST method in the controller gets called, which calls the service layer and then the merge method in the DAO gets executed.
Now what I have observed is that this merge method on the entity manager is updating the attributes which are not there in the form to null.
I think this is the expected behaviour since the object is detached when its called from the POST method. So the right thing to do here is to first fetch the entity object from the database and then to that object set the fields which are changed in the form and then call the merge method.
Can some one let me know if the above what I said is correct ?
If yes, then my next question would be that isnt this quite tedious and kind of bit more effort. I mean there are going to be cases where in I would not want to display the entire object in the form. Also not in hidden fields. I am quite surprise that there is no way to handle this and I have to follow the approach I just described above each time.
Is there a better way to do this ? Wouldn't i just use JDBC template instead ? I know I would be writing boiler plate code there but I am kind of writing getters and setters here as well for each round trip to the UI.
Consider annotating your entity with org.hibernate.annotations.Entity.
#Entity
#Table(name = "user")
#org.hibernate.annotations.Entity(
dynamicInsert = true, dynamicUpdate=true
)
public class User implements java.io.Serializable {
// your properties
}
If you are using a 4.x version of Hibernate, you may want to use #DynamicUpdate instead since the usage of #org.hibernate.annotations.Entity has been deprecated recently.
References:
https://www.mkyong.com/hibernate/hibernate-dynamic-update-attribute-example/
https://docs.jboss.org/hibernate/orm/4.2/javadocs/org/hibernate/annotations/Entity.html
https://docs.jboss.org/hibernate/orm/4.2/javadocs/org/hibernate/annotations/DynamicInsert.html
https://docs.jboss.org/hibernate/orm/4.2/javadocs/org/hibernate/annotations/DynamicUpdate.html
you can put the following code in a util class and invoke it when you want to fill your object based on another reference object:
public static <T> void fillNotNullFields(T reference, T source) {
try {
Arrays.asList(Introspector.getBeanInfo(reference.getClass(), Object.class)
.getPropertyDescriptors())
.stream()
.filter(pd -> Objects.nonNull(pd.getReadMethod()))
.forEach(pd -> {
try {
Object value = pd.getReadMethod().invoke(source);
if (value != null)
pd.getWriteMethod().invoke(reference, value);
} catch (Exception e) {
e.printStackTrace();
}
});
} catch (IntrospectionException e) {
e.printStackTrace();
}
}
So, you can do this on the service:
public UserBO updateUser(String userId, UserBO user ) {
UserBO reference = findOne(Integer.parseInt(userId));
fillNotNullFields(reference, user);
return updateUser(reference);
}
I found the suport for this answer here. Hope it helps.
You will need to store the unnecessary bean properties in hidden fields so that they get remapped when the form is posted to the controller. For example, if you do not want to show the date of birth on the page (assuming that date of birth is already an attribute of the user), simply add the following tag inside the form.
<input type='hidden' th:field="*{dateOfBirth}" />

Passing object's variable to controller in thymeleaf

I'm trying to create my very first CRUD ever.
Here's my journeySite.html table code.
<table>
<tr th:each="trip : ${trips}">
<td th:text="${trip.title}"></td>
<td th:text="${trip.destination}"></td>
<td th:text="${trip.id}"></td>
<form th:action="#{/journeys}" th:object="${trip}" method="post">
<input type="hidden" th:field="${trip.id}" />
<button type="submit">Delete</button>
</form>
</tr>
</table>
And make a my controller looks now like that.
#RequestMapping(value = {"/journeys"}, method = RequestMethod.GET)
public String journeysPage(Model model){
tripRepository.save(new Trip("Asian Trip", "Asia"));
tripRepository.save(new Trip("European Trip", "Europe"));
tripRepository.save(new Trip("African Trip", "Africa"));
model.addAttribute("trips", tripRepository.findAll());
return "journeysSite";
}
#RequestMapping(value = {"/journeys"}, method = RequestMethod.POST)
public String journeysPageTripDeleting(#RequestParam Long id) {
tripRepository.delete(id);
return "journeysSite";
}
All i want is to show my all my trips on the /journeys in table. In each row there'd be a delete button which would POST trip.id, delete it from db and redirect to the exact same page, but with trip deleted.
But obviously error has occured: java.lang.IllegalStateException: Neither BindingResult nor plain target object for bean name 'id' available as request attribute
at org.springframework.web.servlet.support.BindStatus.<init>(BindStatus.java:144) ~[spring-webmvc-4.3.6.RELEASE.jar:4.3.6.RELEASE]
Would anyone give me a tip how to do it? Thanks.
At your form, you defined a th:object="${trip}" object which means whenever this form gets submitted this trip object will be sent as request body.
So, to receive this object you have to accept it in controller's method.
#RequestMapping(value = {"/journeys/"}, method = RequestMethod.POST)
public String journeysPageTripDeleting(#ModelAttribute Trip trip){
tripRepository.delete(trip.getId());
return "redirect:/journeys";
}
The th:field="${id}" will be included with the object that model attribute provides so, trip object will have the id you're looking for.
More on this.
UPDATE:
With your current controller's method implementation, I think all you need to change is this,
<input type="hidden" th:field="*{id}" /> // No trip.id
You need to change your controller method post code from #RequestMapping(value = {"/journeys/"}, method = RequestMethod.POST) to #RequestMapping(value = {"/journeys/{id}"}, method = RequestMethod.POST).
As you can see,You forgot to add {id} in your RequestMapping which is required.
Also its standard to use DELETE http method for deleting an entity not POST method.
This is not good practice to create form for each row. Instead do it this way:
<table>
<tr th:each="trip : ${trips}">
<td th:text="${trip.title}"></td>
<td th:text="${trip.destination}"></td>
<td th:text="${trip.id}"></td>
<td><button class='delete' data-id="${trip.id}">Delete</button></td>
</tr>
</table>
Add this js in your html:
<script>
$(document).ready(function () {
$(document).on('click', '.delete', function () {
$.ajax({
url: "<c:url value="/journeys/delete"/>",
data: {
id: $(this).data("id")
},
success: function(data) {
location.reload();
}
})
})
})
</script>
And change your controller method like this:
#RequestMapping(value = {"/journeys/delete"}, method = RequestMethod.GET)
#ResponseBody
public String journeysPageTripDeleting(#RequestParam Long id) {
tripRepository.delete(id);
return "success";
}

Spring, IllegalStateException when opening page with form

I am new to Spring and I get this exception, when entering campaigns.jsp with browser.
java.lang.IllegalStateException: Neither BindingResult nor plain target object for bean name 'campaign' available as request attribute
I have this controller :
#Controller
#RequestMapping(value = "/admin")
public class AdminIndexController {
#RequestMapping(value = "/secure/campaigns.jsp", method = RequestMethod.GET)
public String campaigns() {
return "campaigns";
}
#RequestMapping(value = "/secure/create", method = RequestMethod.POST)
public String addContact(#ModelAttribute("campaign")
Campaign campaign) {
return "campaigns";
}
}
Campaings.jsp (if I remove this part, it shows the page correctly) :
<form:form method="post" action="create" commandName="campaign">
<table>
<tr>
<td><form:label path="question">Question</form:label></td>
<td><form:input path="question" />
</td>
</tr>
<tr>
<td colspan="2">
<input type="submit" value="Add Campaign"/>
</td>
</tr>
</table>
</form:form>
I suppose there is a problem with mapping action="create", I am not sure where it is pointing to. I thought it is pointing to the same place as the .jsp is. This is address I use http://localhost:8080/server/rest/admin/secure/campaigns.jsp
This
<form:form method="post" action="create" commandName="campaign">
expects a model attribute (or command object), an HttpServletRequest attribute in reality, with the name campaign to use as a template for the form fields.
You haven't added a request attribute with that name. You need to do that before the view is rendered. For example
#RequestMapping(value = "/secure/campaigns.jsp", method = RequestMethod.GET)
public String campaigns(Model model) {
model.addAttribute("campaign", new Campaign());
return "campaigns";
}
The object doesn't need to have any fields set since it is only used as a template.

Binding problems using Spring MVC

I am using Spring MVC 3.1 to develop a Java web app. I have a JSP that has two paired radio buttons, an entry field, and a dropdown select box. I need these values to be available to my mapped controller, via a model class' fields.
The security and URL mapping works fine, as I've seen in debugger before. The issue is that when I tried to get the JSP data values populating my model, I get an error:
java.lang.IllegalStateException: Neither BindingResult nor plain target object for bean name 'cccForm' available as request attribute
Here is part of my JSP:
<c:url var="cccUrl" value="/registers/default/ccPreauth/authorize" />
<div class="mainWrapper">
<form:form id="cccForm" action="${cccUrl}" method="post" modelAttribute="cccForm">
...
<table>
<tbody>
<tr>
<th>Select an option.</th>
</tr>
<tr>
<td>
<div class="field-input">
<form:radiobutton id="paymentOption" path="paymentOption" value="authorizeCC" />
Collect Credit Card Information
</div>
<div class="field-input">
Authorization Amount $
<form:input path="authAmount" maxlength="10" size="10" class="extendWidth"/>
<span class="instructions">
<spring:message code="label.authorization.note" />
</span>
</div>
<div class="field-input">
<form:radiobutton id="paymentOption" path="paymentOption" value="cancelAuth" />
Choose a Reason and Cancel Credit Card Collection
</div>
<div class="field-input right">
<form:select id="selectedReason" path="selectedReason" >
<c:forEach items="${reasonList}" var="reason">
<option value=${reason.reasonText}>${reason.reasonText}</option>
<br />
</c:forEach>
</form:select>
</div></td>
</tr>
</tbody>
</table>
</div>
<div class="right">
<button class="btnBlue" id="submitButton" type="submit">
Here is part of my controller:
#Controller
#RequestMapping(value = "/registers/default/ccPreauth")
#SessionAttributes(ControllerConstants.DEFAULT_REGISTER_ATTR_NM)
public class CCCaptureController {
...
#RequestMapping(value="/authorize" )
public ModelAndView authorize(
final Authentication auth,
final #ModelAttribute("ccCapturePaymentRequest") CCCapturePaymentForm ccCapturePaymentRequest,
final BindingResult result,
final HttpServletResponse response) {
final ModelAndView mav = new ModelAndView(CC_PREAUTH_PAYMENT_VIEW);
return mav;
}
and finally, here is my model class:
public class CCCapturePaymentForm implements Serializable {
private static final long serialVersionUID = 6839171190322687142L;
#NumberFormat(style = Style.CURRENCY)
private BigDecimal authAmount;
private String selectedReason;
private String paymentOption;
public BigDecimal getAuthAmount() {
return authAmount;
}
public void setAuthAmount(BigDecimal authAmount) {
this.authAmount = authAmount;
}
public String getSelectedReason() {
return selectedReason;
}
public void setSelectedReason(String selectedReason) {
this.selectedReason = selectedReason;
}
public String getPaymentOption() {
return paymentOption;
}
public void setPaymentOption(String paymentOption) {
this.paymentOption = paymentOption;
}
}
Can anyone tell me what I need to get this to work correctly? Please don't stop at just the reason for the exception above - please verify and correct my code as I am on a tight schedule as usual and have little experience with Spring MVC. Thanks!
You have this in your form:
modelAttribute="cccForm"
So you should have this in your controller:
#ModelAttribute("cccForm") CCCapturePaymentForm ccCapturePaymentRequest
That's how you bind the form backing object with the model attribute.
I found the answer for those Spring MVC newbies...
I had to set the "cccForm" to a new instance of my next page's form inside the controller search method (that then tries to being up the page that was getting the error).
In a nutshell: I had to set the empty backing bean value in the preceding controller method so that the follow-on method and JSP page have it to work with.
Hope this helps someone else avoid my mistake.
Mark

Categories

Resources