/* * Copyright (C) 2005-2013 Alfresco Software Limited. * * This file is part of Alfresco * * Alfresco 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 3 of the License, or * (at your option) any later version. * * Alfresco 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 Alfresco. If not, see . */ package org.alfresco.repo.forum; import java.io.Serializable; import java.io.UnsupportedEncodingException; import java.net.URLEncoder; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import org.alfresco.model.ContentModel; import org.alfresco.model.ForumModel; import org.alfresco.query.CannedQueryFactory; import org.alfresco.query.CannedQueryResults; import org.alfresco.query.EmptyPagingResults; import org.alfresco.query.PagingRequest; import org.alfresco.query.PagingResults; import org.alfresco.repo.activities.ActivityType; import org.alfresco.repo.content.MimetypeMap; import org.alfresco.repo.node.getchildren.GetChildrenCannedQuery; import org.alfresco.repo.node.getchildren.GetChildrenCannedQueryFactory; import org.alfresco.repo.security.authentication.AuthenticationUtil; import org.alfresco.repo.security.authentication.AuthenticationUtil.RunAsWork; import org.alfresco.service.cmr.activities.ActivityService; import org.alfresco.service.cmr.repository.ChildAssociationRef; import org.alfresco.service.cmr.repository.ContentData; import org.alfresco.service.cmr.repository.ContentService; import org.alfresco.service.cmr.repository.ContentWriter; import org.alfresco.service.cmr.repository.NodeRef; import org.alfresco.service.cmr.repository.NodeService; import org.alfresco.service.cmr.site.SiteService; import org.alfresco.service.namespace.NamespaceService; import org.alfresco.service.namespace.QName; import org.alfresco.util.Pair; import org.alfresco.util.registry.NamedObjectRegistry; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.json.simple.JSONObject; /** * @author Neil Mc Erlean * @since 4.0 */ // TODO consolidate this and ScriptCommentService and the implementations of comments.* web scripts. public class CommentServiceImpl implements CommentService { private static Log logger = LogFactory.getLog(CommentServiceImpl.class); /** * Naming convention for Share comment model. fm:forum contains fm:topic */ private static final QName FORUM_TO_TOPIC_ASSOC_QNAME = QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, "Comments"); private static final String COMMENTS_TOPIC_NAME = "Comments"; private static final String CANNED_QUERY_GET_CHILDREN = "commentsGetChildrenCannedQueryFactory"; // Injected services private NodeService nodeService; private ContentService contentService; private ActivityService activityService; private SiteService siteService; private NamedObjectRegistry> cannedQueryRegistry; public void setSiteService(SiteService siteService) { this.siteService = siteService; } public void setActivityService(ActivityService activityService) { this.activityService = activityService; } public void setNodeService(NodeService nodeService) { this.nodeService = nodeService; } public void setContentService(ContentService contentService) { this.contentService = contentService; } public void setCannedQueryRegistry(NamedObjectRegistry> cannedQueryRegistry) { this.cannedQueryRegistry = cannedQueryRegistry; } @Override public NodeRef getDiscussableAncestor(NodeRef descendantNodeRef) { // For "Share comments" i.e. fm:post nodes created via the Share commenting UI, the containment structure is as follows: // fm:discussable // - fm:forum // - fm:topic // - fm:post // For other fm:post nodes the ancestor structure may be slightly different. (cf. Share discussions, which don't have 'forum') // // In order to ensure that we only return the discussable ancestor relevant to Share comments, we'll climb the // containment tree in a controlled manner. // We could navigate up looking for the first fm:discussable ancestor, but that might not find the correct node - it could, // for example, find a parent folder which was discussable. NodeRef result = null; if (nodeService.getType(descendantNodeRef).equals(ForumModel.TYPE_POST)) { NodeRef topicNode = nodeService.getPrimaryParent(descendantNodeRef).getParentRef(); if (nodeService.getType(topicNode).equals(ForumModel.TYPE_TOPIC)) { NodeRef forumNode = nodeService.getPrimaryParent(topicNode).getParentRef(); if (nodeService.getType(forumNode).equals(ForumModel.TYPE_FORUM)) { NodeRef discussableNode = nodeService.getPrimaryParent(forumNode).getParentRef(); if (nodeService.hasAspect(discussableNode, ForumModel.ASPECT_DISCUSSABLE)) { result = discussableNode; } } } } return result; } @Override public PagingResults listComments(NodeRef discussableNode, PagingRequest paging) { NodeRef commentsFolder = getShareCommentsTopic(discussableNode); if(commentsFolder != null) { List> sort = new ArrayList>(); sort.add(new Pair(ContentModel.PROP_CREATED, false)); // Run the canned query GetChildrenCannedQueryFactory getChildrenCannedQueryFactory = (GetChildrenCannedQueryFactory)cannedQueryRegistry.getNamedObject(CANNED_QUERY_GET_CHILDREN); GetChildrenCannedQuery cq = (GetChildrenCannedQuery)getChildrenCannedQueryFactory.getCannedQuery(commentsFolder, null, null, null, null, sort, paging); // Execute the canned query CannedQueryResults results = cq.execute(); return results; } else { return new EmptyPagingResults(); } } @Override public NodeRef getShareCommentsTopic(NodeRef discussableNode) { NodeRef result = null; if (nodeService.hasAspect(discussableNode, ForumModel.ASPECT_DISCUSSABLE)) { // We navigate down the "Share comments" containment model, which is based on the more general forum model, // but with certain naming conventions. List fora = nodeService.getChildAssocs(discussableNode, ForumModel.ASSOC_DISCUSSION, ForumModel.ASSOC_DISCUSSION, true); // There should only be one such assoc. if ( !fora.isEmpty()) { final NodeRef firstForumNode = fora.get(0).getChildRef(); List topics = nodeService.getChildAssocs(firstForumNode, ContentModel.ASSOC_CONTAINS, FORUM_TO_TOPIC_ASSOC_QNAME, true); // Likewise, only one. if ( !topics.isEmpty()) { final NodeRef firstTopicNode = topics.get(0).getChildRef(); result = firstTopicNode; } } } return result; } // private ScriptNode createCommentsFolder(ScriptNode node) // { // final NodeRef nodeRef = node.getNodeRef(); // // NodeRef commentsFolder = AuthenticationUtil.runAs(new AuthenticationUtil.RunAsWork() // { // public NodeRef doWork() throws Exception // { // NodeRef commentsFolder = null; // // // ALF-5240: turn off auditing round the discussion node creation to prevent // // the source document from being modified by the first user leaving a comment // behaviourFilter.disableBehaviour(nodeRef, ContentModel.ASPECT_AUDITABLE); // // try // { // nodeService.addAspect(nodeRef, QName.createQName(NamespaceService.FORUMS_MODEL_1_0_URI, "discussable"), null); // List assocs = nodeService.getChildAssocs(nodeRef, QName.createQName(NamespaceService.FORUMS_MODEL_1_0_URI, "discussion"), RegexQNamePattern.MATCH_ALL); // if (assocs.size() != 0) // { // NodeRef forumFolder = assocs.get(0).getChildRef(); // // Map props = new HashMap(1, 1.0f); // props.put(ContentModel.PROP_NAME, COMMENTS_TOPIC_NAME); // commentsFolder = nodeService.createNode( // forumFolder, // ContentModel.ASSOC_CONTAINS, // QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, COMMENTS_TOPIC_NAME), // QName.createQName(NamespaceService.FORUMS_MODEL_1_0_URI, "topic"), // props).getChildRef(); // } // } // finally // { // behaviourFilter.enableBehaviour(nodeRef, ContentModel.ASPECT_AUDITABLE); // } // // return commentsFolder; // } // // }, AuthenticationUtil.getSystemUserName()); // // return new ScriptNode(commentsFolder, serviceRegistry, getScope()); // } private String getSiteId(final NodeRef nodeRef) { String siteId = AuthenticationUtil.runAsSystem(new RunAsWork() { @Override public String doWork() throws Exception { return siteService.getSiteShortName(nodeRef); } }); return siteId; } @SuppressWarnings("unchecked") private JSONObject getActivityData(String siteId, final NodeRef nodeRef) { if(siteId != null) { // create an activity (with some Share-specific parts) JSONObject json = new JSONObject(); json.put("title", nodeService.getProperty(nodeRef, ContentModel.PROP_NAME)); try { StringBuilder sb = new StringBuilder("document-details?nodeRef="); sb.append(URLEncoder.encode(nodeRef.toString(), "UTF-8")); json.put("page", sb.toString()); // MNT-11667 "createComment" method creates activity for users who are not supposed to see the file json.put("nodeRef", nodeRef.toString()); } catch (UnsupportedEncodingException e) { logger.warn("Unable to urlencode page for create comment activity"); } return json; } else { logger.warn("Unable to determine site in which node " + nodeRef + " resides."); return null; } } private void postActivity(String siteId, String activityType, JSONObject activityData) { if(activityData != null) { activityService.postActivity(activityType, siteId, "comments", activityData.toString()); } } @Override public NodeRef createComment(final NodeRef discussableNode, String title, String comment, boolean suppressRollups) { if(comment == null) { throw new IllegalArgumentException("Must provide a non-null comment"); } // There is no CommentService, so we have to create the node structure by hand. // This is what happens within e.g. comment.put.json.js when comments are submitted via the REST API. if (!nodeService.hasAspect(discussableNode, ForumModel.ASPECT_DISCUSSABLE)) { nodeService.addAspect(discussableNode, ForumModel.ASPECT_DISCUSSABLE, null); } if (!nodeService.hasAspect(discussableNode, ForumModel.ASPECT_COMMENTS_ROLLUP) && !suppressRollups) { nodeService.addAspect(discussableNode, ForumModel.ASPECT_COMMENTS_ROLLUP, null); } // Forum node is created automatically by DiscussableAspect behaviour. NodeRef forumNode = nodeService.getChildAssocs(discussableNode, ForumModel.ASSOC_DISCUSSION, QName.createQName(NamespaceService.FORUMS_MODEL_1_0_URI, "discussion")).get(0).getChildRef(); final List existingTopics = nodeService.getChildAssocs(forumNode, ContentModel.ASSOC_CONTAINS, QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, "Comments")); NodeRef topicNode = null; if (existingTopics.isEmpty()) { Map props = new HashMap(1, 1.0f); props.put(ContentModel.PROP_NAME, COMMENTS_TOPIC_NAME); topicNode = nodeService.createNode(forumNode, ContentModel.ASSOC_CONTAINS, QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, "Comments"), ForumModel.TYPE_TOPIC, props).getChildRef(); } else { topicNode = existingTopics.get(0).getChildRef(); } NodeRef postNode = nodeService.createNode(topicNode, ContentModel.ASSOC_CONTAINS, ContentModel.ASSOC_CONTAINS, ForumModel.TYPE_POST).getChildRef(); nodeService.setProperty(postNode, ContentModel.PROP_CONTENT, new ContentData(null, MimetypeMap.MIMETYPE_TEXT_PLAIN, 0L, null)); nodeService.setProperty(postNode, ContentModel.PROP_TITLE, title); ContentWriter writer = contentService.getWriter(postNode, ContentModel.PROP_CONTENT, true); writer.setMimetype(MimetypeMap.MIMETYPE_HTML); writer.setEncoding("UTF-8"); writer.putContent(comment); // determine the siteId and activity data of the comment NodeRef String siteId = getSiteId(discussableNode); JSONObject activityData = getActivityData(siteId, discussableNode); postActivity(siteId, ActivityType.COMMENT_CREATED, activityData); return postNode; } public void updateComment(NodeRef commentNodeRef, String title, String comment) { QName nodeType = nodeService.getType(commentNodeRef); if(!nodeType.equals(ForumModel.TYPE_POST)) { throw new IllegalArgumentException("Node to update is not a comment node."); } ContentWriter writer = contentService.getWriter(commentNodeRef, ContentModel.PROP_CONTENT, true); writer.setMimetype(MimetypeMap.MIMETYPE_HTML); // TODO should this be set by the caller? writer.putContent(comment); if(title != null) { nodeService.setProperty(commentNodeRef, ContentModel.PROP_TITLE, title); } // determine the siteId and activity data of the comment NodeRef String siteId = getSiteId(commentNodeRef); NodeRef discussableNodeRef = getDiscussableAncestor(commentNodeRef); if(discussableNodeRef != null) { JSONObject activityData = getActivityData(siteId, discussableNodeRef); postActivity(siteId, "org.alfresco.comments.comment-updated", activityData); } else { logger.warn("Unable to determine discussable node for the comment with nodeRef " + commentNodeRef + ", not posting an activity"); } } public void deleteComment(NodeRef commentNodeRef) { QName nodeType = nodeService.getType(commentNodeRef); if(!nodeType.equals(ForumModel.TYPE_POST)) { throw new IllegalArgumentException("Node to delete is not a comment node."); } // determine the siteId and activity data of the comment NodeRef (do this before removing the comment NodeRef) String siteId = getSiteId(commentNodeRef); NodeRef discussableNodeRef = getDiscussableAncestor(commentNodeRef); JSONObject activityData = null; if(discussableNodeRef != null) { activityData = getActivityData(siteId, discussableNodeRef); } nodeService.deleteNode(commentNodeRef); if(activityData != null) { postActivity(siteId, "org.alfresco.comments.comment-deleted", activityData); } else { logger.warn("Unable to determine discussable node for the comment with nodeRef " + commentNodeRef + ", not posting an activity"); } } }