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