/** * 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.plugin.questionsandanswers.controller; import com.google.common.annotations.VisibleForTesting; import com.google.common.io.ByteStreams; import org.apache.commons.lang3.ObjectUtils; import org.apache.commons.lang.time.DateFormatUtils; import org.apache.velocity.app.VelocityEngine; import org.apache.velocity.exception.VelocityException; import org.apache.velocity.tools.generic.EscapeTool; import org.jtalks.jcommune.model.entity.*; import org.jtalks.jcommune.plugin.api.exceptions.NotFoundException; import org.jtalks.jcommune.plugin.api.service.*; import org.jtalks.jcommune.plugin.api.service.nontransactional.BbToHtmlConverter; import org.jtalks.jcommune.plugin.api.service.nontransactional.PluginLocationServiceImpl; import org.jtalks.jcommune.plugin.api.service.nontransactional.PropertiesHolder; import org.jtalks.jcommune.plugin.api.service.transactional.*; import org.jtalks.jcommune.plugin.api.web.PluginController; 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.*; import org.jtalks.jcommune.plugin.api.web.locale.JcLocaleResolver; import org.jtalks.jcommune.plugin.api.web.util.BreadcrumbBuilder; import org.jtalks.jcommune.plugin.api.web.velocity.tool.JodaDateTimeTool; import org.jtalks.jcommune.plugin.api.web.velocity.tool.PermissionTool; import org.jtalks.jcommune.plugin.questionsandanswers.QuestionsAndAnswersPlugin; import org.jtalks.jcommune.plugin.questionsandanswers.dto.CommentDto; import org.kefirsf.bb.EscapeXmlProcessorFactory; import org.kefirsf.bb.TextProcessor; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; import org.springframework.data.domain.PageImpl; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.ui.velocity.VelocityEngineUtils; import org.springframework.validation.BindingResult; import org.springframework.web.bind.annotation.*; import org.springframework.web.servlet.LocaleResolver; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.validation.Valid; import java.io.IOException; import java.util.*; import static org.jtalks.jcommune.plugin.questionsandanswers.QuestionsAndAnswersPlugin.MESSAGE_PATH; /** * Class for processing question management web requests * * @author Mikhail Stryzhonok */ @Controller @RequestMapping(QuestionsAndAnswersPlugin.CONTEXT) public class QuestionsAndAnswersController implements ApplicationContextAware, PluginController { private static final String PATH_TO_IMAGES = "/org/jtalks/jcommune/plugin/questionsandanswers/images/"; private static final String IF_MODIFIED_SINCE_HEADER = "If-Modified-Since"; private static final String HTTP_HEADER_DATETIME_PATTERN = "E, dd MMM yyyy HH:mm:ss z"; private static final String TEMPLATE_PATH = "org/jtalks/jcommune/plugin/questionsandanswers/template/"; private static final String QUESTION_FORM_TEMPLATE_PATH = TEMPLATE_PATH + "questionForm.vm"; private static final String ANSWER_FORM_TEMPLATE_PATH = TEMPLATE_PATH + "answerForm.vm"; private static final String QUESTION_TEMPLATE_PATH = TEMPLATE_PATH + "question.vm"; private static final String BREADCRUMB_LIST = "breadcrumbList"; private static final String TOPIC_DTO = "topicDto"; private static final String TOPIC_DRAFT = "topicDraft"; private static final String POST_DTO = "postDto"; private static final String EDIT_MODE = "edit"; private static final String RESULT = "result"; private static final String QUESTION = "question"; private static final String POST_PAGE = "postPage"; private static final String SUBSCRIBED = "subscribed"; private static final String CONVERTER = "converter"; private static final String VIEW_LIST = "viewList"; private static final String LIMIT_OF_POSTS_ATTRIBUTE = "postLimit"; public static final int LIMIT_OF_POSTS_VALUE = 50; public static final String PLUGIN_VIEW_NAME = "plugin/plugin"; public static final String BRANCH_ID = "branchId"; public static final String CONTENT = "content"; private static final String QEUSTION_TITLE = "questionTitle"; public static final String POST_ID = "postId"; public static final String COMMENT_ID = "commentId"; private static final String HTML_ESCAPER = "htmlEscaper"; // custom processor is used for escaping of HTML because // standard Velocity escaping utility not correct displays emoji. private TextProcessor htmlEscaper = EscapeXmlProcessorFactory.getInstance().create(); private BreadcrumbBuilder breadcrumbBuilder = new BreadcrumbBuilder(); private String apiPath; private ApplicationContext applicationContext; /** * Shows question creation page * * @param branchId id of the branch in which user want to create question * @param model model for transferring to jsp * @param request HttpServletRequest * * @return plugin view name * @throws NotFoundException if branch with specified id not found */ @RequestMapping(value = "new", method = RequestMethod.GET) public String showNewQuestionPage(@RequestParam(BRANCH_ID) Long branchId, Model model, HttpServletRequest request) throws NotFoundException { VelocityEngine engine = new VelocityEngine(getProperties()); engine.init(); TopicDraft draft = ObjectUtils.defaultIfNull( getPluginTopicDraftService().getDraft(), new TopicDraft()); TopicDto dto = new TopicDto(draft); dto.getTopic().setType(QuestionsAndAnswersPlugin.TOPIC_TYPE); Branch branch = getPluginBranchService().get(branchId); dto.getTopic().setBranch(branch); Map<String, Object> data = getDefaultModel(request); data.put(BREADCRUMB_LIST, breadcrumbBuilder.getForumBreadcrumb(dto.getTopic())); data.put(TOPIC_DTO, dto); data.put(TOPIC_DRAFT, draft); data.put(HTML_ESCAPER, htmlEscaper); data.put(EDIT_MODE, false); model.addAttribute(CONTENT, getMergedTemplate(engine, QUESTION_FORM_TEMPLATE_PATH, "UTF-8", data)); return PLUGIN_VIEW_NAME; } /** * Saves question after form submission. * * @param topicDto Dto populated in form * @param result validation result * @param model model for transferring to jsp * @param branchId branch, where topic will be created * @param request HttpServletRequest * * @return redirect to newly created topic if no validation errors * plugin view name if validation errors occurred * @throws NotFoundException if branch with specified id not found */ @RequestMapping(value = "new", method = RequestMethod.POST) public String createQuestion(@Valid @ModelAttribute TopicDto topicDto, BindingResult result, Model model, @RequestParam(BRANCH_ID) Long branchId, HttpServletRequest request) throws NotFoundException{ VelocityEngine engine = new VelocityEngine(getProperties()); engine.init(); Branch branch = getPluginBranchService().get(branchId); Map<String, Object> data = getDefaultModel(request); topicDto.getTopic().setBranch(branch); topicDto.getTopic().setType(QuestionsAndAnswersPlugin.TOPIC_TYPE); if (result.hasErrors()) { data.put(BREADCRUMB_LIST, breadcrumbBuilder.getNewTopicBreadcrumb(branch)); data.put(TOPIC_DTO, topicDto); data.put(HTML_ESCAPER, htmlEscaper); data.put(RESULT, result); model.addAttribute(CONTENT, getMergedTemplate(engine, QUESTION_FORM_TEMPLATE_PATH, "UTF-8", data)); return PLUGIN_VIEW_NAME; } Topic createdQuestion = getTypeAwarePluginTopicService().createTopic(topicDto.getTopic(), topicDto.getBodyText()); return "redirect:" + QuestionsAndAnswersPlugin.CONTEXT + "/" + createdQuestion.getId(); } /** * Writes question icon into response * * @param request HttpServletRequest * @param response HttpServletResponse * @param name name of icon */ @RequestMapping(value = "/resources/icon/{name}", method = RequestMethod.GET) public void getIcon(HttpServletRequest request, HttpServletResponse response, @PathVariable("name") String name) { try { processIconRequest(request, response, PATH_TO_IMAGES + name); } catch (Exception ex) { response.setStatus(HttpServletResponse.SC_NOT_FOUND); } } /** * Shows question page * * @param request HttpServletRequest * @param model model for transferring to jsp * @param id id of question * * @return plugin view name * @throws NotFoundException if question with specified id not found */ @RequestMapping(value = "{id}", method = RequestMethod.GET) public String showQuestion(HttpServletRequest request, Model model, @PathVariable("id") Long id) throws NotFoundException { Topic topic = getTypeAwarePluginTopicService().get(id, QuestionsAndAnswersPlugin.TOPIC_TYPE); getTypeAwarePluginTopicService().checkViewTopicPermission(topic.getBranch().getId()); JCUser currentUser = getUserReader().getCurrentUser(); PostDto postDto = new PostDto(); PostDraft draft = topic.getDraftForUser(currentUser); if (draft != null) { postDto = PostDto.getDtoFor(draft); } Map<String, Object> data = getDefaultModel(request); data.put(QUESTION, topic); data.put(POST_PAGE, new PageImpl<>(getSortedPosts(topic.getPosts()))); data.put(BREADCRUMB_LIST, breadcrumbBuilder.getForumBreadcrumb(topic)); data.put(SUBSCRIBED, false); data.put(CONVERTER, BbToHtmlConverter.getInstance()); data.put(HTML_ESCAPER, htmlEscaper); data.put(VIEW_LIST, getLocationService().getUsersViewing(topic)); data.put(POST_DTO, postDto); data.put(LIMIT_OF_POSTS_ATTRIBUTE, LIMIT_OF_POSTS_VALUE); getPluginLastReadPostService().markTopicAsRead(topic); VelocityEngine engine = new VelocityEngine(getProperties()); engine.init(); model.addAttribute(CONTENT, getMergedTemplate(engine, QUESTION_TEMPLATE_PATH, "UTF-8", data)); return PLUGIN_VIEW_NAME; } /** * Check if user can answer for question with given id. Yer can answer if question has less than * {@link #LIMIT_OF_POSTS_VALUE} answers * * @param questionId id of question to be checked * * @return Success response in JSON form if user can leave answer and fail response in JSON form otherwise * @throws NotFoundException if question with given id not found */ @RequestMapping(value = "{id}/canpost", method = RequestMethod.GET) @ResponseBody public JsonResponse canPost(@PathVariable("id") Long questionId) throws NotFoundException { Topic topic = getTypeAwarePluginTopicService().get(questionId, QuestionsAndAnswersPlugin.TOPIC_TYPE); if (topic.getPostCount() - 1 >= LIMIT_OF_POSTS_VALUE) { return new JsonResponse(JsonResponseStatus.FAIL); } return new JsonResponse(JsonResponseStatus.SUCCESS); } /** * Shows answer * * @param questionId id of question * @param answerId id of the answer * * @return redirect to the answer url */ @RequestMapping(value = "{questionId}/post/{answerId}", method = RequestMethod.GET) public String showAnswer(@PathVariable Long questionId, @PathVariable Long answerId) { return "redirect:" + QuestionsAndAnswersPlugin.CONTEXT + "/" + questionId + "#" + answerId; } /** * Show edit question page * * @param request HttpServletRequest * @param model model for transferring to jsp * @param id id of question to edit * * @return plugin view name * @throws NotFoundException if question with specified id not found */ @RequestMapping(value = "{id}/edit", method = RequestMethod.GET) public String editQuestionPage(HttpServletRequest request, Model model, @PathVariable("id") Long id) throws NotFoundException{ Topic topic = getTypeAwarePluginTopicService().get(id, QuestionsAndAnswersPlugin.TOPIC_TYPE); TopicDto topicDto = new TopicDto(topic); VelocityEngine engine = new VelocityEngine(getProperties()); engine.init(); Map<String, Object> data = getDefaultModel(request); data.put(BREADCRUMB_LIST, breadcrumbBuilder.getForumBreadcrumb(topic)); data.put(TOPIC_DTO, topicDto); data.put(HTML_ESCAPER, htmlEscaper); data.put(EDIT_MODE, true); model.addAttribute(CONTENT, getMergedTemplate(engine, QUESTION_FORM_TEMPLATE_PATH, "UTF-8", data)); return PLUGIN_VIEW_NAME; } /** * Updates question * * @param topicDto Dto populated in form * @param result validation result * @param model model for transferring to jsp * @param id id of question to edit * @param request HttpServletRequest * * @return redirect to newly created topic if no validation errors * plugin view name if validation errors occurred * @throws NotFoundException if question with specified id not found */ @RequestMapping(value = "{id}/edit", method = RequestMethod.POST) public String updateQuestion(@Valid @ModelAttribute TopicDto topicDto, BindingResult result, Model model, @PathVariable("id") Long id, HttpServletRequest request) throws NotFoundException { Topic topic = getTypeAwarePluginTopicService().get(id, QuestionsAndAnswersPlugin.TOPIC_TYPE); Map<String, Object> data = getDefaultModel(request); if (result.hasErrors()) { topicDto.getTopic().setId(topic.getId()); topicDto.getTopic().setBranch(topic.getBranch()); VelocityEngine engine = new VelocityEngine(getProperties()); engine.init(); data.put(BREADCRUMB_LIST, breadcrumbBuilder.getForumBreadcrumb(topic)); data.put(TOPIC_DTO, topicDto); data.put(HTML_ESCAPER, htmlEscaper); data.put(EDIT_MODE, true); data.put(RESULT, result); model.addAttribute(CONTENT, getMergedTemplate(engine, QUESTION_FORM_TEMPLATE_PATH, "UTF-8", data)); return PLUGIN_VIEW_NAME; } topicDto.fillTopic(topic); getTypeAwarePluginTopicService().updateTopic(topic); return "redirect:" + QuestionsAndAnswersPlugin.CONTEXT + "/" + topic.getId(); } /** * Show edit answer page * * @param request HttpServletRequest * @param model model for transferring to jsp * @param id id of answer to edit * * @return plugin view name * @throws NotFoundException if answer with specified id not found */ @RequestMapping(value = "post/{id}/edit", method = RequestMethod.GET) public String editAnswerPage(HttpServletRequest request, Model model, @PathVariable("id") Long id) throws NotFoundException{ Post answer = getPluginPostService().get(id); PostDto answerDto = PostDto.getDtoFor(answer); VelocityEngine engine = new VelocityEngine(getProperties()); engine.init(); Map<String, Object> data = getDefaultModel(request); data.put(QEUSTION_TITLE, answer.getTopic().getTitle()); data.put(POST_DTO, answerDto); data.put(HTML_ESCAPER, htmlEscaper); model.addAttribute(CONTENT, getMergedTemplate(engine, ANSWER_FORM_TEMPLATE_PATH, "UTF-8", data)); return PLUGIN_VIEW_NAME; } /** * updates answer * * @param postDto Dto populated in form * @param result validation result * @param model model for transferring to template * @param id id of answer to edit * @param request HttpServletRequest * * @return redirect to updated answer if no validation errors * plugin view name if validation errors occurred * @throws NotFoundException if answer with specified id not found */ @RequestMapping(value = "post/{id}/edit", method = RequestMethod.POST) public String updateAnswer(@Valid @ModelAttribute PostDto postDto, BindingResult result, Model model, @PathVariable("id") Long id, HttpServletRequest request) throws NotFoundException { Post answer = getPluginPostService().get(id); Map<String, Object> data = getDefaultModel(request); if (result.hasErrors()) { VelocityEngine engine = new VelocityEngine(getProperties()); engine.init(); data.put(QEUSTION_TITLE, answer.getTopic().getTitle()); data.put(POST_DTO, postDto); data.put(RESULT, result); data.put(HTML_ESCAPER, htmlEscaper); model.addAttribute(CONTENT, getMergedTemplate(engine, ANSWER_FORM_TEMPLATE_PATH, "UTF-8", data)); return PLUGIN_VIEW_NAME; } getPluginPostService().updatePost(answer, postDto.getBodyText()); return "redirect:" + QuestionsAndAnswersPlugin.CONTEXT + "/" + answer.getTopic().getId() + "#" + id; } /** * Process the answer form. Adds new answer to the specified question and redirects to the * question view page. * * @param questionId id of question to which answer will be added * @param postDto dto that contains data entered in form * @param result validation result * @param model model for transferring to template * @param request HttpServletRequest * * @return redirect to the answer or back to answer page if validation failed * @throws NotFoundException when question or branch not found */ @RequestMapping(value = "{questionId}", method = RequestMethod.POST) public String create(@PathVariable("questionId") Long questionId, @Valid @ModelAttribute PostDto postDto, BindingResult result, Model model, HttpServletRequest request) throws NotFoundException { postDto.setTopicId(questionId); Topic topic = getTypeAwarePluginTopicService().get(questionId, QuestionsAndAnswersPlugin.TOPIC_TYPE); //We can't provide limitation properly without database-level locking if (result.hasErrors() || LIMIT_OF_POSTS_VALUE <= topic.getPostCount() - 1) { JCUser currentUser = getUserReader().getCurrentUser(); PostDraft draft = topic.getDraftForUser(currentUser); if (draft != null) { // If we create new dto object instead of using already existing // we lose error messages linked with it postDto.fillFrom(draft); } Map<String, Object> data = getDefaultModel(request); VelocityEngine engine = new VelocityEngine(getProperties()); engine.init(); data.put(QUESTION, topic); data.put(POST_PAGE, new PageImpl<>(getSortedPosts(topic.getPosts()))); data.put(BREADCRUMB_LIST, breadcrumbBuilder.getForumBreadcrumb(topic)); data.put(SUBSCRIBED, false); data.put(RESULT, result); data.put(CONVERTER, BbToHtmlConverter.getInstance()); data.put(VIEW_LIST, getLocationService().getUsersViewing(topic)); data.put(POST_DTO, postDto); data.put(LIMIT_OF_POSTS_ATTRIBUTE, LIMIT_OF_POSTS_VALUE); model.addAttribute(CONTENT, getMergedTemplate(engine, QUESTION_TEMPLATE_PATH, "UTF-8", data)); return PLUGIN_VIEW_NAME; } Post newbie = getTypeAwarePluginTopicService().replyToTopic(questionId, postDto.getBodyText(), topic.getBranch().getId()); getPluginLastReadPostService().markTopicAsRead(newbie.getTopic()); return "redirect:" + QuestionsAndAnswersPlugin.CONTEXT + "/" + questionId + "#" + newbie.getId(); } /** * Closes question with specified id. * * @param id id of question to close * * @return redirect to closed topic * @throws NotFoundException if question with specified id not found */ @RequestMapping(value = "{id}/close", method = RequestMethod.GET) public String closeQuestion(@PathVariable("id") Long id) throws NotFoundException { Topic topic = getTypeAwarePluginTopicService().get(id, QuestionsAndAnswersPlugin.TOPIC_TYPE); getTypeAwarePluginTopicService().closeTopic(topic); return "redirect:" + QuestionsAndAnswersPlugin.CONTEXT + "/" + topic.getId(); } /** * Opens question with specified id. * * @param id of question to open * * @return redirect to opened question * @throws NotFoundException if question with specified if not found */ @RequestMapping(value = "{id}/open", method = RequestMethod.GET) public String openQuestion(@PathVariable("id") Long id) throws NotFoundException { Topic topic = getTypeAwarePluginTopicService().get(id, QuestionsAndAnswersPlugin.TOPIC_TYPE); getTypeAwarePluginTopicService().openTopic(topic); return "redirect:" + QuestionsAndAnswersPlugin.CONTEXT + "/" + topic.getId(); } /** * Deletes answer by given id * * @param answerId id of the answer * @return redirect to question page * @throws NotFoundException when answer was not found */ @RequestMapping(method = RequestMethod.DELETE, value = "post/{answerId}") public String deleteAnswer(@PathVariable Long answerId) throws NotFoundException { Post answer = getPluginPostService().get(answerId); Post neighborAnswer = answer.getTopic().getNeighborPost(answer); getPluginPostService().deletePost(answer); return "redirect:" + QuestionsAndAnswersPlugin.CONTEXT + "/" + answer.getTopic().getId() + "#" + neighborAnswer.getId(); } /** * Adds new comment to post * * @param dto dto populated in form * @param result validation result * @param request http servlet request * * @return result in JSON format */ @RequestMapping(method = RequestMethod.POST, value = "newcomment") @ResponseBody JsonResponse addComment(@Valid @RequestBody CommentDto dto, BindingResult result, HttpServletRequest request) { if (result.hasErrors()) { return new FailValidationJsonResponse(result.getAllErrors()); } PostComment comment; try { Post targetPost = getPluginPostService().get(dto.getPostId()); //We can't provide limitation properly without database-level locking if (targetPost.getNotRemovedComments().size() >= LIMIT_OF_POSTS_VALUE) { return new JsonResponse(JsonResponseStatus.FAIL); } comment = getPluginPostService().addComment(dto.getPostId(), Collections.EMPTY_MAP, dto.getBody()); } catch (NotFoundException ex) { return new FailJsonResponse(JsonResponseReason.ENTITY_NOT_FOUND); } JodaDateTimeTool dateTimeTool = new JodaDateTimeTool(request); return new JsonResponse(JsonResponseStatus.SUCCESS, new CommentDto(comment, dateTimeTool)); } /** * Edits existence comment * * @param dto dto populated in form * @param result validation result * @param branchId id of a branch to check permission * * @return result in JSON format */ @RequestMapping(method = RequestMethod.POST, value = "editcomment") @ResponseBody JsonResponse editComment(@Valid @RequestBody CommentDto dto, BindingResult result, @RequestParam("branchId") long branchId) { if (result.hasErrors()) { return new FailValidationJsonResponse(result.getAllErrors()); } PostComment updatedComment; try { updatedComment = getCommentService().updateComment(dto.getId(), dto.getBody(), branchId); } catch (NotFoundException ex) { return new FailJsonResponse(JsonResponseReason.ENTITY_NOT_FOUND); } return new JsonResponse(JsonResponseStatus.SUCCESS, updatedComment.getBody()); } /** * Deletes comment * * @param commentId id of comment to delete * @param postId id of a post to which this comment belong * * @return result in JSON format */ @RequestMapping(method = RequestMethod.GET, value = "deletecomment") @ResponseBody JsonResponse deleteComment(@RequestParam(COMMENT_ID) Long commentId, @RequestParam(POST_ID) Long postId) { Post post; PostComment postComment; try { post = getPluginPostService().get(postId); postComment = getCommentService().getComment(commentId); } catch (NotFoundException ex) { return new FailJsonResponse(JsonResponseReason.ENTITY_NOT_FOUND); } getCommentService().markCommentAsDeleted(post, postComment); return new JsonResponse(JsonResponseStatus.SUCCESS); } /** * Gets copy of specified collection of posts sorted by rating and creation date * * @param posts collection of posts to sort * * @return collection of posts sorted by rating and creation date */ @VisibleForTesting List<Post> getSortedPosts(List<Post> posts) { List<Post> result = new ArrayList<>(posts); Post question = result.remove(0); Collections.sort(result, new PostComparator()); result.add(0, question); return result; } /** * Writes icon to response and set apropriate response headers * * @param request HttpServletRequest * @param response HttpServletResponse * @param iconPath path to icon to be writed * * @throws IOException if icon not found */ private void processIconRequest(HttpServletRequest request, HttpServletResponse response, String iconPath) throws IOException { if(request.getHeader(IF_MODIFIED_SINCE_HEADER) != null) { response.setStatus(HttpServletResponse.SC_NOT_MODIFIED); return; } byte[] icon = ByteStreams.toByteArray(getClass().getResourceAsStream(iconPath)); response.setContentType("image/png"); response.setContentLength(icon.length); response.getOutputStream().write(icon); response.setHeader("Pragma", "public"); response.setHeader("Cache-Control", "public"); response.addHeader("Cache-Control", "must-revalidate"); response.addHeader("Cache-Control", "max-age=0"); String formattedDateExpires = DateFormatUtils.format(new Date(), HTTP_HEADER_DATETIME_PATTERN, Locale.US); response.setHeader("Expires", formattedDateExpires); Date lastModificationDate = new Date(0); response.setHeader("Last-Modified", DateFormatUtils.format(lastModificationDate, HTTP_HEADER_DATETIME_PATTERN, Locale.US)); } /** * Gets resource bundle with locale of current user * * @param request http servlet request * * @return resource bundle with locale of current user */ private ResourceBundle getLocalizedMessagesBundle(HttpServletRequest request) { return ResourceBundle.getBundle(MESSAGE_PATH, getLocaleResolver().resolveLocale(request)); } /** * {@inheritDoc} */ @Override public void setApplicationContext(ApplicationContext applicationContext) { this.applicationContext = applicationContext; } /** * Gets properties of velocity engine * * @return properties of velocity engine */ protected Properties getProperties() { Properties properties = new Properties(); properties.put("resource.loader", "jar"); properties.put("jar.resource.loader.class", "org.apache.velocity.runtime.resource.loader.JarResourceLoader"); List<String> jars = new ArrayList<>(); jars.add("jar:file:" + this.getClass().getProtectionDomain().getCodeSource().getLocation().getPath()); jars.add("jar:file:" + apiPath); properties.put("jar.resource.loader.path", jars); properties.put("runtime.log.logsystem.class", "org.apache.velocity.runtime.log.NullLogSystem"); return properties; } /** * Create map with default objects needed in velocity template (e.g. request, current user, messages bundle etc.) * * @param request HttpServletRequest * * @return Map which will be passed into velocity template */ private Map<String, Object> getDefaultModel(HttpServletRequest request) { Map<String, Object> model = new HashMap<>(); model.put("request", request); model.put("dateTool", new JodaDateTimeTool(request)); model.put("esc", new EscapeTool()); JCUser currentUser = getUserReader().getCurrentUser(); PermissionTool tool = new PermissionTool(applicationContext); model.put("currentUser", currentUser); model.put("messages", getLocalizedMessagesBundle(request)); model.put("permissionTool", tool); model.put("propertiesHolder", PropertiesHolder.getInstance()); return model; } /** * {@inheritDoc} */ public void setApiPath(String apiPath) { this.apiPath = apiPath; } /** * Needed for mocking * * @return locale resolver used in plugin */ LocaleResolver getLocaleResolver() { return JcLocaleResolver.getInstance(); } /** * Needed for mocking * * @return service for manipulating with posts */ PluginPostService getPluginPostService() { return TransactionalPluginPostService.getInstance(); } /** * Needed for mocking * * @return service for manipulating draft topics */ PluginTopicDraftService getPluginTopicDraftService() { return TransactionalPluginTopicDraftService.getInstance(); } /** * Needed for mocking * * @return service for manipulating with branches */ PluginBranchService getPluginBranchService() { return TransactionalPluginBranchService.getInstance(); } /** * Needed for mocking * * @return service for marking topic as read */ PluginLastReadPostService getPluginLastReadPostService() { return TransactionalPluginLastReadPostService.getInstance(); } /** * Needed for mocking * * @return service for manipulating with topics */ TypeAwarePluginTopicService getTypeAwarePluginTopicService() { return TransactionalTypeAwarePluginTopicService.getInstance(); } /** * Needed for mocking * * @return service for fetching information about users */ UserReader getUserReader() { return ReadOnlySecurityService.getInstance(); } /** * * Merge the specified Velocity template with the given model into a String. * Needed for mocking. * * @param velocityEngine VelocityEngine to work with * @param templateLocation the location of template, relative to Velocity's resource loader path * @param encoding the encoding of the template file * @param model the Map that contains model names as keys and model objects as values * * @return the result as String */ String getMergedTemplate(VelocityEngine velocityEngine, String templateLocation, String encoding, Map<String, Object> model) throws VelocityException { return VelocityEngineUtils.mergeTemplateIntoString(velocityEngine, templateLocation, encoding, model); } /** * Needed for mocking * * @return service for manipulating with user location */ PluginLocationService getLocationService() { return PluginLocationServiceImpl.getInstance(); } /** * Needed for mocking * * @return service for manipulating with comments */ PluginCommentService getCommentService() { return TransactionalPluginCommentService.getInstance(); } /** * Sets specified {@link BreadcrumbBuilder} * Needed for tests * * @param breadcrumbBuilder {@link BreadcrumbBuilder} to set */ void setBreadcrumbBuilder(BreadcrumbBuilder breadcrumbBuilder) { this.breadcrumbBuilder = breadcrumbBuilder; } private static class PostComparator implements Comparator<Post> { @Override public int compare(Post o1, Post o2) { return o1.getRating() == o2.getRating() ? o2.getCreationDate().compareTo(o1.getCreationDate()) : o2.getRating() - o1.getRating(); } } }