/**
* 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.transactional;
import org.joda.time.DateTime;
import org.jtalks.common.model.dao.Crud;
import org.jtalks.common.model.permissions.BranchPermission;
import org.jtalks.jcommune.model.dao.PostDao;
import org.jtalks.jcommune.model.dao.TopicDao;
import org.jtalks.jcommune.model.dto.PageRequest;
import org.jtalks.jcommune.model.entity.*;
import org.jtalks.jcommune.plugin.api.PluginLoader;
import org.jtalks.jcommune.plugin.api.core.Plugin;
import org.jtalks.jcommune.plugin.api.core.TopicPlugin;
import org.jtalks.jcommune.plugin.api.exceptions.NotFoundException;
import org.jtalks.jcommune.plugin.api.filters.StateFilter;
import org.jtalks.jcommune.plugin.api.filters.TypeFilter;
import org.jtalks.jcommune.plugin.api.service.PluginPostService;
import org.jtalks.jcommune.service.BranchLastPostService;
import org.jtalks.jcommune.service.PostService;
import org.jtalks.jcommune.service.UserService;
import org.jtalks.jcommune.service.nontransactional.NotificationService;
import org.jtalks.jcommune.service.security.acl.AclClassName;
import org.jtalks.jcommune.service.security.PermissionService;
import org.jtalks.jcommune.service.security.SecurityService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageImpl;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.access.prepost.PreAuthorize;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
/**
* Post service class. This class contains method needed to manipulate with Post persistent entity.
*
* @author Osadchuck Eugeny
* @author Anuar Nurmakanov
*/
public class TransactionalPostService extends AbstractTransactionalEntityService<Post, PostDao>
implements PostService, PluginPostService {
private final Logger logger = LoggerFactory.getLogger(getClass());
private TopicDao topicDao;
private SecurityService securityService;
private NotificationService notificationService;
private UserService userService;
private BranchLastPostService branchLastPostService;
private PermissionService permissionService;
private PluginLoader pluginLoader;
private Crud<PostDraft> postDraftDao;
/**
* Create an instance of Post entity based service
*
* @param dao data access object, which should be able do all CRUD operations with post entity.
* @param topicDao this dao used for checking branch existance
* @param securityService service for authorization
* @param notificationService to send email updates for subscribed users
* @param userService to get current user
* @param branchLastPostService to refresh the last post of the branch
* @param permissionService service for cheking permissions
* @param pluginLoader loader of pluinf
* @param postDraftDao data access object for manipulating with drafts
*/
public TransactionalPostService(
PostDao dao,
TopicDao topicDao,
SecurityService securityService,
NotificationService notificationService,
UserService userService,
BranchLastPostService branchLastPostService,
PermissionService permissionService,
PluginLoader pluginLoader,
Crud<PostDraft> postDraftDao) {
super(dao);
this.topicDao = topicDao;
this.securityService = securityService;
this.notificationService = notificationService;
this.userService = userService;
this.branchLastPostService = branchLastPostService;
this.permissionService = permissionService;
this.pluginLoader = pluginLoader;
this.postDraftDao = postDraftDao;
}
/**
* Performs update with security checking.
*
* @param post an instance of post, that will be updated
* @param postContent new content of the post
* @throws AccessDeniedException if user tries to update the first post of code review which should be impossible,
* see <a href="http://jtalks.org/display/jcommune/1.1+Larks">Requirements</a>
* for details
*/
@PreAuthorize("(hasPermission(#post.id, 'POST', 'GeneralPermission.WRITE') and " +
"hasPermission(#post.topic.branch.id, 'BRANCH', 'BranchPermission.EDIT_OWN_POSTS')) or " +
"(not hasPermission(#post.id, 'POST', 'GeneralPermission.WRITE') and " +
"hasPermission(#post.topic.branch.id, 'BRANCH', 'BranchPermission.EDIT_OTHERS_POSTS'))")
@Override
public void updatePost(Post post, String postContent) {
Topic postTopic = post.getTopic();
if (postTopic.isCodeReview() && postTopic.getPosts().get(0).getId() == post.getId()) {
throw new AccessDeniedException("It is impossible to edit code review!");
}
post.setPostContent(postContent);
post.updateModificationDate();
this.getDao().saveOrUpdate(post);
userService.notifyAndMarkNewlyMentionedUsers(post);
logger.debug("Post id={} updated.", post.getId());
}
/**
* {@inheritDoc}
*/
@Override
@PreAuthorize("(hasPermission(#post.topic.branch.id, 'BRANCH', 'BranchPermission.DELETE_OWN_POSTS') and " +
"#post.userCreated.username == principal.username) or " +
"(hasPermission(#post.topic.branch.id, 'BRANCH', 'BranchPermission.DELETE_OTHERS_POSTS') and " +
"#post.userCreated.username != principal.username)")
public void deletePost(Post post) {
JCUser postCreator = post.getUserCreated();
postCreator.setPostCount(postCreator.getPostCount() - 1);
Topic topic = post.getTopic();
topic.removePost(post);
Branch branch = topic.getBranch();
boolean deletedPostIsLastPostInBranch = branch.isLastPost(post);
if (deletedPostIsLastPostInBranch) {
branch.clearLastPost();
}
// todo: event API?
topicDao.saveOrUpdate(topic);
securityService.deleteFromAcl(post);
/*
only the creator of the post should be notified when it's removed.
*/
notificationService.subscribedEntityChanged(post);
if (deletedPostIsLastPostInBranch) {
branchLastPostService.refreshLastPostInBranch(branch);
}
logger.debug("Deleted post id={}", post.getId());
}
@Override
@PreAuthorize("hasPermission(#topic.branch.id, 'BRANCH', 'BranchPermission.CREATE_POSTS')")
public PostDraft saveOrUpdateDraft(Topic topic, String content) {
JCUser currentUser = userService.getCurrentUser();
PostDraft draft = topic.getDraftForUser(currentUser);
if (draft == null) {
draft = new PostDraft(content, currentUser);
topic.addDraft(draft);
} else {
draft.setContent(content);
draft.updateLastSavedTime();
}
topicDao.saveOrUpdate(topic);
logger.debug("Draft saved in topic. Topic id={}, Post id={}, Post author={}",
new Object[]{topic.getId(), draft.getId(), currentUser.getUsername()});
return draft;
}
/**
* {@inheritDoc}
*/
@Override
public Page<Post> getPostsOfUser(JCUser userCreated, String page) {
JCUser currentUser = userService.getCurrentUser();
List<Long> allowedBranchesIds = topicDao.getAllowedBranchesIds(currentUser);
PageRequest pageRequest = new PageRequest(page, currentUser.getPageSize());
if (allowedBranchesIds.isEmpty()) {
return new PageImpl<>(Collections.<Post>emptyList());
} else {
return this.getDao().getUserPosts(userCreated, pageRequest, allowedBranchesIds);
}
}
/**
* {@inheritDoc}
*/
@Override
public int calculatePageForPost(Post post) {
Topic topic = post.getTopic();
int index = topic.getPosts().indexOf(post) + 1;
int pageSize = userService.getCurrentUser().getPageSize();
int pageNum = index / pageSize;
if (index % pageSize == 0) {
return pageNum;
} else {
return pageNum + 1;
}
}
/**
* {@inheritDoc}
*/
@Override
public Page<Post> getPosts(Topic topic, String page) {
PageRequest pageRequest = new PageRequest(page, userService.getCurrentUser().getPageSize());
return getDao().getPosts(topic, pageRequest);
}
/**
* {@inheritDoc}
*/
@Override
public Post getLastPostFor(Branch branch) {
return getDao().getLastPostFor(branch);
}
/**
* {@inheritDoc}
*/
@Override
public List<Post> getLastPostsFor(Branch branch, int postCount) {
return getDao().getLastPostsFor(Arrays.asList(branch.getId()), postCount);
}
/**
* {@inheritDoc}
*/
@Override
public PostComment addComment(Long postId, Map<String, String> attributes, String body) throws NotFoundException {
Post targetPost = get(postId);
JCUser currentUser = userService.getCurrentUser();
assertCommentAllowed(targetPost.getTopic());
PostComment comment = new PostComment();
comment.putAllAttributes(attributes);
comment.setBody(body);
comment.setCreationDate(new DateTime(System.currentTimeMillis()));
comment.setAuthor(currentUser);
if (currentUser.isAutosubscribe()) {
targetPost.getTopic().getSubscribers().add(currentUser);
}
targetPost.addComment(comment);
getDao().saveOrUpdate(targetPost);
/**
* Notify subscribers of topic if comment added
*/
notificationService.subscribedEntityChanged(targetPost.getTopic());
return comment;
}
/**
* Checks permissions before deleting comment
*
* {@inheritDoc}
*/
@PreAuthorize("(hasPermission(#post.topic.branch.id, 'BRANCH', 'BranchPermission.DELETE_OWN_POSTS') and " +
"#comment.author.username == principal.username) or " +
"(hasPermission(#post.topic.branch.id, 'BRANCH', 'BranchPermission.DELETE_OTHERS_POSTS') and " +
"#comment.author.username != principal.username)")
public void deleteComment(Post post, PostComment comment) {
post.getComments().remove(comment);
getDao().saveOrUpdate(post);
}
/**
* {@inheritDoc}
*/
@Override
@PreAuthorize("hasPermission(#post.topic.branch.id, 'BRANCH', 'BranchPermission.CREATE_POSTS') " +
"and #post.userCreated.username != principal.username")
public Post vote(Post post, PostVote vote) {
JCUser currentUser = userService.getCurrentUser();
if (!post.canBeVotedBy(currentUser, vote.isVotedUp())) {
logger.info("User [{}] tries to vote for post with id={} in same direction more than one time",
currentUser.getUsername(), post.getId());
throw new AccessDeniedException("User can't vote in same direction more than one time");
}
vote.setUser(currentUser);
int ratingChanges = post.calculateRatingChanges(vote);
post.putVote(vote);
getDao().saveOrUpdate(post);
getDao().changeRating(post.getId(), ratingChanges);
return post;
}
/**
* {@inheritDoc}
*/
@Override
public void deleteDraft(Long draftId) throws NotFoundException{
if (!postDraftDao.isExist(draftId)) {
throw new NotFoundException("Draft with id=" + draftId + " not found");
}
PostDraft draft = postDraftDao.get(draftId);
if (!draft.getAuthor().equals(userService.getCurrentUser())) {
throw new AccessDeniedException("Only author can delete draft");
}
Topic topic = draft.getTopic();
topic.getDrafts().remove(draft);
topicDao.saveOrUpdate(topic);
logger.debug("Deleted draft id={}", draft.getId());
}
/**
* Checks if current user can create comments in specified topic
*
* @param topic topic to check permission
* @throws AccessDeniedException if user can't create comments in specified topic
*/
private void assertCommentAllowed(Topic topic) {
if (topic.isCodeReview()) {
permissionService.checkPermission(topic.getBranch().getId(), AclClassName.BRANCH,
BranchPermission.LEAVE_COMMENTS_IN_CODE_REVIEW);
} else if (topic.isPlugable()) {
assertCommentsAllowedForPlugableTopic(topic);
} else {
throw new AccessDeniedException("Adding comments not allowed for core topic types");
}
if (topic.isClosed()) {
permissionService.checkPermission(topic.getBranch().getId(), AclClassName.BRANCH,
BranchPermission.CLOSE_TOPICS);
}
}
/**
* Checks if current user can create comments in specified plugable topic
*
* @param topic plugable topic to check permission
* @throws AccessDeniedException if user not granted to create comments in specified topic type or if type of
* current topic is unknown
*/
private void assertCommentsAllowedForPlugableTopic(Topic topic) {
List<Plugin> topicPlugins = pluginLoader.getPlugins(new TypeFilter(TopicPlugin.class),
new StateFilter(Plugin.State.ENABLED));
boolean pluginFound = false;
for (Plugin plugin : topicPlugins) {
TopicPlugin topicPlugin = (TopicPlugin)plugin;
if (topicPlugin.getTopicType().equals(topic.getType())) {
pluginFound = true;
permissionService.checkPermission(topic.getBranch().getId(), AclClassName.BRANCH,
topicPlugin.getCommentPermission());
break;
}
}
if (!pluginFound) {
throw new AccessDeniedException("Creation of comments not allowed for unknown topic type");
}
}
}