Enterprise Java

Spring – Adding Spring MVC – part 1

Welcome to the fourth part of this tutorial. In this part, we will write controllers and views using Spring MVC and think about our REST model.

First thing that we must do, is make a web application from what we have so far. We will add web/WEB-INF folder to our project root. Inside WEB-INF create jsp folder. We will put our JSPs in that place. Inside that folder we will put deployment descriptor web.xml file with following contents:

<?xml version='1.0' encoding='UTF-8'?>
<web-app xmlns='http://java.sun.com/xml/ns/javaee'
         xmlns:xsi='http://www.w3.org/2001/XMLSchema-instance'
         xsi:schemaLocation='http://java.sun.com/xml/ns/javaee
    http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd'
         version='3.0'>

    <display-name>timesheet-app</display-name>

    <context-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>
            classpath:persistence-beans.xml
        </param-value>
    </context-param>

    <listener>
        <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
    </listener>

    <servlet>
        <servlet-name>timesheet</servlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
        <load-on-startup>1</load-on-startup>
    </servlet>

    <servlet-mapping>
        <servlet-name>timesheet</servlet-name>
        <url-pattern>/</url-pattern>
    </servlet-mapping>

</web-app>

Note that we are using servlet called timesheet. This is a dispatcher servlet. The following picture illustrates how Spring’s dispatcher servlet works (it’s called Front controller on picture below):

  1. Request is handled by dispatcher servlet
  2. Dispatcher servlet decides to which controller it should deliver request (by request mapping, we’ll see that later) and then it delegates request
  3. Controller creates model and delivers it back to dispatcher servlet
  4. Dispatcher servlet resolves logical name of the view, binds model there and renders the view

The last step is quite mysterious. How does dispatcher servlet resolve logical name of the view? It uses something called ViewResolver. But we are not going to create our own by hand, instead we will just create another configuration file, define a bean with ViewResolver and have it injected by Spring. In WEB-INF create another Spring configuration file. By convention it must be named timesheet-servlet.xml, because we named our DispatcherServlet “timesheet” and this is file name, where Spring will be looking for config by default. Also create package org.timesheet.web. This is where we will be putting our controllers (which are again only annotated POJOs).

Here’s the timesheet-servlet.xml

<?xml version='1.0' encoding='UTF-8'?>
<beans xmlns='http://www.springframework.org/schema/beans'
       xmlns:xsi='http://www.w3.org/2001/XMLSchema-instance'
       xmlns:context='http://www.springframework.org/schema/context'
       xsi:schemaLocation='http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.1.xsd'>

    <context:component-scan base-package='org.timesheet.web' />

    <bean
        class='org.springframework.web.servlet.view.InternalResourceViewResolver'>
        <property name='prefix' value='/WEB-INF/jsp/' />
        <property name='suffix' value='.jsp' />
    </bean>
</beans>

We defined prefix and suffix for resolving logical names. It is really simple. We calculate name like this:
full name = prefix + logical name + suffix
Therefore with our InternalResourceViewResolver, logical name “index” would be resolved to “/WEB-INF/jsp/index.jsp”.

For views we will be using JSP technology with JSTL (tag library), so we need to add another dependencies to our pom.xml file:

        <dependency>
            <groupId>jstl</groupId>
            <artifactId>jstl</artifactId>
            <version>1.2</version>
        </dependency>
        <dependency>
            <groupId>javax.servlet</groupId>
            <artifactId>javax.servlet-api</artifactId>
            <version>3.0.1</version>
        </dependency>

Now, we’d like to handle GET on /timesheet-app/welcome. So we need to write view and controller (for Model we’ll use one from Spring facilities). Let’s start with controller:

package org.timesheet.web;

import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;

import java.util.Date;

@Controller
@RequestMapping('/welcome')
public class WelcomeController {

    @RequestMapping(method = RequestMethod.GET)
    public String showMenu(Model model) {
        model.addAttribute('today', new Date());
        return 'index';
    }

}

So when someone accesses url welcome (in our case http://localhost:8080/timesheet-app/welcome), this controller will handle request. We are also using Model and binding there value with name “today”. This is how we get value to view page.

Note that root of my application is /timesheet-app. It’s called application context. You can change that of course, but all remaining codes asume that you’re application context is set like that. If you’re deploying WAR it’s based on WAR’s name.

From showMenu method we’re returning “index” – which will be resolved to WEB-INF/jsp/index.jsp so let’s create such a page and put there some basic content:

<%@ page contentType='text/html;charset=UTF-8' language='java' %>
<%@ taglib prefix='fmt' uri='http://java.sun.com/jsp/jstl/fmt' %>
<%@ taglib prefix='spring' uri='http://www.springframework.org/tags' %>

<html>
<head>
    <title>Welcome to Timesheet app!</title>
</head>
<body>
    <h1>Welcome to the Timesheet App!</h1>

    <ul>
        <li><a href='managers'>List managers</a></li>
        <li><a href='employees'>List employees</a></li>
        <li><a href='tasks'>List tasks</a></li>
        <li><a href='timesheets'>List timesheets</a></li>
    </ul>

    <h2>Also check out <a href='timesheet-service'>extra services!</a></h2>
    Today is: <fmt:formatDate value='${today}' pattern='dd-MM-yyyy' />
</body>
</html>

OK to recap, we added web.xml file, timesheet-servlet.xml Spring configuration file, controller class and jsp page. Let’s try to run this on some web container. I’ll be using Tomcat7, but if you feel more comfortable with another web container or even application server – feel free to switch. Now there are plenty of ways how to run Tomcat and how to deploy the application. You can:

Whichever you choose, be sure that you can access URL mentioned above before continuing. Honestly, deployment in Java is least fun for me, so don’t give up if you feel frustrated, it might not work for the first time. But with tutorials above, you probably won’t have any problems. Also don’t forget about setting right application context.

Before we write more controllers, let’s prepare some data. When Spring creates welcomeController bean, we’d like to have some data. So for now, let’s just write dummy generator that will just create some entites. Later in tutorial, we will see some more realistic solution.

Put helpers package under web package were controllers are place there EntityGenerator class:

package org.timesheet.web.helpers;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.timesheet.domain.Employee;
import org.timesheet.domain.Manager;
import org.timesheet.domain.Task;
import org.timesheet.domain.Timesheet;
import org.timesheet.service.GenericDao;
import org.timesheet.service.dao.EmployeeDao;
import org.timesheet.service.dao.ManagerDao;
import org.timesheet.service.dao.TaskDao;
import org.timesheet.service.dao.TimesheetDao;

import java.util.List;

/**
 * Small util helper for generating entities to simulate real system.
 */
@Service
public final class EntityGenerator {

    @Autowired
    private EmployeeDao employeeDao;

    @Autowired
    private ManagerDao managerDao;

    @Autowired
    private TaskDao taskDao;

    @Autowired
    private TimesheetDao timesheetDao;

    public void generateDomain() {
        Employee steve = new Employee('Steve', 'Design');
        Employee bill = new Employee('Bill', 'Marketing');
        Employee linus = new Employee('Linus', 'Programming');
        
        // free employees (no tasks/timesheets)
        Employee john = new Employee('John', 'Beatles');
        Employee george = new Employee('George', 'Beatles');
        Employee ringo = new Employee('Ringo', 'Beatles');
        Employee paul = new Employee('Paul', 'Beatles');

        Manager eric = new Manager('Eric');
        Manager larry = new Manager('Larry');
        
        // free managers
        Manager simon = new Manager('Simon');
        Manager garfunkel = new Manager('Garfunkel');

        addAll(employeeDao, steve, bill, linus, john, george, ringo, paul);
        addAll(managerDao, eric, larry, simon, garfunkel);

        Task springTask = new Task('Migration to Spring 3.1', eric, steve, linus);
        Task tomcatTask = new Task('Optimizing Tomcat', eric, bill);
        Task centosTask = new Task('Deploying to CentOS', larry, linus);

        addAll(taskDao, springTask, tomcatTask, centosTask);

        Timesheet linusOnSpring = new Timesheet(linus, springTask, 42);
        Timesheet billOnTomcat = new Timesheet(bill, tomcatTask, 30);

        addAll(timesheetDao, linusOnSpring, billOnTomcat);
    }
    
    public void deleteDomain() {
        List<Timesheet> timesheets = timesheetDao.list();
        for (Timesheet timesheet : timesheets) {
            timesheetDao.remove(timesheet);
        }

        List<Task> tasks = taskDao.list();
        for (Task task : tasks) {
            taskDao.remove(task);
        }

        List<Manager> managers = managerDao.list();
        for (Manager manager : managers) {
            managerDao.remove(manager);
        }

        List<Employee> employees = employeeDao.list();
        for (Employee employee : employees) {
            employeeDao.remove(employee);
        }
    }
    
    private <T> void addAll(GenericDao<T, Long> dao, T... entites) {
        for (T o : entites) {
            dao.add(o);
        }
    }
}

Now let’s use the code for WelcomeController. We’ll inject there generator and place special method annotated with @PostConstruct annotation. That’s JSR-250 annotation for bean lifecycle and Spring supports it. It means, that this method will be invoked right after Spring IoC container instantiates welcomeController bean.

package org.timesheet.web;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.timesheet.web.helpers.EntityGenerator;

import javax.annotation.PostConstruct;
import java.util.Date;

@Controller
@RequestMapping('/welcome')
public class WelcomeController {

    @Autowired
    private EntityGenerator entityGenerator;

    @RequestMapping(method = RequestMethod.GET)
    public String showMenu(Model model) {
        model.addAttribute('today', new Date());
        return 'index';
    }

    @PostConstruct
    public void prepareFakeDomain() {
        entityGenerator.deleteDomain();
        entityGenerator.generateDomain();
    }
    
}

OK let’s write some controllers for domain logic now!

We’ll start by writing Employee’s controller. First, create class EmployeeController under org.timesheet.web package. Mark class as web controller and to handle “/employees” requests:

@Controller
@RequestMapping('/employees')
public class EmployeeController { ... 

For processing persistent data (Employees in this case) we need DAO and have it autowired by Spring’s IoC container, so let’s do just that:

    private EmployeeDao employeeDao;

    @Autowired
    public void setEmployeeDao(EmployeeDao employeeDao) {
        this.employeeDao = employeeDao;
    }

Now we want to handle HTTP GET method. When user accesses http://localhost:8080/timesheet-app/employees with a web browser, controller must handlet GET request. It’s only contacting DAO and collecting all employees and putting them into the model.

@RequestMapping(method = RequestMethod.GET)
    public String showEmployees(Model model) {
        List<Employee> employees = employeeDao.list();
        model.addAttribute('employees', employees);

        return 'employees/list';
    }

Under jsp folder create employees folder where we will put all corresponding JSPs of employees. Probably you already noticed, that page with list of employees will be resolved to /WEB-INF/jsp/employees/list.jsp. So create such a page. We’ll see the contents later, if you want you can put there random text for now to see if it works.

In JSP page we will show a link next to employee for his personal page, which will look like http://localhost:8080/timesheet-app/employees/{id} where ID is employee’s id. This is RESTful URL because it’s resource oriented, and we’re directly identifying resource. RESTless URL would be something like http://localhost:8080/timesheet-app/employees.html?id=123. That’s action oriented and doesn’t identify resource.

Let’s add another method to controller, that handles this URL:

    @RequestMapping(value = '/{id}', method = RequestMethod.GET)
    public String getEmployee(@PathVariable('id') long id, Model model) {
        Employee employee = employeeDao.find(id);
        model.addAttribute('employee', employee);

        return 'employees/view';
    }

Again, create view.jsp page under /jsp/employees folder. From this page, we’d also like to alter employee. We’re simply access same URL but with different web method – POST. That means, we’re giving data from bounded model to update.

This method handles employee updating:

    @RequestMapping(value = '/{id}', method = RequestMethod.POST)
    public String updateEmployee(@PathVariable('id') long id, Employee employee) {
        employee.setId(id);
        employeeDao.update(employee);

        return 'redirect:/employees';
    }

In this case, we were accessing employees/{id} with GET or POST method. But what if we want to delete employee? We’ll access same URL but with different method – DELETE. We will use additional business logic in EmployeeDao. If anything goes wrong, we’ll throw exception containing employee that cannot be deleted. So le’ts add controller method for this case:

    /**
     * Deletes employee with specified ID
     * @param id Employee's ID
     * @return redirects to employees if everything was ok
     * @throws EmployeeDeleteException When employee cannot be deleted
     */
    @RequestMapping(value = '/{id}', method = RequestMethod.DELETE)
    public String deleteEmployee(@PathVariable('id') long id)
            throws EmployeeDeleteException {

        Employee toDelete = employeeDao.find(id);
        boolean wasDeleted = employeeDao.removeEmployee(toDelete);

        if (!wasDeleted) {
            throw new EmployeeDeleteException(toDelete);
        }

        // everything OK, see remaining employees
        return 'redirect:/employees';
    }

Notice that we’re returning redirect from that method. The redirect: prefix signals that the request should be redirected to the path that it precedes.

Create package org.timesheet.web.exceptions and place following EmployeeDeleteException there:

package org.timesheet.web.exceptions;

import org.timesheet.domain.Employee;

/**
 * When employee cannot be deleted.
 */
public class EmployeeDeleteException extends Exception {

    private Employee employee;

    public EmployeeDeleteException(Employee employee) {
        this.employee = employee;
    }

    public Employee getEmployee() {
        return employee;
    }
}

Arguably, this exception could directly be thrown from DAO. Now how do we handle it? Spring has special annotation called @ExceptionHandler. We’ll place it in our controller and when specified exception is thrown, method annotated with ExceptionHandler will handle it and resolve correct view:

    /**
     * Handles EmployeeDeleteException
     * @param e Thrown exception with employee that couldn't be deleted
     * @return binds employee to model and returns employees/delete-error
     */
    @ExceptionHandler(EmployeeDeleteException.class)
    public ModelAndView handleDeleteException(EmployeeDeleteException e) {
        ModelMap model = new ModelMap();
        model.put('employee', e.getEmployee());
        return new ModelAndView('employees/delete-error', model);
    }

Okay, time for JSPs. We will be using some resources (like *.css or *.js) so in your web application root create resources folder. WEB-INF is NOT root, it’s the folder above. So resources and WEB-INF now should be in same level in directory tree. We’ve configured our dispatcher servlet to handle every request (with / url pattern), but we don’t want to have static resources handled by it atm. We’ll resolve that by simply putting mapping for default servlet in our web.xml file:

    <servlet-mapping>
        <servlet-name>default</servlet-name>
        <url-pattern>/resources/*</url-pattern>
    </servlet-mapping>

Under those resource create styles.css file. We’ll put CSS for whole app there right now even though we will use lotsa that stuff later.

table, th {
    margin: 10px;
    padding: 5px;
    width: 300px;
}

.main-table {
    border: 2px solid green;
    border-collapse: collapse;
}

.wide {
    width: 600px;
}

.main-table th {
    background-color: green;
    color: white;
}

.main-table td {
    border: 1px solid green;
}

th {
    text-align: left;
}

h1 {
    margin: 10px;
}

a {
    margin: 10px;
}

label {
    display: block;
    text-align: left;
}

#list {
    padding-left: 10px;
    position: relative;
}

#list ul {
    padding: 0;
}

#list li {
    list-style: none;
    margin-bottom: 1em;
}

.hidden {
    display: none;
}

.delete {
    margin: 0;
    text-align: center;
}

.delete-button {
    border: none;
    background: url('/timesheet-app/resources/delete.png') no-repeat top left;
    color: transparent;
    cursor: pointer;
    padding: 2px 8px;
}

.task-table {
    width: 150px;
    border: 1px solid #dcdcdc;
}

.errors {
    color: #000;
    background-color: #ffEEEE;
    border: 3px solid #ff0000;
    padding: 8px;
    margin: 16px;
}

Let’s now create employess/list.jsp page:

<%@ page contentType='text/html;charset=UTF-8' language='java' %>
<%@ taglib prefix='fmt' uri='http://java.sun.com/jsp/jstl/fmt' %>
<%@ taglib prefix='spring' uri='http://www.springframework.org/tags' %>
<%@ taglib prefix='c' uri='http://java.sun.com/jsp/jstl/core'%>
<%@ taglib prefix='sf' uri='http://www.springframework.org/tags/form'%>

<html>
<head>
    <title>Employees</title>
    <link rel='stylesheet' href='/timesheet-app/resources/style.css' type='text/css'>
</head>
<body>
    <h1>List of employees</h1>
    <a href='employees?new'>Add new employee</a>
    <table cellspacing='5' class='main-table'>
        <tr>
            <th>Name</th>
            <th>Department</th>
            <th>Details</th>
            <th>Delete</th>
        </tr>
        <c:forEach items='#{employees}' var='emp'>
            <tr>
                <td>${emp.name}</td>
                <td>${emp.department}</td>
                <td>
                    <a href='employees/${emp.id}'>Go to page</a>
                </td>
                <td>
                    <sf:form action='employees/${emp.id}' method='delete' cssClass='delete'>
                        <input type='submit' class='delete-button' value='' />
                    </sf:form>
                </td>
            </tr>
        </c:forEach>
    </table>

    <br />
    <a href='welcome'>Go back</a>
</body>
</html>

From that page, we’re linking our css under resources (with full name including application context). There’s also link linking to employee’s detail page (view.jsp) that’s resolved from employee’s id.
The most interesting part is usage of sf taglib. To stay Web 1.0 friendly, we unfortunatelly cannot directly use DELETE. Up to HTML4 and XHTML1, html forms can only use GET and POST. Workaround is to use hidden field that marks, if the POST actually should be used as DELETE. That’s precisely what Spring does for us for free – just using sf:form prefix. So we’re tunneling DELETE via HTTP POST, but it will be dispatched correctly.
To make this work, we must put in our web.xml special Spring’s filter for that:

    <filter>
        <filter-name>httpMethodFilter</filter-name>
        <filter-class>
            org.springframework.web.filter.HiddenHttpMethodFilter
        </filter-class>
    </filter>
    
    <filter-mapping>
        <filter-name>httpMethodFilter</filter-name>
        <url-pattern>/*</url-pattern>
    </filter-mapping>

Even though that JSP is java specific technology that is actually compiled to servlet, we can use it pretty much like any HTML page. We’ve added some CSS and now we add most popular javascript library – jQuery. Go to https://www.javacodegeeks.com/wp-content/litespeed/localres/aHR0cHM6Ly9hamF4Lmdvb2dsZWFwaXMuY29tL2FqYXgvlibs/jquery/1.7.1/jquery.js and download jquery.js file and drop it to your resources folder. We will allow users to update resource with POST, so we will use jQuery for some DOM manipulations – just for the sake of fancyness. You can use pretty much anything you can in common HTML pages.

Let’s now create /employees/view.jsp – that’s kinda detail page for employee.

<%@ page contentType='text/html;charset=UTF-8' language='java' %>
<%@ taglib prefix='sf' uri='http://www.springframework.org/tags/form'%>

<html>
<head>
    <title>Employee page</title>
    <link rel='stylesheet' href='/timesheet-app/resources/style.css' type='text/css'>
</head>
<body>
    <h2>Employee info</h2>
    <div id='list'>
        <sf:form method='post'>
            <ul>
                <li>
                    <label for='name'>Name:</label>
                    <input name='name' id='name' value='${employee.name}' disabled='true'/>
                </li>
                <li>
                    <label for='department'>Department:</label>
                    <input name='department' id='department' value='${employee.department}' disabled='true' />
                </li>
                <li>
                    <input type='button' value='Unlock' id='unlock' />
                    <input type='submit' value='Save' id='save' class='hidden' />
                </li>
            </ul>
        </sf:form>
    </div>

    <br /><br />
    <a href='../employees'>Go Back</a>

    <script src='/timesheet-app/resources/jquery-1.7.1.js'></script>
    <script>
        (function() {
            $('#unlock').on('click', function() {
                $('#unlock').addClass('hidden');

                // enable stuff
                $('#name').removeAttr('disabled');
                $('#department').removeAttr('disabled');
                $('#save').removeClass('hidden');
            });
        })();
    </script>
</body>
</html>

Inside the page we refer to jQuery file and we have self-invoking annonymous function – after button with id “unlock” is clicked, we hide it, bring forward the submit button and unlock fields so employee can be updated. After Save button is pressed, we’re redirected back to list of employees and this one has been updated.

The last functionality that we’re gonna get done for CRUD on Employee is adding. We will handle that by accessing employees with GET and extra prameter that we’ll call new. So URL for adding employee will be: http://localhost:8080/timesheet-app/employees?new
Let’s modify our controller for this:

    @RequestMapping(params = 'new', method = RequestMethod.GET)
    public String createEmployeeForm(Model model) {
        model.addAttribute('employee', new Employee());
        return 'employees/new';
    }

That will serve new JSP page – /WEB-INF/jsp/employees/new.jsp:

<%@ taglib prefix='sf' uri='http://www.springframework.org/tags/form' %>
<%@ page contentType='text/html;charset=UTF-8' language='java' %>
<html>
<head>
    <title>Add new employee</title>
    <link rel='stylesheet' href='/timesheet-app/resources/style.css' type='text/css'>
</head>
<body>
    <h2>Add new Employee</h2>
    <div id='list'>
        <sf:form method='post' action='employees'>
            <ul>
                <li>
                    <label for='name'>Name:</label>
                    <input name='name' id='name' value='${employee.name}'/>
                </li>
                <li>
                    <label for='department'>Department:</label>
                    <input name='department' id='department'
                           value='${employee.department}' />
                </li>
                <li>
                    <input type='submit' value='Save' id='save' />
                </li>
            </ul>
        </sf:form>
    </div>

    <br /><br />
    <a href='employees'>Go Back</a>
</body>
</html>

The page is pretty similar to view.jsp. In real world applications we’d use somerhing like Apache Tiles for reducing redundant code, but now let’s not worry about that.

Note that we submit form with “employees” action. Back to our controller, let’s handle employees with POST http method:

    @RequestMapping(method = RequestMethod.POST)
    public String addEmployee(Employee employee) {
        employeeDao.add(employee);

        return 'redirect:/employees';
    }

And let’s not forget about error JSP page, when we cannot delete employee, jsp/employees/delete-error.jsp:

<html>
<head>
    <title>Cannot delete employee</title>
</head>
<body>
    Oops! Resource <a href='${employee.id}'>${employee.name}</a> can not be deleted.

    <p>
        Make sure employee doesn't have assigned any task or active timesheet.
    </p>

    <br /><br /><br />
    <a href='../welcome'>Back to main page.</a>
</body>
</html>

That’s it, we have whole CRUD functionality for our employees. Let’s recap the basic steps what we just did:

  • Added EmployeeController class
  • Create resources folder in web root for static content
  • Added mapping for default servlet in web.xml
  • Added styles.css to resources folder
  • Configured POST-DELETE tunneling with filter in web.xml
  • Downloaded jQuery.js and added to our resources folder
  • Added employess/list.jsp page
  • Added employess/view.jsp page
  • Added employess/new.jsp page
  • Added employees/delete-error.jsp page

Now, here’s complete code for the controller:

package org.timesheet.web;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.ui.ModelMap;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.servlet.ModelAndView;
import org.timesheet.domain.Employee;
import org.timesheet.service.dao.EmployeeDao;
import org.timesheet.web.exceptions.EmployeeDeleteException;

import java.util.List;

/**
 * Controller for handling Employees.
 */
@Controller
@RequestMapping('/employees')
public class EmployeeController {

    private EmployeeDao employeeDao;

    @Autowired
    public void setEmployeeDao(EmployeeDao employeeDao) {
        this.employeeDao = employeeDao;
    }

    public EmployeeDao getEmployeeDao() {
        return employeeDao;
    }

    /**
     * Retrieves employees, puts them in the model and returns corresponding view
     * @param model Model to put employees to
     * @return employees/list
     */
    @RequestMapping(method = RequestMethod.GET)
    public String showEmployees(Model model) {
        List<Employee> employees = employeeDao.list();
        model.addAttribute('employees', employees);

        return 'employees/list';
    }

    /**
     * Deletes employee with specified ID
     * @param id Employee's ID
     * @return redirects to employees if everything was ok
     * @throws EmployeeDeleteException When employee cannot be deleted
     */
    @RequestMapping(value = '/{id}', method = RequestMethod.DELETE)
    public String deleteEmployee(@PathVariable('id') long id)
            throws EmployeeDeleteException {

        Employee toDelete = employeeDao.find(id);
        boolean wasDeleted = employeeDao.removeEmployee(toDelete);

        if (!wasDeleted) {
            throw new EmployeeDeleteException(toDelete);
        }

        // everything OK, see remaining employees
        return 'redirect:/employees';
    }

    /**
     * Handles EmployeeDeleteException
     * @param e Thrown exception with employee that couldn't be deleted
     * @return binds employee to model and returns employees/delete-error
     */
    @ExceptionHandler(EmployeeDeleteException.class)
    public ModelAndView handleDeleteException(EmployeeDeleteException e) {
        ModelMap model = new ModelMap();
        model.put('employee', e.getEmployee());
        return new ModelAndView('employees/delete-error', model);
    }

    /**
     * Returns employee with specified ID
     * @param id Employee's ID
     * @param model Model to put employee to
     * @return employees/view
     */
    @RequestMapping(value = '/{id}', method = RequestMethod.GET)
    public String getEmployee(@PathVariable('id') long id, Model model) {
        Employee employee = employeeDao.find(id);
        model.addAttribute('employee', employee);

        return 'employees/view';
    }

    /**
     * Updates employee with specified ID
     * @param id Employee's ID
     * @param employee Employee to update (bounded from HTML form)
     * @return redirects to employees
     */
    @RequestMapping(value = '/{id}', method = RequestMethod.POST)
    public String updateEmployee(@PathVariable('id') long id, Employee employee) {
        employee.setId(id);
        employeeDao.update(employee);

        return 'redirect:/employees';
    }

    /**
     * Creates form for new employee
     * @param model Model to bind to HTML form
     * @return employees/new
     */
    @RequestMapping(params = 'new', method = RequestMethod.GET)
    public String createEmployeeForm(Model model) {
        model.addAttribute('employee', new Employee());
        return 'employees/new';
    }

    /**
     * Saves new employee to the database
     * @param employee Employee to save
     * @return redirects to employees
     */
    @RequestMapping(method = RequestMethod.POST)
    public String addEmployee(Employee employee) {
        employeeDao.add(employee);

        return 'redirect:/employees';
    }
    
}

If you’re using SpringSource Tool Suite, you can check mappings directly in IDE. Add to your project “Spring Project Nature”, in Properties->Spring->Bean Support configure your Spring’s config file. Then right click project and press Spring Tools->Show Request Mappings and you should see something like this:

Last thing about employees that remains is writing JUnit test. Since we have our timesheet-servlet.xml in WEB-INF, we can’t access its beans in JUnit test. What we’ll do is remove following line from timesheet-servlet.xml:

<context:component-scan base-package='org.timesheet.web' />

We now create new Spring’s bean config in src/main/resources and call it controllers.xml. The only thing we care about is to put autoscanning for controllers here, so content is pretty simple:

<?xml version='1.0' encoding='UTF-8'?>
<beans xmlns='http://www.springframework.org/schema/beans'
       xmlns:xsi='http://www.w3.org/2001/XMLSchema-instance'
       xmlns:context='http://www.springframework.org/schema/context'
       xsi:schemaLocation='http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.1.xsd'>

    <context:component-scan base-package='org.timesheet.web' />

</beans>

To make context aware of those spring beans, alter context-param in web.xml like this:

    <context-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>
            classpath:persistence-beans.xml
            classpath:controllers.xml
        </param-value>
    </context-param>

Also, we now must import beans from controllers.xml to timesheet-servlet.xml, so instead of removed line <context:component-scan … from timesheet-servlet.xml, we add following:

<import resource='classpath:controllers.xml' />

That’ll allow us to have controllers autowired to tests. Okay, so in the test source folder create package org.timesheet.web and let’s put there EmployeeControllerTest. It’s pretty straightforward and we only test controller as POJO and how does it affect persistence layer (verifying via DAO). We made one exception however. In method testDeleteEmployeeThrowsException, we will explicitelly tell DAO to return false when trying to delete employee. This will save us complicated object creation and injection of additional DAOs. We will use popular mocking framework for this – Mockito.

Add dependency to your pom.xml:

        <dependency>
            <groupId>org.mockito</groupId>
            <artifactId>mockito-all</artifactId>
            <version>1.9.0</version>
        </dependency>

Test for EmployeeController:

package org.timesheet.web;

import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.ui.ExtendedModelMap;
import org.springframework.ui.Model;
import org.springframework.web.servlet.ModelAndView;
import org.timesheet.DomainAwareBase;
import org.timesheet.domain.Employee;
import org.timesheet.service.dao.EmployeeDao;
import org.timesheet.web.exceptions.EmployeeDeleteException;

import java.util.Collection;
import java.util.List;

import static org.junit.Assert.*;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

@ContextConfiguration(locations = {'/persistence-beans.xml', '/controllers.xml'})
public class EmployeeControllerTest extends DomainAwareBase {
    
    @Autowired
    private EmployeeDao employeeDao;

    @Autowired
    private EmployeeController controller;
    
    private Model model; // used for controller
    
    @Before
    public void setUp() {
        model = new ExtendedModelMap();
    }

    @After
    public void cleanUp() {
        List<Employee> employees = employeeDao.list();
        for (Employee employee : employees) {
            employeeDao.remove(employee);
        }
    }

    @Test
    public void testShowEmployees() {
        // prepare some data
        Employee employee = new Employee('Lucky', 'Strike');
        employeeDao.add(employee);

        // use controller
        String view = controller.showEmployees(model);
        assertEquals('employees/list', view);

        List<Employee> listFromDao = employeeDao.list();
        Collection<?> listFromModel = (Collection<?>) model.asMap().get('employees');

        assertTrue(listFromModel.contains(employee));
        assertTrue(listFromDao.containsAll(listFromModel));
    }
    
    @Test
    public void testDeleteEmployeeOk() throws EmployeeDeleteException {
        // prepare ID to delete
        Employee john = new Employee('John Lennon', 'Singing');
        employeeDao.add(john);
        long id = john.getId();

        // delete & assert
        String view = controller.deleteEmployee(id);
        assertEquals('redirect:/employees', view);
        assertNull(employeeDao.find(id));
    }

    @Test(expected = EmployeeDeleteException.class)
    public void testDeleteEmployeeThrowsException() throws EmployeeDeleteException {
        // prepare ID to delete
        Employee john = new Employee('John Lennon', 'Singing');
        employeeDao.add(john);
        long id = john.getId();

        // mock DAO for this call
        EmployeeDao mockedDao = mock(EmployeeDao.class);
        when(mockedDao.removeEmployee(john)).thenReturn(false);

        EmployeeDao originalDao = controller.getEmployeeDao();
        try {
            // delete & expect exception
            controller.setEmployeeDao(mockedDao);
            controller.deleteEmployee(id);
        } finally {
            controller.setEmployeeDao(originalDao);
        }
    }

    @Test
    public void testHandleDeleteException() {
        Employee john = new Employee('John Lennon', 'Singing');
        EmployeeDeleteException e = new EmployeeDeleteException(john);
        ModelAndView modelAndView = controller.handleDeleteException(e);

        assertEquals('employees/delete-error', modelAndView.getViewName());
        assertTrue(modelAndView.getModelMap().containsValue(john));
    }
    
    @Test
    public void testGetEmployee() {
        // prepare employee
        Employee george = new Employee('George Harrison', 'Singing');
        employeeDao.add(george);
        long id = george.getId();
        
        // get & assert
        String view = controller.getEmployee(id, model);
        assertEquals('employees/view', view);
        assertEquals(george, model.asMap().get('employee'));
    }

    @Test
    public void testUpdateEmployee() {
        // prepare employee
        Employee ringo = new Employee('Ringo Starr', 'Singing');
        employeeDao.add(ringo);
        long id = ringo.getId();

        // user alters Employee in HTML form
        ringo.setDepartment('Drums');

        // update & assert
        String view = controller.updateEmployee(id, ringo);
        assertEquals('redirect:/employees', view);
        assertEquals('Drums', employeeDao.find(id).getDepartment());
    }

    @Test
    public void testAddEmployee() {
        // prepare employee
        Employee paul = new Employee('Paul McCartney', 'Singing');
        
        // save but via controller
        String view = controller.addEmployee(paul);
        assertEquals('redirect:/employees', view);

        // employee is stored in DB
        assertEquals(paul, employeeDao.find(paul.getId()));
    }
}

Notice how do we use mocked dao for setting it in try/finally block. It’s just for that one call to ensure that correct exception is thrown. If you’ve never seen mocking, I definitelly suggest learning more about this technique. There are plenty of mocking frameworks. The one we picked – Mockito – comes with really neat syntax that heavily uses java static imports.

Now, Managers are pretty similar to Employees, so without any big problems, let’s add pretty similar stuff for managers:

First, create managers folder in WEB-INF/jsp.

Now let’s write controller and inject corresponding DAO:

@Controller
@RequestMapping('/managers')
public class ManagerController {

    private ManagerDao managerDao;

    @Autowired
    public void setManagerDao(ManagerDao managerDao) {
        this.managerDao = managerDao;
    }

    public ManagerDao getManagerDao() {
        return managerDao;
    }
}

Add method for listing managers:

    /**
     * Retrieves managers, puts them in the model and returns corresponding view
     * @param model Model to put employees to
     * @return managers/list
     */
    @RequestMapping(method = RequestMethod.GET)
    public String showManagers(Model model) {
        List<Manager> employees = managerDao.list();
        model.addAttribute('managers', employees);

        return 'managers/list';
    }

Add list.jsp to jsp/managers:

<%@ page contentType='text/html;charset=UTF-8' language='java' %>
<%@ taglib prefix='fmt' uri='http://java.sun.com/jsp/jstl/fmt' %>
<%@ taglib prefix='spring' uri='http://www.springframework.org/tags' %>
<%@ taglib prefix='c' uri='http://java.sun.com/jsp/jstl/core'%>
<%@ taglib prefix='sf' uri='http://www.springframework.org/tags/form'%>

<html>
<head>
    <title>Managers</title>
    <link rel='stylesheet' href='/timesheet-app/resources/style.css' type='text/css'>
</head>
<body>
    <h1>List of managers</h1>
    <a href='managers?new'>Add new manager</a>
    <table cellspacing='5' class='main-table'>
        <tr>
            <th>Name</th>
            <th>Details</th>
            <th>Delete</th>
        </tr>
        <c:forEach items='#{managers}' var='man'>
            <tr>
                <td>${man.name}</td>
                <td>
                    <a href='managers/${man.id}'>Go to page</a>
                </td>
                <td>
                    <sf:form action='managers/${man.id}' method='delete' cssClass='delete'>
                        <input type='submit' value='' class='delete-button' />
                    </sf:form>
                </td>
            </tr>
        </c:forEach>
    </table>

    <br />
    <a href='welcome'>Go back</a>
</body>
</html>

Add method for deleting managers:

    /**
     * Deletes manager with specified ID
     * @param id Manager's ID
     * @return redirects to managers if everything was OK
     * @throws ManagerDeleteException When manager cannot be deleted
     */
    @RequestMapping(value = '/{id}', method = RequestMethod.DELETE)
    public String deleteManager(@PathVariable('id') long id)
            throws ManagerDeleteException {

        Manager toDelete = managerDao.find(id);
        boolean wasDeleted = managerDao.removeManager(toDelete);

        if (!wasDeleted) {
            throw new ManagerDeleteException(toDelete);
        }

        // everything OK, see remaining managers
        return 'redirect:/managers';
    }

Exception when deleting fails:

package org.timesheet.web.exceptions;

import org.timesheet.domain.Manager;

/**
 * When manager cannot be deleted
 */
public class ManagerDeleteException extends Exception {

    private Manager manager;

    public ManagerDeleteException(Manager manager) {
        this.manager = manager;
    }

    public Manager getManager() {
        return manager;
    }
}

Method for handling this exception:

    /**
     * Handles ManagerDeleteException
     * @param e Thrown exception with manager that couldn't be deleted
     * @return binds manager to model and returns managers/delete-error
     */
    @ExceptionHandler(ManagerDeleteException.class)
    public ModelAndView handleDeleteException(ManagerDeleteException e) {
        ModelMap model = new ModelMap();
        model.put('manager', e.getManager());
        return new ModelAndView('managers/delete-error', model);
    }

Add method for getting manager’s page:

    /**
     * Returns manager with specified ID
     * @param id Managers's ID
     * @param model Model to put manager to
     * @return managers/view
     */
    @RequestMapping(value = '/{id}', method = RequestMethod.GET)
    public String getManager(@PathVariable('id') long id, Model model) {
        Manager manager = managerDao.find(id);
        model.addAttribute('manager', manager);

        return 'managers/view';
    }

Add manager’s page view.jsp under jsp/managers:

<%@ page contentType='text/html;charset=UTF-8' language='java' %>
<%@ taglib prefix='sf' uri='http://www.springframework.org/tags/form'%>

<html>
<head>
    <title>Manager page</title>
    <link rel='stylesheet' href='/timesheet-app/resources/style.css' type='text/css'>
</head>
<body>
    <h2>Manager info</h2>
    <div id='list'>
        <sf:form method='post'>
            <ul>
                <li>
                    <label for='name'>Name:</label>
                    <input name='name' id='name' value='${manager.name}' disabled='true'/>
                </li>
                <li>
                    <input type='button' value='Unlock' id='unlock' />
                    <input type='submit' value='Save' id='save' class='hidden' />
                </li>
            </ul>
        </sf:form>
    </div>

    <br /><br />
    <a href='../managers'>Go Back</a>

    <script src='/timesheet-app/resources/jquery-1.7.1.js'></script>
    <script>
        (function() {
            $('#unlock').on('click', function() {
                $('#unlock').addClass('hidden');

                // enable stuff
                $('#name').removeAttr('disabled');
                $('#save').removeClass('hidden');
            });
        })();
    </script>
</body>
</html>

JSP page for handling error on deleting:

<html>
<head>
    <title>Cannot delete manager</title>
</head>
<body>
    Oops! Resource <a href='${manager.id}'>${manager.name}</a> can not be deleted.

    <p>
        Make sure manager doesn't have assigned any task or active timesheet.
    </p>

    <br /><br /><br />
    <a href='../welcome'>Back to main page.</a>
</body>
</html>

Add method for updating manager:

    /**
     * Updates manager with specified ID
     * @param id Manager's ID
     * @param manager Manager to update (bounded from HTML form)
     * @return redirects to managers
     */
    @RequestMapping(value = '/{id}', method = RequestMethod.POST)
    public String updateManager(@PathVariable('id') long id, Manager manager) {
        manager.setId(id);
        managerDao.update(manager);

        return 'redirect:/managers';
    }

Add method for returning new manager’s form:

    /**
     * Creates form for new manager
     * @param model Model to bind to HTML form
     * @return manager/new
     */
    @RequestMapping(params = 'new', method = RequestMethod.GET)
    public String createManagerForm(Model model) {
        model.addAttribute('manager', new Manager());
        return 'managers/new';
    }

Add page for new manager new.jsp under jsp/managers:

<%@ taglib prefix='sf' uri='http://www.springframework.org/tags/form' %>
<%@ page contentType='text/html;charset=UTF-8' language='java' %>
<html>
<head>
    <title>Add new manager</title>
    <link rel='stylesheet' href='/timesheet-app/resources/style.css' type='text/css'>
</head>
<body>
    <h2>Add new Manager</h2>
    <div id='list'>
        <sf:form method='post' action='managers'>
            <ul>
                <li>
                    <label for='name'>Name:</label>
                    <input name='name' id='name' value='${manager.name}'/>
                </li>
                <li>
                    <input type='submit' value='Save' id='save' />
                </li>
            </ul>
        </sf:form>
    </div>

    <br /><br />
    <a href='managers'>Go Back</a>
</body>
</html>

And finally, add method for adding manager:

    /**
     * Saves new manager to the database
     * @param manager Manager to save
     * @return redirects to managers
     */
    @RequestMapping(method = RequestMethod.POST)
    public String addManager(Manager manager) {
        managerDao.add(manager);

        return 'redirect:/managers';
    }

Ok last piece of code for this part is test case for ManagerController:

package org.timesheet.web;

import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.ui.ExtendedModelMap;
import org.springframework.ui.Model;
import org.springframework.web.servlet.ModelAndView;
import org.timesheet.DomainAwareBase;
import org.timesheet.domain.Manager;
import org.timesheet.service.dao.ManagerDao;
import org.timesheet.web.exceptions.ManagerDeleteException;

import java.util.Collection;
import java.util.List;

import static org.junit.Assert.*;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

@ContextConfiguration(locations = {'/persistence-beans.xml', '/controllers.xml'})
public class ManagerControllerTest extends DomainAwareBase {
    
    @Autowired
    private ManagerDao managerDao;

    @Autowired
    private ManagerController controller;
    
    private Model model; // used for controller
    
    @Before
    public void setUp() {
        model = new ExtendedModelMap();
    }

    @After
    public void cleanUp() {
        List<Manager> managers = managerDao.list();
        for (Manager manager : managers) {
            managerDao.remove(manager);
        }
    }

    @Test
    public void testShowManagers() {
        // prepare some data
        Manager manager = new Manager('Bob Dylan');
        managerDao.add(manager);

        // use controller
        String view = controller.showManagers(model);
        assertEquals('managers/list', view);

        List<Manager> listFromDao = managerDao.list();
        Collection<?> listFromModel = (Collection<?>) model.asMap().get('managers');

        assertTrue(listFromModel.contains(manager));
        assertTrue(listFromDao.containsAll(listFromModel));
    }
    
    @Test
    public void testDeleteManagerOk() throws ManagerDeleteException {
        // prepare ID to delete
        Manager john = new Manager('John Lennon');
        managerDao.add(john);
        long id = john.getId();

        // delete & assert
        String view = controller.deleteManager(id);
        assertEquals('redirect:/managers', view);
        assertNull(managerDao.find(id));
    }

    @Test(expected = ManagerDeleteException.class)
    public void testDeleteManagerThrowsException() throws ManagerDeleteException {
        // prepare ID to delete
        Manager john = new Manager('John Lennon');
        managerDao.add(john);
        long id = john.getId();

        // mock DAO for this call
        ManagerDao mockedDao = mock(ManagerDao.class);
        when(mockedDao.removeManager(john)).thenReturn(false);

        ManagerDao originalDao = controller.getManagerDao();
        try {
            // delete & expect exception
            controller.setManagerDao(mockedDao);
            controller.deleteManager(id);
        } finally {
            controller.setManagerDao(originalDao);
        }
    }

    @Test
    public void testHandleDeleteException() {
        Manager john = new Manager('John Lennon');
        ManagerDeleteException e = new ManagerDeleteException(john);
        ModelAndView modelAndView = controller.handleDeleteException(e);

        assertEquals('managers/delete-error', modelAndView.getViewName());
        assertTrue(modelAndView.getModelMap().containsValue(john));
    }

    @Test
    public void testGetManager() {
        // prepare manager
        Manager george = new Manager('George Harrison');
        managerDao.add(george);
        long id = george.getId();
        
        // get & assert
        String view = controller.getManager(id, model);
        assertEquals('managers/view', view);
        assertEquals(george, model.asMap().get('manager'));
    }

    @Test
    public void testUpdateManager() {
        // prepare manager
        Manager ringo = new Manager('Ringo Starr');
        managerDao.add(ringo);
        long id = ringo.getId();

        // user alters manager in HTML form
        ringo.setName('Rango Starr');

        // update & assert
        String view = controller.updateManager(id, ringo);
        assertEquals('redirect:/managers', view);
        assertEquals('Rango Starr', managerDao.find(id).getName());
    }

    @Test
    public void testAddManager() {
        // prepare manager
        Manager paul = new Manager('Paul McCartney');
        
        // save but via controller
        String view = controller.addManager(paul);
        assertEquals('redirect:/managers', view);

        // manager is stored in DB
        assertEquals(paul, managerDao.find(paul.getId()));
    }
}

Request mappings looks like this now:

So in this part, we learned what is Spring MVC, how to use our entities as models, how to write controllers in POJO style, how RESTful design looks like, how to create views with JSPs and how to setup application for using CSS and JavaScript.

We wrote controllers for Employees and Managers. In the next part we’ll continue with writing controllers for Tasks and Timesheets. Make sure everything works well so far before you proceed to the next part.

Here’s src folder (only new stuff is expanded. Don’t worry about .iml files, they’re for IntelliJ):

Here’s web folder:

Reference: Part 4 – Adding Spring MVC – part 1 from our JCG partner Michal Vrtiak at the vrtoonjava blog.

Michal Vrtiak

Michal is a freelancer currently located in Prague, Czech Republic with huge passion for Java platform. He is very enthusiastic about Dependency Injection, IntelliJ IDEA and loves to use both Spring and Java EE.
Subscribe
Notify of
guest

This site uses Akismet to reduce spam. Learn how your comment data is processed.

2 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
Kirill
Kirill
9 years ago

Thank you for your post. It’s very useful. What about persistence-beans.xml file? Whithout this I have errors when I try ro run application on Tomcat server.

St1mpy
St1mpy
3 years ago

Nice article. A bit rotten maybe.In 2020 jsps are old fashioned and spring is using java config not XML. But you can still learn from this well structured tutorial

Back to top button