001/* 002 * Copyright 2010 Anyware Services 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 017package org.ametys.cms.repository.comment; 018 019import java.time.ZonedDateTime; 020import java.util.ArrayList; 021import java.util.List; 022import java.util.Optional; 023 024import org.apache.commons.lang3.StringUtils; 025 026import org.ametys.cms.repository.ReactionableObject; 027import org.ametys.cms.repository.ReactionableObjectHelper; 028import org.ametys.cms.repository.ReportableObject; 029import org.ametys.cms.repository.ReportableObjectHelper; 030import org.ametys.core.user.UserIdentity; 031import org.ametys.plugins.repository.AmetysRepositoryException; 032import org.ametys.plugins.repository.data.holder.ModifiableModelLessDataHolder; 033import org.ametys.plugins.repository.data.holder.group.ModelLessComposite; 034import org.ametys.plugins.repository.data.holder.group.ModifiableModelLessComposite; 035import org.ametys.plugins.repository.data.repositorydata.ModifiableRepositoryData; 036import org.ametys.runtime.model.ModelItem; 037import org.ametys.runtime.model.type.ModelItemTypeConstants; 038 039/** 040 * A comment on a commentable content 041 */ 042public class Comment implements ReactionableObject, ReportableObject 043{ 044 /** Constants for comments Metadat* */ 045 public static final String METADATA_COMMENTS = "comments"; 046 /** Constants for comments Metadata not validted */ 047 public static final String METADATA_COMMENTS_VALIDATED = "validated"; 048 /** Constants for comments Metadata validated */ 049 public static final String METADATA_COMMENTS_NOTVALIDATED = "not-validated"; 050 051 /** Constants for creation Metadata */ 052 public static final String METADATA_COMMENT_CREATIONDATE = "creation"; 053 /** Constants for author name Metadata */ 054 public static final String METADATA_COMMENT_AUTHORNAME = "author-name"; 055 /** Constants for author email Metadata */ 056 public static final String METADATA_COMMENT_AUTHOREMAIL = "author-email"; 057 /** Constants for author email hidden Metadata */ 058 public static final String METADATA_COMMENT_AUTHOREMAIL_HIDDEN = "author-email-hidden"; 059 /** Constants for author url Metadata */ 060 public static final String METADATA_COMMENT_AUTHORURL = "author-url"; 061 /** Constants for the content Metadata */ 062 public static final String METADATA_COMMENT_CONTENT = "content"; 063 /** Constants for the validated status Metadata */ 064 public static final String METADATA_COMMENT_VALIDATED = "validated"; 065 /** Constants for the is edited status Metadata */ 066 public static final String METADATA_COMMENT_IS_EDITED = "is-edited"; 067 /** Constants for the is deleted status Metadata */ 068 public static final String METADATA_COMMENT_IS_DELETED = "is-deleted"; 069 070 /** Constants for the separator */ 071 public static final String ID_SEPARATOR = "_"; 072 073 /** The content to comment */ 074 protected ModifiableModelLessDataHolder _contentDataHolder; 075 /** The node of the comment */ 076 protected ModifiableModelLessComposite _commentComposite; 077 /** The id of the comment (unique in the content) */ 078 protected String _id; 079 080 /** 081 * Retrieves a comment by its id 082 * @param contentUnversionedDataHolder The unversioned data holder of the content hosting the comment 083 * @param commentId The id of the comment to retrieve 084 * @throws AmetysRepositoryException if an error occurred 085 */ 086 public Comment(ModifiableModelLessDataHolder contentUnversionedDataHolder, String commentId) 087 { 088 _contentDataHolder = contentUnversionedDataHolder; 089 _id = commentId; 090 091 String commentDataPath = getCommentDataPath(_id); 092 _commentComposite = _contentDataHolder.getComposite(commentDataPath); 093 } 094 095 /** 096 * Creates a new comment on the content 097 * @param contentUnversionedDataHolder The unversioned data holder of the content where to add the new comment 098 */ 099 public Comment(ModifiableModelLessDataHolder contentUnversionedDataHolder) 100 { 101 this(contentUnversionedDataHolder, Optional.empty(), Optional.empty()); 102 } 103 104 /** 105 * Creates a new comment on the content, with the given id and creation date 106 * This method allow to create a comment from existing data (ex: data import from archive) 107 * The id is not generated here, the source is trusted. Be careful using this method 108 * @param contentUnversionedDataHolder The unversioned data holder of the content where to add the new comment 109 * @param commentId the comment's id 110 * @param creationDate the comment's creation date 111 */ 112 public Comment(ModifiableModelLessDataHolder contentUnversionedDataHolder, String commentId, ZonedDateTime creationDate) 113 { 114 this(contentUnversionedDataHolder, Optional.ofNullable(commentId), Optional.ofNullable(creationDate)); 115 } 116 117 private Comment(ModifiableModelLessDataHolder contentUnversionedDataHolder, Optional<String> commentId, Optional<ZonedDateTime> creationDate) 118 { 119 _contentDataHolder = contentUnversionedDataHolder; 120 ModifiableModelLessComposite parent = _contentDataHolder.getComposite(METADATA_COMMENTS, true); 121 122 _id = commentId.orElseGet(() -> _getNextCommentName(parent)); 123 _commentComposite = parent.getComposite(_id, true); 124 _commentComposite.setValue(METADATA_COMMENT_CREATIONDATE, creationDate.orElseGet(ZonedDateTime::now)); 125 126 update(); 127 } 128 129 /** 130 * Creates a new sub comment of the comment 131 * @param comment The parent comment 132 */ 133 public Comment(Comment comment) 134 { 135 this(comment, Optional.empty(), Optional.empty()); 136 } 137 138 /** 139 * Creates a new sub comment of the comment, with the given id and creation date 140 * This method allow to create a sub comment from existing data (ex: data import from archive) 141 * The id is not generated here, the source is trusted. Be careful using this method 142 * @param comment The parent comment 143 * @param commentId the sub comment's id 144 * @param creationDate the sub comment's creation date 145 */ 146 public Comment(Comment comment, String commentId, ZonedDateTime creationDate) 147 { 148 this(comment, Optional.ofNullable(commentId), Optional.ofNullable(creationDate)); 149 } 150 151 private Comment(Comment comment, Optional<String> commentId, Optional<ZonedDateTime> creationDate) 152 { 153 _contentDataHolder = comment._contentDataHolder; 154 ModifiableModelLessComposite parent = comment._commentComposite.getComposite(METADATA_COMMENTS, true); 155 156 String commentName = commentId.map(id -> StringUtils.substringAfterLast(id, ID_SEPARATOR)) 157 .orElseGet(() -> _getNextCommentName(parent)); 158 _id = commentId.orElseGet(() -> comment.getId() + ID_SEPARATOR + commentName); 159 160 _commentComposite = parent.getComposite(commentName, true); 161 _commentComposite.setValue(METADATA_COMMENT_CREATIONDATE, creationDate.orElseGet(ZonedDateTime::now)); 162 163 update(); 164 } 165 166 private static String _getNextCommentName(ModelLessComposite composite) 167 { 168 String base = "comment-"; 169 int i = 0; 170 while (composite.hasValueOrEmpty(base + i)) 171 { 172 i++; 173 } 174 175 return base + i; 176 } 177 178 /** 179 * Retrieves the path of the comment with the given identifier 180 * @param commentId the comment identifier 181 * @return the path of the comment 182 */ 183 protected String getCommentDataPath(String commentId) 184 { 185 List<String> commentsPath = new ArrayList<>(); 186 for (String currentCommentId : commentId.split(ID_SEPARATOR)) 187 { 188 commentsPath.add(METADATA_COMMENTS + ModelItem.ITEM_PATH_SEPARATOR + currentCommentId); 189 } 190 191 return StringUtils.join(commentsPath, ModelItem.ITEM_PATH_SEPARATOR); 192 } 193 194 /** 195 * Retrieves the repository data of the {@link Comment} 196 * @return the repository data of the {@link Comment} 197 */ 198 public ModifiableRepositoryData getRepositoryData() 199 { 200 return _commentComposite.getRepositoryData(); 201 } 202 203 /** 204 * The comment id (unique to the content) 205 * @return The id. Cannot be null. 206 */ 207 public String getId() 208 { 209 return _id; 210 } 211 212 /** 213 * Get the date and time the comment was created 214 * @return The non null date of creation of the comment. 215 */ 216 public ZonedDateTime getCreationDate() 217 { 218 return _commentComposite.getValue(METADATA_COMMENT_CREATIONDATE); 219 } 220 221 /** 222 * Get the readable name of the author. 223 * @return The full name. Can be null. 224 */ 225 public String getAuthorName() 226 { 227 return _commentComposite.getValueOfType(METADATA_COMMENT_AUTHORNAME, ModelItemTypeConstants.STRING_TYPE_ID); 228 } 229 230 /** 231 * Set the readable name of the author. 232 * @param name The full name. Can be null to remove the name. 233 */ 234 public void setAuthorName(String name) 235 { 236 _commentComposite.setValue(METADATA_COMMENT_AUTHORNAME, name, ModelItemTypeConstants.STRING_TYPE_ID); 237 } 238 239 /** 240 * Get the email of the author. 241 * @return The ameil. Can be null. 242 */ 243 public String getAuthorEmail() 244 { 245 return _commentComposite.getValueOfType(METADATA_COMMENT_AUTHOREMAIL, ModelItemTypeConstants.STRING_TYPE_ID); 246 } 247 248 /** 249 * Set the email of the author. 250 * @param email The email. Can be null to remove the email. 251 */ 252 public void setAuthorEmail(String email) 253 { 254 _commentComposite.setValue(METADATA_COMMENT_AUTHOREMAIL, email, ModelItemTypeConstants.STRING_TYPE_ID); 255 } 256 257 /** 258 * Get the url given by the author as its personal site url. 259 * @return The url. Can be null. 260 */ 261 public String getAuthorURL() 262 { 263 return _commentComposite.getValueOfType(METADATA_COMMENT_AUTHORURL, ModelItemTypeConstants.STRING_TYPE_ID); 264 } 265 266 /** 267 * Set the personal site url of the author 268 * @param url The url. Can be null to remove url. 269 */ 270 public void setAuthorURL(String url) 271 { 272 _commentComposite.setValue(METADATA_COMMENT_AUTHORURL, url, ModelItemTypeConstants.STRING_TYPE_ID); 273 } 274 275 /** 276 * Does the email of the authors have to be hidden ? 277 * @return true (default value) if the email does not have to appears to others users. Can still be used for administration. 278 */ 279 public boolean isEmailHidden() 280 { 281 return _commentComposite.getValueOfType(METADATA_COMMENT_AUTHOREMAIL_HIDDEN, ModelItemTypeConstants.BOOLEAN_TYPE_ID, true); 282 } 283 /** 284 * Set the email hidden status. 285 * @param hideEmail true to set the email as hidden. 286 */ 287 public void setEmailHiddenStatus(boolean hideEmail) 288 { 289 _commentComposite.setValue(METADATA_COMMENT_AUTHOREMAIL_HIDDEN, hideEmail, ModelItemTypeConstants.BOOLEAN_TYPE_ID); 290 } 291 292 /** 293 * Does the comment is edited 294 * @return true the email is edited 295 */ 296 public boolean isEdited() 297 { 298 return _commentComposite.getValueOfType(METADATA_COMMENT_IS_EDITED, ModelItemTypeConstants.BOOLEAN_TYPE_ID, false); 299 } 300 /** 301 * Set the comment to edited. 302 * @param isEdited true to set the comment to edited. 303 */ 304 public void setEdited(boolean isEdited) 305 { 306 _commentComposite.setValue(METADATA_COMMENT_IS_EDITED, isEdited, ModelItemTypeConstants.BOOLEAN_TYPE_ID); 307 } 308 309 /** 310 * Does the comment is deleted 311 * @return true the comment is deleted 312 */ 313 public boolean isDeleted() 314 { 315 return _commentComposite.getValueOfType(METADATA_COMMENT_IS_DELETED, ModelItemTypeConstants.BOOLEAN_TYPE_ID, false); 316 } 317 /** 318 * Set the comment to deleted. 319 * @param isEdited true to set the comment to deleted. 320 */ 321 public void setDeleted(boolean isEdited) 322 { 323 _commentComposite.setValue(METADATA_COMMENT_IS_DELETED, isEdited, ModelItemTypeConstants.BOOLEAN_TYPE_ID); 324 } 325 326 /** 327 * Get the content of the comment. A simple String (with \n or \t). 328 * @return The content. Can be null. 329 */ 330 public String getContent() 331 { 332 return _commentComposite.getValueOfType(METADATA_COMMENT_CONTENT, ModelItemTypeConstants.STRING_TYPE_ID); 333 } 334 /** 335 * Set the content of the comment. 336 * @param content The content to set. Can be null to remove the content. Have to be a simple String (with \n or \t). 337 */ 338 public void setContent(String content) 339 { 340 _commentComposite.setValue(METADATA_COMMENT_CONTENT, content, ModelItemTypeConstants.STRING_TYPE_ID); 341 } 342 343 /** 344 * Is the comment validated 345 * @return the status of validation of the comment 346 */ 347 public boolean isValidated() 348 { 349 return _commentComposite.getValueOfType(METADATA_COMMENT_VALIDATED, ModelItemTypeConstants.BOOLEAN_TYPE_ID, false); 350 } 351 352 /** 353 * Set the validation status of the comment 354 * @param validated true the comment is validated 355 */ 356 public void setValidated(boolean validated) 357 { 358 _commentComposite.setValue(METADATA_COMMENT_VALIDATED, validated, ModelItemTypeConstants.BOOLEAN_TYPE_ID); 359 update(); 360 } 361 362 public void addReport() 363 { 364 ReportableObjectHelper.addReport(_commentComposite); 365 } 366 367 public void setReportsCount(long reportsCount) 368 { 369 ReportableObjectHelper.setReportsCount(_commentComposite, reportsCount); 370 } 371 372 public void clearReports() 373 { 374 ReportableObjectHelper.clearReports(_commentComposite); 375 } 376 377 public long getReportsCount() 378 { 379 return ReportableObjectHelper.getReportsCount(_commentComposite); 380 } 381 382 /** 383 * Remove the comment. 384 */ 385 public void remove() 386 { 387 String commentDataPath = getCommentDataPath(_id); 388 _contentDataHolder.removeValue(commentDataPath); 389 update(); 390 } 391 392 /** 393 * Get sub comments of the comment 394 * @param includeNotValidatedComments True to include the comments that are not validated 395 * @param includeValidatedComments True to include the comments that are validated 396 * @return the list of comments 397 */ 398 public List<Comment> getSubComment(boolean includeNotValidatedComments, boolean includeValidatedComments) 399 { 400 return getComments(this, includeNotValidatedComments, includeValidatedComments); 401 } 402 403 /** 404 * Create sub comment from this comment 405 * @return the sub comment 406 */ 407 public Comment createSubComment() 408 { 409 return new Comment(this); 410 } 411 412 /** 413 * Creates a sub comment from this comment, with the given id and creation date 414 * This method allow to create a sub comment from existing data (ex: data import from archive) 415 * The id is not generated here, the source is trusted. Be careful using this method 416 * @param commentId the comment's id 417 * @param creationDate the comment's creation date 418 * @return the new comment 419 */ 420 public Comment createSubComment(String commentId, ZonedDateTime creationDate) 421 { 422 return new Comment(this, commentId, creationDate); 423 } 424 425 /** 426 * Update the comment tag statistics 427 */ 428 protected void update() 429 { 430 long validated = 0; 431 long notValidated = 0; 432 433 List<Comment> comments = getComments(_contentDataHolder, true, true, false); 434 435 for (Comment comment : comments) 436 { 437 if (comment.isValidated()) 438 { 439 validated++; 440 } 441 else 442 { 443 notValidated++; 444 } 445 } 446 447 _contentDataHolder.setValue(METADATA_COMMENTS + ModelItem.ITEM_PATH_SEPARATOR + METADATA_COMMENTS_VALIDATED, validated); 448 _contentDataHolder.setValue(METADATA_COMMENTS + ModelItem.ITEM_PATH_SEPARATOR + METADATA_COMMENTS_NOTVALIDATED, notValidated); 449 } 450 451 /** 452 * Get a comment 453 * @param contentUnversionedDataHolder the content data holder 454 * @param commentId The comment identifier 455 * @return The comment 456 * @throws AmetysRepositoryException if the comment does not exist 457 */ 458 public static Comment getComment(ModifiableModelLessDataHolder contentUnversionedDataHolder, String commentId) throws AmetysRepositoryException 459 { 460 return new Comment(contentUnversionedDataHolder, commentId); 461 } 462 463 /** 464 * Get the comments of a content 465 * @param parentComment The parent comment 466 * @param includeNotValidatedComments True to include the comments that are not validated 467 * @param includeValidatedComments True to include the comments that are validated 468 * @return the list of comments 469 * @throws AmetysRepositoryException If an error occurred 470 */ 471 public static List<Comment> getComments(Comment parentComment, boolean includeNotValidatedComments, boolean includeValidatedComments) throws AmetysRepositoryException 472 { 473 return getComments(parentComment, includeNotValidatedComments, includeValidatedComments, false); 474 } 475 476 /** 477 * Get the comments of a content 478 * @param parentComment The parent comment 479 * @param includeNotValidatedComments True to include the comments that are not validated 480 * @param includeValidatedComments True to include the comments that are validated 481 * @param withSubComment true if we want to get all child comments 482 * @return the list of comments 483 * @throws AmetysRepositoryException If an error occurred 484 */ 485 public static List<Comment> getComments(Comment parentComment, boolean includeNotValidatedComments, boolean includeValidatedComments, boolean withSubComment) throws AmetysRepositoryException 486 { 487 ModifiableModelLessComposite parentComposite = parentComment._commentComposite; 488 List<Comment> comments = new ArrayList<>(); 489 490 if (parentComposite.hasValue(METADATA_COMMENTS, org.ametys.plugins.repository.data.type.ModelItemTypeConstants.COMPOSITE_TYPE_ID)) 491 { 492 ModifiableModelLessComposite parentCommentsComposite = parentComposite.getComposite(METADATA_COMMENTS); 493 for (String name : parentCommentsComposite.getDataNames()) 494 { 495 if (METADATA_COMMENTS_NOTVALIDATED.equals(name) || METADATA_COMMENTS_VALIDATED.equals(name)) 496 { 497 continue; 498 } 499 500 String id = parentComment.getId() + ID_SEPARATOR + name; 501 Comment c = new Comment(parentComment._contentDataHolder, id); 502 if (includeNotValidatedComments && !c.isValidated() || includeValidatedComments && c.isValidated()) 503 { 504 comments.add(c); 505 if (withSubComment) 506 { 507 comments.addAll(getComments(c, includeNotValidatedComments, includeValidatedComments, withSubComment)); 508 } 509 } 510 } 511 } 512 513 return comments; 514 } 515 516 /** 517 * Get the comments of a content 518 * @param contentUnversionedDataHolder The content unversioned data holder 519 * @param includeNotValidatedComments True to include the comments that are not validated 520 * @param includeValidatedComments True to include the comments that are validated 521 * @return the list of comments 522 * @throws AmetysRepositoryException If an error occurred 523 */ 524 public static List<Comment> getComments(ModifiableModelLessDataHolder contentUnversionedDataHolder, boolean includeNotValidatedComments, boolean includeValidatedComments) throws AmetysRepositoryException 525 { 526 return getComments(contentUnversionedDataHolder, includeNotValidatedComments, includeValidatedComments, false); 527 } 528 529 /** 530 * Get the comments of a content 531 * @param contentUnversionedDataHolder The content unversioned metadata holder 532 * @param includeNotValidatedComments True to include the comments that are not validated 533 * @param includeValidatedComments True to include the comments that are validated 534 * @param isRecursive true if we want to have sub comments 535 * @return the list of comments 536 * @throws AmetysRepositoryException If an error occurred 537 */ 538 public static List<Comment> getComments(ModifiableModelLessDataHolder contentUnversionedDataHolder, boolean includeNotValidatedComments, boolean includeValidatedComments, boolean isRecursive) throws AmetysRepositoryException 539 { 540 List<Comment> comments = new ArrayList<>(); 541 542 if (contentUnversionedDataHolder.hasValue(METADATA_COMMENTS, org.ametys.plugins.repository.data.type.ModelItemTypeConstants.COMPOSITE_TYPE_ID)) 543 { 544 ModifiableModelLessComposite contentCommentsComposite = contentUnversionedDataHolder.getComposite(METADATA_COMMENTS); 545 for (String name : contentCommentsComposite.getDataNames()) 546 { 547 if (METADATA_COMMENTS_NOTVALIDATED.equals(name) || METADATA_COMMENTS_VALIDATED.equals(name)) 548 { 549 continue; 550 } 551 552 Comment c = new Comment(contentUnversionedDataHolder, name); 553 if (includeNotValidatedComments && !c.isValidated() || includeValidatedComments && c.isValidated()) 554 { 555 comments.add(c); 556 if (isRecursive) 557 { 558 comments.addAll(getComments(c, includeNotValidatedComments, includeValidatedComments, isRecursive)); 559 } 560 } 561 } 562 } 563 564 return comments; 565 } 566 567 @Override 568 public void addReaction(UserIdentity user, ReactionType reactionType) 569 { 570 ReactionableObjectHelper.addReaction(_commentComposite, user, reactionType); 571 } 572 573 @Override 574 public void removeReaction(UserIdentity user, ReactionType reactionType) 575 { 576 ReactionableObjectHelper.removeReaction(_commentComposite, user, reactionType); 577 } 578 579 @Override 580 public List<UserIdentity> getReactionUsers(ReactionType reactionType) 581 { 582 return ReactionableObjectHelper.getReactionUsers(_commentComposite, reactionType); 583 } 584}