001/* 002 * (C) Copyright 2007-2018 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 static java.util.stream.Collectors.collectingAndThen; 025import static java.util.stream.Collectors.toList; 026import static org.nuxeo.ecm.platform.comment.api.ExternalEntityConstants.EXTERNAL_ENTITY_FACET; 027import static org.nuxeo.ecm.platform.comment.workflow.utils.CommentsConstants.COMMENT_ANCESTOR_IDS; 028import static org.nuxeo.ecm.platform.comment.workflow.utils.CommentsConstants.COMMENT_DOC_TYPE; 029import static org.nuxeo.ecm.platform.comment.workflow.utils.CommentsConstants.COMMENT_PARENT_ID; 030 031import java.io.Serializable; 032import java.text.SimpleDateFormat; 033import java.util.ArrayList; 034import java.util.Calendar; 035import java.util.Collections; 036import java.util.Comparator; 037import java.util.Date; 038import java.util.List; 039import java.util.Map; 040 041import org.apache.commons.logging.Log; 042import org.apache.commons.logging.LogFactory; 043import org.nuxeo.ecm.core.api.CloseableCoreSession; 044import org.nuxeo.ecm.core.api.CoreInstance; 045import org.nuxeo.ecm.core.api.CoreSession; 046import org.nuxeo.ecm.core.api.DocumentModel; 047import org.nuxeo.ecm.core.api.DocumentRef; 048import org.nuxeo.ecm.core.api.IdRef; 049import org.nuxeo.ecm.core.api.NuxeoException; 050import org.nuxeo.ecm.core.api.PartialList; 051import org.nuxeo.ecm.core.api.PathRef; 052import org.nuxeo.ecm.core.api.PropertyException; 053import org.nuxeo.ecm.core.api.pathsegment.PathSegmentService; 054import org.nuxeo.ecm.platform.comment.api.Comment; 055import org.nuxeo.ecm.platform.comment.api.CommentConverter; 056import org.nuxeo.ecm.platform.comment.api.CommentEvents; 057import org.nuxeo.ecm.platform.comment.api.Comments; 058import org.nuxeo.ecm.platform.comment.api.ExternalEntity; 059import org.nuxeo.ecm.platform.comment.api.exceptions.CommentNotFoundException; 060import org.nuxeo.ecm.platform.comment.api.exceptions.CommentSecurityException; 061import org.nuxeo.ecm.platform.comment.service.CommentServiceConfig; 062import org.nuxeo.ecm.platform.comment.workflow.utils.CommentsConstants; 063import org.nuxeo.ecm.platform.relations.api.Graph; 064import org.nuxeo.ecm.platform.relations.api.RelationManager; 065import org.nuxeo.ecm.platform.relations.api.Resource; 066import org.nuxeo.ecm.platform.relations.api.ResourceAdapter; 067import org.nuxeo.ecm.platform.relations.api.Statement; 068import org.nuxeo.ecm.platform.relations.api.impl.QNameResourceImpl; 069import org.nuxeo.ecm.platform.relations.api.impl.ResourceImpl; 070import org.nuxeo.ecm.platform.relations.api.impl.StatementImpl; 071import org.nuxeo.ecm.platform.relations.jena.JenaGraph; 072import org.nuxeo.runtime.api.Framework; 073 074/** 075 * @author <a href="mailto:[email protected]">George Lefter</a> 076 * @deprecated since 10.3, use {@link PropertyCommentManager} instead. 077 */ 078@Deprecated 079public class CommentManagerImpl extends AbstractCommentManager { 080 081 private static final Log log = LogFactory.getLog(CommentManagerImpl.class); 082 083 final SimpleDateFormat timeFormat = new SimpleDateFormat("dd-HHmmss.S"); 084 085 final SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM"); 086 087 final CommentServiceConfig config; 088 089 final CommentConverter commentConverter; 090 091 public CommentManagerImpl(CommentServiceConfig config) { 092 this.config = config; 093 commentConverter = config.getCommentConverter(); 094 } 095 096 @Override 097 public List<DocumentModel> getComments(CoreSession session, DocumentModel docModel) 098 throws CommentSecurityException { 099 Map<String, Object> ctxMap = Collections.<String, Object> singletonMap(ResourceAdapter.CORE_SESSION_CONTEXT_KEY, 100 session); 101 RelationManager relationManager = Framework.getService(RelationManager.class); 102 Graph graph = relationManager.getGraph(config.graphName, session); 103 Resource docResource = relationManager.getResource(config.documentNamespace, docModel, ctxMap); 104 if (docResource == null) { 105 throw new NuxeoException("Could not adapt document model to relation resource ; " 106 + "check the service relation adapters configuration"); 107 } 108 109 // FIXME AT: why no filter on the predicate? 110 List<Statement> statementList = graph.getStatements(null, null, docResource); 111 if (graph instanceof JenaGraph) { 112 // XXX AT: BBB for when repository name was not included in the 113 // resource uri 114 Resource oldDocResource = new QNameResourceImpl(config.documentNamespace, docModel.getId()); 115 statementList.addAll(graph.getStatements(null, null, oldDocResource)); 116 } 117 118 List<DocumentModel> commentList = new ArrayList<DocumentModel>(); 119 for (Statement stmt : statementList) { 120 QNameResourceImpl subject = (QNameResourceImpl) stmt.getSubject(); 121 122 DocumentModel commentDocModel = (DocumentModel) relationManager.getResourceRepresentation( 123 config.commentNamespace, subject, ctxMap); 124 if (commentDocModel == null) { 125 // XXX AT: maybe user cannot see the comment 126 log.warn("Could not adapt comment relation subject to a document " 127 + "model; check the service relation adapters configur ation"); 128 continue; 129 } 130 commentList.add(commentDocModel); 131 } 132 133 CommentSorter sorter = new CommentSorter(true); 134 Collections.sort(commentList, sorter); 135 136 return commentList; 137 } 138 139 @Override 140 public DocumentModel createComment(DocumentModel docModel, String comment, String author) { 141 try (CloseableCoreSession session = CoreInstance.openCoreSessionSystem(docModel.getRepositoryName())) { 142 DocumentModel commentDM = session.createDocumentModel(COMMENT_DOC_TYPE); 143 commentDM.setPropertyValue(CommentsConstants.COMMENT_TEXT, comment); 144 commentDM.setPropertyValue(CommentsConstants.COMMENT_AUTHOR, author); 145 commentDM.setPropertyValue(CommentsConstants.COMMENT_CREATION_DATE, Calendar.getInstance()); 146 commentDM = internalCreateComment(session, docModel, commentDM, null); 147 session.save(); 148 149 return commentDM; 150 } 151 } 152 153 @Override 154 public DocumentModel createComment(DocumentModel docModel, String comment) { 155 String author = getCurrentUser(docModel); 156 return createComment(docModel, comment, author); 157 } 158 159 /** 160 * If the author property on comment is not set, retrieve the author name from the session 161 * 162 * @param docModel The document model that holds the session id 163 * @param comment The comment to update 164 */ 165 private static String updateAuthor(DocumentModel docModel, DocumentModel comment) { 166 // update the author if not set 167 String author = (String) comment.getProperty("comment", "author"); 168 if (author == null) { 169 log.debug("deprecated use of createComment: the client should set the author property on document"); 170 author = getCurrentUser(docModel); 171 comment.setProperty("comment", "author", author); 172 } 173 return author; 174 } 175 176 @Override 177 public DocumentModel createComment(DocumentModel docModel, DocumentModel comment) throws CommentSecurityException { 178 try (CloseableCoreSession session = CoreInstance.openCoreSessionSystem(docModel.getRepositoryName())) { 179 comment.setPropertyValue(COMMENT_ANCESTOR_IDS, 180 (Serializable) computeAncestorIds(session, docModel.getId())); 181 DocumentModel doc = internalCreateComment(session, docModel, comment, null); 182 session.save(); 183 doc.detach(true); 184 return doc; 185 } 186 } 187 188 protected DocumentModel internalCreateComment(CoreSession session, DocumentModel docModel, DocumentModel comment, 189 String path) { 190 DocumentModel createdComment; 191 192 createdComment = createCommentDocModel(session, docModel, comment, path); 193 194 RelationManager relationManager = Framework.getService(RelationManager.class); 195 196 Resource commentRes = relationManager.getResource(config.commentNamespace, createdComment, null); 197 198 Resource documentRes = relationManager.getResource(config.documentNamespace, docModel, null); 199 200 if (commentRes == null || documentRes == null) { 201 throw new NuxeoException("Could not adapt document model to relation resource ; " 202 + "check the service relation adapters configuration"); 203 } 204 205 Resource predicateRes = new ResourceImpl(config.predicateNamespace); 206 207 Statement stmt = new StatementImpl(commentRes, predicateRes, documentRes); 208 relationManager.getGraph(config.graphName, session).add(stmt); 209 210 notifyEvent(session, CommentEvents.COMMENT_ADDED, docModel, createdComment); 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(mySession, 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(mySession, commentDocModel); 266 log.debug("created comment with id=" + commentDocModel.getId()); 267 268 return commentDocModel; 269 } 270 271 private String[] getCommentPathList(DocumentModel comment) { 272 String[] pathList = new String[2]; 273 pathList[0] = COMMENTS_DIRECTORY; 274 275 pathList[1] = dateFormat.format(getCommentTimeStamp(comment)); 276 return pathList; 277 } 278 279 /** 280 * @deprecated if the caller is remote, we cannot obtain the session 281 */ 282 @Deprecated 283 private static String getCurrentUser(DocumentModel target) { 284 CoreSession userSession = target.getCoreSession(); 285 if (userSession == null) { 286 throw new NuxeoException("userSession is null, do not invoke this method when the user is not local"); 287 } 288 return userSession.getPrincipal().getName(); 289 } 290 291 private String getCommentName(DocumentModel target, DocumentModel comment) { 292 String author = (String) comment.getProperty("comment", "author"); 293 if (author == null) { 294 author = getCurrentUser(target); 295 } 296 Date creationDate = getCommentTimeStamp(comment); 297 return "COMMENT-" + author + '-' + timeFormat.format(creationDate.getTime()); 298 } 299 300 private static Date getCommentTimeStamp(DocumentModel comment) { 301 Calendar creationDate; 302 try { 303 creationDate = (Calendar) comment.getProperty("dublincore", "created"); 304 } catch (PropertyException e) { 305 creationDate = null; 306 } 307 if (creationDate == null) { 308 creationDate = Calendar.getInstance(); 309 } 310 return creationDate.getTime(); 311 } 312 313 @Override 314 public void deleteComment(DocumentModel docModel, DocumentModel comment) { 315 try (CloseableCoreSession session = CoreInstance.openCoreSessionSystem(docModel.getRepositoryName())) { 316 DocumentRef ref = comment.getRef(); 317 if (!session.exists(ref)) { 318 throw new NuxeoException("Comment Document does not exist: " + comment.getId()); 319 } 320 321 session.removeDocument(ref); 322 323 notifyEvent(session, CommentEvents.COMMENT_REMOVED, docModel, comment); 324 325 session.save(); 326 } 327 } 328 329 @Override 330 public DocumentModel createComment(DocumentModel docModel, DocumentModel parent, DocumentModel child) { 331 try (CloseableCoreSession session = CoreInstance.openCoreSessionSystem(docModel.getRepositoryName())) { 332 DocumentModel parentDocModel = session.getDocument(parent.getRef()); 333 String containerPath = parent.getPath().removeLastSegments(1).toString(); 334 DocumentModel newComment = internalCreateComment(session, parentDocModel, child, containerPath); 335 336 session.save(); 337 return newComment; 338 } 339 } 340 341 @Override 342 public List<DocumentModel> getDocumentsForComment(DocumentModel comment) { 343 Map<String, Object> ctxMap = Collections.<String, Object> singletonMap(ResourceAdapter.CORE_SESSION_CONTEXT_KEY, 344 comment.getCoreSession()); 345 RelationManager relationManager = Framework.getService(RelationManager.class); 346 Graph graph = relationManager.getGraph(config.graphName, comment.getCoreSession()); 347 Resource commentResource = relationManager.getResource(config.commentNamespace, comment, ctxMap); 348 if (commentResource == null) { 349 throw new NuxeoException("Could not adapt document model to relation resource ; " 350 + "check the service relation adapters configuration"); 351 } 352 Resource predicate = new ResourceImpl(config.predicateNamespace); 353 354 List<Statement> statementList = graph.getStatements(commentResource, predicate, null); 355 if (graph instanceof JenaGraph) { 356 // XXX AT: BBB for when repository name was not included in the 357 // resource uri 358 Resource oldDocResource = new QNameResourceImpl(config.commentNamespace, comment.getId()); 359 statementList.addAll(graph.getStatements(oldDocResource, predicate, null)); 360 } 361 362 List<DocumentModel> docList = new ArrayList<DocumentModel>(); 363 for (Statement stmt : statementList) { 364 QNameResourceImpl subject = (QNameResourceImpl) stmt.getObject(); 365 DocumentModel docModel = (DocumentModel) relationManager.getResourceRepresentation(config.documentNamespace, 366 subject, ctxMap); 367 if (docModel == null) { 368 log.warn("Could not adapt comment relation subject to a document " 369 + "model; check the service relation adapters configuration"); 370 continue; 371 } 372 docList.add(docModel); 373 } 374 return docList; 375 376 } 377 378 @Override 379 public DocumentModel createLocatedComment(DocumentModel docModel, DocumentModel comment, String path) 380 throws CommentSecurityException { 381 try (CloseableCoreSession session = CoreInstance.openCoreSessionSystem(docModel.getRepositoryName())) { 382 DocumentModel createdComment = internalCreateComment(session, docModel, comment, path); 383 session.save(); 384 return createdComment; 385 } 386 } 387 388 @Override 389 public DocumentModel getThreadForComment(DocumentModel comment) throws CommentSecurityException { 390 List<DocumentModel> threads = getDocumentsForComment(comment); 391 if (threads.size() > 0) { 392 DocumentModel thread = threads.get(0); 393 while (thread.getType().equals("Post") || thread.getType().equals(COMMENT_DOC_TYPE)) { 394 thread = getThreadForComment(thread); 395 } 396 return thread; 397 } 398 return null; 399 } 400 401 @Override 402 public Comment createComment(CoreSession session, Comment comment) 403 throws CommentNotFoundException, CommentSecurityException { 404 DocumentRef commentRef = new IdRef(comment.getParentId()); 405 if (!session.exists(commentRef)) { 406 throw new CommentNotFoundException("The document " + comment.getParentId() + " does not exist."); 407 } 408 DocumentModel docToComment = session.getDocument(commentRef); 409 DocumentModel commentModel = session.createDocumentModel(COMMENT_DOC_TYPE); 410 commentModel.setPropertyValue("dc:created", Calendar.getInstance()); 411 412 Comments.commentToDocumentModel(comment, commentModel); 413 if (comment instanceof ExternalEntity) { 414 commentModel.addFacet(EXTERNAL_ENTITY_FACET); 415 Comments.externalEntityToDocumentModel((ExternalEntity) comment, commentModel); 416 } 417 418 DocumentModel createdCommentModel = createComment(docToComment, commentModel); 419 return Comments.newComment(createdCommentModel); 420 } 421 422 @Override 423 public Comment getComment(CoreSession session, String commentId) 424 throws CommentNotFoundException, CommentSecurityException { 425 DocumentRef commentRef = new IdRef(commentId); 426 if (!session.exists(commentRef)) { 427 throw new CommentNotFoundException("The document " + commentId + " does not exist."); 428 } 429 DocumentModel commentModel = session.getDocument(commentRef); 430 return Comments.newComment(commentModel); 431 } 432 433 @Override 434 @SuppressWarnings("unchecked") 435 public PartialList<Comment> getComments(CoreSession session, String documentId, Long pageSize, 436 Long currentPageIndex, boolean sortAscending) throws CommentSecurityException { 437 DocumentRef docRef = new IdRef(documentId); 438 if (!session.exists(docRef)) { 439 return new PartialList<>(Collections.emptyList(), 0); 440 } 441 DocumentModel commentedDoc = session.getDocument(docRef); 442 // do a dummy implementation of pagination for former comment manager implementation 443 List<DocumentModel> comments = getComments(commentedDoc); 444 long maxSize = pageSize == null || pageSize <= 0 ? comments.size() : pageSize; 445 long offset = currentPageIndex == null || currentPageIndex <= 0 ? 0 : currentPageIndex * pageSize; 446 return comments.stream() 447 .sorted(Comparator.comparing(doc -> (Calendar) doc.getPropertyValue("dc:created"))) 448 .skip(offset) 449 .limit(maxSize) 450 .map(Comments::newComment) 451 .collect(collectingAndThen(toList(), list -> new PartialList<>(list, comments.size()))); 452 } 453 454 @Override 455 public Comment updateComment(CoreSession session, String commentId, Comment comment) throws NuxeoException { 456 throw new UnsupportedOperationException("Update a comment is not possible through this implementation"); 457 } 458 459 @Override 460 public void deleteComment(CoreSession session, String commentId) 461 throws CommentNotFoundException, CommentSecurityException { 462 DocumentRef commentRef = new IdRef(commentId); 463 if (!session.exists(commentRef)) { 464 throw new CommentNotFoundException("The comment " + commentId + " does not exist."); 465 } 466 DocumentModel comment = session.getDocument(commentRef); 467 DocumentModel commentedDoc = session.getDocument( 468 new IdRef((String) comment.getPropertyValue(COMMENT_PARENT_ID))); 469 deleteComment(commentedDoc, comment); 470 } 471 472 @Override 473 public Comment getExternalComment(CoreSession session, String entityId) throws NuxeoException { 474 throw new UnsupportedOperationException( 475 "Get a comment from its external entity id is not possible through this implementation"); 476 } 477 478 @Override 479 public Comment updateExternalComment(CoreSession session, String entityId, Comment comment) throws NuxeoException { 480 throw new UnsupportedOperationException( 481 "Update a comment from its external entity id is not possible through this implementation"); 482 } 483 484 @Override 485 public void deleteExternalComment(CoreSession session, String entityId) throws NuxeoException { 486 throw new UnsupportedOperationException( 487 "Delete a comment from its external entity id is not possible through this implementation"); 488 } 489 490 @Override 491 public boolean hasFeature(Feature feature) { 492 switch (feature) { 493 case COMMENTS_LINKED_WITH_PROPERTY: 494 return false; 495 default: 496 throw new UnsupportedOperationException(feature.name()); 497 } 498 } 499}