001/* 002 * (C) Copyright 2007-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 * Nuxeo - initial API and implementation 018 */ 019 020package org.nuxeo.ecm.platform.comment.impl; 021 022import static java.util.Objects.requireNonNullElseGet; 023import static java.util.stream.Collectors.collectingAndThen; 024import static java.util.stream.Collectors.toList; 025import static org.nuxeo.ecm.core.io.marshallers.json.document.DocumentModelJsonReader.applyDirtyPropertyValues; 026import static org.nuxeo.ecm.platform.comment.api.CommentConstants.COMMENT_AUTHOR_PROPERTY; 027import static org.nuxeo.ecm.platform.comment.api.CommentConstants.COMMENT_CREATION_DATE_PROPERTY; 028import static org.nuxeo.ecm.platform.comment.api.CommentConstants.COMMENT_DOC_TYPE; 029import static org.nuxeo.ecm.platform.comment.api.CommentConstants.COMMENT_SCHEMA; 030import static org.nuxeo.ecm.platform.comment.api.CommentConstants.COMMENT_TEXT_PROPERTY; 031import static org.nuxeo.ecm.platform.comment.api.ExternalEntityConstants.EXTERNAL_ENTITY_FACET; 032 033import java.text.SimpleDateFormat; 034import java.util.ArrayList; 035import java.util.Calendar; 036import java.util.Collection; 037import java.util.Collections; 038import java.util.Comparator; 039import java.util.Date; 040import java.util.List; 041import java.util.Map; 042import java.util.function.Function; 043import java.util.stream.Stream; 044 045import org.apache.logging.log4j.LogManager; 046import org.apache.logging.log4j.Logger; 047import org.nuxeo.ecm.core.api.CoreInstance; 048import org.nuxeo.ecm.core.api.CoreSession; 049import org.nuxeo.ecm.core.api.DocumentModel; 050import org.nuxeo.ecm.core.api.DocumentRef; 051import org.nuxeo.ecm.core.api.IdRef; 052import org.nuxeo.ecm.core.api.NuxeoException; 053import org.nuxeo.ecm.core.api.PartialList; 054import org.nuxeo.ecm.core.api.PathRef; 055import org.nuxeo.ecm.core.api.PropertyException; 056import org.nuxeo.ecm.core.api.pathsegment.PathSegmentService; 057import org.nuxeo.ecm.platform.comment.api.Comment; 058import org.nuxeo.ecm.platform.comment.api.CommentConverter; 059import org.nuxeo.ecm.platform.comment.api.CommentEvents; 060import org.nuxeo.ecm.platform.comment.api.exceptions.CommentNotFoundException; 061import org.nuxeo.ecm.platform.comment.api.exceptions.CommentSecurityException; 062import org.nuxeo.ecm.platform.comment.service.CommentServiceConfig; 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:glefter@nuxeo.com">George Lefter</a> 076 * @deprecated since 10.3, use {@link PropertyCommentManager} instead. 077 */ 078@Deprecated(since = "10.3") 079public class CommentManagerImpl extends AbstractCommentManager { 080 081 private static final Logger log = LogManager.getLogger(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 s, DocumentModel docModel) throws CommentSecurityException { 098 return doPrivileged(s, docModel.getRepositoryName(), session -> { 099 Map<String, Object> ctxMap = Collections.singletonMap(ResourceAdapter.CORE_SESSION_CONTEXT_KEY, session); 100 RelationManager relationManager = Framework.getService(RelationManager.class); 101 Graph graph = relationManager.getGraph(config.graphName, session); 102 Resource docResource = relationManager.getResource(config.documentNamespace, docModel, ctxMap); 103 if (docResource == null) { 104 throw new NuxeoException("Could not adapt document model to relation resource ; " 105 + "check the service relation adapters configuration"); 106 } 107 108 // FIXME AT: why no filter on the predicate? 109 List<Statement> statementList = graph.getStatements(null, null, docResource); 110 if (graph instanceof JenaGraph) { 111 // XXX AT: BBB for when repository name was not included in the 112 // resource uri 113 Resource oldDocResource = new QNameResourceImpl(config.documentNamespace, docModel.getId()); 114 statementList.addAll(graph.getStatements(null, null, oldDocResource)); 115 } 116 117 List<DocumentModel> commentList = new ArrayList<>(); 118 for (Statement stmt : statementList) { 119 QNameResourceImpl subject = (QNameResourceImpl) stmt.getSubject(); 120 121 DocumentModel commentDocModel = (DocumentModel) relationManager.getResourceRepresentation( 122 config.commentNamespace, subject, ctxMap); 123 if (commentDocModel == null) { 124 // XXX AT: maybe user cannot see the comment 125 log.warn( 126 "Could not adapt comment relation subject to a document model; check the service relation adapters configuration"); 127 continue; 128 } 129 commentList.add(commentDocModel); 130 } 131 132 CommentSorter sorter = new CommentSorter(true); 133 commentList.sort(sorter); 134 135 return commentList; 136 }); 137 } 138 139 @Override 140 @SuppressWarnings("removal") 141 public DocumentModel createComment(DocumentModel docModel, String comment, String author) { 142 CoreSession session = CoreInstance.getCoreSessionSystem(docModel.getRepositoryName()); 143 DocumentModel commentDM = session.createDocumentModel(COMMENT_DOC_TYPE); 144 commentDM.setPropertyValue(COMMENT_TEXT_PROPERTY, comment); 145 commentDM.setPropertyValue(COMMENT_AUTHOR_PROPERTY, author); 146 commentDM.setPropertyValue(COMMENT_CREATION_DATE_PROPERTY, Calendar.getInstance()); 147 commentDM = internalCreateComment(session, docModel, commentDM, null); 148 session.save(); 149 150 return commentDM; 151 } 152 153 @Override 154 public DocumentModel getThreadForComment(DocumentModel comment) throws CommentSecurityException { 155 List<DocumentModel> threads = getDocumentsForComment(comment); 156 if (threads.size() > 0) { 157 DocumentModel thread = threads.get(0); 158 while (thread.getType().equals("Post") || thread.getType().equals(COMMENT_DOC_TYPE)) { 159 thread = getThreadForComment(thread); 160 } 161 return thread; 162 } 163 return null; 164 } 165 166 @Override 167 public DocumentModel createComment(DocumentModel docModel, String comment) { 168 String author = getCurrentUser(docModel); 169 return createComment(docModel, comment, author); 170 } 171 172 /** 173 * If the author property on comment is not set, retrieve the author name from the session 174 * 175 * @param docModel The document model that holds the session id 176 * @param comment The comment to update 177 */ 178 private static String updateAuthor(DocumentModel docModel, DocumentModel comment) { 179 // update the author if not set 180 String author = (String) comment.getProperty("comment", "author"); 181 if (author == null) { 182 log.debug("deprecated use of createComment: the client should set the author property on document"); 183 author = getCurrentUser(docModel); 184 comment.setProperty("comment", "author", author); 185 } 186 return author; 187 } 188 189 @Override 190 public DocumentModel createComment(DocumentModel docModel, DocumentModel comment) throws CommentSecurityException { 191 CoreSession session = CoreInstance.getCoreSessionSystem(docModel.getRepositoryName()); 192 DocumentModel doc = internalCreateComment(session, docModel, comment, null); 193 session.save(); 194 doc.detach(true); 195 return doc; 196 } 197 198 protected DocumentModel internalCreateComment(CoreSession session, DocumentModel docModel, DocumentModel comment, 199 String path) { 200 DocumentModel createdComment; 201 202 createdComment = createCommentDocModel(session, docModel, comment, path); 203 204 RelationManager relationManager = Framework.getService(RelationManager.class); 205 206 Resource commentRes = relationManager.getResource(config.commentNamespace, createdComment, null); 207 208 Resource documentRes = relationManager.getResource(config.documentNamespace, docModel, null); 209 210 if (commentRes == null || documentRes == null) { 211 throw new NuxeoException("Could not adapt document model to relation resource ; " 212 + "check the service relation adapters configuration"); 213 } 214 215 Resource predicateRes = new ResourceImpl(config.predicateNamespace); 216 217 Statement stmt = new StatementImpl(commentRes, predicateRes, documentRes); 218 relationManager.getGraph(config.graphName, session).add(stmt); 219 220 notifyEvent(session, CommentEvents.COMMENT_ADDED, docModel, createdComment); 221 222 return createdComment; 223 } 224 225 private DocumentModel createCommentDocModel(CoreSession mySession, DocumentModel docModel, DocumentModel comment, 226 String path) { 227 228 String domainPath; 229 updateAuthor(docModel, comment); 230 231 String[] pathList = getCommentPathList(comment); 232 233 domainPath = requireNonNullElseGet(path, () -> "/" + docModel.getPath().segment(0)); 234 if (mySession == null) { 235 return null; 236 } 237 238 // TODO GR upgrade this code. It can't work if current user 239 // doesn't have admin rights 240 241 DocumentModel parent = mySession.getDocument(new PathRef(domainPath)); 242 for (String name : pathList) { 243 String pathStr = parent.getPathAsString(); 244 245 PathRef ref = new PathRef(pathStr, name); 246 if (mySession.exists(ref)) { 247 parent = mySession.getDocument(ref); 248 if (!parent.isFolder()) { 249 throw new NuxeoException(parent.getPathAsString() + " is not folderish"); 250 } 251 } else { 252 parent = createHiddenFolder(mySession, pathStr, name); 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 log.debug("created comment with id={}", commentDocModel.getId()); 266 267 return commentDocModel; 268 } 269 270 protected DocumentModel createHiddenFolder(CoreSession session, String parentPath, String name) { 271 DocumentModel dm = session.createDocumentModel(parentPath, name, "HiddenFolder"); 272 dm.setProperty("dublincore", "title", name); 273 dm.setProperty("dublincore", "description", ""); 274 Framework.doPrivileged(() -> dm.setProperty("dublincore", "created", Calendar.getInstance())); 275 DocumentModel parent = session.createDocument(dm); // change variable name to be effectively final 276 setFolderPermissions(session, parent); 277 return parent; 278 } 279 280 private String[] getCommentPathList(DocumentModel comment) { 281 String[] pathList = new String[2]; 282 pathList[0] = COMMENTS_DIRECTORY; 283 284 pathList[1] = dateFormat.format(getCommentTimeStamp(comment)); 285 return pathList; 286 } 287 288 /** 289 * @deprecated if the caller is remote, we cannot obtain the session 290 */ 291 @Deprecated 292 private static String getCurrentUser(DocumentModel target) { 293 CoreSession userSession = target.getCoreSession(); 294 if (userSession == null) { 295 throw new NuxeoException("userSession is null, do not invoke this method when the user is not local"); 296 } 297 return userSession.getPrincipal().getName(); 298 } 299 300 private String getCommentName(DocumentModel target, DocumentModel comment) { 301 String author = (String) comment.getProperty("comment", "author"); 302 if (author == null) { 303 author = getCurrentUser(target); 304 } 305 Date creationDate = getCommentTimeStamp(comment); 306 return "COMMENT-" + author + '-' + timeFormat.format(creationDate.getTime()); 307 } 308 309 private static Date getCommentTimeStamp(DocumentModel comment) { 310 Calendar creationDate; 311 try { 312 creationDate = (Calendar) comment.getProperty("dublincore", "created"); 313 } catch (PropertyException e) { 314 creationDate = null; 315 } 316 if (creationDate == null) { 317 creationDate = Calendar.getInstance(); 318 } 319 return creationDate.getTime(); 320 } 321 322 @Override 323 @SuppressWarnings("removal") 324 public void deleteComment(DocumentModel docModel, DocumentModel comment) { 325 CoreSession session = CoreInstance.getCoreSessionSystem(docModel.getRepositoryName()); 326 DocumentRef ref = comment.getRef(); 327 if (!session.exists(ref)) { 328 throw new NuxeoException("Comment Document does not exist: " + comment.getId()); 329 } 330 331 // fetch top level doc before deleting document 332 DocumentModel topLevelDoc = getTopLevelDocument(session, comment); 333 // finally remove the doc and fire event 334 session.removeDocument(ref); 335 notifyEvent(session, CommentEvents.COMMENT_REMOVED, topLevelDoc, docModel, comment); 336 337 session.save(); 338 } 339 340 @Override 341 @SuppressWarnings("removal") 342 public DocumentModel createComment(DocumentModel docModel, DocumentModel parent, DocumentModel child) { 343 CoreSession session = CoreInstance.getCoreSessionSystem(docModel.getRepositoryName()); 344 DocumentModel parentDocModel = session.getDocument(parent.getRef()); 345 String containerPath = parent.getPath().removeLastSegments(1).toString(); 346 DocumentModel newComment = internalCreateComment(session, parentDocModel, child, containerPath); 347 348 session.save(); 349 return newComment; 350 } 351 352 @Override 353 @SuppressWarnings("removal") 354 public List<DocumentModel> getDocumentsForComment(DocumentModel comment) { 355 return doPrivileged(comment.getCoreSession(), comment.getRepositoryName(), session -> { 356 Map<String, Object> ctxMap = Collections.singletonMap(ResourceAdapter.CORE_SESSION_CONTEXT_KEY, session); 357 RelationManager relationManager = Framework.getService(RelationManager.class); 358 Graph graph = relationManager.getGraph(config.graphName, session); 359 Resource commentResource = relationManager.getResource(config.commentNamespace, comment, ctxMap); 360 if (commentResource == null) { 361 throw new NuxeoException("Could not adapt document model to relation resource ; " 362 + "check the service relation adapters configuration"); 363 } 364 Resource predicate = new ResourceImpl(config.predicateNamespace); 365 366 List<Statement> statementList = graph.getStatements(commentResource, predicate, null); 367 if (graph instanceof JenaGraph) { 368 // XXX AT: BBB for when repository name was not included in the 369 // resource uri 370 Resource oldDocResource = new QNameResourceImpl(config.commentNamespace, comment.getId()); 371 statementList.addAll(graph.getStatements(oldDocResource, predicate, null)); 372 } 373 374 List<DocumentModel> docList = new ArrayList<>(); 375 for (Statement stmt : statementList) { 376 QNameResourceImpl subject = (QNameResourceImpl) stmt.getObject(); 377 DocumentModel docModel = (DocumentModel) relationManager.getResourceRepresentation( 378 config.documentNamespace, subject, ctxMap); 379 if (docModel == null) { 380 log.warn( 381 "Could not adapt comment relation subject to a document model; check the service relation adapters configuration"); 382 continue; 383 } 384 // detach the document as it was loaded by a system session, not the user session. 385 docModel.detach(true); 386 docList.add(docModel); 387 } 388 return docList; 389 }); 390 } 391 392 /** 393 * @since 11.1 394 */ 395 protected List<DocumentModel> doPrivileged(CoreSession session, String repositoryName, 396 Function<CoreSession, List<DocumentModel>> function) { 397 // the comment may be detached therefore without a session. 398 if (session == null) { 399 return CoreInstance.doPrivileged(repositoryName, function); 400 } 401 return CoreInstance.doPrivileged(session, function); 402 } 403 404 @Override 405 public DocumentModel createLocatedComment(DocumentModel docModel, DocumentModel comment, String path) 406 throws CommentSecurityException { 407 CoreSession session = CoreInstance.getCoreSessionSystem(docModel.getRepositoryName()); 408 DocumentModel createdComment = internalCreateComment(session, docModel, comment, path); 409 session.save(); 410 return createdComment; 411 } 412 413 @Override 414 public Comment createComment(CoreSession session, Comment comment) 415 throws CommentNotFoundException, CommentSecurityException { 416 DocumentRef commentRef = new IdRef(comment.getParentId()); 417 if (!session.exists(commentRef)) { 418 throw new CommentNotFoundException("The document " + comment.getParentId() + " does not exist."); 419 } 420 DocumentModel docToComment = session.getDocument(commentRef); 421 DocumentModel commentModel = session.createDocumentModel(COMMENT_DOC_TYPE); 422 commentModel.setPropertyValue("dc:created", Calendar.getInstance()); 423 424 fillCommentForCreation(session, comment); 425 426 if (comment.getDocument().hasFacet(EXTERNAL_ENTITY_FACET)) { 427 commentModel.addFacet(EXTERNAL_ENTITY_FACET); 428 } 429 applyDirtyPropertyValues(comment.getDocument(), commentModel); 430 431 DocumentModel createdCommentModel = createComment(docToComment, commentModel); 432 return createdCommentModel.getAdapter(Comment.class); 433 } 434 435 @Override 436 public Comment getComment(CoreSession session, String commentId) 437 throws CommentNotFoundException, CommentSecurityException { 438 DocumentRef commentRef = new IdRef(commentId); 439 if (!session.exists(commentRef)) { 440 throw new CommentNotFoundException("The document " + commentId + " does not exist."); 441 } 442 DocumentModel commentModel = session.getDocument(commentRef); 443 return commentModel.getAdapter(Comment.class); 444 } 445 446 @Override 447 public PartialList<Comment> getComments(CoreSession s, String documentId, Long pageSize, Long currentPageIndex, 448 boolean sortAscending) throws CommentSecurityException { 449 return CoreInstance.doPrivileged(s, session -> { 450 DocumentRef docRef = new IdRef(documentId); 451 if (!session.exists(docRef)) { 452 return new PartialList<Comment>(Collections.emptyList(), 0); // NOSONAR 453 } 454 DocumentModel commentedDoc = session.getDocument(docRef); 455 // do a dummy implementation of pagination for former comment manager implementation 456 List<DocumentModel> comments = getComments(commentedDoc); 457 long maxSize = pageSize == null || pageSize <= 0 ? comments.size() : pageSize; 458 long offset = currentPageIndex == null || currentPageIndex <= 0 ? 0 : currentPageIndex * pageSize; 459 return comments.stream() 460 .sorted(Comparator.comparing(doc -> (Calendar) doc.getPropertyValue("dc:created"))) 461 .skip(offset) 462 .limit(maxSize) 463 .map(doc -> doc.getAdapter(Comment.class)) 464 .collect(collectingAndThen(toList(), list -> new PartialList<>(list, comments.size()))); 465 }); 466 } 467 468 @Override 469 public List<Comment> getComments(CoreSession session, Collection<String> documentIds) { 470 return documentIds.stream().flatMap(docId -> streamAllComments(session, docId)).collect(toList()); 471 } 472 473 protected Stream<Comment> streamAllComments(CoreSession session, String docId) { 474 return getComments(session, 475 docId).stream().flatMap(c -> Stream.concat(Stream.of(c), streamAllComments(session, c.getId()))); 476 } 477 478 @Override 479 public Comment updateComment(CoreSession session, String commentId, Comment comment) throws NuxeoException { 480 throw new UnsupportedOperationException("Update a comment is not possible through this implementation"); 481 } 482 483 @Override 484 public void deleteComment(CoreSession s, String commentId) 485 throws CommentNotFoundException, CommentSecurityException { 486 DocumentRef commentRef = new IdRef(commentId); 487 CoreInstance.doPrivileged(s, session -> { 488 if (!session.exists(commentRef)) { 489 throw new CommentNotFoundException("The comment " + commentId + " does not exist."); 490 } 491 492 DocumentModel commentDocModel = session.getDocument(commentRef); 493 DocumentModel commentedDocModel = getDocumentsForComment(commentDocModel).get(0); 494 deleteComment(commentedDocModel, commentDocModel); 495 }); 496 } 497 498 @Override 499 public Comment getExternalComment(CoreSession session, String documentId, String entityId) throws NuxeoException { 500 throw new UnsupportedOperationException( 501 "Get a comment from its external entity id is not possible through this implementation"); 502 } 503 504 @Override 505 public Comment updateExternalComment(CoreSession session, String documentId, String entityId, Comment comment) 506 throws NuxeoException { 507 throw new UnsupportedOperationException( 508 "Update a comment from its external entity id is not possible through this implementation"); 509 } 510 511 @Override 512 public void deleteExternalComment(CoreSession session, String documentId, String entityId) throws NuxeoException { 513 throw new UnsupportedOperationException( 514 "Delete a comment from its external entity id is not possible through this implementation"); 515 } 516 517 @Override 518 public boolean hasFeature(Feature feature) { 519 switch (feature) { 520 case COMMENTS_LINKED_WITH_PROPERTY: 521 case COMMENTS_ARE_SPECIAL_CHILDREN: 522 return false; 523 default: 524 throw new UnsupportedOperationException(feature.name()); 525 } 526 } 527 528 @Override 529 protected DocumentModel getTopLevelDocument(CoreSession s, DocumentModel commentDoc) { 530 return CoreInstance.doPrivileged(s, session -> { 531 DocumentModel documentModel = commentDoc; 532 while (documentModel != null && documentModel.hasSchema(COMMENT_SCHEMA)) { 533 List<DocumentModel> ancestors = getDocumentsForComment(documentModel); 534 documentModel = ancestors.isEmpty() ? null : ancestors.get(0); 535 } 536 return documentModel; 537 }); 538 } 539 540 @Override 541 protected DocumentModel getCommentedDocument(CoreSession session, DocumentModel commentDoc) { 542 throw new UnsupportedOperationException(); 543 } 544}