/** * Copyright (C) 2011 JTalks.org Team * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ package org.jtalks.jcommune.web.controller; import org.apache.commons.lang3.ObjectUtils; import org.joda.time.DateTime; import org.jtalks.jcommune.model.entity.*; import org.jtalks.jcommune.plugin.api.exceptions.NotFoundException; import org.jtalks.jcommune.plugin.api.web.dto.PostDto; import org.jtalks.jcommune.plugin.api.web.dto.TopicDto; import org.jtalks.jcommune.plugin.api.web.dto.json.JsonResponse; import org.jtalks.jcommune.plugin.api.web.dto.json.JsonResponseStatus; import org.jtalks.jcommune.plugin.api.web.util.BreadcrumbBuilder; import org.jtalks.jcommune.service.*; import org.jtalks.jcommune.service.dto.EntityToDtoConverter; import org.jtalks.jcommune.service.nontransactional.LocationService; import org.jtalks.jcommune.web.validation.editors.DateTimeEditor; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.propertyeditors.StringTrimmerEditor; import org.springframework.data.domain.Page; import org.springframework.retry.RetryCallback; import org.springframework.retry.RetryContext; import org.springframework.retry.support.RetryTemplate; import org.springframework.security.access.AccessDeniedException; import org.springframework.security.core.session.SessionRegistry; import org.springframework.stereotype.Controller; import org.springframework.validation.BindingResult; import org.springframework.web.bind.WebDataBinder; import org.springframework.web.bind.annotation.*; import org.springframework.web.context.request.WebRequest; import org.springframework.web.servlet.ModelAndView; import javax.validation.Valid; /** * Serves topic management web requests * * @author Kravchenko Vitaliy * @author Kirill Afonin * @author Teterin Alexandre * @author Max Malakhov * @author Evgeniy Naumenko * @author Eugeny Batov * @author Dmitry S. Dolzhenko * @see Topic */ @Controller public class TopicController { public static final String TOPIC_ID = "topicId"; public static final String BRANCH_ID = "branchId"; public static final String BREADCRUMB_LIST = "breadcrumbList"; private static final String SUBMIT_URL = "submitUrl"; private static final String TOPIC_VIEW = "topic/topicForm"; private static final String TOPIC_DTO = "topicDto"; private static final String REDIRECT_URL = "redirect:/topics/"; public static final String POST_DTO = "postDto"; private static final Logger LOGGER = LoggerFactory.getLogger(UserController.class); public static final String POLL = "poll"; private static final String TOPIC_DRAFT = "topicDraft"; private TopicModificationService topicModificationService; private TopicFetchService topicFetchService; private TopicDraftService topicDraftService; private PostService postService; private BranchService branchService; private UserService userService; private BreadcrumbBuilder breadcrumbBuilder; private LocationService locationService; private SessionRegistry sessionRegistry; private EntityToDtoConverter converter; private RetryTemplate retryTemplate; /** * This method turns the trim binder on. Trim binder * removes leading and trailing spaces from the submitted fields. * So, it ensures, that all validations will be applied to * trimmed field values only. * * @param binder Binder object to be injected */ @InitBinder public void initBinder(WebDataBinder binder) { binder.registerCustomEditor(String.class, new StringTrimmerEditor(true)); binder.registerCustomEditor(DateTime.class, new DateTimeEditor("dd-MM-yyyy")); } /** * @param topicModificationService the object which provides actions on * {@link org.jtalks.jcommune.model.entity.Topic} entity * @param postService the object which provides actions on * {@link org.jtalks.jcommune.model.entity.Post} entity * @param branchService the object which provides actions on * {@link org.jtalks.jcommune.model.entity.Branch} entity * @param userService to determine the current user logged in * @param breadcrumbBuilder to create Breadcrumbs for pages * @param locationService to track user location on forum (what page he is viewing now) * @param sessionRegistry to obtain list of users currently online * @param topicFetchService to load topics from a database */ @Autowired public TopicController(TopicModificationService topicModificationService, PostService postService, BranchService branchService, UserService userService, BreadcrumbBuilder breadcrumbBuilder, LocationService locationService, SessionRegistry sessionRegistry, TopicFetchService topicFetchService, TopicDraftService topicDraftService, EntityToDtoConverter converter, RetryTemplate retryTemplate) { this.topicModificationService = topicModificationService; this.postService = postService; this.branchService = branchService; this.userService = userService; this.breadcrumbBuilder = breadcrumbBuilder; this.locationService = locationService; this.sessionRegistry = sessionRegistry; this.topicFetchService = topicFetchService; this.topicDraftService = topicDraftService; this.converter = converter; this.retryTemplate = retryTemplate; } /** * Shows page with form for new topic * * @param branchId {@link Branch} branch, where topic will be created * @return {@code ModelAndView} object with "newTopic" view, new {@link TopicDto} and branch id * @throws NotFoundException when branch was not found */ @RequestMapping(value = "/topics/new", method = RequestMethod.GET) public ModelAndView showNewTopicPage(@RequestParam(BRANCH_ID) Long branchId) throws NotFoundException { TopicDraft topicDraft = ObjectUtils.defaultIfNull( topicDraftService.getDraft(), new TopicDraft()); TopicDto dto = new TopicDto(topicDraft); Branch branch = branchService.get(branchId); dto.getTopic().setBranch(branch); dto.getTopic().setType(TopicTypeName.DISCUSSION.getName()); return new ModelAndView(TOPIC_VIEW) .addObject(TOPIC_DTO, dto) .addObject(TOPIC_DRAFT, topicDraft) .addObject(BRANCH_ID, branchId) .addObject(SUBMIT_URL, "/topics/new?branchId=" + branchId) .addObject(BREADCRUMB_LIST, breadcrumbBuilder.getNewTopicBreadcrumb(branch)); } /** * Create topic from data entered in form * * @param topicDto object with data from form * @param result {@link BindingResult} validation result * @param branchId branch, where topic will be created * @return {@code ModelAndView} object which will be redirect to forum.html * @throws NotFoundException when branch not found */ @RequestMapping(value = "/topics/new", method = RequestMethod.POST) public ModelAndView createTopic(@Valid @ModelAttribute final TopicDto topicDto, BindingResult result, @RequestParam(BRANCH_ID) Long branchId) throws NotFoundException { Branch branch = branchService.get(branchId); topicDto.getTopic().setType(TopicTypeName.DISCUSSION.getName()); if (result.hasErrors()) { TopicDraft topicDraft = ObjectUtils.defaultIfNull( topicDraftService.getDraft(), new TopicDraft()); return new ModelAndView(TOPIC_VIEW) .addObject(BRANCH_ID, branchId) .addObject(TOPIC_DTO, topicDto) .addObject(TOPIC_DRAFT, topicDraft) .addObject(SUBMIT_URL, "/topics/new?branchId=" + branchId) .addObject(BREADCRUMB_LIST, breadcrumbBuilder.getNewTopicBreadcrumb(branch)); } final Topic topic = topicDto.getTopic(); topic.setBranch(branch); Topic createdTopic = retryTemplate.execute(new RetryCallback<Topic, NotFoundException>() { @Override public Topic doWithRetry(RetryContext context) throws NotFoundException { return topicModificationService.createTopic(topic, topicDto.getBodyText()); } }); return new ModelAndView(REDIRECT_URL + createdTopic.getId()); } /** * Saves new draft or update if it already exists * * @param topicDraft draft topic * @param result validation result * @return response in JSON format */ @RequestMapping(value = "/topics/draft", method = RequestMethod.POST) @ResponseBody public JsonResponse saveDraft(@Valid @RequestBody TopicDraft topicDraft, BindingResult result) throws NotFoundException { if (result.hasErrors()) { return new JsonResponse(JsonResponseStatus.FAIL); } topicDraft = topicDraftService.saveOrUpdateDraft(topicDraft); return new JsonResponse(JsonResponseStatus.SUCCESS, topicDraft.getId()); } /** * Deletes a draft topic of the current user if it exists * * @return response in JSON format */ @RequestMapping(value = "/topics/draft", method = RequestMethod.DELETE) @ResponseBody public JsonResponse deleteDraft() { topicDraftService.deleteDraft(); return new JsonResponse(JsonResponseStatus.SUCCESS); } /** * Delete topic * * @param topicId topic id, this is the topic which contains the first post which should be deleted * @return redirect to branch page * @throws NotFoundException when topic not found */ @RequestMapping(value = "/topics/{topicId}", method = RequestMethod.DELETE) public ModelAndView deleteTopic(@PathVariable(TOPIC_ID) Long topicId) throws NotFoundException { Topic topic = topicFetchService.get(topicId); topicModificationService.deleteTopic(topic); return new ModelAndView("redirect:/branches/" + topic.getBranch().getId()); } /** * Displays to user a list of messages from the topic with pagination * * @param topicId the id of selected Topic * @param page page * @return {@code ModelAndView} * @throws NotFoundException when topic or branch not found */ @RequestMapping(value = "/topics/{topicId}", method = RequestMethod.GET) public ModelAndView showTopicPage(WebRequest request, @PathVariable(TOPIC_ID) Long topicId, @RequestParam(value = "page", defaultValue = "1", required = false) String page) throws NotFoundException { JCUser currentUser = userService.getCurrentUser(); Topic topic = topicFetchService.get(topicId); topicFetchService.checkViewTopicPermission(topic.getBranch().getId()); Page<Post> postsPage = postService.getPosts(topic, page); if (request.checkNotModified(topic.getLastModificationPostDate().getMillis())) { return null; } PostDto postDto = new PostDto(); PostDraft draft = topic.getDraftForUser(currentUser); if (draft != null) { postDto = PostDto.getDtoFor(draft); } return new ModelAndView("topic/postList") .addObject("viewList", locationService.getUsersViewing(topic)) .addObject("usersOnline", sessionRegistry.getAllPrincipals()) .addObject("postsPage", postsPage) .addObject("topic", topic) .addObject(POST_DTO, postDto) .addObject("subscribed", topic.getSubscribers().contains(currentUser)) .addObject(BREADCRUMB_LIST, breadcrumbBuilder.getForumBreadcrumb(topic)) .addObject("markAsReadLink", topic.getMarkAsReadUrl(currentUser, page).orNull()); } /** * Shows edit topic page with form, populated with fields from topic. * * @param topicId the id of selected Topic * @return {@code ModelAndView} * @throws NotFoundException when topic or branch not found */ @RequestMapping(value = "/topics/{topicId}/edit", method = RequestMethod.GET) public ModelAndView editTopicPage(@PathVariable(TOPIC_ID) Long topicId) throws NotFoundException { Topic topic = topicFetchService.get(topicId); if (topic.isCodeReview()) { throw new AccessDeniedException("Edit page for code review"); } TopicDto topicDto = new TopicDto(topic); return new ModelAndView(TOPIC_VIEW) .addObject(BRANCH_ID, topic.getBranch().getId()) .addObject(TOPIC_DTO, topicDto) .addObject(POLL, topic.getPoll()) .addObject(SUBMIT_URL, "/topics/" + topicId + "/edit") .addObject(BREADCRUMB_LIST, breadcrumbBuilder.getForumBreadcrumb(topic)); } /** * Saves topic after edit topic form submission. * * @param topicDto Dto populated in form * @param result validation result * @param topicId the current topicId * @return {@code ModelAndView} with redirect to saved topic or the same page with validation errors, if any * @throws NotFoundException when topic or branch not found */ @RequestMapping(value = "/topics/{topicId}/edit", method = RequestMethod.POST) public ModelAndView editTopic(@Valid @ModelAttribute TopicDto topicDto, BindingResult result, @PathVariable(TOPIC_ID) Long topicId) throws NotFoundException { Topic topic = topicFetchService.get(topicId); if (result.hasErrors()) { return new ModelAndView(TOPIC_VIEW) .addObject(BRANCH_ID, topic.getBranch().getId()) .addObject(TOPIC_DTO, topicDto) .addObject(POLL, topic.getPoll()) .addObject(SUBMIT_URL, "/topics/" + topicId + "/edit") .addObject(BREADCRUMB_LIST, breadcrumbBuilder.getForumBreadcrumb(topic)); } topicDto.fillTopic(topic); topicModificationService.updateTopic(topic, topicDto.getPoll()); return new ModelAndView(REDIRECT_URL + topicId); } /** * Moves topic to another branch. * * @param topicId id of moving topic * @param branchId id of target branch * @throws NotFoundException when topic or branch with given id not found */ @RequestMapping(value = "/topics/move/json/{topicId}", method = RequestMethod.POST) @ResponseBody public JsonResponse moveTopic(@PathVariable(TOPIC_ID) Long topicId, @RequestParam(BRANCH_ID) Long branchId) throws NotFoundException { Topic topic = topicFetchService.get(topicId); topicModificationService.moveTopic(topic, branchId); return new JsonResponse(JsonResponseStatus.SUCCESS, converter.convertTopicToDto(topic).getTopicUrl()); } /** * Closes given topic or throws an exception if there is no such topic. * Closed topic is unavailable for posting until it's opened again. * * @param topicId identifies topic to be closed for further posting * @return redirection to the same topic * @throws NotFoundException if there is no topic for id given */ @RequestMapping(value = "/topics/{topicId}/close") public String closeTopic(@PathVariable(TOPIC_ID) Long topicId) throws NotFoundException { Topic topic = topicFetchService.get(topicId); topicModificationService.closeTopic(topic); return REDIRECT_URL + topicId; } /** * Reopens topic for posting. If topic as opened already, does nothing * * @param topicId topic to be opened for posting * @return redirection to the same topic * @throws NotFoundException if there is no topic for id given */ @RequestMapping(value = "/topics/{topicId}/open") public String openTopic(@PathVariable(TOPIC_ID) Long topicId) throws NotFoundException { Topic topic = topicFetchService.get(topicId); topicModificationService.openTopic(topic); return REDIRECT_URL + topicId; } }