Enterprise Java

Spring – Adding Spring MVC – part 2

In the previous part we’ve implemented controllers for managers and employees. Now that we know our way around, we’ll do little (but just little) more complicated stuff – controllers for tasks & timesheets.

So let’s start with org.timesheet.web.TaskController. First create a class and this time we will be accessing richer domain, so we’ll need to autowire three DAOS – for tasks, employees and managers.

@Controller
@RequestMapping('/tasks')
public class TaskController {

    private TaskDao taskDao;
    private EmployeeDao employeeDao;
    private ManagerDao managerDao;

    @Autowired
    public void setTaskDao(TaskDao taskDao) {
        this.taskDao = taskDao;
    }

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

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

    public EmployeeDao getEmployeeDao() {
        return employeeDao;
    }

    public TaskDao getTaskDao() {
        return taskDao;
    }

    public ManagerDao getManagerDao() {
        return managerDao;
    }
}

Let’s handle GET request on /tasks:

    /**
     * Retrieves tasks, puts them in the model and returns corresponding view
     * @param model Model to put tasks to
     * @return tasks/list
     */
    @RequestMapping(method = RequestMethod.GET)
    public String showTasks(Model model) {
        model.addAttribute('tasks', taskDao.list());

        return 'tasks/list';
    }

We will place JSPs in tasks subfolder. First is list.jsp for showing all tasks. It does not only iterate through all tasks, but on each task it iterates through employees:

<%@ 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'%>

<!-- resolve variables -->
<%--@elvariable id='tasks' type='java.util.List<org.timesheet.domain.Task>'--%>

<html>
<head>
    <title>Tasks</title>
    <link rel='stylesheet' href='/timesheet-app/resources/style.css' type='text/css'>
</head>
<body>
    <h1>List of tasks</h1>
    <a href='tasks?new'>Add new task</a>
    <table cellspacing='5' class='main-table wide'>
        <tr>
            <th style='width: 35%;'>Description</th>
            <th>Manager</th>
            <th>Employees</th>
            <th>Completed</th>
            <th style='width: 20%;'>Details</th>
            <th>Delete</th>
        </tr>
        <c:forEach items='${tasks}' var='task'>
            <tr>
                <td>${task.description}</td>
                <td>
                    <a href='managers/${task.manager.id}'>${task.manager.name}</a>
                </td>
                <td>
                    <c:forEach items='${task.assignedEmployees}' var='emp'>
                        <a href='employees/${emp.id}'>${emp.name}</a>
                    </c:forEach>
                </td>
                <td>
                    <div class='delete'>
                        <c:choose>
                            <c:when test='${task.completed}'>
                                Done
                            </c:when>
                            <c:when test='${!task.completed}'>
                                In progress
                            </c:when>
                        </c:choose>
                    </div>
                </td>
                <td>
                    <a href='tasks/${task.id}'>Go to page</a>
                </td>
                <td>
                    <sf:form action='tasks/${task.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>

Deleting task as usual:

    /**
     * Deletes task with specified ID
     * @param id Task's ID
     * @return redirects to tasks if everything was ok
     * @throws TaskDeleteException When task cannot be deleted
     */
    @RequestMapping(value = '/{id}', method = RequestMethod.DELETE)
    public String deleteTask(@PathVariable('id') long id) 
            throws TaskDeleteException {

        Task toDelete = taskDao.find(id);
        boolean wasDeleted = taskDao.removeTask(toDelete);

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

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

TaskDeleteException:

package org.timesheet.web.exceptions;

import org.timesheet.domain.Task;

/**
 * When task cannot be deleted.
 */
public class TaskDeleteException extends Exception {

    private Task task;

    public TaskDeleteException(Task task) {
        this.task = task;
    }

    public Task getTask() {
        return task;
    }
}

Method for handling this exception:

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

JSP page jsp/tasks/delete-error.jsp for showing deletion error:

<%--@elvariable id='task' type='org.timesheet.domain.Task'--%>

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

    <p>
        Make sure there are no timesheets assigned on task.
    </p>

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

Showing task’s detail will be accessed with URI /tasks/{id}. We’ll put in the model both task and unassigned employees that can be added to the task. It’ll be handled like so:

    /**
     * Returns task with specified ID
     * @param id Tasks's ID
     * @param model Model to put task to
     * @return tasks/view
     */
    @RequestMapping(value = '/{id}', method = RequestMethod.GET)
    public String getTask(@PathVariable('id') long id, Model model) {
        Task task = taskDao.find(id);
        model.addAttribute('task', task);

        // add all remaining employees
        List<Employee> employees = employeeDao.list();
        Set<Employee> unassignedEmployees = new HashSet<Employee>();

        for (Employee employee : employees) {
            if (!task.getAssignedEmployees().contains(employee)) {
                unassignedEmployees.add(employee);
            }
        }

        model.addAttribute('unassigned', unassignedEmployees);

        return 'tasks/view';
    }

Now something slightly more complicated. We would like to show user detail page of the task. On this task we’d like to add/remove employees assigned on it.
First, let’s think of URL. Tasks have assigned employees, so our URL for accessing employee on task will be like this:
/tasks/{id}/employees/{employeeId}
To remove employee, we will simply access this resource with DELETE method, so let’s add method to controller:

    /**
     * Removes assigned employee from task
     * @param taskId Task's ID
     * @param employeeId Assigned employee's ID
     */
    @RequestMapping(value = '/{id}/employees/{employeeId}', method = RequestMethod.DELETE)
    @ResponseStatus(HttpStatus.NO_CONTENT)
    public void removeEmployee(
            @PathVariable('id') long taskId,
            @PathVariable('employeeId') long employeeId) {

        Employee employee = employeeDao.find(employeeId);
        Task task = taskDao.find(taskId);

        task.removeEmployee(employee);
        taskDao.update(task);
    }

On the view page (we’ll see that just in moment), we will simply alter DOM model using jQuery and remove assigned employee from list.
Let’s pretend that nothing can go wrong (we have NO_CONTENT response) so employee will always be successfully removed from DB. So we can simply alter that DOM model.

For adding employee, we will have selection list (or combo box) of unassigned employees. When employee is removed we will append this to selection of available employees (he is available again). When employee will be added, we will alter Task with DAO and redirect back to same task (everything will be updated). Here’s code for assigning employee to task:

    /**
     * Assigns employee to tak
     * @param taskId Task's ID
     * @param employeeId Employee's ID (to assign)
     * @return redirects back to altered task: tasks/taskId
     */
    @RequestMapping(value = '/{id}/employees/{employeeId}', method = RequestMethod.PUT)
    public String addEmployee(
            @PathVariable('id') long taskId,
            @PathVariable('employeeId') long employeeId) {

        Employee employee = employeeDao.find(employeeId);
        Task task = taskDao.find(taskId);

        task.addEmployee(employee);
        taskDao.update(task);
        
        return 'redirect:/tasks/' + taskId;
    }

And finally, tasks/view.jsp for details of Task. As I mentioned, there is lot of DOM altering so this code might seem little more difficult than usually.

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

<%--@elvariable id='task' type='org.timesheet.domain.Task'--%>
<%--@elvariable id='unassigned' type='java.util.List<org.timesheet.domain.Employee>'--%>

<html>
<head>
    <title>Task page</title>
    <link rel='stylesheet' href='/timesheet-app/resources/style.css' type='text/css'>
</head>
<body>
    <h2>Task info</h2>
    <div id='list'>
        <ul>
            <li>
                <label for='description'>Description:</label>
                <input name='description' id='description' value='${task.description}'
                       disabled='${task.completed ? 'disabled' : ''}' />
            </li>
            <li>
                <label for='manager'>Manager:</label>
                <input name='manager' id='manager' value='${task.manager.name}'
                        disabled='true' />
            </li>
            <li>
                <label for='employees'>Employees:</label>
                <table id='employees' class='task-table'>
                    <c:forEach items='${task.assignedEmployees}' var='emp'>
                        <tr>
                            <sf:form action='${task.id}/employees/${emp.id}' method='delete'>
                                <td>
                                    <a href='../employees/${emp.id}' id='href-${emp.id}'>${emp.name}</a>
                                </td>
                                <td>
                                    <input type='submit' value='Remove' id='remove-${emp.id}' />
                                    <script src='/timesheet-app/resources/jquery-1.7.1.js'></script>
                                    <script type='text/javascript'>
                                        $('#remove-${emp.id}').on('click', function() {
                                            $('#remove-${emp.id}').addClass('hidden');
                                            $('#href-${emp.id}').remove();

                                            // add to list of unassigned
                                            var opt = document.createElement('option');
                                            opt.setAttribute('value', '${emp.id}');
                                            opt.textContent = '${emp.name}';
                                            $('#selected-emp').append(opt);
                                        });
                                    </script>
                                </td>
                            </sf:form>
                        </tr>
                    </c:forEach>
                </table>
            </li>
            <li>
                <label for='unassigned'>Unassgined:</label>
                <table id='unassigned' class='task-table'>
                    <tr>
                        <sf:form method='put' id='add-form'>
                            <td>
                                <select id='selected-emp'>
                                    <c:forEach items='${unassigned}' var='uemp'>
                                        <option value='${uemp.id}'>
                                            ${uemp.name}
                                        </option>
                                    </c:forEach>
                                </select>
                            </td>
                            <td>
                                <input type='submit' value='Add' id='add-employee' />
                                <script src='/timesheet-app/resources/jquery-1.7.1.js'></script>
                                <script type='text/javascript'>
                                    $('#add-employee').on('click', function() {
                                        $('#selected-emp').selected().remove();
                                    });
                                </script>
                            </td>
                        </sf:form>
                    </tr>
                </table>
            </li>
        </ul>
    </div>

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

    <script src='/timesheet-app/resources/jquery-1.7.1.js'></script>
    <script type='text/javascript'>
        (function() {
            // prepare default form action
            setAddAction();

            // handler for changing action
            $('#selected-emp').on('change', function() {
                setAddAction();
            });

            function setAddAction() {
                var id = $('#selected-emp').val();
                $('#add-form').attr('action', '${task.id}/employees/' + id);
            }
        })();
    </script>
</body>
</html>

As you can observe from the code, we’re again using only HTML + JavaScript. Only thing that is JSP specific is bringing data from model to the page.

OK, now we must be able to create new Task. Let’s prepare our controller for serving form for adding task that will be accessed from /tasks?new:

    /**
     * Creates form for new task.
     * @param model Model to bind to HTML form
     * @return tasks/new
     */
    @RequestMapping(params = 'new', method = RequestMethod.GET)
    public String createTaskForm(Model model) {
        model.addAttribute('task', new Task());

        // list of managers to choose from
        List<Manager> managers = managerDao.list();
        model.addAttribute('managers', managers);

        return 'tasks/new';
    }

Task consists of name, manager and assigned employees. For scope of this tutorial, I decided not to implement the last one. We will simply generate some employees. If you will ever want to be able to pick employees from some sort of selection list and assign them to task, then please note, that this should be done asynchronously. For that purposes you can map special methods to controller and do AJAX posts for example with jQuery with $.post. I think that would be little too much for this tutorial, but if you’re interested how to use AJAX with Spring, check out this blog post on ajax simplifications in Spring 3.
When we were creating Employees and Managers, we only used primitive types for properties. Now we would like to assign actual Manager instance to task. So we will have to tell Spring how it should convert value from select list (manager’s id) to actual instance. For this we will use custom PropertyEditorSupport facility. Add new org.timesheet.web.editors package and create new class ManagerEditor with following code:

public class ManagerEditor extends PropertyEditorSupport {

    private ManagerDao managerDao;

    public ManagerEditor(ManagerDao managerDao) {
        this.managerDao = managerDao;
    }

    @Override
    public void setAsText(String text) throws IllegalArgumentException {
        long id = Long.parseLong(text);
        Manager manager = managerDao.find(id);
        setValue(manager);
    }
}

ManagerEditor will have passed DAO in it’s constructor. It will lookup actual manager by it’s ID and call parent’s setValue.
Spring should now know there is such an editor, so we must register it in our controller. We only need method that has WebDataBinder as parameter and we need to annotate it with @InitBinder annotation like so:

    @InitBinder
    protected void initBinder(WebDataBinder binder) {
        binder.registerCustomEditor(Manager.class, new ManagerEditor(managerDao));
    }

And that’s it, Spring now knows how to assign manager to our task directly from form.

Finally code for saving Task. As I said earlier, we will generate some employees to task just before saving it:

    /**
     * Saves new task to the database
     * @param task Task to save
     * @return redirects to tasks
     */
    @RequestMapping(method = RequestMethod.POST)
    public String addTask(Task task) {
        // generate employees
        List<Employee> employees = reduce(employeeDao.list());

        task.setAssignedEmployees(employees);
        taskDao.add(task);

        return 'redirect:/tasks';
    }

There is reduce method, which is simple helper method for reducing employees in memory. This is not terribly effective, we could do that rather with more sophisticated query, but for now it’ll do just fine. Also feel free to roll your own reduce logic if you want:

    /**
     * Reduces list of employees to some smaller amount.
     * Simulates user interaction.
     * @param employees Employees to reduced
     * @return New list of some employees from original employees list
     */
    private List<Employee> reduce(List<Employee> employees) {
        List<Employee> reduced = new ArrayList<Employee>();
        Random random = new Random();
        int amount = random.nextInt(employees.size()) + 1;

        // max. five employees
        amount = amount > 5 ? 5 : amount;

        for (int i = 0; i < amount; i++) {
            int randomIdx = random.nextInt(employees.size());
            Employee employee = employees.get(randomIdx);
            reduced.add(employee);
            employees.remove(employee);
        }

        return reduced;
    }

Let’s see tasks/new.jsp page now:

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

<%--@elvariable id='task' type='org.timesheet.domain.Task'--%>
<%--@elvariable id='managers' type='java.util.List<org.timesheet.domain.Manager'--%>

<html>
<head>
    <title>Add new task</title>
    <link rel='stylesheet' href='/timesheet-app/resources/style.css' type='text/css'>
</head>
<body>
    <h2>Add new Task</h2>
    <div id='list'>
        <sf:form method='post' action='tasks' commandName='task'>
            <ul>
                <li>
                    <label for='description'>Description:</label>
                    <input name='description' id='description' value='${task.description}' />
                </li>
                <li>
                    <label for='manager-select'>Manager:</label>
                    <sf:select path='manager' id='manager-select'>
                        <sf:options items='${managers}' itemLabel='name' itemValue='id' />
                    </sf:select>
                </li>
                <li>
                    Employees will be generated ...
                </li>
                <li>
                    <input type='submit' value='Save'>
                </li>
            </ul>
        </sf:form>
    </div>

    <br /><br />
    <a href='tasks'>Go Back</a>

</body>
</html>

And of course test for controller:

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.domain.Manager;
import org.timesheet.domain.Task;
import org.timesheet.service.dao.EmployeeDao;
import org.timesheet.service.dao.ManagerDao;
import org.timesheet.service.dao.TaskDao;
import org.timesheet.web.exceptions.TaskDeleteException;

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 TaskControllerTest extends DomainAwareBase {

    private Model model; // used for controller
    
    @Autowired
    private TaskDao taskDao;

    @Autowired
    private ManagerDao managerDao;

    @Autowired
    private EmployeeDao employeeDao;
    
    @Autowired
    private TaskController controller;
    
    @Before
    public void setUp() {
        model = new ExtendedModelMap();    
    }
    
    @After
    public void cleanUp() {
        List<Task> tasks = taskDao.list();
        for (Task task : tasks) {
            taskDao.remove(task);
        }
    }

    @Test
    public void testShowTasks() {
        // prepare some data
        Task task = sampleTask();
        
        // use controller
        String view = controller.showTasks(model);
        assertEquals('tasks/list', view);

        List<Task> listFromDao = taskDao.list();
        Collection<?> listFromModel = (Collection<?>) model.asMap ().get('tasks');

        assertTrue(listFromModel.contains(task));
        assertTrue(listFromDao.containsAll(listFromModel));
    }
    
    @Test
    public void testDeleteTaskOk() throws TaskDeleteException {
        Task task = sampleTask();
        long id = task.getId();

        // delete & assert
        String view = controller.deleteTask(id);
        assertEquals('redirect:/tasks', view);
        assertNull(taskDao.find(id));
    }
    
    @Test(expected = TaskDeleteException.class)
    public void testDeleteTaskThrowsException() throws TaskDeleteException {
        Task task = sampleTask();
        long id = task.getId();
        
        // mock DAO for this call
        TaskDao mockedDao = mock(TaskDao.class);
        when(mockedDao.removeTask(task)).thenReturn(false);

        TaskDao originalDao = controller.getTaskDao();
        try {
            // delete & expect exception
            controller.setTaskDao(mockedDao);
            controller.deleteTask(id);
        } finally {
            controller.setTaskDao(originalDao);
        }
    }
    
    @Test
    public void testHandleDeleteException() {
        Task task = sampleTask();
        TaskDeleteException e = new TaskDeleteException(task);
        ModelAndView modelAndView = controller.handleDeleteException(e);

        assertEquals('tasks/delete-error', modelAndView.getViewName());
        assertTrue(modelAndView.getModelMap().containsValue(task));
    }
    
    @Test
    public void testGetTask() {
        Task task = sampleTask();
        long id = task.getId();

        // get & assert
        String view = controller.getTask(id, model);
        assertEquals('tasks/view', view);
        assertEquals(task, model.asMap().get('task'));
    }
    
    @Test
    public void testRemoveEmployee() {
        Task task = sampleTask();
        long id = task.getAssignedEmployees().get(0).getId();
        controller.removeEmployee(task.getId(), id);

        // task was updated inside controller in other transaction -> refresh
        task = taskDao.find(task.getId());

        // get employee & assert
        Employee employee = employeeDao.find(id);
        assertFalse(task.getAssignedEmployees().contains(employee));
    }
    
    @Test
    public void testAddEmployee() {
        Task task = sampleTask();
        Employee cassidy = new Employee('Butch Cassidy', 'Cowboys');
        employeeDao.add(cassidy);
        controller.addEmployee(task.getId(), cassidy.getId());

        // task was updated inside controller in other transaction -> refresh
        task = taskDao.find(task.getId());

        // get employee & assert
        Employee employee = employeeDao.find(cassidy.getId());
        assertTrue(task.getAssignedEmployees().contains(employee));
    }
    
    @Test
    public void testAddTask() {
        Task task = sampleTask();
        
        // save via controller
        String view = controller.addTask(task);
        assertEquals('redirect:/tasks', view);
        
        // task is in DB
        assertEquals(task, taskDao.find(task.getId()));
    }

    private Task sampleTask() {
        Manager manager = new Manager('Jesse James');
        managerDao.add(manager);

        Employee terrence = new Employee('Terrence', 'Cowboys');
        Employee kid = new Employee('Sundance Kid', 'Cowboys');
        employeeDao.add(terrence);
        employeeDao.add(kid);

        Task task = new Task('Wild West', manager, terrence, kid);
        taskDao.add(task);
        
        return task;
    }
}

That’s it for Tasks. Now let’s create controllers for timesheets. Add basic boilerplate for controller and autowired DAOs that we’ll require:

@Controller
@RequestMapping('/timesheets')
public class TimesheetController {

    private TimesheetDao timesheetDao;
    private TaskDao taskDao;
    private EmployeeDao employeeDao;

    @Autowired
    public void setTimesheetDao(TimesheetDao timesheetDao) {
        this.timesheetDao = timesheetDao;
    }

    @Autowired
    public void setTaskDao(TaskDao taskDao) {
        this.taskDao = taskDao;
    }

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

    public TimesheetDao getTimesheetDao() {
        return timesheetDao;
    }

    public TaskDao getTaskDao() {
        return taskDao;
    }

    public EmployeeDao getEmployeeDao() {
        return employeeDao;
    }
}

Method for handling GET request on timesheets:

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

        return 'timesheets/list';
    }

JSPs will be placed in timesheets subfolder. Add list.jsp page, that will basically iterate through Timesheet’s properties and roll deleting form:

<%@ 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'%>

<!-- resolve variables -->
<%--@elvariable id='timesheets' type='java.util.List<org.timesheet.domain.Timesheet>'--%>

<html>
<head>
    <title>Timesheets</title>
    <link rel='stylesheet' href='/timesheet-app/resources/style.css' type='text/css'>
</head>
<body>
    <h1>List of timesheets</h1>
    <a href='timesheets?new'>Add new timesheet</a>
    <table cellspacing='5' class='main-table wide'>
        <tr>
            <th style='width: 30%'>Employee</th>
            <th style='width: 50%'>Task</th>
            <th>Hours</th>
            <th>Details</th>
            <th>Delete</th>
        </tr>
        <c:forEach items='${timesheets}' var='ts'>
            <tr>
                <td>
                    <a href='employees/${ts.who.id}'>${ts.who.name}</a>
                </td>
                <td>
                    <a href='tasks/${ts.task.id}'>${ts.task.description}</a>
                </td>
                <td>${ts.hours}</td>
                <td>
                    <a href='timesheets/${ts.id}'>Go to page</a>
                </td>
                <td>
                    <sf:form action='timesheets/${ts.id}' method='delete' cssClass='delete'>
                        <input type='submit' class='delete-button'>
                    </sf:form>
                </td>
            </tr>
        </c:forEach>
    </table>

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

Deleting Timesheet is easier than deleting task, because we won’t break any constraint in database, so we can simply use default remove method on DAO:

    /**
     * Deletes timeshet with specified ID
     * @param id Timesheet's ID
     * @return redirects to timesheets
     */
    @RequestMapping(value = '/{id}', method = RequestMethod.DELETE)
    public String deleteTimesheet(@PathVariable('id') long id) {
        Timesheet toDelete = timesheetDao.find(id);
        timesheetDao.remove(toDelete);

        return 'redirect:/timesheets';
    }

We will access individual Timesheet resource by adding it’s ID to URI as usual, so we’ll handle /timesheets/{id}. But there are objects assigned to timesheet – Task instance and Employee instance. We don’t want form to null them out. Therefore we will introduce lightweight command backing object for form. We will update only hours and then set those new hours on real Timesheet instance:

    /**
     * Returns timesheet with specified ID
     * @param id Timesheet's ID
     * @param model Model to put timesheet to
     * @return timesheets/view
     */
    @RequestMapping(value = '/{id}', method = RequestMethod.GET)
    public String getTimesheet(@PathVariable('id') long id, Model model) {
        Timesheet timesheet = timesheetDao.find(id);
        TimesheetCommand tsCommand = new TimesheetCommand(timesheet);
        model.addAttribute('tsCommand', tsCommand);

        return 'timesheets/view';
    }

And here’s code for TimesheetCommand which is now under new package org.timesheet.web.commands:

package org.timesheet.web.commands;

import org.hibernate.validator.constraints.Range;
import org.timesheet.domain.Timesheet;

import javax.validation.constraints.NotNull;

public class TimesheetCommand {

    @NotNull
    @Range(min = 1, message = 'Hours must be 1 or greater')
    private Integer hours;
    private Timesheet timesheet;

    // default c-tor for bean instantiation
    public TimesheetCommand() {}

    public TimesheetCommand(Timesheet timesheet) {
        hours = timesheet.getHours();
        this.timesheet = timesheet;
    }

    public Integer getHours() {
        return hours;
    }

    public void setHours(Integer hours) {
        this.hours = hours;
    }

    public Timesheet getTimesheet() {
        return timesheet;
    }

    public void setTimesheet(Timesheet timesheet) {
        this.timesheet = timesheet;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) {
            return true;
        }
        if (o == null || getClass() != o.getClass()) {
            return false;
        }

        TimesheetCommand that = (TimesheetCommand) o;

        if (hours != null ? !hours.equals(that.hours) : that.hours != null) {
            return false;
        }
        if (timesheet != null ? !timesheet.equals(that.timesheet) : that.timesheet != null) {
            return false;
        }

        return true;
    }

    @Override
    public int hashCode() {
        int result = hours != null ? hours.hashCode() : 0;
        result = 31 * result + (timesheet != null ? timesheet.hashCode() : 0);
        return result;
    }
}

Pretty straightforward, but what are those @NotNull and @Range annotations? Well, we certailny don’t want user to enter negative or zero number for amount of hours, so we will use this neat JSR 303 Bean Validation API. To make it work, simply add dependency to hibernate validator to your pom.xml:

        <dependency>
            <groupId>org.hibernate</groupId>
            <artifactId>hibernate-validator</artifactId>
            <version>4.2.0.Final</version>
        </dependency>

Once Hibernate Validator is in our classpath, default validator will automatically be picked. To make it work though, we must enable annotation driven MVC, so add following line to timesheet-servlet.xml bean config file:

<mvc:annotation-driven />

We’ll see usage of a valid model a few lines later.

Under timesheets folder we’ll now create view.jsp page that’ll contain info about single timesheet:

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

<%--@elvariable id='tsCommand' type='org.timesheet.web.commands.TimesheetCommand'--%>

<html>
<head>
    <title>Timesheet page</title>
    <link rel='stylesheet' href='/timesheet-app/resources/style.css' type='text/css'>
</head>
<body>
    <h2>Timesheet info</h2>
    <div id='list'>
        <sf:form method='post' modelAttribute='tsCommand'>
            <sf:errors path='*' cssClass='errors' element='div' />
            <ul>
                <li>
                    <label for='employeeName'>Assigned employee:</label>
                    <a id='employee' href='../employees/${tsCommand.timesheet.who.id}'>
                        ${tsCommand.timesheet.who.name}
                    </a>
                </li>
                <li>
                    <label for='task'>Task:</label>
                    <a id='task' href='../tasks/${tsCommand.timesheet.task.id}'>
                        ${tsCommand.timesheet.task.description}
                    </a>
                </li>
                <li>
                    <label for='hours'>Hours:</label>
                    <input name='hours' id='hours' value='${tsCommand.hours}' />
                </li>
                <li>
                    <input type='submit' value='Save' />
                </li>
            </ul>
        </sf:form>
    </div>

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

In this view page we have submit button that’ll trigger POST request on /timesheets/{id} and pass updated model (TimesheetCommand instance in that). So let’s handle this. We will use @Valid annotation, which is part of JSR 303 Bean Validation API which marks object to validate. Also note, that TimesheetCommand must be annotated with @ModelAttribute annotation, because this command is bound to web view. Validation errors are stored in BindingResult object:

    /**
     * Updates timesheet with given ID
     * @param id ID of timesheet to lookup from DB
     * @param tsCommand Lightweight command object with changed hours
     * @return redirects to timesheets
     */
    @RequestMapping(value = '/{id}', method = RequestMethod.POST)
    public String updateTimesheet(@PathVariable('id') long id,
            @Valid @ModelAttribute('tsCommand') TimesheetCommand tsCommand,
            BindingResult result) {

        Timesheet timesheet = timesheetDao.find(id);
        if (result.hasErrors()) {
            tsCommand.setTimesheet(timesheet);
            return 'timesheets/view';
        }

        // no errors, update timesheet
        timesheet.setHours(tsCommand.getHours());
        timesheetDao.update(timesheet);

        return 'redirect:/timesheets';
    }

For adding, we will have to pick from select menus of existing Tasks and Employees, so we’ll pass list of those when serving new form:

    /**
     * Creates form for new timesheet
     * @param model Model to bind to HTML form
     * @return timesheets/new
     */
    @RequestMapping(params = 'new', method = RequestMethod.GET)
    public String createTimesheetForm(Model model) {
        model.addAttribute('timesheet', new Timesheet());
        model.addAttribute('tasks', taskDao.list());
        model.addAttribute('employees', employeeDao.list());
        
        return 'timesheets/new';
    }

For showing select lists of Employees and Tasks we again need to create editors for them. We saw this approach earlier, so as before let’s add 2 new editors to our project that use corresponding DAOs:

package org.timesheet.web.editors;

import org.timesheet.domain.Employee;
import org.timesheet.service.dao.EmployeeDao;

import java.beans.PropertyEditorSupport;

/**
 * Will convert ID from combobox to employee's instance.
 */
public class EmployeeEditor extends PropertyEditorSupport {

    private EmployeeDao employeeDao;

    public EmployeeEditor(EmployeeDao employeeDao) {
        this.employeeDao = employeeDao;
    }

    @Override
    public void setAsText(String text) throws IllegalArgumentException {
        long id = Long.parseLong(text);
        Employee employee = employeeDao.find(id);
        setValue(employee);
    }
}
package org.timesheet.web.editors;

import org.timesheet.domain.Task;
import org.timesheet.service.dao.TaskDao;

import java.beans.PropertyEditorSupport;

public class TaskEditor extends PropertyEditorSupport {

    private TaskDao taskDao;

    public TaskEditor(TaskDao taskDao) {
        this.taskDao = taskDao;
    }

    @Override
    public void setAsText(String text) throws IllegalArgumentException {
        long id = Long.parseLong(text);
        Task task = taskDao.find(id);
        setValue(task);
    }
}

We’ll register these editors in TimesheetController initBinder method:

    @InitBinder
    protected void initBinder(WebDataBinder binder) {
        binder.registerCustomEditor(Employee.class, new EmployeeEditor(employeeDao));
        binder.registerCustomEditor(Task.class, new TaskEditor(taskDao));
    }

Now we can safely add new.jsp under timesheets folder, because select lists will correctly be populated with data passed in the model:

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

<%--@elvariable id='employees' type='java.util.List<org.timesheet.domain.Employee'--%>
<%--@elvariable id='tasks' type='java.util.List<org.timesheet.domain.Task'--%>

<html>
<head>
    <title>Add new timesheet</title>
    <link rel='stylesheet' href='/timesheet-app/resources/style.css' type='text/css'>
</head>
<body>
    <h2>Add new Timesheet</h2>
    <div id='list'>
        <sf:form method='post' action='timesheets' commandName='timesheet'>
            <ul>
                <li>
                    <label for='employees'>Pick employee:</label>
                    <sf:select path='who' id='employees'>
                        <sf:options items='${employees}' itemLabel='name' itemValue='id' />
                    </sf:select>
                </li>
                <li>
                    <label for='tasks'>Pick task:</label>
                    <sf:select path='task' id='tasks'>
                        <sf:options items='${tasks}' itemLabel='description' itemValue='id' />
                    </sf:select>
                </li>
                <li>
                    <label for='hours'>Hours:</label>
                    <sf:input path='hours' />
                </li>
                <li>
                    <input type='submit' value='Save' />
                </li>
            </ul>
        </sf:form>
    </div>

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

Submit button submits POST request on /timesheets path, so we’ll handle this with pretty straightforward controller method:

    /**
     * Saves new Timesheet to the database
     * @param timesheet Timesheet to save
     * @return redirects to timesheets
     */
    @RequestMapping(method = RequestMethod.POST)
    public String addTimesheet(Timesheet timesheet) {
        timesheetDao.add(timesheet);

        return 'redirect:/timesheets';
    }

So all timesheet functionalities should be working now, just make sure by using application for a while. Of course, we will also write unit test for TimesheetController now. In test methods testUpdateTimesheetValid and testUpdateTimesheetInValid we are not validating object manually, but we’re mocking validator instead:

package org.timesheet.web;

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.validation.BindingResult;
import org.timesheet.DomainAwareBase;
import org.timesheet.domain.Employee;
import org.timesheet.domain.Manager;
import org.timesheet.domain.Task;
import org.timesheet.domain.Timesheet;
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 org.timesheet.web.commands.TimesheetCommand;

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 TimesheetControllerTest extends DomainAwareBase {
    
    @Autowired
    private TimesheetDao timesheetDao;

    @Autowired
    private EmployeeDao employeeDao;

    @Autowired
    private ManagerDao managerDao;

    @Autowired
    private TaskDao taskDao;

    @Autowired
    private TimesheetController controller;
    
    private Model model; // used for controller

    @Before
    public void setUp() {
        model = new ExtendedModelMap();
    }

    @Test
    public void testShowTimesheets() {
        // prepare some data
        Timesheet timesheet = sampleTimesheet();

        // use controller
        String view = controller.showTimesheets(model);
        assertEquals('timesheets/list', view);

        List<Timesheet> listFromDao = timesheetDao.list();
        Collection<?> listFromModel = (Collection<?>) model.asMap().get('timesheets');

        assertTrue(listFromModel.contains(timesheet));
        assertTrue(listFromDao.containsAll(listFromModel));
    }
    
    @Test
    public void testDeleteTimesheet() {
        // prepare ID to delete
        Timesheet timesheet = sampleTimesheet();
        timesheetDao.add(timesheet);
        long id = timesheet.getId();

        // delete & assert
        String view = controller.deleteTimesheet(id);
        assertEquals('redirect:/timesheets', view);
        assertNull(timesheetDao.find(id));
    }

    @Test
    public void testGetTimesheet() {
        // prepare timesheet
        Timesheet timesheet = sampleTimesheet();
        timesheetDao.add(timesheet);
        long id = timesheet.getId();
        TimesheetCommand tsCommand = new TimesheetCommand(timesheet);

        // get & assert
        String view = controller.getTimesheet(id, model);
        assertEquals('timesheets/view', view);
        assertEquals(tsCommand, model.asMap().get('tsCommand'));
    }

    @Test
    public void testUpdateTimesheetValid() {
        // prepare ID to delete
        Timesheet timesheet = sampleTimesheet();
        timesheetDao.add(timesheet);
        long id = timesheet.getId();
        TimesheetCommand tsCommand = new TimesheetCommand(timesheet);

        // user alters Timesheet hours in HTML form with valid value
        tsCommand.setHours(1337);
        BindingResult result = mock(BindingResult.class);
        when(result.hasErrors()).thenReturn(false);

        // update & assert
        String view = controller.updateTimesheet(id, tsCommand, result);
        assertEquals('redirect:/timesheets', view);
        assertTrue(1337 == timesheetDao.find(id).getHours());
    }

    @Test
    public void testUpdateTimesheetInValid() {
        // prepare ID to delete
        Timesheet timesheet = sampleTimesheet();
        timesheetDao.add(timesheet);
        long id = timesheet.getId();

        TimesheetCommand tsCommand = new TimesheetCommand(timesheet);
        Integer originalHours = tsCommand.getHours();

        // user alters Timesheet hours in HTML form with valid value
        tsCommand.setHours(-1);
        BindingResult result = mock(BindingResult.class);
        when(result.hasErrors()).thenReturn(true);

        // update & assert
        String view = controller.updateTimesheet(id, tsCommand, result);
        assertEquals('timesheets/view', view);
        assertEquals(originalHours, timesheetDao.find(id).getHours());
    }

    @Test
    public void testAddTimesheet() {
        // prepare timesheet
        Timesheet timesheet = sampleTimesheet();

        // save but via controller
        String view = controller.addTimesheet(timesheet);
        assertEquals('redirect:/timesheets', view);

        // timesheet is stored in DB
        assertEquals(timesheet, timesheetDao.find(timesheet.getId()));
    }

    private Timesheet sampleTimesheet() {
        Employee marty = new Employee('Martin Brodeur', 'NHL');
        employeeDao.add(marty);

        Manager jeremy = new Manager('Jeremy');
        managerDao.add(jeremy);

        Task winStanleyCup = new Task('NHL finals', jeremy, marty);
        taskDao.add(winStanleyCup);

        Timesheet stanelyCupSheet = new Timesheet(marty, winStanleyCup, 100);
        timesheetDao.add(stanelyCupSheet);

        return stanelyCupSheet;
    }
}

Last controller we have to do is for our special business service – TimesheetService. We already have implemented and tested it’s logic. Controller will simply merge this functionalities to one menu page and we’ll handle each with controller. So let’s add some boilerplate controller definition and DAO wiring at first:

@Controller
@RequestMapping('/timesheet-service')
public class TimesheetServiceController {

    private TimesheetService service;
    private EmployeeDao employeeDao;
    private ManagerDao managerDao;

    @Autowired
    public void setService(TimesheetService service) {
        this.service = service;
    }

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

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

}

When user enters /timesheet-service with GET request, we will server him menu with populated data:

    /**
     * Shows menu of timesheet service: 
     * that contains busiest task and employees and managers to
     * look for their assigned tasks.
     * @param model Model to put data to
     * @return timesheet-service/list
     */
    @RequestMapping(method = RequestMethod.GET)
    public String showMenu(Model model) {
        model.addAttribute('busiestTask', service.busiestTask());
        model.addAttribute('employees', employeeDao.list());
        model.addAttribute('managers', managerDao.list());

        return 'timesheet-service/menu';
    }

Again, to make stuff work from select lists we will register editors (we’ll just reuse editors that we created recently):

    @InitBinder
    protected void initBinder(WebDataBinder binder) {
        binder.registerCustomEditor(Employee.class, new EmployeeEditor(employeeDao));
        binder.registerCustomEditor(Manager.class, new ManagerEditor(managerDao));
    }

Now we will be providing a service. We will have RESTful URLs once again, but actual resources won’t be directly mapped to domain models as before, but results of some internal service. So getting tasks for manager with id 123 will result to GET request timesheets/manager-tasks/123. Same for tasks for employee. We will form actual URLs with jQuery using listeners for select lists. Add timesheet-service folder and add there menu.jsp page with following content:

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

<%--@elvariable id='busiestTask' type='org.timesheet.domain.Task'--%>
<%--@elvariable id='managers' type='java.util.List<org.timesheet.domain.Manager>'--%>
<%--@elvariable id='employees' type='java.util.List<org.timesheet.domain.Employee>'--%>

<html>
<head>
    <title>Timesheet Service</title>
    <link rel='stylesheet' href='/timesheet-app/resources/style.css' type='text/css'>
</head>
<body>
    <h1>Timesheet services</h1>
    <div id='list'>
        <h3>Busiest task</h3>
        <ul>
            <li>
                <a href='/timesheet-app/tasks/${busiestTask.id}'
                   id='busiest-task'>${busiestTask.description}</a>
            </li>
        </ul>

        <h3>Tasks for manager</h3>
        <sf:form method='get' id='manager-form'>
            <ul>
                <li>
                    <select id='select-managers'>
                        <c:forEach items='${managers}' var='man'>
                            <option value='${man.id}'>${man.name}</option>
                        </c:forEach>
                    </select>
                </li>
                <li>
                    <input type='submit' value='Search' />
                </li>
            </ul>
        </sf:form>

        <h3>Tasks for employee</h3>
        <sf:form method='get' id='employee-form'>
            <ul>
                <li>
                    <select id='select-employees'>
                        <c:forEach items='${employees}' var='emp'>
                            <option value='${emp.id}'>${emp.name}</option>
                        </c:forEach>
                    </select>
                </li>
                <li>
                    <input type='submit' value='Search'>
                </li>
            </ul>
        </sf:form>
    </div>

    <br /><br />
    <a href='/timesheet-app/welcome'>Go Back</a>

    <script src='/timesheet-app/resources/jquery-1.7.1.js'></script>
    <script type='text/javascript'>
        (function() {
            // set default actions
            setAddAction('#select-managers', '#manager-form', 'manager-tasks');
            setAddAction('#select-employees', '#employee-form', 'employee-tasks');

            // handler for chaning action
            $('#select-managers').on('change', function() {
                setAddAction('#select-managers', '#manager-form', 'manager-tasks');
            });
            $('#select-employees').on('change', function() {
                setAddAction('#select-employees', '#employee-form', 'employee-tasks');
            });

            function setAddAction(selectName, formName, action) {
                var id = $(selectName).val();
                $(formName).attr('action',
                        '/timesheet-app/timesheet-service/' + action + '/' + id);
            }
        })();
    </script>
</body>
</html>

Getting tasks for given manager:

    /**
     * Returns tasks for given manager
     * @param id ID of manager
     * @param model Model to put tasks and manager
     * @return timesheet-service/manager-tasks
     */
    @RequestMapping(value = '/manager-tasks/{id}', method = RequestMethod.GET)
    public String showManagerTasks(@PathVariable('id') long id, Model model) {
        Manager manager = managerDao.find(id);
        List<Task> tasks = service.tasksForManager(manager);

        model.addAttribute('manager', manager);
        model.addAttribute('tasks', tasks);

        return 'timesheet-service/manager-tasks';
    }

And as a result page timesheet-service/manager-tasks.jsp will be rendered:

<%@ taglib prefix='c' uri='http://java.sun.com/jsp/jstl/core' %>
<%@ page contentType='text/html;charset=UTF-8' language='java' %>

<%--@elvariable id='manager' type='org.timesheet.domain.Manager'--%>
<%--@elvariable id='tasks' type='java.util.List<org.timesheet.domain.Task>'--%>

<html>
<head>
    <title>Tasks for manager</title>
    <link rel='stylesheet' href='/timesheet-app/resources/style.css' type='text/css'>
</head>
<body>
    <h3>
        Current manager: <a href='/timesheet-app/managers/${manager.id}'>${manager.name}</a>
    </h3>
    <div id='list'>
        <c:forEach items='${tasks}' var='task'>
            <li>
                <a href='/timesheet-app/tasks/${task.id}'>${task.description}</a>
            </li>
        </c:forEach>
    </div>

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

We’ll do pretty much the same for employee:

    /**
     * Returns tasks for given employee
     * @param id ID of employee
     * @param model Model to put tasks and employee
     * @return timesheet-service/employee-tasks
     */
    @RequestMapping(value = '/employee-tasks/{id}', method = RequestMethod.GET)
    public String showEmployeeTasks(@PathVariable('id') long id, Model model) {
        Employee employee = employeeDao.find(id);
        List<Task> tasks = service.tasksForEmployee(employee);
        
        model.addAttribute('employee', employee);
        model.addAttribute('tasks', tasks);
        
        return 'timesheet-service/employee-tasks';
    }

And jsp view employee-tasks.jsp:

<%@ taglib prefix='c' uri='http://java.sun.com/jsp/jstl/core' %>
<%@ page contentType='text/html;charset=UTF-8' language='java' %>

<%--@elvariable id='employee' type='org.timesheet.domain.Employee'--%>
<%--@elvariable id='tasks' type='java.util.List<org.timesheet.domain.Task>'--%>

<html>
<head>
    <title>Tasks for employee</title>
    <link rel='stylesheet' href='/timesheet-app/resources/style.css' type='text/css'>
</head>
<body>
    <h3>
        Current employee: <a href='/timesheet-app/employees/${employee.id}'>${employee.name}</a>
    </h3>
    <div id='list'>
        <c:forEach items='${tasks}' var='task'>
            <li>
                <a href='/timesheet-app/tasks/${task.id}'>${task.description}</a>
            </li>
        </c:forEach>
    </div>

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

So lets make sure everything is well integrated and add unit test for this new contoller:

package org.timesheet.web;

import org.junit.Before;
import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.io.FileSystemResource;
import org.springframework.jdbc.core.simple.SimpleJdbcTemplate;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.jdbc.SimpleJdbcTestUtils;
import org.springframework.ui.ExtendedModelMap;
import org.springframework.ui.Model;
import org.timesheet.DomainAwareBase;
import org.timesheet.domain.Employee;
import org.timesheet.domain.Manager;
import org.timesheet.service.TimesheetService;
import org.timesheet.service.dao.EmployeeDao;
import org.timesheet.service.dao.ManagerDao;

import static org.junit.Assert.assertEquals;

/**
 * This test relies on fact that DAOs and Services are tested individually.
 * Only compares, if controller returns the same as individual services.
 */
@ContextConfiguration(locations = {'/persistence-beans.xml', '/controllers.xml'})
public class TimesheetServiceControllerTest extends DomainAwareBase {

    @Autowired
    private TimesheetServiceController controller;

    @Autowired
    private TimesheetService timesheetService;

    @Autowired
    private EmployeeDao employeeDao;

    @Autowired
    private ManagerDao managerDao;

    @Autowired
    private SimpleJdbcTemplate jdbcTemplate;

    private Model model;
    private final String createScript = 'src/main/resources/sql/create-data.sql';

    @Before
    public void setUp() {
        model = new ExtendedModelMap();
        SimpleJdbcTestUtils.executeSqlScript(jdbcTemplate,
                new FileSystemResource(createScript), false);
    }

    @Test
    public void testShowMenu() {
        String view = controller.showMenu(model);
        assertEquals('timesheet-service/menu', view);
        assertEquals(timesheetService.busiestTask(),
                model.asMap().get('busiestTask'));

        // this should be done only on small data sample
        // might cause serious performance cost for complete
        assertEquals(employeeDao.list(), model.asMap().get('employees'));
        assertEquals(managerDao.list(), model.asMap().get('managers'));
    }

    @Test
    public void testShowManagerTasks() {
        // prepare some ID
        Manager manager = managerDao.list().get(0);
        long id = manager.getId();

        String view = controller.showManagerTasks(id, model);
        assertEquals('timesheet-service/manager-tasks', view);
        assertEquals(manager, model.asMap().get('manager'));
        assertEquals(timesheetService.tasksForManager(manager),
                model.asMap().get('tasks'));
    }

    @Test
    public void testShowEmployeeTasks() {
        // prepare some ID
        Employee employee = employeeDao.list().get(0);
        long id = employee.getId();

        String view = controller.showEmployeeTasks(id, model);
        assertEquals('timesheet-service/employee-tasks', view);
        assertEquals(employee, model.asMap().get('employee'));
        assertEquals(timesheetService.tasksForEmployee(employee),
                model.asMap().get('tasks'));
    }
}

Project structure after this part (all new stuff is visible):

Final request mappings:

Reference: Part 5 – Adding Spring MVC part 2 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.

0 Comments
Inline Feedbacks
View all comments
Back to top button