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}