В первом посте я уже описал начальную настройку веб-приложения. Логика работы с БД уже готова, теперь мы займёмся описанием представлений и контроллеров.
Контроллеры
Для
начала создадим пару контроллеров, в которых будет реализована вся
бизнес-логика для работы с заявками и комментариями к ним.
TicketController:
@Controller @RequestMapping("/ticket") public class TicketController { @Autowired private TicketRepository ticketRepository; @Autowired private TicketCommentRepository ticketCommentRepository; /** * Список заявок. * * @param all Отображать все заявки * @return */ @RequestMapping({"", "/", "/index"}) public ModelAndView index(@RequestParam(required = false, defaultValue = "false") boolean all) { return new ModelAndView("ticket/index") .addObject("all", all) .addObject("tickets", all ? ticketRepository.findAll(new Sort(Sort.Direction.DESC, "createDate")) : ticketRepository.findByResolveDateIsNull(new Sort(Sort.Direction.DESC, "createDate"))); } /** * Создание новой заявки. * * @param ticket Заявка * @return */ @RequestMapping(method = RequestMethod.POST) public ModelAndView create(@ModelAttribute Ticket ticket) { ticket.setCreateDate(new Date()); ticket = ticketRepository.save(ticket); return new ModelAndView("redirect:/ticket/" + ticket.getId() + "/view"); } /** * Просмотр заявки. * * @param id Идентификатор запрашиваемой заявки * @return */ @RequestMapping("/{id:\\d+}/view") public ModelAndView view(@PathVariable long id) { Ticket ticket = ticketRepository.findOne(id); return new ModelAndView("ticket/view") .addObject("ticket", ticket) .addObject("ticketComments", ticketCommentRepository.findByTicketId(id)); } /** * Закрытие заявки. * * @param id Идентификатор запрашиваемой заявки * @return */ @RequestMapping("/{id:\\d+}/resolve") public ModelAndView resolve(@PathVariable long id) { ticketRepository.resolveTicket(id); return new ModelAndView("redirect:/ticket/" + id + "/view"); } /** * Повторное открытие заявки. * * @param id Идентификатор запрашиваемой заявки * @return */ @RequestMapping("/{id:\\d+}/reopen") public ModelAndView reopen(@PathVariable long id) { ticketRepository.reopenTicket(id); return new ModelAndView("redirect:/ticket/" + id + "/view"); } }
Человек, знакомый с Spring WebMVC, ничего нового не увидит в этом контроллере.
Аналогичным образом выглядит и контроллер для работы с комментариями к заявкам, TicketCommentController:
@Controller @RequestMapping("/ticket/{ticketId:\\d+}/ticketComment") public class TicketCommentController { @Autowired private TicketCommentRepository ticketCommentRepository; @Autowired private TicketRepository ticketRepository; @RequestMapping(method = RequestMethod.POST) public ModelAndView create(@PathVariable long ticketId, @ModelAttribute TicketComment ticketComment) { ticketComment.setTicket(ticketRepository.findOne(ticketId)); ticketComment.setCreateDate(new Date()); ticketCommentRepository.save(ticketComment); return new ModelAndView("redirect:/ticket/" + ticketId + "/view"); } }
Отображения с использованием Thymeleaf
Как уже было сказано, по умолчанию Thymeleaf в связке с Spring Boot будет искать шаблоны в classpath:/templates. Создадим поддиректорию ticket, в которой будут находиться отображения для заявок.
Страница со списком заявок:
<!DOCTYPE html> <html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org"> <head> <title>Bootdesk :: Tickets</title> <meta charset="UTF-8"/> <meta name="viewport" content="width=device-width, initial-scale=1.0"/> <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.min.css"/> </head> <body> <div class="container"> <div class="row"> <div class="col-lg-4" th:include="ticket/create :: #_ticket_form"></div> <div class="col-lg-8"> <h1>Tickets list <small> <a th:if="!${all}" th:href="@{/ticket(all=true)}">All</a> <a th:if="${all}" th:href="@{/ticket}">Not resolved</a> </small> </h1> <table class="table table-striped"> <thead> <tr> <th> ID </th> <th> Issue </th> <th> Opened </th> </tr> </thead> <tbody> <tr th:each="ticket : ${tickets}"> <td th:text="${ticket.id}">ID</td> <td> <a th:style="${ticket.resolveDate != null}?'text-decoration:line-through;':''" th:href="@{/ticket/{id}/view(id=${ticket.id})}" th:text="${ticket.issue}"></a> </td> <td th:text="${ticket.createDate}">Opened</td> </tr> </tbody> </table> </div> </div> </div> </body> </html>
В качестве небольшого украшения дизайна я использовал Twitter Bootstrap. В глаза сразу бросаются Thymeleaf-аттрибуты в HTML-тэгах. Пробежимся по ним вкратце:
- th:include вкладывает в текущий шаблон часть другого. В качестве значения указывается путь к вкладываемому файлу отображения. Если нужно вложить не весь файл, а лишь его часть, то после имени добавляется :: и CSS-селектор вкладываемого компонента. В данном случае будет вложен элемент с id="_ticket_form".
- th:if - условный аттрибут. В качестве значения используется SPEL-выражение. Если выражение возвращает true, то элемент будет отображён.
- th:href - аттрибут, генерирующий значение аттрибута href у ссылки. С тонкостями этого аттрибута лучше ознакомиться в официальной документации Thymeleaf.
- th:text - определяет тектовое содержимое элемента.
- th:style - настройки CSS-стиля элемента.
Вкладываемое отображение выглядит следующим образом:
<!DOCTYPE html> <html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org"> <head> <title>Bootdesk :: Create New Ticket</title> <meta charset="UTF-8"/> <meta name="viewport" content="width=device-width, initial-scale=1.0"/> </head> <body> <div id="_ticket_form"> <form method="post" th:action="@{/ticket}"> <div class="form-group"> <label for="_ticket_issue">Issue</label> <input type="text" class="form-control" name="issue" placeholder="Type your problem" id="_ticket_issue"/> </div> <div class="form-group"> <label for="_ticket_description">Description</label> <textarea placeholder="Input a detailed description of your problem here ..." id="_ticket_description" name="description" class="form-control"></textarea> </div> <button class="btn btn-primary btn-block"><i class="glyphicon glyphicon-send"></i> Submit</button> </form> </div> </body> </html>
Thymeleaf в связке с Spring Boot ищет вкладываемые отображения в той же самой директории - classpath:/templates.
В этом отображении используется аттибут th:action у тэга form, он задаёт URL, который будет указан в аттрибуте action. Его поведение аналогично th:href у ссылок.
Осталось отображение заявки с её обсуждением:
<!DOCTYPE html> <html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org"> <head> <title>Bootdesk :: Open Tickets</title> <meta charset="UTF-8"/> <meta name="viewport" content="width=device-width, initial-scale=1.0"/> <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.min.css"/> </head> <body> <div class="container"> <div class="row"> <div class="col-lg-4" th:include="ticket/create :: #_ticket_form"></div> <div class="col-lg-8"> <a th:href="@{/ticket}">← All tickets</a> <h1 th:style="${ticket.resolveDate != null}?'text-decoration:line-through;':''">Ticket #<span th:text="${ticket.id}"></span>: <span th:text="${ticket.issue}"></span></h1> <div>Created: <span th:text="${ticket.createDate}"></span></div> <p th:text="${ticket.description}"></p> <br/> <div> <a th:if="${ticket.resolveDate == null}" th:href="@{/ticket/{ticketId}/resolve(ticketId=${ticket.id})}">Resolve</a> <a th:if="${ticket.resolveDate != null}" th:href="@{/ticket/{ticketId}/reopen(ticketId=${ticket.id})}">Re-open</a> </div> <hr/> <h3>Discussion</h3> <div th:each="ticketComment : ${ticketComments}" class="well well-sm"> <strong th:text="${ticketComment.createDate}"></strong> <p th:text="${ticketComment.comment}"></p> </div> <form th:action="@{/ticket/{ticketId}/ticketComment(ticketId=${ticket.id})}" method="post"> <div class="form-group"> <label>Your comment:</label> <textarea name="comment" placeholder="Input your comment here..." class="form-control"></textarea> </div> <button class="btn btn-primary pull-right"><i class="glyphicon glyphicon-comment"></i> Comment</button> </form> </div> </div> </div> </body> </html>
В следующем посте я опишу работу со Spring Security в связке со Spring Boot, а так же добавлю в демонстрационное приложение аутентификацию и авторизацию.