Spring MVC update without redundant code? - spring-mvc

I'm working on a Spring MVC controller and views to implement CRUD operations for a simple object named Partner. The update operation troubles me. It seems like I need to write several lines of code manually that I expected Spring MVC would take care of automatically. Is there a best practice that I'm missing here?
Here's my view:
<%# include file="include.jsp"%>
<form:form commandName="partner">
<input type="hidden" name="_method" value="PUT" />
<table>
<tr>
<td>Id:</td>
<td><form:input path="id" disabled="true" /></td>
</tr>
<tr>
<td>Name:</td>
<td><form:input path="name" /></td>
</tr>
<tr>
<td>Logo:</td>
<td><form:input path="logo" /></td>
</tr>
<tr>
<td>On-screen text:</td>
<td><form:textarea path="onScreenText" /></td>
</tr>
<tr>
<td colspan="2"><input type="submit" value="Save Changes" /></td>
</tr>
</table>
</form:form>
And here's my controller method for the update operation:
#RequestMapping(value="/partner/{partnerId}", method=RequestMethod.PUT)
public ModelAndView updatePartner(#ModelAttribute Partner partner, #PathVariable int partnerId) {
EntityManager entityManager = DatabaseHelper.getEntityManager();
try {
Partner partnerToUpdate = entityManager.find(Partner.class, partnerId);
entityManager.getTransaction().begin();
partnerToUpdate.setId(partnerId);
partnerToUpdate.setName(partner.getName());
partnerToUpdate.setLogo(partner.getLogo());
partnerToUpdate.setOnScreenText(partner.getOnScreenText());
entityManager.persist(partnerToUpdate);
entityManager.getTransaction().commit();
}
finally {
entityManager.close();
}
return new ModelAndView("redirect:/partner");
}
The lines of code that trouble me are:
Partner partnerToUpdate = entityManager.find(Partner.class, partnerId);
partnerToUpdate.setId(partnerId);
partnerToUpdate.setName(partner.getName());
partnerToUpdate.setLogo(partner.getLogo());
partnerToUpdate.setOnScreenText(partner.getOnScreenText());
Do I really need to look up the existing Partner in the database and explicitly update each field of that object? I already have a Partner object with all the right values. Is there no way to store that object directly to the database?
I've already looked at Spring MVC CRUD controller best pactice, but that didn't quite answer my question.

As one possible suggestion, you could lookup the existing Partner object as part of model creation so that this model instance backs the form and Spring binds the fields directly to it. One way to achieve this would be to create an explicit method in the controller responsible for creating the model.
For example:
#ModelAttribute("partner")
public Partner createModel(#PathVariable int partnerId) {
Partner partner = entityManager.find(Partner.class, partnerId);
return partner;
}
And then you could remove the copying from the updatePartner method - because Spring will already have bound the form fields directly to the loaded Partner object.
#RequestMapping(value="/partner/{partnerId}", method=RequestMethod.PUT)
public ModelAndView updatePartner(#ModelAttribute("partner") Partner partner) {
EntityManager entityManager = DatabaseHelper.getEntityManager();
try {
entityManager.getTransaction().begin();
entityManager.persist(partner);
entityManager.getTransaction().commit();
}
finally {
entityManager.close();
}
return new ModelAndView("redirect:/partner");
}
One caveat - because the createModel method would get called for every request to the controller (not just updatePartner), the partnerId path variable would need to be present in all requests.
There's a post here which goes over a solution to that issue.

You can use merge to update the desired values, something like:
#RequestMapping(value="/partner/{partnerId}", method=RequestMethod.PUT)
public ModelAndView updatePartner(#ModelAttribute Partner partner, #PathVariable int partnerId) {
EntityManager entityManager = DatabaseHelper.getEntityManager();
try {
entityManager.getTransaction().begin();
partner.setId(partnerId);
entityManager.merge(partner);
entityManager.getTransaction().commit();
}
finally {
entityManager.close();
}
return new ModelAndView("redirect:/partner");
}
Also, I recommend You to use the DAO Pattern and Spring Transaction Support with #Transactional and #Repository

Related

Passing an object from view to controller

I want to add new message to database from my .jsp inputs via controller. I tried to just create new object of message in controller and put it in database and it works fine. But when I try to do it using inputs i receive error like this:
SEVERE [http-nio-8080-exec-2]org.springframework.web.servlet.tags.form.InputTag.doStartTag Neither BindingResult nor plain target object for bean name 'message' available as request attribute
java.lang.IllegalStateException: Neither BindingResult nor plain target object for bean name 'message' available as request attribute
My code:
#Controller
public class DemoController {
#Autowired
UserService userService;
#Autowired
MessageService messageService;
#PostMapping("/messages/sendNewMessage")
public String sendNewMessage(#ModelAttribute("message") MessagesEntity tempMessage) {
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
tempMessage.setFromUsername(userService.getUser(auth.getName()));
messageService.sendNewMessage(tempMessage);
return "redirect:/messages";
}
}
JSP here
<form:form action="sendNewMessage" modelAttribute="message" method="POST">
<table>
<tbody>
<tr>
<td><label>Username:</label></td>
<td><form:input path="toUsername" /></td>
</tr>
<tr>
<td><label>Subject:</label></td>
<td><form:input path="subject" /></td>
</tr>
<tr>
<td><label>Content:</label></td>
<td><form:input path="content" /></td>
</tr>
<tr>
<td><label></label></td>
<td><input type="submit" value="Send" class="save" /></td>
</tr>
</tbody>
</table>
</form:form>
you are using action url as sendNewMessage in jsp view but in the controller, it is /messages/sendNewMessage.
You need to put MessagesEntity object in ModelAttribute.
Either define the ModelAttribute while loading the form page like below.
#RequestMapping(value = "/", method = RequestMethod.GET)
public String messageForm(Model model) {
model.addAttribute("message", new MessagesEntity());
return "messageFormPageName";
}
or Put below method in the controller which will be common for complete controller so ModelAttribute will be available always.
#ModelAttribute("message")
public MessagesEntity createModel() {
return new MessagesEntity();
}

Neither BindingResult nor plain target object for bean name xxx available as request attribute

"Neither BindingResult nor plain target object for bean name 'loginCommand' available as request attribute"
I keep getting this Binding result error and nothing I've tried seems to be making it stop. I've seen other posts for this question, but none of them seem to fix whatever issue I'm having.
This is the first controller of my new project and I had some issues getting the xml squared away. I think that's all fixed, but if nothing looks off I supposed the problem could be there. The weird thing is that all this code is almost straight copied from another project I have and it works just fine.
Also I'm running on glassfish if that matters at all. Thanks in advance!
edit: The webpage is /morencore/login.jsp. I tried going to login.html assuming that would bring it up, but it only seems to work when I go to login.jsp. I believe I tried changing my controller to map to the jsp instead, but that did not work.
here is my login.jsp page:
<form:form method="post" modelAttribute="loginCommand">
<form:errors cssClass="error" element="p" />
<table border="0">
<tr>
<td align="right">Username:</td>
<td><form:input path="userName" /> <form:errors path="userName" cssClass="error" /></td>
</tr>
<tr>
<td align="right">Password:</td>
<td><form:password path="password" /> <form:errors path="password" cssClass="error" /></td>
</tr>
<tr>
<td colspan="2" align="center"><input type="submit" id="submit" name="submit" value="Log In" disabled="disabled"></td>
</tr>
</table>
</form:form>
and here is my controller:
#Controller
#ControllerAdvice
#RequestMapping("/login.html")
public class LoginController {
protected final Logger logger = LogManager.getLogger(getClass());
#Autowired
protected LoginValidator loginValidator;
#RequestMapping(method= RequestMethod.GET)
protected String initializeForm(#ModelAttribute("loginCommand")LoginCommand loginCommand,
BindingResult result,
ModelMap model)
{
logger.info("INITIALIZING LOGIN FORM");
model.addAttribute("loginCommand", new LoginCommand());
return "login";
}
#InitBinder("loginCommand")
protected void initBinder(ServletRequestDataBinder binder) throws Exception
{
binder.addValidators(loginValidator);
}
#RequestMapping(method=RequestMethod.POST)
protected String onSubmit(#ModelAttribute("loginCommand")LoginCommand loginCommand,
BindingResult result,
HttpServletRequest request) throws Exception
{
logger.info("validating login input");
loginValidator.validate(loginCommand, result);
if (result.hasErrors())
{
result.reject("login.failure");
return "login";
}
UserDao userDao = new UserDao();
User user = userDao.by_name(loginCommand.getUserName());
if (user == null
|| !user.getName().equals(loginCommand.getUserName())
|| !user.getPassword().equals(loginCommand.getPassword()))
{
result.reject("login.failure");
return "login";
}
return "redirect:main.html";
}
}
Here is my LoginCommand class:
#XmlRootElement
public class LoginCommand
{
private String userName;
private String password;
/** blah blah blah getters and setters*/
}
Here is the full stack trace as requested:
Neither BindingResult nor plain target object for bean name 'loginCommand' available as request attribute
java.lang.IllegalStateException: Neither BindingResult nor plain target object for bean name 'loginCommand' available as request attribute
at org.springframework.web.servlet.support.BindStatus.<init>(BindStatus.java:142)
at org.springframework.web.servlet.tags.form.AbstractDataBoundFormElementTag.getBindStatus(AbstractDataBoundFormElementTag.java:168)
at org.springframework.web.servlet.tags.form.AbstractDataBoundFormElementTag.getPropertyPath(AbstractDataBoundFormElementTag.java:188)
at org.springframework.web.servlet.tags.form.AbstractDataBoundFormElementTag.getName(AbstractDataBoundFormElementTag.java:154)
at org.springframework.web.servlet.tags.form.AbstractDataBoundFormElementTag.autogenerateId(AbstractDataBoundFormElementTag.java:141)
at org.springframework.web.servlet.tags.form.AbstractDataBoundFormElementTag.resolveId(AbstractDataBoundFormElementTag.java:132)
at org.springframework.web.servlet.tags.form.AbstractDataBoundFormElementTag.writeDefaultAttributes(AbstractDataBoundFormElementTag.java:116)
at org.springframework.web.servlet.tags.form.AbstractHtmlElementTag.writeDefaultAttributes(AbstractHtmlElementTag.java:422)
at org.springframework.web.servlet.tags.form.InputTag.writeTagContent(InputTag.java:142)
at org.springframework.web.servlet.tags.form.AbstractFormTag.doStartTagInternal(AbstractFormTag.java:84)
at org.springframework.web.servlet.tags.RequestContextAwareTag.doStartTag(RequestContextAwareTag.java:80)
at org.apache.jsp.login_jsp._jspx_meth_form_input_0(login_jsp.java:233)
at org.apache.jsp.login_jsp._jspService(login_jsp.java:126)
at org.apache.jasper.runtime.HttpJspBase.service(HttpJspBase.java:111)
at javax.servlet.http.HttpServlet.service(HttpServlet.java:791)
at org.apache.jasper.servlet.JspServletWrapper.service(JspServletWrapper.java:411)
at org.apache.jasper.servlet.JspServlet.serviceJspFile(JspServlet.java:473)
at org.apache.jasper.servlet.JspServlet.service(JspServlet.java:377)
at javax.servlet.http.HttpServlet.service(HttpServlet.java:791)
at org.apache.catalina.core.StandardWrapper.service(StandardWrapper.java:1580)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:338)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:208)
at org.glassfish.tyrus.servlet.TyrusServletFilter.doFilter(TyrusServletFilter.java:305)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:250)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:208)
at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:256)
at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:160)
at org.apache.catalina.core.StandardPipeline.doInvoke(StandardPipeline.java:652)
at org.apache.catalina.core.StandardPipeline.invoke(StandardPipeline.java:591)
at com.sun.enterprise.web.WebPipeline.invoke(WebPipeline.java:99)
at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:155)
at org.apache.catalina.connector.CoyoteAdapter.doService(CoyoteAdapter.java:371)
at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:238)
at com.sun.enterprise.v3.services.impl.ContainerMapper$HttpHandlerCallable.call(ContainerMapper.java:463)
at com.sun.enterprise.v3.services.impl.ContainerMapper.service(ContainerMapper.java:168)
at org.glassfish.grizzly.http.server.HttpHandler.runService(HttpHandler.java:206)
at org.glassfish.grizzly.http.server.HttpHandler.doHandle(HttpHandler.java:180)
at org.glassfish.grizzly.http.server.HttpServerFilter.handleRead(HttpServerFilter.java:242)
at org.glassfish.grizzly.filterchain.ExecutorResolver$9.execute(ExecutorResolver.java:119)
at org.glassfish.grizzly.filterchain.DefaultFilterChain.executeFilter(DefaultFilterChain.java:284)
at org.glassfish.grizzly.filterchain.DefaultFilterChain.executeChainPart(DefaultFilterChain.java:201)
at org.glassfish.grizzly.filterchain.DefaultFilterChain.execute(DefaultFilterChain.java:133)
at org.glassfish.grizzly.filterchain.DefaultFilterChain.process(DefaultFilterChain.java:112)
at org.glassfish.grizzly.ProcessorExecutor.execute(ProcessorExecutor.java:77)
at org.glassfish.grizzly.nio.transport.TCPNIOTransport.fireIOEvent(TCPNIOTransport.java:539)
at org.glassfish.grizzly.strategies.AbstractIOStrategy.fireIOEvent(AbstractIOStrategy.java:112)
at org.glassfish.grizzly.strategies.WorkerThreadIOStrategy.run0(WorkerThreadIOStrategy.java:117)
Among other things, it seems like your mappings need to be modified. Here is what I would try. There are a lot of adjustments so no guarantees on whether it will work completely, but it should get you in the right direction.
#Controller
public class LoginController {
protected final Logger logger = LogManager.getLogger(getClass()); //look at SLF4J instead. Then you're not tied to a specific logger and you use a facade.
#Autowired //may want to use constructor wiring instead on these
private LoginValidator loginValidator;
#Autowired
private UserDao userDao; //this should be wired and not simply instantiated - Spring won't know about it otherwise
#Autowired
private LoginValidator loginValidator;
#GetMapping("/login")
public String initializeForm(Model model) {
logger.info("INITIALIZING LOGIN FORM");
model.addAttribute("loginCommand", new LoginCommand());
return "login";
}
#PostMapping("/loginPost")
public String onSubmit(#ModelAttribute("loginCommand") LoginCommand loginCommand,
BindingResult result) throws Exception {
logger.info("validating login input");
loginValidator.validate(loginCommand, result);
if (result.hasErrors()) {
result.reject("login.failure");
return "login";
}
User user = userDao.by_name(loginCommand.getUserName());
if (user == null
|| !user.getName().equals(loginCommand.getUserName())
|| !user.getPassword().equals(loginCommand.getPassword())) { //you should really refactor this and move it outside of your controller. Just keep routing code in your controller, not logic
result.reject("login.failure");
return "login";
}
return "main"; //you should return just "main" or redirect:/main depending on what you're trying to do - you want the JSP to be processed. Leaving off the extension allows you to change frameworks without changing the server-side code and allows the page to be compiled. You could switch to Thymeleaf, for example, and not touch any of this code.
}
}
Add an action to your form:
<form:errors cssClass="error" element="p" />
<table border="0">
<tr>
<td align="right">Username:</td>
<td><form:input path="userName" /> <form:errors path="userName" cssClass="error" /></td>
</tr>
<tr>
<td align="right">Password:</td>
<td><form:password path="password" /> <form:errors path="password" cssClass="error" /></td>
</tr>
<tr>
<td colspan="2" align="center"><input type="submit" id="submit" name="submit" value="Log In"></td>
</tr>
</table>
For the next developer reading your code, I'd rename LoginCommand to something closer to what it actually is - like UserDetailsAdapter or something along those lines. I am assuming that LoginCommand will implement UserDetails from Spring Security if you're using that.
You may also want to consider updating your UserDao to have the method called findOneByUsername instead of by_name. The naming convention can help you later when you use Spring Repositories.
Lastly, look at Project Lombok for your beans. It'll save you lots of headaches.

How to perform a nested Thymeleaf loop to create a table containing fields from two JPA table classes

I'm trying to create a web page using a thymeleaf template to present a table of Orders with a field that provided a list of products associated with a specific order.
My controller class:
#Controller
public class WebPage {
#Autowired
private OrderRepository orderRepository;
#Autowired
private ProductRepository productRepository;
#RequestMapping("/test")
public String index(Model model) {
model.addAttribute("ordertable", orderRepository.findAll());
model.addAttribute("producttable", productRepository.findAll());
return "tablepage";
}
}
Relevant part of thymeleaf template:
<table class="table">
<thead>
<tr>
<th>ID</th>
<th>stuff</th>
<th>Stuuff</th>
<th>stuff</th>
<th>products</th>
</tr>
</thead>
<tbody>
<tr th:each="ordertbl: ${ordertable}">
<td th:text="${ordertbl.stuffId}"/>
<td th:text="${ordertbl.stuffname}"/>
<td th:text="${ordertbl.stuffname}"/>
<td th:text="${ordertbl.stuff}"/>
<td>
<span th:each="producttbl: ${producttable}"><span th:text="${ordertbl.products}"/></span>
</td>
</tr>
</tbody>
</table>
What this does is creates a table of orders but in the final field, it lists all the products contained in the order several times depending how many products are in the product table.
How would I change this so that the the order field lists the products belonging to each row just once. I am aware that this is most likely a nested for loop error, or a problem with my use of findall() method but I'm not sure how to fix it.
I would prefer to use the nested product table rather than fetching the products from the order jpa entity class.
Thanks.
If you're trying to display products of each order, then this line is wrong:
<span th:each="producttbl: ${producttable}"> <span th:text="${ordertbl.products}" /> </span>
You're iterating against the producttable list you have in your model, not of the current ordertbl's products in the loop. It should be
<span th:each="producttbl: ${ordertbl.products}"> <span th:text="${producttbl}" /></span>

java.lang.IllegalStateException: Neither BindingResult nor plain target object for bean name 'loginForm' available as request attribute

Getting java.lang.IllegalStateException: Neither BindingResult nor plain target object for bean name 'loginForm' available as request attribute. My login.jsp is as below.
<form:form method="POST" action="/loginPage" commandName = "loginForm">
<tr>
<td><form:label path="name">Username</form:label></td>
<td><form:input path="name" /></td>
</tr>
<tr>
<td><form:label path="age">Password</form:label></td>
<td><form:input path="age" /></td>
</tr>
<tr>
<td colspan="2">
<input type="submit" value="Submit"/>
</td>
</tr>
and my controller is
#RequestMapping(value="/loginPage",method = RequestMethod.POST)
public String processForm(#ModelAttribute("loginForm") LoginForm loginForm, BindingResult result,
Map model)
can anyone suggest how can i resolve this exception.
You need to add "loginForm" key to ModelMap before you start using it..
Example:
//This code needs to be invoked first
#RequestMapping(value="/onPageLoadOfLogin",method = RequestMethod.GET)
public String onLoadOfLoginPage(ModelMap m){
m.put("loginForm",new LoginForm());
}
model.put("loginForm",new LoginForm());
This means that, your command name should be exactly the same as the modelAttribute name... so if your commandName in your jsp file is "loginForm" then you model should add the attribute with the name "loginForm".
Please add model map attribute before rendering to login page.
<form:form method="POST" action="/loginPage" modelAttribute = "loginForm">
try adding modelAttribute in
also u need to first bind the form with the controller. To do this you can first perform a GET to perform the binding.
#RequestMapping(value="/loginPage",method = RequestMethod.GET)
public String processForm(#ModelAttribute("loginForm") LoginForm loginForm, BindingResult result,
Map model)
after this do POST. And submit button will do the binding of its own in POST method calling. Your form will be like
<form:form modelAttribute = "loginForm">

Thymeleaf send parameter from html to controller

I'm newbie in Thymeleaf. I'm trying to create simple crud application. I'm trying to delete object of Customer class on delete button. How can I set parameter(for example - id) to the method which called deleteUser using Thymeleaf. Here's my controller.
package controllers;
//imports
#Controller
public class WebController extends WebMvcConfigurerAdapter {
#Autowired
private CustomerDAO customerDAO;
#Override
public void addViewControllers(ViewControllerRegistry registry) {
registry.addViewController("/results").setViewName("results");
}
//show all users
#RequestMapping(value="/users", method=RequestMethod.GET)
public String contacts(Model model) {
model.addAttribute("users",customerDAO.findAll());
return "list";
}
//show form
#RequestMapping(value="/users/add", method=RequestMethod.GET)
public String showForm(Customer customer) {
return "form";
}
//add user
#RequestMapping(value="/users/doAdd", method=RequestMethod.POST)
public String addUser(#RequestParam("firstName") String firstName,
#RequestParam("lastName") String lastName,
#RequestParam("lastName") String email) {
customerDAO.save(new Customer(firstName, lastName, email));
return "redirect:/users";
}
//delete user
#RequestMapping(value="users/doDelete/{id}", method = RequestMethod.POST)
public String deleteUser (#PathVariable Long id) {
customerDAO.delete(id);
return "redirect:/users";
}
}
Here's my view.
<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<title>Getting Started: Serving Web Content</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
</head>
<body>
List of users
Add new user
<table>
<tr>
<th>Id</th>
<th>First Name</th>
<th>Last Name</th>
<th>Email</th>
<th>Action</th>
</tr>
<tr th:each="user : ${users}">
<td th:text="${user.id}">Id</td>
<td th:text="${user.firstName}">First name</td>
<td th:text="${user.lastName}">Last Name</td>
<td th:text="${user.email}">Email</td>
<td>
<form th:action="#{/users/doDelete/}" th:object="${customer}" method="post">
<button type="submit">Delete</button>
</form>
</td>
</tr>
</table>
</body>
</html>
You do not need form to do this:
<td>
<a th:href="#{'/users/doDelete/' + ${user.id}}">
<span>Delete</span>
</a>
</td>
Blejzer answer is an easy and straight forward solution unless you are also working with spring security (always recommended) in which case you should prefer POST instead of GET for all modification operations such as delete to protect against CSRF attack. This is exactly why spring recommends logout be done like this only. In order for you to adapt to POST, change your controller to read this parameter from request parameters instead of path variable
//delete user
#RequestMapping(value="users/doDelete", method = RequestMethod.POST)
public String deleteUser (#RequestParam Long id) {
customerDAO.delete(id);
return "redirect:/users";
}
Add a hidden field in the form which posts with the id as name and it's value in hidden parameter
<form th:action="#{/users/doDelete}" th:object="${customer}" method="post">
<input type="hidden" th:field="${id}" />
<button type="submit">Delete</button>
</form>
On a side note, even if you are not using spring security, it is always recommended to use post for any entity modification operations (like delete or update). Saves you from lots of trouble on web in long run. Take a look at Quick Checklist for Choosing HTTP GET or POST for detailed information.
path variables can be set as:
<a th:href="#{/users/doDelete/__${user.id}__}"><span>Delete</span></a>

Resources