/** * 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.nontransactional; import org.apache.commons.collections.CollectionUtils; import org.apache.commons.lang.StringUtils; import org.jtalks.jcommune.model.dao.PostDao; import org.jtalks.jcommune.model.dao.UserDao; import org.jtalks.jcommune.model.entity.JCUser; import org.jtalks.jcommune.model.entity.Post; 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 javax.servlet.http.HttpServletRequest; import java.util.*; import java.util.regex.Matcher; import java.util.regex.Pattern; import static java.lang.String.format; /** * This service provides send email notifications to all mentioned users * in some components of forum:topics, posts. Also it provides an ability * to extract users mentioning from text. * * @author Anuar_Nurmakanov * @author Andrei Alikov */ public class MentionedUsers { public static final String MENTIONED_NOT_NOTIFIED_USER_TEMPLATE = "[user]%s[/user]"; public static final String MENTIONED_AND_NOTIFIED_USER_TEMPLATE = "[user notified=true]%s[/user]"; public static final String USER_WITH_LINK_TO_PROFILE_TEMPLATE = "[user=%s]%s[/user]"; private static final Logger LOGGER = LoggerFactory.getLogger(MentionedUsers.class); private static final Pattern ALL_MENTIONED_USERS_PATTERN = Pattern.compile("\\[user\\].+?(\\[/user\\])+|\\[user notified=true\\].+?(\\[/user\\])+"); private static final Pattern MENTIONED_AND_NOT_NOTIFIED_USERS_PATTERN = Pattern.compile("\\[user\\].+?(\\[/user\\])+"); private static final String CLOSE_BRACKET_CODE_PLACEHOLDER = "@w0956756wo@"; private static final String OPEN_BRACKET_CODE_PLACEHOLDER = "@ywdffgg434y@"; private static final String SLASH_CODE_PLACEHOLDER = "14@123435vggv4f"; private static final String LOWER_THEN_PLACEHOLDER = "gertfgertgf@@@@@#4324234"; private static final Map<String, String> CHARS_PLACEHOLDERS = new HashMap<>(); static { CHARS_PLACEHOLDERS.put("[", OPEN_BRACKET_CODE_PLACEHOLDER); CHARS_PLACEHOLDERS.put("]", CLOSE_BRACKET_CODE_PLACEHOLDER); CHARS_PLACEHOLDERS.put("\\", SLASH_CODE_PLACEHOLDER); CHARS_PLACEHOLDERS.put("<", LOWER_THEN_PLACEHOLDER); } private final Map<String, String> encodedUserNames = new HashMap<>(); /** * Content of the post */ private String postContent; /** * Post with mentioned users */ private Post post; private MentionedUsers(String postContent) { this.postContent = postContent; } private MentionedUsers(Post post) { this.post = post; this.postContent = post.getPostContent(); } /** * Creates new instance of MentionedUsers based on the Post data * * @param postContent content of the post where user was mentioned */ public static MentionedUsers parse(String postContent) { return new MentionedUsers(postContent); } /** * Creates new instance of MentionedUsers based on the Post data * * @param post the post where user was mentioned */ public static MentionedUsers parse(Post post) { return new MentionedUsers(post); } /** * Get list of the users which have to receive notification * * @param userDao service for user related operations * @return list of the users which should be notified that they were mentioned * @throws IllegalStateException when instance was not created based on Post object */ public List<JCUser> getNewUsersToNotify(UserDao userDao) { if (post == null) { throw new IllegalStateException("To call this method you should create class with Post type parameter"); } Set<String> mentionedUsersNames = extractNotNotifiedMentionedUsers(postContent); if (!CollectionUtils.isEmpty(mentionedUsersNames)) { return getNewUsersToNotify(mentionedUsersNames, userDao); } return new ArrayList<>(); } /** * Marks all users in user BB codes as already notified * * @param postDao service for post related operations * @throws IllegalStateException when instance was not created based on Post object */ public void markUsersAsAlreadyNotified(PostDao postDao) { if (post == null) { throw new IllegalStateException("To call this method you should create class with Post type parameter"); } Set<String> mentionedUsersNames = extractNotNotifiedMentionedUsers(postContent); if (!CollectionUtils.isEmpty(mentionedUsersNames)) { markUsersAsAlreadyNotified(mentionedUsersNames, postDao); } } /** * Returns post text with BB codes replaced by user profile links * * @param userDao service for working with user objects * @return text with BB codes replaced by user profile links */ public String getTextWithProcessedUserTags(UserDao userDao) { Set<String> mentionedUsers = extractAllMentionedUsers(postContent); Map<String, String> userToUserProfileLinkMap = new HashMap<>(); for (String mentionedUser : mentionedUsers) { String mentionedUserProfileLink = getLinkToUserProfile(mentionedUser, userDao); userToUserProfileLinkMap.put(mentionedUser, mentionedUserProfileLink); } return addLinksToUserProfileForMentionedUsers(postContent, userToUserProfileLinkMap); } /** * Extract names of all users that were mentioned in passed text. * * @return extracted users' names */ public Set<String> extractAllMentionedUsers(String canContainMentionedUsers) { return extractMentionedUsers(canContainMentionedUsers, ALL_MENTIONED_USERS_PATTERN); } /** * Extract names of users that were mentioned but not notified yet * * @return names of users that were mentioned but not notified yet */ private Set<String> extractNotNotifiedMentionedUsers(String canContainMentionedUsers) { return extractMentionedUsers(canContainMentionedUsers, MENTIONED_AND_NOT_NOTIFIED_USERS_PATTERN); } /** * Extract names of users that were mentioned in passed text. * * @param canContainMentionedUsers can contain users mentioning * @param mentionedUserPattern pattern to extract mentioned user in given text * @return extracted users' names */ private Set<String> extractMentionedUsers(String canContainMentionedUsers, Pattern mentionedUserPattern) { if (!StringUtils.isEmpty(canContainMentionedUsers)) { Matcher matcher = mentionedUserPattern.matcher(canContainMentionedUsers); Set<String> mentionedUsernames = new HashSet<>(); while (matcher.find()) { String userBBCode = matcher.group(); String mentionedUser; if (userBBCode.contains("[user notified=true]")) { mentionedUser = StringUtils.substring(userBBCode, 20, userBBCode.length() - 7); } else { mentionedUser = StringUtils.substring(userBBCode, 6, userBBCode.length() - 7); } mentionedUsernames.add(replacePlaceholdersWithChars(mentionedUser)); } return mentionedUsernames; } return Collections.emptySet(); } private String replacePlaceholdersWithChars(String userNameWithPlaceholders) { String formattedUserName = userNameWithPlaceholders; for (Map.Entry<String, String> decodeEntry : CHARS_PLACEHOLDERS.entrySet()) { formattedUserName = formattedUserName.replace(decodeEntry.getValue(), decodeEntry.getKey()); } encodedUserNames.put(formattedUserName, userNameWithPlaceholders); return formattedUserName; } private String encodeUsername(String decodedUsername) { return encodedUserNames.get(decodedUsername); } /** * Gets list of users which should be notified * * @param mentionedUsernames the set of names of mentioned users * @param userDao service for working with JCUser objects * @return list of users which should be notified */ private List<JCUser> getNewUsersToNotify(Set<String> mentionedUsernames, UserDao userDao) { List<JCUser> mentionedUsers = userDao.getByUsernames(mentionedUsernames); List<JCUser> usersToNotify = new ArrayList<>(); for (JCUser mentionedUser : mentionedUsers) { if (shouldNotificationBeSent(mentionedUser)) { usersToNotify.add(mentionedUser); } } return usersToNotify; } /** * Determines if it is needed to send notification to the user * * @param mentionedUser this user was mentioned * @return true if we need to send notification and false otherwise */ private boolean shouldNotificationBeSent(JCUser mentionedUser) { boolean isOtherNotificationAlreadySent = post.getSubscribers().contains(mentionedUser); return !isOtherNotificationAlreadySent && mentionedUser.isMentioningNotificationsEnabled(); } /** * Mark user tags as already notified * * @param mentionedUsernames the set of names of mentioned users * @param postDao service for working with Post objects */ private void markUsersAsAlreadyNotified(Set<String> mentionedUsernames, PostDao postDao) { for (String user : mentionedUsernames) { markUserAsAlreadyNotified(user, postDao); } } /** * Change BB user tag to mark user as already notified * * @param username this user was mentioned * @param postDao service for working with Post objects */ private void markUserAsAlreadyNotified(String username, PostDao postDao) { String initialUserMentioning = format(MENTIONED_NOT_NOTIFIED_USER_TEMPLATE, username); String notifiedUserMentioning = format(MENTIONED_AND_NOTIFIED_USER_TEMPLATE, username); String newPostContent = post.getPostContent().replace(initialUserMentioning, notifiedUserMentioning); post.setPostContent(newPostContent); postDao.saveOrUpdate(post); } /** * Get link to user's profile. * * @param username user's name * @return null when user doesn't exist, otherwise link to user's profile */ private String getLinkToUserProfile(String username, UserDao userDao) { String userPofileLink = null; JCUser user = userDao.getByUsername(username); if (user != null && user.getUsername().equals(username)) { userPofileLink = getApplicationNameAsContextPath() + "/users/" + user.getId(); LOGGER.trace("{} has the following url of profile - {}", username, userPofileLink); } else { LOGGER.trace("Mentioned user wasn't find: {}", username); } return userPofileLink; } /** * Get the name of application as context path. * * @return forum application name */ private String getApplicationNameAsContextPath() { RequestAttributes attributes = RequestContextHolder.currentRequestAttributes(); HttpServletRequest request = ((ServletRequestAttributes) attributes).getRequest(); return request.getContextPath(); } /** * Add links to users' profiles for mentioned users. * * @param source will be changed and all mentioned users in it will contain links to their * profiles * @param userToUserProfileLinkMap user to it links of profile map * @return source with users with attached links to profiles */ private String addLinksToUserProfileForMentionedUsers( String source, Map<String, String> userToUserProfileLinkMap) { String changedSource = source; for (Map.Entry<String, String> userToLinkMap : userToUserProfileLinkMap.entrySet()) { String username = encodeUsername(userToLinkMap.getKey()); String userNotNotifiedBBCode = format(MENTIONED_NOT_NOTIFIED_USER_TEMPLATE, username); String userNotifiedBBCode = format(MENTIONED_AND_NOTIFIED_USER_TEMPLATE, username); String userBBCodeWithLink = username; if (userToLinkMap.getValue() != null) { userBBCodeWithLink = format(USER_WITH_LINK_TO_PROFILE_TEMPLATE, userToLinkMap.getValue(), username); } changedSource = changedSource.replace(userNotNotifiedBBCode, userBBCodeWithLink); changedSource = changedSource.replace(userNotifiedBBCode, userBBCodeWithLink); } return changedSource; } }