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