Статьи

Spring — Добавление Spring MVC — часть 2

В предыдущей части мы реализовали контроллеры для менеджеров и сотрудников. Теперь, когда мы разбираемся в этом, мы сделаем немного (но немного) более сложных вещей — контроллеров для задач и расписаний.

Итак, начнем с org.timesheet.web. TaskController . Сначала создайте класс, и на этот раз у нас будет доступ к более богатому домену, поэтому нам нужно автоматически подключить три DAOS — для задач, сотрудников и менеджеров.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
@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;
    }
}

Давайте обработаем запрос GET на / tasks:

01
02
03
04
05
06
07
08
09
10
11
/**
 * 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';
}

Мы разместим JSP в подпапке задач . Сначала list.jsp для отображения всех задач. Он не только перебирает все задачи, но и над каждой задачей перебирает сотрудников:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
<%@ 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>

Удаление задачи как обычно:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
/**
 * 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:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
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;
    }
}

Метод для обработки этого исключения:

01
02
03
04
05
06
07
08
09
10
11
/**
 * 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 jsp / tasks / delete-error.jsp для отображения ошибки удаления:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
<%--@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>

Отображение деталей задачи будет доступно с помощью URI / tasks / {id}. Мы добавим в модель как задачу, так и неназначенных сотрудников, которых можно добавить в задачу. Это будет обрабатываться так:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
/**
 * 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';
}

Теперь что-то немного сложнее. Мы хотели бы показать пользовательскую страницу с описанием задачи. В этой задаче мы хотели бы добавить / удалить назначенных сотрудников.
Сначала давайте подумаем об URL. Задачи назначают сотрудников, поэтому наш URL для доступа к сотруднику в задаче будет выглядеть следующим образом:
/ задачи / {ID} / сотрудников / {EmployeeID}
Чтобы удалить сотрудника, мы просто получим доступ к этому ресурсу с помощью метода DELETE, поэтому давайте добавим метод в контроллер:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
/**
 * 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);
}

На странице просмотра (мы увидим это только сейчас) мы просто изменим модель DOM с помощью jQuery и удалим назначенного сотрудника из списка.
Давайте представим, что ничто не может пойти не так (у нас есть ответ NO_CONTENT), поэтому сотрудник всегда будет успешно удален из БД. Таким образом, мы можем просто изменить эту модель DOM.

Для добавления сотрудника у нас будет список выбора (или поле со списком) неназначенных сотрудников. Когда сотрудник будет удален, мы добавим это к выбору доступных сотрудников (он снова доступен). Когда сотрудник будет добавлен, мы изменим задачу с помощью DAO и перенаправим обратно на ту же задачу (все будет обновлено). Вот код для назначения сотрудника на задачу:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
/**
 * 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;
}

И, наконец, tasks / view.jsp для получения подробной информации о Task. Как я уже упоминал, DOM часто изменяется, поэтому этот код может показаться немного сложнее, чем обычно.

001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023
024
025
026
027
028
029
030
031
032
033
034
035
036
037
038
039
040
041
042
043
044
045
046
047
048
049
050
051
052
053
054
055
056
057
058
059
060
061
062
063
064
065
066
067
068
069
070
071
072
073
074
075
076
077
078
079
080
081
082
083
084
085
086
087
088
089
090
091
092
093
094
095
096
097
098
099
100
101
102
103
104
105
106
107
108
<%@ 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>

Как видно из кода, мы снова используем только HTML + JavaScript. Единственное, что специфично для JSP, — это перенос данных из модели на страницу.

Хорошо, теперь мы должны создать новую задачу. Давайте подготовим наш контроллер для обслуживания формы для добавления задачи, которая будет доступна из / tasks? New:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
/**
 * 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';
}

Задача состоит из имени, менеджера и назначенных сотрудников. Для целей этого урока я решил не реализовывать последний. Мы просто создадим несколько сотрудников. Если вы когда-нибудь захотите иметь возможность выбирать сотрудников из какого-либо списка выбора и назначать их для задачи, то обратите внимание, что это должно выполняться асинхронно. Для этого вы можете привязать специальные методы к контроллеру и делать записи AJAX, например, с помощью jQuery с $ .post . Я думаю, что для этого урока это будет слишком много, но если вам интересно, как использовать AJAX в Spring, ознакомьтесь с этой статьей в блоге об упрощениях ajax в Spring 3 .
Когда мы создавали сотрудников и менеджеров, мы использовали только примитивные типы для свойств. Теперь мы хотели бы назначить фактический экземпляр Manager для задачи. Поэтому нам нужно будет сообщить Spring, как он должен преобразовать значение из списка выбора (идентификатор менеджера) в фактический экземпляр. Для этого мы будем использовать пользовательскую функцию PropertyEditorSupport . Добавьте новый пакет org.timesheet.web.editors и создайте новый класс ManagerEditor со следующим кодом:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
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 передаст DAO в своем конструкторе. Он будет искать фактического менеджера по его идентификатору и вызывать setValue родителя.
Spring теперь должен знать, что такой редактор есть, поэтому мы должны зарегистрировать его в нашем контроллере. Нам нужен только метод с WebDataBinder в качестве параметра, и нам нужно аннотировать его аннотацией @InitBinder следующим образом:

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

И все, Spring теперь знает, как назначить менеджера для нашей задачи прямо из формы.

Наконец код для сохранения Задачи. Как я уже говорил ранее, мы сгенерируем некоторых сотрудников для выполнения задач перед их сохранением:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
/**
 * 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';
}

Существует метод сокращения , который является простым вспомогательным методом для сокращения сотрудников в памяти. Это не очень эффективно, мы могли бы сделать это с более сложным запросом, но сейчас это будет хорошо. Также не стесняйтесь использовать свою собственную логику сокращения, если хотите

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/**
 * 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;
}

Давайте теперь посмотрим на страницу tasks / new.jsp :

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
<%@ 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>

И конечно тест для контроллера:

001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023
024
025
026
027
028
029
030
031
032
033
034
035
036
037
038
039
040
041
042
043
044
045
046
047
048
049
050
051
052
053
054
055
056
057
058
059
060
061
062
063
064
065
066
067
068
069
070
071
072
073
074
075
076
077
078
079
080
081
082
083
084
085
086
087
088
089
090
091
092
093
094
095
096
097
098
099
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
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;
    }
}

Вот и все для задач. Теперь давайте создадим контроллеры для расписаний. Добавьте базовый шаблон для контроллера и автоматических DAO, которые нам потребуются:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
@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;
    }
}

Способ обработки запроса GET на расписаниях:

01
02
03
04
05
06
07
08
09
10
11
12
/**
 * 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';
}

JSP будут помещены в подпапку расписаний . Добавьте страницу list.jsp, которая будет в основном перебирать свойства расписания и удалять форму удаления:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
<%@ 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>

Удалить расписание проще, чем удалить задачу, потому что мы не нарушим никаких ограничений в базе данных, поэтому мы можем просто использовать метод удаления по умолчанию в DAO:

01
02
03
04
05
06
07
08
09
10
11
12
/**
 * 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';
}

Мы получим доступ к отдельному ресурсу расписания, добавив его идентификатор в URI, как обычно, поэтому мы будем обрабатывать / timesheets / {id}. Но есть расписания, назначенные расписанию — экземпляр задачи и экземпляр сотрудника. Мы не хотим, чтобы форма обнуляла их. Поэтому мы введем облегченный командный объект поддержки для формы. Мы обновим только часы, а затем установим эти новые часы в реальном экземпляре расписания:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
/**
 * 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';
}

А вот код для TimesheetCommand, который теперь находится в новом пакете org.timesheet.web. команды :

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
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;
    }
}

Довольно просто, но что это за аннотации @NotNull и @Range ? Ну, мы не хотим, чтобы пользователь вводил отрицательное или нулевое число в течение количества часов, поэтому мы будем использовать этот удобный API-интерфейс проверки компонентов JSR 303. Чтобы заставить его работать, просто добавьте зависимость для валидатора hibernate в ваш файл pom.xml :

1
2
3
4
5
<dependency>
    <groupId>org.hibernate</groupId>
    <artifactId>hibernate-validator</artifactId>
    <version>4.2.0.Final</version>
</dependency>

Когда Hibernate Validator находится в нашем пути к классам, по умолчанию будет выбран валидатор. Чтобы заставить его работать, мы должны включить MVC, управляемый аннотациями, поэтому добавьте следующую строку в файл конфигурации bean-файла timesheet-servlet.xml :

1
<mvc:annotation-driven />

Мы увидим использование действующей модели через несколько строк.

Под папкой расписаний мы теперь создадим страницу view.jsp, которая будет содержать информацию об одном расписании:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
<%@ 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>

На этой странице просмотра у нас есть кнопка отправки, которая вызовет запрос POST для / timesheets / {id} и передаст обновленную модель (экземпляр TimesheetCommand в этом). Итак, давайте справимся с этим. Мы будем использовать аннотацию @Valid , которая является частью JSR 303 Bean Validation API, которая помечает объект для проверки. Также обратите внимание, что TimesheetCommand должен быть аннотирован аннотацией @ModelAttribute , потому что эта команда привязана к веб-представлению. Ошибки валидации хранятся в объекте BindingResult:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/**
 * 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';
}

Для добавления нам нужно будет выбрать из некоторых меню существующих задач и сотрудников, поэтому мы передадим список из них при обслуживании новой формы:

01
02
03
04
05
06
07
08
09
10
11
12
13
/**
 * 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';
}

Для отображения списков выбора сотрудников и задач нам снова нужно создать редакторы для них. Мы видели этот подход ранее, поэтому, как и прежде, давайте добавим 2 новых редактора в наш проект, которые используют соответствующие DAO:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
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);
    }
}
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
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);
    }
}

Мы зарегистрируем эти редакторы в методе initBinder TimesheetController:

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

Теперь мы можем смело добавлять new.jsp в папку расписаний, поскольку списки выбора будут правильно заполнены данными, передаваемыми в модели:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
<%@ 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 отправляет запрос POST по пути / timesheets, поэтому мы справимся с этим довольно простым методом контроллера:

01
02
03
04
05
06
07
08
09
10
11
/**
 * 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';
}

Так что все функции расписания должны работать сейчас, просто убедитесь, что какое-то время пользуетесь приложением. Конечно, сейчас мы также напишем модульный тест для TimesheetController. В тестовых методах testUpdateTimesheetValid и testUpdateTimesheetInValid мы не проверяем объект вручную, но вместо этого мы имитируем валидатор:

001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023
024
025
026
027
028
029
030
031
032
033
034
035
036
037
038
039
040
041
042
043
044
045
046
047
048
049
050
051
052
053
054
055
056
057
058
059
060
061
062
063
064
065
066
067
068
069
070
071
072
073
074
075
076
077
078
079
080
081
082
083
084
085
086
087
088
089
090
091
092
093
094
095
096
097
098
099
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
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;
    }
}

Последний контроллер, который мы должны сделать, — это наш специальный бизнес-сервис — TimesheetService. Мы уже реализовали и протестировали его логику. Контроллер просто объединит эти функциональные возможности с одной страницей меню, и мы будем обрабатывать каждую с помощью контроллера. Итак, давайте сначала добавим некоторое определение контроллера контроллера и проводку DAO:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@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;
    }
 
}

Когда пользователь входит в сервис / расписания с GET-запросом, мы отправляем ему меню с заполненными данными:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
/**
 * 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';
}

Опять же, чтобы заставить вещи работать из избранных списков, мы зарегистрируем редакторов (мы просто повторно используем редакторы, которые мы недавно создали):

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

Теперь мы будем предоставлять услугу. У нас снова будут URL-адреса RESTful, но фактические ресурсы не будут напрямую сопоставлены с моделями доменов, как раньше, а с результатами некоторых внутренних служб. Таким образом, получение задач для менеджера с идентификатором 123 приведет к получению GET-запроса расписания / manager-tasks / 123 То же самое для задач для работника. Мы сформируем реальные URL с помощью jQuery, используя слушателей для списков выбора. Добавьте папку сервиса расписания и добавьте туда страницу menu.jsp со следующим содержимым:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
<%@ 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>

Получение заданий для данного менеджера:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
/**
 * 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';
}

И в результате будет отображена страница расписания-service / manager-tasks.jsp :

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<%@ 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>

Мы будем делать то же самое для сотрудника:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
/**
 * 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';
}

И jsp view employee-tasks.jsp:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<%@ 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>

Итак, давайте убедимся, что все хорошо интегрировано и добавим модульный тест для этого нового контроллера:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
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'));
    }
}

Структура проекта после этой части (все новое видно):

Окончательный запрос сопоставлений:

Ссылка: Часть 5. Добавление Spring MVC часть 2 от нашего партнера по JCG Михала Вртиака в блоге vrtoonjava .