因此,让我们从org.timesheet.web开始。 TaskController 。 首先创建一个类,这次我们将访问更丰富的域,因此我们需要为任务,员工和经理自动连接三个DAOS。
@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;
}
}
让我们处理/ tasks上的GET请求:
/**
* 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。 它不仅遍历所有任务,而且在每个任务上遍历员工:
<%@ 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>
照常删除任务:
/**
* 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;
}
}
处理此异常的方法:
/**
* 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用于显示删除错误:
<%--@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}访问。 我们将在模型中添加任务和可以添加到任务中的未分配员工。 它将像这样处理:
/**
* 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将如下所示: / tasks / {id} / employees / {employeeId} 要删除员工,我们只需使用DELETE方法访问此资源,因此让我们向控制器添加方法:
/**
* 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);
}
在视图页面上(我们稍后会看到),我们将使用jQuery更改DOM模型并从列表中删除分配的员工。
让我们假装什么都不会出错(我们有NO_CONTENT响应),因此员工将总是成功地从数据库中删除。 因此,我们可以简单地更改DOM模型。
对于添加员工,我们将有未分配员工的选择列表(或组合框)。 删除员工后,我们会将其添加到可用员工的选择中(他再次可用)。 添加员工后,我们将使用DAO更改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;
}
最后,使用task / view.jsp了解Task的详细信息。 正如我所提到的,有很多DOM更改,因此此代码似乎比平时更难。
<%@ 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的是将数据从模型带到页面。
OK,现在我们必须能够创建新的Task。 让我们为提供表单的添加控件准备控制器,该任务将从/ 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';
}
任务包括名称,经理和分配的员工。 在本教程的范围内,我决定不实施最后一个。 我们只会产生一些员工。 如果您希望能够从某种选择列表中挑选员工并将他们分配给任务,那么请注意,这应该异步进行。 为此,您可以将特殊方法映射到控制器,并执行AJAX发布,例如使用带有$ .post的 jQuery。 我认为对于本教程来说,这太少了,但是如果您对如何在Spring中使用AJAX感兴趣,请查看Spring 3中有关简化Ajax的博客文章 。
在创建员工和经理时,我们仅将原始类型用于属性。 现在,我们想为任务分配实际的Manager实例。 因此,我们将不得不告诉Spring如何将选择列表(经理的ID)中的值转换为实际实例。 为此,我们将使用自定义的PropertyEditorSupport工具。 添加新的org.timesheet.web.editors包,创建新类ManagerEditor与下面的代码:
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。 它将通过ID查找实际的管理员,并调用父级的setValue。
Spring现在应该知道有这样一个编辑器,因此我们必须在控制器中注册它。 我们只需要将WebDataBinder作为参数的方法,并需要使用@InitBinder注释对其进行注释,如下所示:
@InitBinder
protected void initBinder(WebDataBinder binder) {
binder.registerCustomEditor(Manager.class, new ManagerEditor(managerDao));
}
就是这样,Spring现在知道如何直接从表单将经理分配给我们的任务。
最后的代码用于保存Task。 如前所述,在保存之前,我们将招募一些员工来执行任务:
/**
* 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';
}
有reduce方法,这是减少内存中员工的简单辅助方法。 这并不是非常有效,我们可以通过更复杂的查询来做到这一点,但是现在就可以了。 如果需要,也可以随意滚动自己的归约逻辑:
/**
* 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;
}
现在让我们看一下task / new.jsp页面:
<%@ 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>
当然要测试控制器:
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添加基本样板:
@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请求的方法:
/**
* 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页面,该页面基本上将遍历Timesheet的属性并滚动删除表单:
<%@ 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上使用默认的remove方法:
/**
* 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';
}
我们将照常通过将其ID添加到URI中来访问单个时间表资源,因此我们将处理/ timesheets / {id}。 但是有分配给时间表的对象-任务实例和员工实例。 我们不希望表单将其无效。 因此,我们将为表单引入轻量级的命令支持对象。 我们将只更新小时,然后在实际的时间表实例中设置这些新小时:
/**
* 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下。 命令 :
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注释是什么? 好吧,我们绝对不希望用户输入小时数为负数或零,因此我们将使用此简洁的JSR 303 Bean验证API。 要使其工作,只需将依赖于休眠验证器的依赖项添加到pom.xml中 :
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-validator</artifactId>
<version>4.2.0.Final</version>
</dependency>
将Hibernate Validator放入我们的类路径后,将自动选择默认验证器。 为了使其工作,我们必须启用注释驱动的MVC,因此将以下行添加到timesheet-servlet.xml bean配置文件中:
<mvc:annotation-driven />
稍后将看到有效模型的用法。
在时间表文件夹下,我们现在将创建view.jsp页面,其中将包含有关单个时间表的信息:
<%@ 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>
在此视图页面中,我们具有“提交”按钮,该按钮将触发/ timesheets / {id}上的POST请求并传递更新的模型(该模型中的TimesheetCommand实例)。 因此,让我们处理一下。 我们将使用@Valid批注,它是JSR 303 Bean验证API的一部分,用于标记要验证的对象。 另请注意,必须使用@ModelAttribute批注对TimesheetCommand进行批注,因为此命令已绑定到Web视图。 验证错误存储在BindingResult对象中:
/**
* 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';
}
要进行添加,我们必须从现有任务和员工的选择菜单中进行选择,因此在提供新表单时,我们将传递这些列表:
/**
* 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的新编辑器:
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);
}
}
我们将在TimesheetController initBinder方法中注册这些编辑器:
@InitBinder
protected void initBinder(WebDataBinder binder) {
binder.registerCustomEditor(Employee.class, new EmployeeEditor(employeeDao));
binder.registerCustomEditor(Task.class, new TaskEditor(taskDao));
}
现在,我们可以安全地在timesheets文件夹下添加new.jsp ,因为选择列表将正确地填充有模型中传递的数据:
<%@ 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按钮在/ timesheets路径上提交POST请求,因此我们将使用非常简单的控制器方法来处理此请求:
/**
* 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中,我们不是手动验证对象,而是模拟验证器:
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。 我们已经实现并测试了它的逻辑。 Controller会将这些功能简单地合并到一个菜单页面,我们将使用Controller处理每个功能。 因此,首先让我们添加一些样板控制器定义和DAO布线:
@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请求进入/ timesheet-service时,我们将使用填充的数据为他提供菜单服务:
/**
* 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';
}
再次,为了使内容在选择列表中起作用,我们将注册编辑器(我们将仅重用最近创建的编辑器):
@InitBinder
protected void initBinder(WebDataBinder binder) {
binder.registerCustomEditor(Employee.class, new EmployeeEditor(employeeDao));
binder.registerCustomEditor(Manager.class, new ManagerEditor(managerDao));
}
现在我们将提供服务。 我们将再次拥有RESTful URL,但是实际资源不会像以前那样直接映射到域模型,而是一些内部服务的结果。 因此,获取ID为123的经理的任务将导致GET请求时间表/ manager-tasks / 123。 员工任务相同。 我们将使用选择列表的侦听器与jQuery形成实际的URL。 添加时间表服务文件夹,并在其中添加menu.jsp页面,其内容如下:
<%@ 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>
获取给定经理的任务:
/**
* 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';
}
结果页面timesheet -service / manager-tasks.jsp将被呈现:
<%@ 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>
我们将为员工做几乎相同的事情:
/**
* 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查看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>
因此,请确保一切都集成良好,并为此新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'));
}
}
这部分之后的项目结构(所有新内容都可见):
最终请求映射:
参考: 第5部分–在vrtoonjava博客上从我们的JCG合作伙伴 Michal Vrtiak 添加Spring MVC第2部分 。
翻译自: https://www.javacodegeeks.com/2012/09/spring-adding-spring-mvc-part-2.html