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}