001/* 002 * (C) Copyright 2019-2020 Nuxeo (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 * Salem Aouana 018 */ 019 020package org.nuxeo.ecm.platform.comment.impl; 021 022import static java.lang.Boolean.TRUE; 023import static java.util.Collections.emptyList; 024import static java.util.Collections.singletonList; 025import static java.util.Collections.singletonMap; 026import static java.util.Objects.requireNonNull; 027import static java.util.stream.Collectors.collectingAndThen; 028import static java.util.stream.Collectors.toList; 029import static org.apache.commons.lang3.StringUtils.isBlank; 030import static org.apache.commons.lang3.StringUtils.isEmpty; 031import static org.nuxeo.ecm.core.api.VersioningOption.NONE; 032import static org.nuxeo.ecm.core.api.security.SecurityConstants.EVERYTHING; 033import static org.nuxeo.ecm.core.api.versioning.VersioningService.VERSIONING_OPTION; 034import static org.nuxeo.ecm.core.io.marshallers.json.document.DocumentModelJsonReader.applyDirtyPropertyValues; 035import static org.nuxeo.ecm.core.query.sql.NXQL.ECM_ANCESTORID; 036import static org.nuxeo.ecm.core.query.sql.NXQL.ECM_UUID; 037import static org.nuxeo.ecm.core.schema.FacetNames.HAS_RELATED_TEXT; 038import static org.nuxeo.ecm.core.storage.BaseDocument.RELATED_TEXT; 039import static org.nuxeo.ecm.core.storage.BaseDocument.RELATED_TEXT_ID; 040import static org.nuxeo.ecm.core.storage.BaseDocument.RELATED_TEXT_RESOURCES; 041import static org.nuxeo.ecm.platform.comment.api.CommentConstants.COMMENT_ANCESTOR_IDS_PROPERTY; 042import static org.nuxeo.ecm.platform.comment.api.CommentConstants.COMMENT_AUTHOR_PROPERTY; 043import static org.nuxeo.ecm.platform.comment.api.CommentConstants.COMMENT_CREATION_DATE_PROPERTY; 044import static org.nuxeo.ecm.platform.comment.api.CommentConstants.COMMENT_PARENT_ID_PROPERTY; 045import static org.nuxeo.ecm.platform.comment.api.CommentConstants.COMMENT_ROOT_DOC_TYPE; 046import static org.nuxeo.ecm.platform.comment.api.CommentConstants.COMMENT_SCHEMA; 047import static org.nuxeo.ecm.platform.comment.api.CommentConstants.COMMENT_TEXT_PROPERTY; 048import static org.nuxeo.ecm.platform.comment.api.ExternalEntityConstants.EXTERNAL_ENTITY_FACET; 049import static org.nuxeo.ecm.platform.dublincore.listener.DublinCoreListener.DISABLE_DUBLINCORE_LISTENER; 050import static org.nuxeo.ecm.platform.ec.notification.NotificationConstants.DISABLE_NOTIFICATION_SERVICE; 051import static org.nuxeo.ecm.platform.query.nxql.CoreQueryAndFetchPageProvider.CORE_SESSION_PROPERTY; 052 053import java.io.Serializable; 054import java.time.Instant; 055import java.util.ArrayList; 056import java.util.Collection; 057import java.util.Collections; 058import java.util.List; 059import java.util.Map; 060import java.util.Optional; 061 062import org.apache.logging.log4j.LogManager; 063import org.apache.logging.log4j.Logger; 064import org.nuxeo.ecm.core.api.CoreInstance; 065import org.nuxeo.ecm.core.api.CoreSession; 066import org.nuxeo.ecm.core.api.DocumentModel; 067import org.nuxeo.ecm.core.api.DocumentNotFoundException; 068import org.nuxeo.ecm.core.api.DocumentRef; 069import org.nuxeo.ecm.core.api.DocumentSecurityException; 070import org.nuxeo.ecm.core.api.IdRef; 071import org.nuxeo.ecm.core.api.NuxeoPrincipal; 072import org.nuxeo.ecm.core.api.PartialList; 073import org.nuxeo.ecm.core.api.SortInfo; 074import org.nuxeo.ecm.core.api.security.SecurityConstants; 075import org.nuxeo.ecm.platform.comment.api.Comment; 076import org.nuxeo.ecm.platform.comment.api.CommentEvents; 077import org.nuxeo.ecm.platform.comment.api.exceptions.CommentNotFoundException; 078import org.nuxeo.ecm.platform.comment.api.exceptions.CommentSecurityException; 079import org.nuxeo.ecm.platform.ec.notification.NotificationConstants; 080import org.nuxeo.ecm.platform.notification.api.NotificationManager; 081import org.nuxeo.ecm.platform.query.api.PageProvider; 082import org.nuxeo.ecm.platform.query.api.PageProviderService; 083import org.nuxeo.runtime.api.Framework; 084import org.nuxeo.runtime.services.config.ConfigurationService; 085 086/** 087 * Comment service implementation. The comments are linked together as a tree under a folder related to the root 088 * document that we comment. 089 * 090 * @since 11.1 091 */ 092public class TreeCommentManager extends AbstractCommentManager { 093 094 private static final Logger log = LogManager.getLogger(TreeCommentManager.class); 095 096 public static final String COMMENT_RELATED_TEXT_ID = "commentRelatedTextId_%s"; 097 098 /** The key to the config turning on or off autosubscription. */ 099 public static final String AUTOSUBSCRIBE_CONFIG_KEY = "org.nuxeo.ecm.platform.comment.service.notification.autosubscribe"; 100 101 protected static final String COMMENT_NAME = "comment"; 102 103 /** @deprecated since 11.1, use {@link #GET_EXTERNAL_COMMENT_PAGE_PROVIDER_NAME} instead */ 104 @Deprecated(since = "11.1") 105 @SuppressWarnings("DeprecatedIsStillUsed") 106 protected static final String GET_COMMENT_PAGE_PROVIDER_NAME = "GET_COMMENT_AS_EXTERNAL_ENTITY"; 107 108 protected static final String GET_EXTERNAL_COMMENT_PAGE_PROVIDER_NAME = "GET_EXTERNAL_COMMENT_BY_ECM_ANCESTOR"; 109 110 protected static final String GET_COMMENTS_FOR_DOCUMENT_PAGE_PROVIDER_NAME = "GET_COMMENTS_FOR_DOCUMENT_BY_ECM_PARENT"; 111 112 protected static final String GET_COMMENTS_FOR_DOCUMENTS_PAGE_PROVIDER_NAME = "GET_COMMENTS_FOR_DOCUMENTS_BY_COMMENT_ANCESTOR"; 113 114 protected static final String SERVICE_WITHOUT_IMPLEMENTATION_MESSAGE = "This service implementation does not implement deprecated API."; 115 116 /** 117 * Counts how many comments where made on a specific document. 118 */ 119 protected static final String QUERY_GET_COMMENTS_UUID_BY_COMMENT_ANCESTOR = // 120 "SELECT " + ECM_UUID + " FROM Comment WHERE " + ECM_ANCESTORID + " = '%s'"; 121 122 /** 123 * Counts how many comments where made by a specific user on a specific document. 124 */ 125 protected static final String QUERY_GET_COMMENTS_UUID_BY_COMMENT_ANCESTOR_AND_AUTHOR = // 126 QUERY_GET_COMMENTS_UUID_BY_COMMENT_ANCESTOR + " AND " + COMMENT_AUTHOR_PROPERTY + " = '%s'"; 127 128 @Override 129 public List<DocumentModel> getComments(CoreSession session, DocumentModel doc) { 130 return getCommentDocuments(session, doc.getId(), null, null, true); 131 } 132 133 @Override 134 public Comment getComment(CoreSession session, String commentId) { 135 var commentDoc = getCommentDocumentModel(session, commentId); 136 return commentDoc.getAdapter(Comment.class); 137 } 138 139 @Override 140 public PartialList<Comment> getComments(CoreSession session, String documentId, Long pageSize, 141 Long currentPageIndex, boolean sortAscending) { 142 var result = getCommentDocuments(session, documentId, pageSize, currentPageIndex, sortAscending); 143 return result.stream() 144 .map(doc -> doc.getAdapter(Comment.class)) 145 .collect(collectingAndThen(toList(), list -> new PartialList<>(list, result.totalSize()))); 146 } 147 148 @Override 149 public List<Comment> getComments(CoreSession session, Collection<String> documentIds) { 150 PageProviderService ppService = Framework.getService(PageProviderService.class); 151 152 Map<String, Serializable> props = Map.of(CORE_SESSION_PROPERTY, (Serializable) session); 153 @SuppressWarnings("unchecked") 154 var pageProvider = (PageProvider<DocumentModel>) ppService.getPageProvider( 155 GET_COMMENTS_FOR_DOCUMENTS_PAGE_PROVIDER_NAME, null, null, null, props, new ArrayList<>(documentIds)); 156 return pageProvider.getCurrentPage().stream().map(doc -> doc.getAdapter(Comment.class)).collect(toList()); 157 } 158 159 @Override 160 @SuppressWarnings("removal") 161 public List<DocumentModel> getDocumentsForComment(DocumentModel comment) { 162 throw new UnsupportedOperationException(SERVICE_WITHOUT_IMPLEMENTATION_MESSAGE); 163 } 164 165 @Override 166 public DocumentModel getThreadForComment(DocumentModel comment) { 167 throw new UnsupportedOperationException(SERVICE_WITHOUT_IMPLEMENTATION_MESSAGE); 168 } 169 170 @Override 171 public Comment getExternalComment(CoreSession session, String documentId, String entityId) { 172 var commentDoc = getExternalCommentModel(session, documentId, entityId); 173 return commentDoc.getAdapter(Comment.class); 174 } 175 176 @Override 177 public Comment createComment(CoreSession session, Comment comment) { 178 var parentRef = new IdRef(comment.getParentId()); 179 checkCreateCommentPermissions(session, parentRef); 180 181 fillCommentForCreation(session, comment); 182 183 return CoreInstance.doPrivileged(session, s -> { 184 DocumentModel commentedDoc = s.getDocument(parentRef); 185 // Get the location where comment will be stored 186 DocumentRef locationDocRef = getLocationRefOfCommentCreation(s, commentedDoc); 187 188 DocumentModel commentDoc = s.newDocumentModel(locationDocRef, COMMENT_NAME, 189 comment.getDocument().getType()); 190 if (comment.getDocument().hasFacet(EXTERNAL_ENTITY_FACET)) { 191 commentDoc.addFacet(EXTERNAL_ENTITY_FACET); 192 } 193 applyDirtyPropertyValues(comment.getDocument(), commentDoc); 194 195 commentDoc.setPropertyValue(COMMENT_ANCESTOR_IDS_PROPERTY, 196 computeAncestorIds(session, comment.getParentId())); 197 198 // Create the comment document model 199 commentDoc = s.createDocument(commentDoc); 200 Comment createdComment = commentDoc.getAdapter(Comment.class); 201 202 DocumentModel topLevelDoc = getTopLevelDocument(s, commentDoc); 203 manageRelatedTextOfTopLevelDocument(s, topLevelDoc, createdComment.getId(), createdComment.getText()); 204 205 handleNotificationAutoSubscriptions(s, topLevelDoc, commentDoc); 206 207 notifyEvent(s, CommentEvents.COMMENT_ADDED, commentedDoc, commentDoc); 208 209 return createdComment; 210 }); 211 } 212 213 @Override 214 public DocumentModel createComment(DocumentModel commentedDoc, DocumentModel commentDoc) { 215 // Check the right permissions on document that we want to comment 216 checkCreateCommentPermissions(commentDoc.getCoreSession(), commentedDoc.getRef()); 217 218 return CoreInstance.doPrivileged(commentDoc.getCoreSession(), session -> { 219 // Get the location to store the comment 220 DocumentRef locationDocRef = getLocationRefOfCommentCreation(session, commentedDoc); 221 222 DocumentModel commentModelToCreate = session.newDocumentModel(locationDocRef, COMMENT_NAME, 223 commentDoc.getType()); 224 commentModelToCreate.copyContent(commentDoc); 225 226 // Should compute ancestors and set comment:parentId for backward compatibility 227 commentModelToCreate.setPropertyValue(COMMENT_PARENT_ID_PROPERTY, commentedDoc.getId()); 228 commentModelToCreate.setPropertyValue(COMMENT_ANCESTOR_IDS_PROPERTY, 229 computeAncestorIds(session, commentedDoc.getId())); 230 231 // Create the comment doc model 232 commentModelToCreate = session.createDocument(commentModelToCreate); 233 234 DocumentModel topLevelDoc = getTopLevelDocument(session, commentModelToCreate); 235 manageRelatedTextOfTopLevelDocument(session, topLevelDoc, commentModelToCreate.getId(), 236 (String) commentDoc.getPropertyValue(COMMENT_TEXT_PROPERTY)); 237 238 handleNotificationAutoSubscriptions(session, topLevelDoc, commentDoc); 239 240 commentModelToCreate.detach(true); 241 notifyEvent(session, CommentEvents.COMMENT_ADDED, commentedDoc, commentModelToCreate); 242 return commentModelToCreate; 243 }); 244 } 245 246 @Override 247 public DocumentModel createLocatedComment(DocumentModel doc, DocumentModel comment, String path) { 248 throw new UnsupportedOperationException(SERVICE_WITHOUT_IMPLEMENTATION_MESSAGE); 249 } 250 251 @Override 252 public DocumentModel createComment(DocumentModel doc, String text) { 253 throw new UnsupportedOperationException(SERVICE_WITHOUT_IMPLEMENTATION_MESSAGE); 254 } 255 256 @Override 257 @SuppressWarnings("removal") 258 public DocumentModel createComment(DocumentModel doc, String text, String author) { 259 throw new UnsupportedOperationException(SERVICE_WITHOUT_IMPLEMENTATION_MESSAGE); 260 } 261 262 @Override 263 @SuppressWarnings("removal") 264 public DocumentModel createComment(DocumentModel doc, DocumentModel parent, DocumentModel child) { 265 throw new UnsupportedOperationException(SERVICE_WITHOUT_IMPLEMENTATION_MESSAGE); 266 } 267 268 @Override 269 public Comment updateComment(CoreSession session, String commentId, Comment comment) { 270 // Get the comment doc model 271 DocumentModel commentDoc = getCommentDocumentModel(session, commentId); 272 return update(session, comment, commentDoc); 273 } 274 275 @Override 276 public Comment updateExternalComment(CoreSession session, String documentId, String entityId, Comment comment) { 277 // Get the external comment doc model 278 DocumentModel commentDoc = getExternalCommentModel(session, documentId, entityId); 279 return update(session, comment, commentDoc); 280 } 281 282 /** 283 * @param session the user session, in order to check permissions 284 * @param comment the comment holding new data 285 * @param commentDoc the {@link DocumentModel} just retrieved from DB 286 */ 287 protected Comment update(CoreSession session, Comment comment, DocumentModel commentDoc) { 288 NuxeoPrincipal principal = session.getPrincipal(); 289 return CoreInstance.doPrivileged(session, s -> { 290 DocumentModel topLevelDoc = getTopLevelDocument(s, commentDoc); 291 if (!principal.isAdministrator() 292 && !commentDoc.getPropertyValue(COMMENT_AUTHOR_PROPERTY).equals(principal.getName()) 293 && !session.hasPermission(principal, topLevelDoc.getRef(), EVERYTHING)) { 294 throw new CommentSecurityException(String.format("The user %s cannot edit comments of document %s", 295 principal.getName(), commentDoc.getPropertyValue(COMMENT_PARENT_ID_PROPERTY))); 296 } 297 if (comment.getModificationDate() == null) { 298 comment.setModificationDate(Instant.now()); 299 } 300 if (comment.getDocument().hasFacet(EXTERNAL_ENTITY_FACET)) { 301 commentDoc.addFacet(EXTERNAL_ENTITY_FACET); 302 } 303 applyDirtyPropertyValues(comment.getDocument(), commentDoc); 304 var updatedDoc = s.saveDocument(commentDoc); 305 Comment updatedComment = updatedDoc.getAdapter(Comment.class); 306 307 manageRelatedTextOfTopLevelDocument(s, topLevelDoc, updatedComment.getId(), updatedComment.getText()); 308 DocumentModel commentedDoc = getCommentedDocument(session, commentDoc); 309 notifyEvent(session, CommentEvents.COMMENT_UPDATED, topLevelDoc, commentedDoc, updatedDoc); 310 return updatedComment; 311 }); 312 } 313 314 @Override 315 public void deleteExternalComment(CoreSession session, String documentId, String entityId) { 316 DocumentModel commentDoc = getExternalCommentModel(session, documentId, entityId); 317 removeComment(session, commentDoc.getRef()); 318 } 319 320 @Override 321 public void deleteComment(CoreSession s, String commentId) { 322 removeComment(s, new IdRef(commentId)); 323 } 324 325 @Override 326 @SuppressWarnings("removal") 327 public void deleteComment(DocumentModel doc, DocumentModel comment) { 328 throw new UnsupportedOperationException(SERVICE_WITHOUT_IMPLEMENTATION_MESSAGE); 329 } 330 331 /** 332 * Returns the {@link DocumentRef} of the comments location in repository for the given commented document model. 333 * 334 * @param session the session needs to be privileged 335 * @return the document model container of the comments of the given {@code commentedDoc} 336 * @since 11.1 337 */ 338 protected DocumentRef getLocationRefOfCommentCreation(CoreSession session, DocumentModel commentedDoc) { 339 if (commentedDoc.hasSchema(COMMENT_SCHEMA)) { 340 // reply case, store the reply under the comment 341 return commentedDoc.getRef(); 342 } 343 // regular document case, store the comment under a CommentRoot folder under the regular document 344 DocumentModel commentsFolder = session.newDocumentModel(commentedDoc.getRef(), COMMENTS_DIRECTORY, 345 COMMENT_ROOT_DOC_TYPE); 346 // no need to notify the creation of the Comments folder 347 commentsFolder.putContextData(DISABLE_NOTIFICATION_SERVICE, TRUE); 348 commentsFolder = session.getOrCreateDocument(commentsFolder); 349 session.save(); 350 return commentsFolder.getRef(); 351 } 352 353 @Override 354 public boolean hasFeature(Feature feature) { 355 switch (feature) { 356 case COMMENTS_LINKED_WITH_PROPERTY: 357 case COMMENTS_ARE_SPECIAL_CHILDREN: 358 return true; 359 default: 360 throw new UnsupportedOperationException(feature.name()); 361 } 362 } 363 364 @Override 365 protected DocumentModel getTopLevelDocument(CoreSession session, DocumentModel commentDoc) { 366 DocumentModel docModel = commentDoc; 367 while (docModel.getParentRef() != null 368 && (docModel.hasSchema(COMMENT_SCHEMA) || COMMENT_ROOT_DOC_TYPE.equals(docModel.getType()))) { 369 docModel = session.getDocument(docModel.getParentRef()); 370 } 371 return docModel; 372 } 373 374 /** 375 * Checks if the user related to the {@code session} can comments the document linked to the {@code documentRef}. 376 */ 377 protected void checkCreateCommentPermissions(CoreSession session, DocumentRef documentRef) { 378 try { 379 if (!session.hasPermission(documentRef, SecurityConstants.READ)) { 380 throw new CommentSecurityException(String.format("The user %s can not create comments on document %s", 381 session.getPrincipal().getName(), documentRef)); 382 } 383 } catch (DocumentNotFoundException dnfe) { 384 throw new CommentNotFoundException(String.format("The comment %s does not exist.", documentRef), dnfe); 385 } 386 } 387 388 /** 389 * @param session the user session, in order to implicitly check permissions 390 * @return the external document model for the given {@code entityId}, if it exists, otherwise throws a 391 * {@link CommentNotFoundException} 392 */ 393 @SuppressWarnings("unchecked") 394 protected DocumentModel getExternalCommentModel(CoreSession session, String documentId, String entityId) { 395 PageProviderService ppService = Framework.getService(PageProviderService.class); 396 Map<String, Serializable> props = singletonMap(CORE_SESSION_PROPERTY, (Serializable) session); 397 PageProvider<DocumentModel> pageProvider; 398 // backward compatibility 399 if (isBlank(documentId)) { 400 pageProvider = (PageProvider<DocumentModel>) ppService.getPageProvider(GET_COMMENT_PAGE_PROVIDER_NAME, 401 Collections.emptyList(), 1L, 0L, props, entityId); 402 } else { 403 pageProvider = (PageProvider<DocumentModel>) ppService.getPageProvider( 404 GET_EXTERNAL_COMMENT_PAGE_PROVIDER_NAME, Collections.emptyList(), 1L, 0L, props, documentId, 405 entityId); 406 } 407 List<DocumentModel> documents = pageProvider.getCurrentPage(); 408 if (documents.isEmpty()) { 409 throw new CommentNotFoundException(String.format("The external comment %s does not exist.", entityId)); 410 } 411 return documents.get(0); 412 } 413 414 /** 415 * Remove the comment of the given {@code documentRef} 416 * 417 * @param session the user session, in order to check permissions 418 * @param documentRef the documentRef of the comment document model to delete 419 */ 420 protected void removeComment(CoreSession session, DocumentRef documentRef) { 421 NuxeoPrincipal principal = session.getPrincipal(); 422 CoreInstance.doPrivileged(session, s -> { 423 DocumentRef ancestorRef = getTopLevelDocumentRef(s, documentRef); 424 DocumentModel commentDoc = s.getDocument(documentRef); 425 Serializable author = commentDoc.getPropertyValue(COMMENT_AUTHOR_PROPERTY); 426 if (!(principal.isAdministrator() // 427 || author.equals(principal.getName()) // 428 || s.hasPermission(principal, ancestorRef, EVERYTHING))) { 429 throw new CommentSecurityException(String.format( 430 "The user %s cannot delete comments of the document %s", principal.getName(), ancestorRef)); 431 } 432 Comment comment = commentDoc.getAdapter(Comment.class); 433 434 // fetch parents before deleting document 435 DocumentModel topLevelDoc = getTopLevelDocument(s, commentDoc); 436 DocumentModel commentedDoc = getCommentedDocument(s, commentDoc); 437 // nullify related text for this comment 438 manageRelatedTextOfTopLevelDocument(s, topLevelDoc, comment.getId(), null); 439 // finally delete document 440 s.removeDocument(documentRef); 441 notifyEvent(s, CommentEvents.COMMENT_REMOVED, topLevelDoc, commentedDoc, commentDoc); 442 }); 443 } 444 445 /** 446 * @param session the user session, in order to implicitly check permissions 447 * @return the comment document model of the given {@code documentRef} if it exists, otherwise throws a 448 * {@link CommentNotFoundException} 449 */ 450 protected DocumentModel getCommentDocumentModel(CoreSession session, String id) { 451 try { 452 return session.getDocument(new IdRef(id)); 453 } catch (DocumentNotFoundException dnfe) { 454 throw new CommentNotFoundException(String.format("The comment %s does not exist.", id), dnfe); 455 } catch (DocumentSecurityException dse) { 456 throw new CommentSecurityException(String.format("The user %s does not have access to the comment %s", 457 session.getPrincipal().getName(), id), dse); 458 } 459 } 460 461 /** 462 * @return the page provider current page 463 */ 464 @SuppressWarnings("unchecked") 465 protected PartialList<DocumentModel> getCommentDocuments(CoreSession session, String documentId, Long pageSize, 466 Long currentPageIndex, boolean sortAscending) { 467 try { 468 DocumentModel doc = session.getDocument(new IdRef(documentId)); 469 // Depending on the case, the `doc` can be a comment or the top level document 470 // if it's the top level document, then we should retrieve all comments under `Comments` folder 471 // if it's a comment, then get all comments under it 472 if (!doc.hasSchema(COMMENT_SCHEMA) && session.hasChild(doc.getRef(), COMMENTS_DIRECTORY)) { 473 DocumentModel commentsFolder = session.getChild(doc.getRef(), COMMENTS_DIRECTORY); 474 documentId = commentsFolder.getId(); 475 } 476 477 PageProviderService ppService = Framework.getService(PageProviderService.class); 478 479 Map<String, Serializable> props = Collections.singletonMap(CORE_SESSION_PROPERTY, (Serializable) session); 480 List<SortInfo> sortInfos = singletonList(new SortInfo(COMMENT_CREATION_DATE_PROPERTY, sortAscending)); 481 var pageProvider = (PageProvider<DocumentModel>) ppService.getPageProvider( 482 GET_COMMENTS_FOR_DOCUMENT_PAGE_PROVIDER_NAME, sortInfos, pageSize, currentPageIndex, props, 483 documentId); 484 return new PartialList<>(pageProvider.getCurrentPage(), pageProvider.getResultsCount()); 485 } catch (DocumentNotFoundException dnfe) { 486 return new PartialList<>(emptyList(), 0); 487 } catch (DocumentSecurityException dse) { 488 throw new CommentSecurityException( 489 String.format("The user %s does not have access to the comments of document %s", 490 session.getPrincipal().getName(), documentId), 491 dse); 492 } 493 } 494 495 /** 496 * Manages (Add, Update or Remove) the related text {@link org.nuxeo.ecm.core.schema.FacetNames#HAS_RELATED_TEXT} of 497 * the top level document ancestor {@link #getTopLevelDocumentRef(CoreSession, DocumentRef)} for the given comment / 498 * annotation. Each action of adding, updating or removing the comment / annotation text will call this method, 499 * which allow us to make the right action on the related text of the top level document. 500 * <ul> 501 * <li>Add a new Comment / Annotation will create a separate entry</li> 502 * <li>Update a text Comment / Annotation will update this specific entry</li> 503 * <li>Remove a Comment / Annotation will remove this specific entry</li> 504 * </ul> 505 * 506 * @since 11.1 507 **/ 508 protected void manageRelatedTextOfTopLevelDocument(CoreSession session, DocumentModel topLevelDoc, String commentId, 509 String commentText) { 510 requireNonNull(topLevelDoc, "Top level document is required"); 511 512 // Get the Top level document model (the first document of our comments tree) 513 // which will contains the text of comments / annotations 514 topLevelDoc.addFacet(HAS_RELATED_TEXT); 515 516 // Get the related text id (the related text key is different in the case of Comment or Annotation) 517 String relatedTextId = String.format(COMMENT_RELATED_TEXT_ID, commentId); 518 519 @SuppressWarnings("unchecked") 520 List<Map<String, String>> resources = (List<Map<String, String>>) topLevelDoc.getPropertyValue( 521 RELATED_TEXT_RESOURCES); 522 523 Optional<Map<String, String>> optional = resources.stream() 524 .filter(m -> relatedTextId.equals(m.get(RELATED_TEXT_ID))) 525 .findAny(); 526 527 if (isEmpty(commentText)) { 528 // Remove 529 optional.ifPresent(resources::remove); 530 } else { 531 optional.ifPresentOrElse( // 532 map -> map.put(RELATED_TEXT, commentText), // Update 533 () -> resources.add(Map.of(RELATED_TEXT_ID, relatedTextId, RELATED_TEXT, commentText))); // Creation 534 } 535 536 topLevelDoc.setPropertyValue(RELATED_TEXT_RESOURCES, (Serializable) resources); 537 topLevelDoc.putContextData(DISABLE_NOTIFICATION_SERVICE, TRUE); 538 topLevelDoc.putContextData(VERSIONING_OPTION, NONE); 539 topLevelDoc.putContextData(DISABLE_DUBLINCORE_LISTENER, TRUE); 540 session.saveDocument(topLevelDoc); 541 } 542 543 @Override 544 protected DocumentModel getCommentedDocument(CoreSession session, DocumentModel commentDoc) { 545 // if comment is a reply then its direct parent is the commented document 546 DocumentModel commentedDoc = session.getParentDocument(commentDoc.getRef()); 547 548 // if direct parent is the Comments folder then the commented document is Comments parent 549 if (COMMENT_ROOT_DOC_TYPE.equals(commentedDoc.getType())) { 550 commentedDoc = session.getDocument(commentedDoc.getParentRef()); 551 } 552 return commentedDoc; 553 } 554 555 /** 556 * Returns {@code true} if the document has comments. 557 * 558 * @since 11.1 559 */ 560 protected boolean hasComments(CoreSession session, DocumentModel document) { 561 String query = String.format( // 562 QUERY_GET_COMMENTS_UUID_BY_COMMENT_ANCESTOR, document.getId()); 563 return !session.queryProjection(query, 1, 0).isEmpty(); 564 } 565 566 /** 567 * Returns {@code true} if the documents has comments from the given user. 568 * 569 * @since 11.1 570 */ 571 protected boolean hasComments(CoreSession session, DocumentModel document, String user) { 572 String query = String.format( // 573 QUERY_GET_COMMENTS_UUID_BY_COMMENT_ANCESTOR_AND_AUTHOR, document.getId(), user); 574 return !session.queryProjection(query, 1, 0).isEmpty(); 575 } 576 577 protected void handleNotificationAutoSubscriptions(CoreSession session, DocumentModel topLevelDoc, 578 DocumentModel commentDoc) { 579 if (Framework.getService(ConfigurationService.class).isBooleanFalse(AUTOSUBSCRIBE_CONFIG_KEY)) { 580 log.trace("autosubscription to new comments is disabled"); 581 return; 582 } 583 584 NuxeoPrincipal topLevelDocumentAuthor = getAuthor(topLevelDoc); 585 if (!hasComments(session, topLevelDoc)) { 586 // Document author is subscribed on first comment by anybody 587 subscribeToNotifications(topLevelDoc, topLevelDocumentAuthor); 588 } 589 590 NuxeoPrincipal commentAuthor = getAuthor(commentDoc); 591 if (topLevelDocumentAuthor != null && topLevelDocumentAuthor.equals(commentAuthor)) { 592 // Document author is comment author. He doesn't need to be resubscribed 593 return; 594 } 595 596 if (commentAuthor != null && !hasComments(session, topLevelDoc, commentAuthor.getName())) { 597 // Comment author is writing his first comment on the document 598 subscribeToNotifications(topLevelDoc, commentAuthor); 599 } 600 } 601 602 /** 603 * Subscribes a user to notifications on the document. 604 * 605 * @since 11.1 606 */ 607 protected void subscribeToNotifications(DocumentModel document, NuxeoPrincipal user) { 608 // User may have been deleted 609 if (user == null) { 610 return; 611 } 612 String subscriber = NotificationConstants.USER_PREFIX + user.getName(); 613 NotificationManager notificationManager = Framework.getService(NotificationManager.class); 614 if (notificationManager != null) { 615 notificationManager.addSubscriptions(subscriber, document, false, user); 616 } 617 } 618 619}