Java tutorial
/* * #%L * Alfresco Repository * %% * Copyright (C) 2005 - 2016 Alfresco Software Limited * %% * This file is part of the Alfresco software. * If the software was purchased under a paid Alfresco license, the terms of * the paid license agreement will prevail. Otherwise, the software is * provided under the following open source license terms: * * 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 <http://www.gnu.org/licenses/>. * #L% */ 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 java.util.Set; 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.NodeServicePolicies; import org.alfresco.repo.node.getchildren.GetChildrenCannedQuery; import org.alfresco.repo.node.getchildren.GetChildrenCannedQueryFactory; import org.alfresco.repo.policy.BehaviourFilter; import org.alfresco.repo.policy.JavaBehaviour; import org.alfresco.repo.policy.PolicyComponent; import org.alfresco.repo.security.authentication.AuthenticationUtil; import org.alfresco.repo.security.authentication.AuthenticationUtil.RunAsWork; import org.alfresco.repo.security.permissions.AccessDeniedException; import org.alfresco.repo.site.SiteModel; import org.alfresco.service.cmr.activities.ActivityService; import org.alfresco.service.cmr.dictionary.DictionaryService; import org.alfresco.service.cmr.dictionary.PropertyDefinition; import org.alfresco.service.cmr.lock.LockService; import org.alfresco.service.cmr.lock.LockStatus; import org.alfresco.service.cmr.lock.NodeLockedException; import org.alfresco.service.cmr.repository.ChildAssociationRef; import org.alfresco.service.cmr.repository.ContentData; import org.alfresco.service.cmr.repository.ContentIOException; 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.security.AccessStatus; import org.alfresco.service.cmr.security.PermissionService; import org.alfresco.service.cmr.site.SiteService; import org.alfresco.service.namespace.NamespaceService; import org.alfresco.service.namespace.QName; import org.alfresco.util.EqualsHelper; 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; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationEvent; import org.springframework.extensions.surf.util.AbstractLifecycleBean; /** * @author Neil Mc Erlean * @since 4.0 */ // TODO consolidate this and ScriptCommentService and the implementations of comments.* web scripts. public class CommentServiceImpl extends AbstractLifecycleBean implements CommentService, NodeServicePolicies.BeforeDeleteNodePolicy, NodeServicePolicies.OnUpdatePropertiesPolicy { 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 PolicyComponent policyComponent; private BehaviourFilter behaviourFilter; private PermissionService permissionService; private LockService lockService; private DictionaryService dictionaryService; private NamedObjectRegistry<CannedQueryFactory<? extends Object>> 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<CannedQueryFactory<? extends Object>> cannedQueryRegistry) { this.cannedQueryRegistry = cannedQueryRegistry; } public void setPolicyComponent(PolicyComponent policyComponent) { this.policyComponent = policyComponent; } public void setBehaviourFilter(BehaviourFilter behaviourFilter) { this.behaviourFilter = behaviourFilter; } public void setPermissionService(PermissionService permissionService) { this.permissionService = permissionService; } public void setLockService(LockService lockService) { this.lockService = lockService; } public void setDictionaryService(DictionaryService dictionaryService) { this.dictionaryService = dictionaryService; } @Override protected void onBootstrap(ApplicationEvent event) { // belts-and-braces (in case of broken spring-bean override) ApplicationContext ctx = getApplicationContext(); if (ctx != null) { if (this.nodeService == null) { this.nodeService = (NodeService) ctx.getBean("NodeService"); } if (this.contentService == null) { this.contentService = (ContentService) ctx.getBean("ContentService"); } if (this.siteService == null) { this.siteService = (SiteService) ctx.getBean("SiteService"); } if (this.activityService == null) { this.activityService = (ActivityService) ctx.getBean("activityService"); } if (this.cannedQueryRegistry == null) { this.cannedQueryRegistry = (NamedObjectRegistry<CannedQueryFactory<? extends Object>>) ctx .getBean("commentsCannedQueryRegistry"); } if (this.policyComponent == null) { this.policyComponent = (PolicyComponent) ctx.getBean("policyComponent"); } if (this.behaviourFilter == null) { this.behaviourFilter = (BehaviourFilter) ctx.getBean("policyBehaviourFilter"); } if (this.permissionService == null) { this.permissionService = (PermissionService) ctx.getBean("PermissionService"); } if (this.lockService == null) { this.lockService = (LockService) ctx.getBean("LockService"); } if (this.dictionaryService == null) { this.dictionaryService = (DictionaryService) ctx.getBean("DictionaryService"); } } this.policyComponent.bindClassBehaviour(NodeServicePolicies.OnUpdatePropertiesPolicy.QNAME, ForumModel.TYPE_POST, new JavaBehaviour(this, "onUpdateProperties")); this.policyComponent.bindClassBehaviour(NodeServicePolicies.BeforeDeleteNodePolicy.QNAME, ForumModel.TYPE_POST, new JavaBehaviour(this, "beforeDeleteNode")); } @Override protected void onShutdown(ApplicationEvent event) { } @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)) { ChildAssociationRef forumToTopicChildAssocRef = nodeService.getPrimaryParent(topicNode); if (forumToTopicChildAssocRef.getQName().equals(FORUM_TO_TOPIC_ASSOC_QNAME)) { NodeRef forumNode = forumToTopicChildAssocRef.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<NodeRef> listComments(NodeRef discussableNode, PagingRequest paging) { NodeRef commentsFolder = getShareCommentsTopic(discussableNode); if (commentsFolder != null) { List<Pair<QName, Boolean>> sort = new ArrayList<Pair<QName, Boolean>>(); sort.add(new Pair<QName, Boolean>(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<NodeRef> results = cq.execute(); return results; } else { return new EmptyPagingResults<NodeRef>(); } } @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<ChildAssociationRef> 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<ChildAssociationRef> 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<NodeRef>() // { // 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<ChildAssociationRef> 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<QName, Serializable> props = new HashMap<QName, Serializable>(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<String>() { @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<ChildAssociationRef> existingTopics = nodeService.getChildAssocs(forumNode, ContentModel.ASSOC_CONTAINS, FORUM_TO_TOPIC_ASSOC_QNAME); NodeRef topicNode = null; if (existingTopics.isEmpty()) { Map<QName, Serializable> props = new HashMap<QName, Serializable>(1, 1.0f); props.put(ContentModel.PROP_NAME, COMMENTS_TOPIC_NAME); topicNode = nodeService.createNode(forumNode, ContentModel.ASSOC_CONTAINS, FORUM_TO_TOPIC_ASSOC_QNAME, 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."); } try { 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); } catch (ContentIOException cioe) { Throwable cause = cioe.getCause(); if (cause instanceof AccessDeniedException) { throw (AccessDeniedException) cause; } else if (cause instanceof NodeLockedException) { throw (NodeLockedException) cause; } else { throw cioe; } } 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"); } } // TODO review against ACE-5437 (eg. introduce CommentInfo as part of the CommentService) public Map<String, Boolean> getCommentPermissions(NodeRef discussableNode, NodeRef commentNodeRef) { boolean canEdit = false; boolean canDelete = false; // belts-and-braces NodeRef discussableNodeRef = getDiscussableAncestor(commentNodeRef); if (discussableNodeRef.equals(discussableNode)) { if (!isWorkingCopyOrLocked(discussableNode)) { canEdit = canEditPermission(commentNodeRef); canDelete = canDeletePermission(commentNodeRef); } } Map<String, Boolean> map = new HashMap<>(2); map.put(CommentService.CAN_EDIT, canEdit); map.put(CommentService.CAN_DELETE, canDelete); return map; } private boolean canEditPermission(NodeRef commentNodeRef) { String creator = (String) nodeService.getProperty(commentNodeRef, ContentModel.PROP_CREATOR); Serializable owner = nodeService.getProperty(commentNodeRef, ContentModel.PROP_OWNER); String currentUser = AuthenticationUtil.getFullyAuthenticatedUser(); boolean isSiteManager = permissionService.hasPermission(commentNodeRef, SiteModel.SITE_MANAGER) == (AccessStatus.ALLOWED); boolean isCoordinator = permissionService.hasPermission(commentNodeRef, PermissionService.COORDINATOR) == (AccessStatus.ALLOWED); return (isSiteManager || isCoordinator || currentUser.equals(creator) || currentUser.equals(owner)); } private boolean canDeletePermission(NodeRef commentNodeRef) { return permissionService.hasPermission(commentNodeRef, PermissionService.DELETE) == AccessStatus.ALLOWED; } private boolean isWorkingCopyOrLocked(NodeRef nodeRef) { boolean isWorkingCopy = false; boolean isLocked = false; if (nodeRef != null) { Set<QName> aspects = nodeService.getAspects(nodeRef); isWorkingCopy = aspects.contains(ContentModel.ASPECT_WORKING_COPY); if (!isWorkingCopy) { if (aspects.contains(ContentModel.ASPECT_LOCKABLE)) { LockStatus lockStatus = lockService.getLockStatus(nodeRef); if (lockStatus == LockStatus.LOCKED || lockStatus == LockStatus.LOCK_OWNER) { isLocked = true; } } } } return (isWorkingCopy || isLocked); } @Override public void onUpdateProperties(NodeRef commentNodeRef, Map<QName, Serializable> before, Map<QName, Serializable> after) { NodeRef discussableNodeRef = getDiscussableAncestor(commentNodeRef); if (discussableNodeRef != null) { if (behaviourFilter.isEnabled(ContentModel.ASPECT_LOCKABLE) && isWorkingCopyOrLocked(discussableNodeRef)) { List<QName> changedProperties = getChangedProperties(before, after); // check if comment updated (rather than some other change, eg. addition of lockable aspect only) boolean commentUpdated = false; for (QName changedProperty : changedProperties) { PropertyDefinition propertyDef = dictionaryService.getProperty(changedProperty); if (propertyDef != null) { if (propertyDef.getContainerClass().getName().equals(ContentModel.TYPE_CONTENT)) { commentUpdated = true; break; } } } if (commentUpdated) { throw new NodeLockedException(discussableNodeRef); } } boolean canEdit = canEditPermission(commentNodeRef); if (!canEdit) { throw new AccessDeniedException("Cannot edit comment"); } } } // see also RenditionedAspect private List<QName> getChangedProperties(Map<QName, Serializable> before, Map<QName, Serializable> after) { List<QName> results = new ArrayList<QName>(); for (QName propQName : before.keySet()) { if (after.keySet().contains(propQName) == false) { // property was deleted results.add(propQName); } else { Serializable beforeValue = before.get(propQName); Serializable afterValue = after.get(propQName); if (EqualsHelper.nullSafeEquals(beforeValue, afterValue) == false) { // Property was changed results.add(propQName); } } } for (QName propQName : after.keySet()) { if (before.containsKey(propQName) == false) { // property was added results.add(propQName); } } return results; } @Override public void beforeDeleteNode(final NodeRef commentNodeRef) { NodeRef discussableNodeRef = getDiscussableAncestor(commentNodeRef); if (discussableNodeRef != null) { if (behaviourFilter.isEnabled(ContentModel.ASPECT_LOCKABLE) // eg. delete site (MNT-14671) && isWorkingCopyOrLocked(discussableNodeRef)) { throw new NodeLockedException(discussableNodeRef); } boolean canDelete = canDeletePermission(commentNodeRef); if (!canDelete) { throw new AccessDeniedException("Cannot delete comment"); } } } }