/* * (C) Copyright 2007 Nuxeo SA (http://nuxeo.com/) and others. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * Contributors: * Nuxeo - initial API and implementation * * $Id$ */ package org.nuxeo.ecm.platform.comment.impl; import java.io.Serializable; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Calendar; import java.util.Collections; import java.util.Date; import java.util.HashMap; import java.util.List; import java.util.Map; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.nuxeo.ecm.core.api.CoreInstance; import org.nuxeo.ecm.core.api.CoreSession; import org.nuxeo.ecm.core.api.DocumentModel; import org.nuxeo.ecm.core.api.DocumentRef; import org.nuxeo.ecm.core.api.NuxeoException; import org.nuxeo.ecm.core.api.NuxeoPrincipal; import org.nuxeo.ecm.core.api.PathRef; import org.nuxeo.ecm.core.api.PropertyException; import org.nuxeo.ecm.core.api.pathsegment.PathSegmentService; import org.nuxeo.ecm.core.api.security.ACE; import org.nuxeo.ecm.core.api.security.ACL; import org.nuxeo.ecm.core.api.security.ACP; import org.nuxeo.ecm.core.api.security.SecurityConstants; import org.nuxeo.ecm.core.api.security.impl.ACLImpl; import org.nuxeo.ecm.core.api.security.impl.ACPImpl; import org.nuxeo.ecm.core.event.Event; import org.nuxeo.ecm.core.event.EventProducer; import org.nuxeo.ecm.core.event.impl.DocumentEventContext; import org.nuxeo.ecm.platform.comment.api.CommentConstants; import org.nuxeo.ecm.platform.comment.api.CommentConverter; import org.nuxeo.ecm.platform.comment.api.CommentEvents; import org.nuxeo.ecm.platform.comment.api.CommentManager; import org.nuxeo.ecm.platform.comment.service.CommentServiceConfig; import org.nuxeo.ecm.platform.comment.workflow.utils.CommentsConstants; import org.nuxeo.ecm.platform.relations.api.Graph; import org.nuxeo.ecm.platform.relations.api.RelationManager; import org.nuxeo.ecm.platform.relations.api.Resource; import org.nuxeo.ecm.platform.relations.api.ResourceAdapter; import org.nuxeo.ecm.platform.relations.api.Statement; import org.nuxeo.ecm.platform.relations.api.impl.QNameResourceImpl; import org.nuxeo.ecm.platform.relations.api.impl.ResourceImpl; import org.nuxeo.ecm.platform.relations.api.impl.StatementImpl; import org.nuxeo.ecm.platform.relations.jena.JenaGraph; import org.nuxeo.ecm.platform.usermanager.UserManager; import org.nuxeo.runtime.api.Framework; /** * @author <a href="mailto:glefter@nuxeo.com">George Lefter</a> */ public class CommentManagerImpl implements CommentManager { private static final Log log = LogFactory.getLog(CommentManagerImpl.class); final SimpleDateFormat timeFormat = new SimpleDateFormat("dd-HHmmss.S"); final SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM"); final CommentServiceConfig config; final CommentConverter commentConverter; public static final String COMMENTS_DIRECTORY = "Comments"; public CommentManagerImpl(CommentServiceConfig config) { this.config = config; commentConverter = config.getCommentConverter(); } public List<DocumentModel> getComments(DocumentModel docModel) { Map<String, Object> ctxMap = Collections.<String, Object> singletonMap( ResourceAdapter.CORE_SESSION_CONTEXT_KEY, docModel.getCoreSession()); RelationManager relationManager = Framework.getService(RelationManager.class); Graph graph = relationManager.getGraph(config.graphName, docModel.getCoreSession()); Resource docResource = relationManager.getResource(config.documentNamespace, docModel, ctxMap); if (docResource == null) { throw new NuxeoException("Could not adapt document model to relation resource ; " + "check the service relation adapters configuration"); } // FIXME AT: why no filter on the predicate? List<Statement> statementList = graph.getStatements(null, null, docResource); if (graph instanceof JenaGraph) { // XXX AT: BBB for when repository name was not included in the // resource uri Resource oldDocResource = new QNameResourceImpl(config.documentNamespace, docModel.getId()); statementList.addAll(graph.getStatements(null, null, oldDocResource)); } List<DocumentModel> commentList = new ArrayList<DocumentModel>(); for (Statement stmt : statementList) { QNameResourceImpl subject = (QNameResourceImpl) stmt.getSubject(); DocumentModel commentDocModel = (DocumentModel) relationManager.getResourceRepresentation( config.commentNamespace, subject, ctxMap); if (commentDocModel == null) { // XXX AT: maybe user cannot see the comment log.warn("Could not adapt comment relation subject to a document " + "model; check the service relation adapters configur ation"); continue; } commentList.add(commentDocModel); } CommentSorter sorter = new CommentSorter(true); Collections.sort(commentList, sorter); return commentList; } public DocumentModel createComment(DocumentModel docModel, String comment, String author) { try (CoreSession session = CoreInstance.openCoreSessionSystem(docModel.getRepositoryName())) { DocumentModel commentDM = session.createDocumentModel(CommentsConstants.COMMENT_DOC_TYPE); commentDM.setPropertyValue(CommentsConstants.COMMENT_TEXT, comment); commentDM.setPropertyValue(CommentsConstants.COMMENT_AUTHOR, author); commentDM.setPropertyValue(CommentsConstants.COMMENT_CREATION_DATE, Calendar.getInstance()); commentDM = internalCreateComment(session, docModel, commentDM, null); session.save(); return commentDM; } } public DocumentModel createComment(DocumentModel docModel, String comment) { String author = getCurrentUser(docModel); return createComment(docModel, comment, author); } /** * If the author property on comment is not set, retrieve the author name from the session * * @param docModel The document model that holds the session id * @param comment The comment to update */ private static String updateAuthor(DocumentModel docModel, DocumentModel comment) { // update the author if not set String author = (String) comment.getProperty("comment", "author"); if (author == null) { log.debug("deprecated use of createComment: the client should set the author property on document"); author = getCurrentUser(docModel); comment.setProperty("comment", "author", author); } return author; } public DocumentModel createComment(DocumentModel docModel, DocumentModel comment) { try (CoreSession session = CoreInstance.openCoreSessionSystem(docModel.getRepositoryName())) { DocumentModel doc = internalCreateComment(session, docModel, comment, null); session.save(); doc.detach(true); return doc; } } protected DocumentModel internalCreateComment(CoreSession session, DocumentModel docModel, DocumentModel comment, String path) { String author = updateAuthor(docModel, comment); DocumentModel createdComment; createdComment = createCommentDocModel(session, docModel, comment, path); RelationManager relationManager = Framework.getService(RelationManager.class); Resource commentRes = relationManager.getResource(config.commentNamespace, createdComment, null); Resource documentRes = relationManager.getResource(config.documentNamespace, docModel, null); if (commentRes == null || documentRes == null) { throw new NuxeoException("Could not adapt document model to relation resource ; " + "check the service relation adapters configuration"); } Resource predicateRes = new ResourceImpl(config.predicateNamespace); Statement stmt = new StatementImpl(commentRes, predicateRes, documentRes); relationManager.getGraph(config.graphName, session).add(stmt); UserManager userManager = Framework.getService(UserManager.class); if (userManager != null) { // null in tests NuxeoPrincipal principal = userManager.getPrincipal(author); if (principal != null) { notifyEvent(session, docModel, CommentEvents.COMMENT_ADDED, null, createdComment, principal); } } return createdComment; } private DocumentModel createCommentDocModel(CoreSession mySession, DocumentModel docModel, DocumentModel comment, String path) { String domainPath; updateAuthor(docModel, comment); String[] pathList = getCommentPathList(comment); if (path == null) { domainPath = "/" + docModel.getPath().segment(0); } else { domainPath = path; } if (mySession == null) { return null; } // TODO GR upgrade this code. It can't work if current user // doesn't have admin rights DocumentModel parent = mySession.getDocument(new PathRef(domainPath)); for (String name : pathList) { String pathStr = parent.getPathAsString(); PathRef ref = new PathRef(pathStr, name); if (mySession.exists(ref)) { parent = mySession.getDocument(ref); if (!parent.isFolder()) { throw new NuxeoException(parent.getPathAsString() + " is not folderish"); } } else { DocumentModel dm = mySession.createDocumentModel(pathStr, name, "HiddenFolder"); dm.setProperty("dublincore", "title", name); dm.setProperty("dublincore", "description", ""); dm.setProperty("dublincore", "created", Calendar.getInstance()); dm = mySession.createDocument(dm); setFolderPermissions(dm); parent = dm; } } String pathStr = parent.getPathAsString(); String commentName = getCommentName(docModel, comment); CommentConverter converter = config.getCommentConverter(); PathSegmentService pss = Framework.getService(PathSegmentService.class); DocumentModel commentDocModel = mySession.createDocumentModel(comment.getType()); commentDocModel.setProperty("dublincore", "title", commentName); converter.updateDocumentModel(commentDocModel, comment); commentDocModel.setPathInfo(pathStr, pss.generatePathSegment(commentDocModel)); commentDocModel = mySession.createDocument(commentDocModel); setCommentPermissions(commentDocModel); log.debug("created comment with id=" + commentDocModel.getId()); return commentDocModel; } private static void notifyEvent(CoreSession session, DocumentModel docModel, String eventType, DocumentModel parent, DocumentModel child, NuxeoPrincipal principal) { DocumentEventContext ctx = new DocumentEventContext(session, principal, docModel); Map<String, Serializable> props = new HashMap<String, Serializable>(); if (parent != null) { props.put(CommentConstants.PARENT_COMMENT, parent); } props.put(CommentConstants.COMMENT_DOCUMENT, child); props.put(CommentConstants.COMMENT, (String) child.getProperty("comment", "text")); // Keep comment_text for compatibility props.put(CommentConstants.COMMENT_TEXT, (String) child.getProperty("comment", "text")); props.put("category", CommentConstants.EVENT_COMMENT_CATEGORY); ctx.setProperties(props); Event event = ctx.newEvent(eventType); EventProducer evtProducer = Framework.getService(EventProducer.class); evtProducer.fireEvent(event); // send also a synchronous Seam message so the CommentManagerActionBean // can rebuild its list // Events.instance().raiseEvent(eventType, docModel); } private static void setFolderPermissions(DocumentModel dm) { ACP acp = new ACPImpl(); ACE grantAddChildren = new ACE("members", SecurityConstants.ADD_CHILDREN, true); ACE grantRemoveChildren = new ACE("members", SecurityConstants.REMOVE_CHILDREN, true); ACE grantRemove = new ACE("members", SecurityConstants.REMOVE, true); ACL acl = new ACLImpl(); acl.setACEs(new ACE[] { grantAddChildren, grantRemoveChildren, grantRemove }); acp.addACL(acl); dm.setACP(acp, true); } private static void setCommentPermissions(DocumentModel dm) { ACP acp = new ACPImpl(); ACE grantRead = new ACE(SecurityConstants.EVERYONE, SecurityConstants.READ, true); ACE grantRemove = new ACE("members", SecurityConstants.REMOVE, true); ACL acl = new ACLImpl(); acl.setACEs(new ACE[] { grantRead, grantRemove }); acp.addACL(acl); dm.setACP(acp, true); } private String[] getCommentPathList(DocumentModel comment) { String[] pathList = new String[2]; pathList[0] = COMMENTS_DIRECTORY; pathList[1] = dateFormat.format(getCommentTimeStamp(comment)); return pathList; } /** * @deprecated if the caller is remote, we cannot obtain the session */ @Deprecated private static String getCurrentUser(DocumentModel target) { CoreSession userSession = target.getCoreSession(); if (userSession == null) { throw new NuxeoException("userSession is null, do not invoke this method when the user is not local"); } return userSession.getPrincipal().getName(); } private String getCommentName(DocumentModel target, DocumentModel comment) { String author = (String) comment.getProperty("comment", "author"); if (author == null) { author = getCurrentUser(target); } Date creationDate = getCommentTimeStamp(comment); return "COMMENT-" + author + '-' + timeFormat.format(creationDate.getTime()); } private static Date getCommentTimeStamp(DocumentModel comment) { Calendar creationDate; try { creationDate = (Calendar) comment.getProperty("dublincore", "created"); } catch (PropertyException e) { creationDate = null; } if (creationDate == null) { creationDate = Calendar.getInstance(); } return creationDate.getTime(); } public void deleteComment(DocumentModel docModel, DocumentModel comment) { NuxeoPrincipal author = comment.getCoreSession() != null ? (NuxeoPrincipal) comment.getCoreSession().getPrincipal() : getAuthor(comment); try (CoreSession session = CoreInstance.openCoreSessionSystem(docModel.getRepositoryName())) { DocumentRef ref = comment.getRef(); if (!session.exists(ref)) { throw new NuxeoException("Comment Document does not exist: " + comment.getId()); } session.removeDocument(ref); notifyEvent(session, docModel, CommentEvents.COMMENT_REMOVED, null, comment, author); session.save(); } } public DocumentModel createComment(DocumentModel docModel, DocumentModel parent, DocumentModel child) { try (CoreSession session = CoreInstance.openCoreSessionSystem(docModel.getRepositoryName())) { DocumentModel parentDocModel = session.getDocument(parent.getRef()); DocumentModel newComment = internalCreateComment(session, parentDocModel, child, null); session.save(); return newComment; } } private static NuxeoPrincipal getAuthor(DocumentModel docModel) { String[] contributors; try { contributors = (String[]) docModel.getProperty("dublincore", "contributors"); } catch (PropertyException e) { log.error("Error building principal for comment author", e); return null; } UserManager userManager = Framework.getService(UserManager.class); return userManager.getPrincipal(contributors[0]); } public List<DocumentModel> getComments(DocumentModel docModel, DocumentModel parent) { return getComments(parent); } public List<DocumentModel> getDocumentsForComment(DocumentModel comment) { Map<String, Object> ctxMap = Collections.<String, Object> singletonMap( ResourceAdapter.CORE_SESSION_CONTEXT_KEY, comment.getCoreSession()); RelationManager relationManager = Framework.getService(RelationManager.class); Graph graph = relationManager.getGraph(config.graphName, comment.getCoreSession()); Resource commentResource = relationManager.getResource(config.commentNamespace, comment, ctxMap); if (commentResource == null) { throw new NuxeoException("Could not adapt document model to relation resource ; " + "check the service relation adapters configuration"); } Resource predicate = new ResourceImpl(config.predicateNamespace); List<Statement> statementList = graph.getStatements(commentResource, predicate, null); if (graph instanceof JenaGraph) { // XXX AT: BBB for when repository name was not included in the // resource uri Resource oldDocResource = new QNameResourceImpl(config.commentNamespace, comment.getId()); statementList.addAll(graph.getStatements(oldDocResource, predicate, null)); } List<DocumentModel> docList = new ArrayList<DocumentModel>(); for (Statement stmt : statementList) { QNameResourceImpl subject = (QNameResourceImpl) stmt.getObject(); DocumentModel docModel = (DocumentModel) relationManager.getResourceRepresentation( config.documentNamespace, subject, ctxMap); if (docModel == null) { log.warn("Could not adapt comment relation subject to a document " + "model; check the service relation adapters configuration"); continue; } docList.add(docModel); } return docList; } public DocumentModel createLocatedComment(DocumentModel docModel, DocumentModel comment, String path) { try (CoreSession session = CoreInstance.openCoreSessionSystem(docModel.getRepositoryName())) { DocumentModel createdComment = internalCreateComment(session, docModel, comment, path); session.save(); return createdComment; } } public DocumentModel getThreadForComment(DocumentModel comment) { List<DocumentModel> threads = getDocumentsForComment(comment); if (threads.size() > 0) { DocumentModel thread = (DocumentModel) threads.get(0); while (thread.getType().equals("Post") || thread.getType().equals(CommentsConstants.COMMENT_DOC_TYPE)) { thread = getThreadForComment(thread); } return thread; } return null; } }