/** * 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.service.bb2htmlprocessors; import com.google.common.annotations.VisibleForTesting; import org.jtalks.jcommune.service.TopicModificationService; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.web.context.request.RequestAttributes; import org.springframework.web.context.request.RequestContextHolder; import org.springframework.web.context.request.ServletRequestAttributes; import org.kefirsf.bb.TextProcessor; import org.kefirsf.bb.TextProcessorAdapter; import javax.servlet.http.HttpServletRequest; import java.util.ArrayList; import java.util.List; import java.util.regex.Matcher; import java.util.regex.Pattern; /** * Processor for bb2html which saves inner "[/code]" tags in code review posts. This is required because Code Reviews * initially have surrounding [code], but user himself also might have put there same string, without this processor all * this stuff is treated as bb codes and is replaced with HTML.<br/> Consider such situation: <ul> <li>User creates code * review which itself means that all the text is surrounded with [code]. This results in {@code * [code][-code][/code][/code]}</li> <li>We replace internal [/code] with [-code] inside of this pre-processor and place * that information inside of the {@link #REPLACE_HISTORY_LIST_ATTRIBUTE} attribute like this {@code false, true} which * means that internal [-code] is user input and not the work of our pre-processor [/code]</li> <li>In the {@link * #postProcess(String)} we find this attribute and we know what second [-code] should be replaced back to [/code]</li> * </ul> * * @author Evgeny Kapinos * @see <a href="http://jira.jtalks.org/browse/JC-1261">JIRA</a> for high-level details. */ public class BbCodeReviewProcessor extends TextProcessorAdapter implements TextProcessor, TextPostProcessor { private final Logger logger = LoggerFactory.getLogger(getClass()); private static final String CODE_JAVA_BBCODE_END_REPLACEMENT = "[-code]"; private static final String CODE_JAVA_BBCODE_END_REPLACEMENT_PATTERN = "\\[-code\\]"; /** * This is an attribute in the http request which contains a list of bb-codes that should or should not be replaced * back. */ @VisibleForTesting protected static final String REPLACE_HISTORY_LIST_ATTRIBUTE = "BBCodeReviewPreprocessor_replaceHistoryList"; /** * Process incoming encoded text and replacing [/code] tags to [-code] * * @param bbEncodedText BB encoded text to process * @return processed text */ @Override public String process(String bbEncodedText) { HttpServletRequest httpServletRequest = getServletRequest(); httpServletRequest.removeAttribute(REPLACE_HISTORY_LIST_ATTRIBUTE); if (!isCodeReviewPost(httpServletRequest) || !isValidCodeReviewBbCodeString(bbEncodedText)) { return bbEncodedText; } List<Boolean> replaceHistoryList = getReplaceHistoryList(bbEncodedText); if (replaceHistoryList.isEmpty()) { return bbEncodedText; } httpServletRequest.setAttribute(REPLACE_HISTORY_LIST_ATTRIBUTE, replaceHistoryList); return substituteCloseCodeTagsWithTemporaryReplacementInEncodedText(bbEncodedText); } /** * Process incoming encoded text with replacing [/code] tags to [-code] * * @param bbEncodedText BB encoded text to process * @return processed text */ @Override public CharSequence process(CharSequence bbEncodedText) { String result = process(bbEncodedText.toString()); return new StringBuilder(result).subSequence(0, result.length()); } /** * Process incoming decoded text by replacing [-code] tags to [/code]. * * @param bbDecodedText text returned after to BBCode processor * @return resultant text */ @Override public String postProcess(String bbDecodedText) { HttpServletRequest httpServletRequest = getServletRequest(); if (!isCodeReviewPost(httpServletRequest)) { return bbDecodedText; } @SuppressWarnings("unchecked") List<Boolean> replaceHistoryList = (List<Boolean>) httpServletRequest.getAttribute(REPLACE_HISTORY_LIST_ATTRIBUTE); if (replaceHistoryList == null) { return bbDecodedText; } httpServletRequest.removeAttribute(REPLACE_HISTORY_LIST_ATTRIBUTE); return removeTemporaryReplacementSubstitutionFromDecodedText(bbDecodedText, replaceHistoryList); } /** * Gets current request * * @return native {@link HttpServletRequest} */ @VisibleForTesting protected HttpServletRequest getServletRequest() { RequestAttributes attributes = RequestContextHolder.currentRequestAttributes(); return ((ServletRequestAttributes) attributes).getRequest(); } /** * Checks in current request attributes Code Review token * * @return {@code true, false} */ private boolean isCodeReviewPost(HttpServletRequest httpServletRequest) { String isCodeReviewPost = (String) httpServletRequest.getAttribute("isCodeReviewPost"); return (isCodeReviewPost != null); } /** * Checks regular wrap [code]...[/code] in Code Review text * * @return {@code true, false} */ private boolean isValidCodeReviewBbCodeString(String bbEncodedText) { // We use Pattern.DOTALL flag for extend ".*" pattern behavior to line terminators too Pattern pattern = Pattern.compile(TopicModificationService.CODE_JAVA_BBCODE_START_PATTERN + ".*" + TopicModificationService.CODE_JAVA_BBCODE_END_PATTERN, Pattern.DOTALL); Matcher matcher = pattern.matcher(bbEncodedText); if (!matcher.matches()) { logger.warn("BbCodeReviewProcessor called, but target encoded text \"" + bbEncodedText + "\" doesn't wrapped with " + TopicModificationService.CODE_JAVA_BBCODE_START + "..." + TopicModificationService.CODE_JAVA_BBCODE_END + " BBCodes. Check \"isCodeReviewPost\" request attribute"); return false; } return true; } /** * Removes regular wrap [code]...[/code] in Code Review text * * @return user text from full Code Review with wrap */ private String getUserCodeReviewText(String bbEncodedText) { return bbEncodedText.substring(TopicModificationService.CODE_JAVA_BBCODE_START.length(), bbEncodedText.length() - TopicModificationService.CODE_JAVA_BBCODE_END.length()); } /** * Analyze Code Review text and creates replace history list * * @return replace history list */ private List<Boolean> getReplaceHistoryList(String bbEncodedText) { String textOnly = getUserCodeReviewText(bbEncodedText); List<Boolean> replaceHistoryList = new ArrayList<Boolean>(); Pattern pattern = Pattern.compile(TopicModificationService.CODE_JAVA_BBCODE_END_PATTERN + "|" + CODE_JAVA_BBCODE_END_REPLACEMENT_PATTERN); Matcher matcher = pattern.matcher(textOnly); while (matcher.find()) { replaceHistoryList.add(matcher.group().equals(TopicModificationService.CODE_JAVA_BBCODE_END)); } return replaceHistoryList; } /** * Creates new BBcode string without [/code] tags in user part (replaced by [-code] tag) * * @return safety BBcode string with temporary replacements */ private String substituteCloseCodeTagsWithTemporaryReplacementInEncodedText(String bbEncodedText) { return TopicModificationService.CODE_JAVA_BBCODE_START + getUserCodeReviewText(bbEncodedText).replaceAll(TopicModificationService.CODE_JAVA_BBCODE_END_PATTERN, CODE_JAVA_BBCODE_END_REPLACEMENT) + TopicModificationService.CODE_JAVA_BBCODE_END; } /** * Returns back original tags into already processed string. To be precise: [-code] tags are replaced back with * [code] tags * * @param bbDecodedText the text after it was processed into HTML, it yet contains [-code] tags * @param replaceHistoryList true in the list means that [-code] should be replaced with [/code], false would mean * that [-code] is the original text written by user and it shouldn't be replaced with * [/code] * @return BBcode string without temporary replacements */ private String removeTemporaryReplacementSubstitutionFromDecodedText(String bbDecodedText, List<Boolean> replaceHistoryList) { int index = 0; Pattern pattern = Pattern.compile(CODE_JAVA_BBCODE_END_REPLACEMENT_PATTERN); Matcher matcher = pattern.matcher(bbDecodedText); StringBuffer sb = new StringBuffer(); try { while (matcher.find()) { if (replaceHistoryList.get(index)) { matcher.appendReplacement(sb, TopicModificationService.CODE_JAVA_BBCODE_END); } index++; } matcher.appendTail(sb); return sb.toString(); } catch (IndexOutOfBoundsException e) { logger.warn("BbCodeReviewProcessor called, but target decoded text \"" + bbDecodedText + "\" doesn't contain " + replaceHistoryList.size() + " expected temporary replacement elements " + CODE_JAVA_BBCODE_END_REPLACEMENT); return bbDecodedText; } } }