001/* 002 * (C) Copyright 2007 Nuxeo SA (http://nuxeo.com/) and others. 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 * 016 * Contributors: 017 * Nuxeo - initial API and implementation 018 * 019 * $Id$ 020 */ 021 022package org.nuxeo.ecm.platform.comment.impl; 023 024import java.io.Serializable; 025import java.text.SimpleDateFormat; 026import java.util.ArrayList; 027import java.util.Calendar; 028import java.util.Collections; 029import java.util.Date; 030import java.util.HashMap; 031import java.util.List; 032import java.util.Map; 033 034import org.apache.commons.logging.Log; 035import org.apache.commons.logging.LogFactory; 036import org.nuxeo.ecm.core.api.CoreInstance; 037import org.nuxeo.ecm.core.api.CoreSession; 038import org.nuxeo.ecm.core.api.DocumentModel; 039import org.nuxeo.ecm.core.api.DocumentRef; 040import org.nuxeo.ecm.core.api.NuxeoException; 041import org.nuxeo.ecm.core.api.NuxeoPrincipal; 042import org.nuxeo.ecm.core.api.PathRef; 043import org.nuxeo.ecm.core.api.PropertyException; 044import org.nuxeo.ecm.core.api.pathsegment.PathSegmentService; 045import org.nuxeo.ecm.core.api.security.ACE; 046import org.nuxeo.ecm.core.api.security.ACL; 047import org.nuxeo.ecm.core.api.security.ACP; 048import org.nuxeo.ecm.core.api.security.SecurityConstants; 049import org.nuxeo.ecm.core.api.security.impl.ACLImpl; 050import org.nuxeo.ecm.core.api.security.impl.ACPImpl; 051import org.nuxeo.ecm.core.event.Event; 052import org.nuxeo.ecm.core.event.EventProducer; 053import org.nuxeo.ecm.core.event.impl.DocumentEventContext; 054import org.nuxeo.ecm.platform.comment.api.CommentConstants; 055import org.nuxeo.ecm.platform.comment.api.CommentConverter; 056import org.nuxeo.ecm.platform.comment.api.CommentEvents; 057import org.nuxeo.ecm.platform.comment.api.CommentManager; 058import org.nuxeo.ecm.platform.comment.service.CommentServiceConfig; 059import org.nuxeo.ecm.platform.comment.workflow.utils.CommentsConstants; 060import org.nuxeo.ecm.platform.relations.api.Graph; 061import org.nuxeo.ecm.platform.relations.api.RelationManager; 062import org.nuxeo.ecm.platform.relations.api.Resource; 063import org.nuxeo.ecm.platform.relations.api.ResourceAdapter; 064import org.nuxeo.ecm.platform.relations.api.Statement; 065import org.nuxeo.ecm.platform.relations.api.impl.QNameResourceImpl; 066import org.nuxeo.ecm.platform.relations.api.impl.ResourceImpl; 067import org.nuxeo.ecm.platform.relations.api.impl.StatementImpl; 068import org.nuxeo.ecm.platform.relations.jena.JenaGraph; 069import org.nuxeo.ecm.platform.usermanager.UserManager; 070import org.nuxeo.runtime.api.Framework; 071 072/** 073 * @author <a href="mailto:glefter@nuxeo.com">George Lefter</a> 074 */ 075public class CommentManagerImpl implements CommentManager { 076 077 private static final Log log = LogFactory.getLog(CommentManagerImpl.class); 078 079 final SimpleDateFormat timeFormat = new SimpleDateFormat("dd-HHmmss.S"); 080 081 final SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM"); 082 083 final CommentServiceConfig config; 084 085 final CommentConverter commentConverter; 086 087 public static final String COMMENTS_DIRECTORY = "Comments"; 088 089 public CommentManagerImpl(CommentServiceConfig config) { 090 this.config = config; 091 commentConverter = config.getCommentConverter(); 092 } 093 094 public List<DocumentModel> getComments(DocumentModel docModel) { 095 Map<String, Object> ctxMap = Collections.<String, Object> singletonMap( 096 ResourceAdapter.CORE_SESSION_CONTEXT_KEY, docModel.getCoreSession()); 097 RelationManager relationManager = Framework.getService(RelationManager.class); 098 Graph graph = relationManager.getGraph(config.graphName, docModel.getCoreSession()); 099 Resource docResource = relationManager.getResource(config.documentNamespace, docModel, ctxMap); 100 if (docResource == null) { 101 throw new NuxeoException("Could not adapt document model to relation resource ; " 102 + "check the service relation adapters configuration"); 103 } 104 105 // FIXME AT: why no filter on the predicate? 106 List<Statement> statementList = graph.getStatements(null, null, docResource); 107 if (graph instanceof JenaGraph) { 108 // XXX AT: BBB for when repository name was not included in the 109 // resource uri 110 Resource oldDocResource = new QNameResourceImpl(config.documentNamespace, docModel.getId()); 111 statementList.addAll(graph.getStatements(null, null, oldDocResource)); 112 } 113 114 List<DocumentModel> commentList = new ArrayList<DocumentModel>(); 115 for (Statement stmt : statementList) { 116 QNameResourceImpl subject = (QNameResourceImpl) stmt.getSubject(); 117 118 DocumentModel commentDocModel = (DocumentModel) relationManager.getResourceRepresentation( 119 config.commentNamespace, subject, ctxMap); 120 if (commentDocModel == null) { 121 // XXX AT: maybe user cannot see the comment 122 log.warn("Could not adapt comment relation subject to a document " 123 + "model; check the service relation adapters configur ation"); 124 continue; 125 } 126 commentList.add(commentDocModel); 127 } 128 129 CommentSorter sorter = new CommentSorter(true); 130 Collections.sort(commentList, sorter); 131 132 return commentList; 133 } 134 135 public DocumentModel createComment(DocumentModel docModel, String comment, String author) { 136 try (CoreSession session = CoreInstance.openCoreSessionSystem(docModel.getRepositoryName())) { 137 DocumentModel commentDM = session.createDocumentModel(CommentsConstants.COMMENT_DOC_TYPE); 138 commentDM.setPropertyValue(CommentsConstants.COMMENT_TEXT, comment); 139 commentDM.setPropertyValue(CommentsConstants.COMMENT_AUTHOR, author); 140 commentDM.setPropertyValue(CommentsConstants.COMMENT_CREATION_DATE, Calendar.getInstance()); 141 commentDM = internalCreateComment(session, docModel, commentDM, null); 142 session.save(); 143 144 return commentDM; 145 } 146 } 147 148 public DocumentModel createComment(DocumentModel docModel, String comment) { 149 String author = getCurrentUser(docModel); 150 return createComment(docModel, comment, author); 151 } 152 153 /** 154 * If the author property on comment is not set, retrieve the author name from the session 155 * 156 * @param docModel The document model that holds the session id 157 * @param comment The comment to update 158 */ 159 private static String updateAuthor(DocumentModel docModel, DocumentModel comment) { 160 // update the author if not set 161 String author = (String) comment.getProperty("comment", "author"); 162 if (author == null) { 163 log.debug("deprecated use of createComment: the client should set the author property on document"); 164 author = getCurrentUser(docModel); 165 comment.setProperty("comment", "author", author); 166 } 167 return author; 168 } 169 170 public DocumentModel createComment(DocumentModel docModel, DocumentModel comment) { 171 try (CoreSession session = CoreInstance.openCoreSessionSystem(docModel.getRepositoryName())) { 172 DocumentModel doc = internalCreateComment(session, docModel, comment, null); 173 session.save(); 174 doc.detach(true); 175 return doc; 176 } 177 } 178 179 protected DocumentModel internalCreateComment(CoreSession session, DocumentModel docModel, DocumentModel comment, 180 String path) { 181 String author = updateAuthor(docModel, comment); 182 DocumentModel createdComment; 183 184 createdComment = createCommentDocModel(session, docModel, comment, path); 185 186 RelationManager relationManager = Framework.getService(RelationManager.class); 187 188 Resource commentRes = relationManager.getResource(config.commentNamespace, createdComment, null); 189 190 Resource documentRes = relationManager.getResource(config.documentNamespace, docModel, null); 191 192 if (commentRes == null || documentRes == null) { 193 throw new NuxeoException("Could not adapt document model to relation resource ; " 194 + "check the service relation adapters configuration"); 195 } 196 197 Resource predicateRes = new ResourceImpl(config.predicateNamespace); 198 199 Statement stmt = new StatementImpl(commentRes, predicateRes, documentRes); 200 relationManager.getGraph(config.graphName, session).add(stmt); 201 202 UserManager userManager = Framework.getService(UserManager.class); 203 if (userManager != null) { 204 // null in tests 205 NuxeoPrincipal principal = userManager.getPrincipal(author); 206 if (principal != null) { 207 notifyEvent(session, docModel, CommentEvents.COMMENT_ADDED, null, createdComment, principal); 208 } 209 } 210 211 return createdComment; 212 } 213 214 private DocumentModel createCommentDocModel(CoreSession mySession, DocumentModel docModel, DocumentModel comment, 215 String path) { 216 217 String domainPath; 218 updateAuthor(docModel, comment); 219 220 String[] pathList = getCommentPathList(comment); 221 222 if (path == null) { 223 domainPath = "/" + docModel.getPath().segment(0); 224 } else { 225 domainPath = path; 226 } 227 if (mySession == null) { 228 return null; 229 } 230 231 // TODO GR upgrade this code. It can't work if current user 232 // doesn't have admin rights 233 234 DocumentModel parent = mySession.getDocument(new PathRef(domainPath)); 235 for (String name : pathList) { 236 String pathStr = parent.getPathAsString(); 237 238 PathRef ref = new PathRef(pathStr, name); 239 if (mySession.exists(ref)) { 240 parent = mySession.getDocument(ref); 241 if (!parent.isFolder()) { 242 throw new NuxeoException(parent.getPathAsString() + " is not folderish"); 243 } 244 } else { 245 DocumentModel dm = mySession.createDocumentModel(pathStr, name, "HiddenFolder"); 246 dm.setProperty("dublincore", "title", name); 247 dm.setProperty("dublincore", "description", ""); 248 dm.setProperty("dublincore", "created", Calendar.getInstance()); 249 dm = mySession.createDocument(dm); 250 setFolderPermissions(dm); 251 parent = dm; 252 } 253 } 254 255 String pathStr = parent.getPathAsString(); 256 String commentName = getCommentName(docModel, comment); 257 CommentConverter converter = config.getCommentConverter(); 258 PathSegmentService pss = Framework.getService(PathSegmentService.class); 259 DocumentModel commentDocModel = mySession.createDocumentModel(comment.getType()); 260 commentDocModel.setProperty("dublincore", "title", commentName); 261 converter.updateDocumentModel(commentDocModel, comment); 262 commentDocModel.setPathInfo(pathStr, pss.generatePathSegment(commentDocModel)); 263 commentDocModel = mySession.createDocument(commentDocModel); 264 setCommentPermissions(commentDocModel); 265 log.debug("created comment with id=" + commentDocModel.getId()); 266 267 return commentDocModel; 268 } 269 270 private static void notifyEvent(CoreSession session, DocumentModel docModel, String eventType, 271 DocumentModel parent, DocumentModel child, NuxeoPrincipal principal) { 272 273 DocumentEventContext ctx = new DocumentEventContext(session, principal, docModel); 274 Map<String, Serializable> props = new HashMap<String, Serializable>(); 275 if (parent != null) { 276 props.put(CommentConstants.PARENT_COMMENT, parent); 277 } 278 props.put(CommentConstants.COMMENT_DOCUMENT, child); 279 props.put(CommentConstants.COMMENT, (String) child.getProperty("comment", "text")); 280 // Keep comment_text for compatibility 281 props.put(CommentConstants.COMMENT_TEXT, (String) child.getProperty("comment", "text")); 282 props.put("category", CommentConstants.EVENT_COMMENT_CATEGORY); 283 ctx.setProperties(props); 284 Event event = ctx.newEvent(eventType); 285 286 EventProducer evtProducer = Framework.getService(EventProducer.class); 287 evtProducer.fireEvent(event); 288 // send also a synchronous Seam message so the CommentManagerActionBean 289 // can rebuild its list 290 // Events.instance().raiseEvent(eventType, docModel); 291 } 292 293 private static void setFolderPermissions(DocumentModel dm) { 294 ACP acp = new ACPImpl(); 295 ACE grantAddChildren = new ACE("members", SecurityConstants.ADD_CHILDREN, true); 296 ACE grantRemoveChildren = new ACE("members", SecurityConstants.REMOVE_CHILDREN, true); 297 ACE grantRemove = new ACE("members", SecurityConstants.REMOVE, true); 298 ACL acl = new ACLImpl(); 299 acl.setACEs(new ACE[] { grantAddChildren, grantRemoveChildren, grantRemove }); 300 acp.addACL(acl); 301 dm.setACP(acp, true); 302 } 303 304 private static void setCommentPermissions(DocumentModel dm) { 305 ACP acp = new ACPImpl(); 306 ACE grantRead = new ACE(SecurityConstants.EVERYONE, SecurityConstants.READ, true); 307 ACE grantRemove = new ACE("members", SecurityConstants.REMOVE, true); 308 ACL acl = new ACLImpl(); 309 acl.setACEs(new ACE[] { grantRead, grantRemove }); 310 acp.addACL(acl); 311 dm.setACP(acp, true); 312 } 313 314 private String[] getCommentPathList(DocumentModel comment) { 315 String[] pathList = new String[2]; 316 pathList[0] = COMMENTS_DIRECTORY; 317 318 pathList[1] = dateFormat.format(getCommentTimeStamp(comment)); 319 return pathList; 320 } 321 322 /** 323 * @deprecated if the caller is remote, we cannot obtain the session 324 */ 325 @Deprecated 326 private static String getCurrentUser(DocumentModel target) { 327 CoreSession userSession = target.getCoreSession(); 328 if (userSession == null) { 329 throw new NuxeoException("userSession is null, do not invoke this method when the user is not local"); 330 } 331 return userSession.getPrincipal().getName(); 332 } 333 334 private String getCommentName(DocumentModel target, DocumentModel comment) { 335 String author = (String) comment.getProperty("comment", "author"); 336 if (author == null) { 337 author = getCurrentUser(target); 338 } 339 Date creationDate = getCommentTimeStamp(comment); 340 return "COMMENT-" + author + '-' + timeFormat.format(creationDate.getTime()); 341 } 342 343 private static Date getCommentTimeStamp(DocumentModel comment) { 344 Calendar creationDate; 345 try { 346 creationDate = (Calendar) comment.getProperty("dublincore", "created"); 347 } catch (PropertyException e) { 348 creationDate = null; 349 } 350 if (creationDate == null) { 351 creationDate = Calendar.getInstance(); 352 } 353 return creationDate.getTime(); 354 } 355 356 public void deleteComment(DocumentModel docModel, DocumentModel comment) { 357 NuxeoPrincipal author = comment.getCoreSession() != null ? (NuxeoPrincipal) comment.getCoreSession().getPrincipal() 358 : getAuthor(comment); 359 try (CoreSession session = CoreInstance.openCoreSessionSystem(docModel.getRepositoryName())) { 360 DocumentRef ref = comment.getRef(); 361 if (!session.exists(ref)) { 362 throw new NuxeoException("Comment Document does not exist: " + comment.getId()); 363 } 364 365 session.removeDocument(ref); 366 367 notifyEvent(session, docModel, CommentEvents.COMMENT_REMOVED, null, comment, author); 368 369 session.save(); 370 } 371 } 372 373 public DocumentModel createComment(DocumentModel docModel, DocumentModel parent, DocumentModel child) 374 { 375 try (CoreSession session = CoreInstance.openCoreSessionSystem(docModel.getRepositoryName())) { 376 DocumentModel parentDocModel = session.getDocument(parent.getRef()); 377 DocumentModel newComment = internalCreateComment(session, parentDocModel, child, null); 378 379 session.save(); 380 return newComment; 381 } 382 } 383 384 private static NuxeoPrincipal getAuthor(DocumentModel docModel) { 385 String[] contributors; 386 try { 387 contributors = (String[]) docModel.getProperty("dublincore", "contributors"); 388 } catch (PropertyException e) { 389 log.error("Error building principal for comment author", e); 390 return null; 391 } 392 UserManager userManager = Framework.getService(UserManager.class); 393 return userManager.getPrincipal(contributors[0]); 394 } 395 396 public List<DocumentModel> getComments(DocumentModel docModel, DocumentModel parent) { 397 return getComments(parent); 398 } 399 400 public List<DocumentModel> getDocumentsForComment(DocumentModel comment) { 401 Map<String, Object> ctxMap = Collections.<String, Object> singletonMap( 402 ResourceAdapter.CORE_SESSION_CONTEXT_KEY, comment.getCoreSession()); 403 RelationManager relationManager = Framework.getService(RelationManager.class); 404 Graph graph = relationManager.getGraph(config.graphName, comment.getCoreSession()); 405 Resource commentResource = relationManager.getResource(config.commentNamespace, comment, ctxMap); 406 if (commentResource == null) { 407 throw new NuxeoException("Could not adapt document model to relation resource ; " 408 + "check the service relation adapters configuration"); 409 } 410 Resource predicate = new ResourceImpl(config.predicateNamespace); 411 412 List<Statement> statementList = graph.getStatements(commentResource, predicate, null); 413 if (graph instanceof JenaGraph) { 414 // XXX AT: BBB for when repository name was not included in the 415 // resource uri 416 Resource oldDocResource = new QNameResourceImpl(config.commentNamespace, comment.getId()); 417 statementList.addAll(graph.getStatements(oldDocResource, predicate, null)); 418 } 419 420 List<DocumentModel> docList = new ArrayList<DocumentModel>(); 421 for (Statement stmt : statementList) { 422 QNameResourceImpl subject = (QNameResourceImpl) stmt.getObject(); 423 DocumentModel docModel = (DocumentModel) relationManager.getResourceRepresentation( 424 config.documentNamespace, subject, ctxMap); 425 if (docModel == null) { 426 log.warn("Could not adapt comment relation subject to a document " 427 + "model; check the service relation adapters configuration"); 428 continue; 429 } 430 docList.add(docModel); 431 } 432 return docList; 433 434 } 435 436 public DocumentModel createLocatedComment(DocumentModel docModel, DocumentModel comment, String path) 437 { 438 try (CoreSession session = CoreInstance.openCoreSessionSystem(docModel.getRepositoryName())) { 439 DocumentModel createdComment = internalCreateComment(session, docModel, comment, path); 440 session.save(); 441 return createdComment; 442 } 443 } 444 445 public DocumentModel getThreadForComment(DocumentModel comment) { 446 List<DocumentModel> threads = getDocumentsForComment(comment); 447 if (threads.size() > 0) { 448 DocumentModel thread = (DocumentModel) threads.get(0); 449 while (thread.getType().equals("Post") || thread.getType().equals(CommentsConstants.COMMENT_DOC_TYPE)) { 450 thread = getThreadForComment(thread); 451 } 452 return thread; 453 } 454 return null; 455 } 456}