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