001/*
002 *  Copyright 2016 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 */
016package org.ametys.plugins.workspaces.tasks;
017
018import java.io.InputStream;
019import java.time.LocalDate;
020import java.time.ZonedDateTime;
021import java.time.format.DateTimeFormatter;
022import java.util.ArrayList;
023import java.util.Collections;
024import java.util.HashMap;
025import java.util.List;
026import java.util.Map;
027import java.util.Optional;
028import java.util.stream.Collectors;
029
030import org.apache.avalon.framework.service.ServiceException;
031import org.apache.avalon.framework.service.ServiceManager;
032import org.apache.cocoon.servlet.multipart.Part;
033import org.apache.commons.lang.IllegalClassException;
034import org.apache.commons.lang3.StringUtils;
035
036import org.ametys.cms.data.Binary;
037import org.ametys.cms.repository.ReactionableObject.ReactionType;
038import org.ametys.cms.repository.comment.Comment;
039import org.ametys.core.observation.Event;
040import org.ametys.core.right.RightManager.RightResult;
041import org.ametys.core.ui.Callable;
042import org.ametys.core.user.User;
043import org.ametys.core.user.UserIdentity;
044import org.ametys.core.user.UserManager;
045import org.ametys.plugins.explorer.resources.ModifiableResourceCollection;
046import org.ametys.plugins.repository.AmetysObject;
047import org.ametys.plugins.repository.AmetysObjectResolver;
048import org.ametys.plugins.repository.AmetysRepositoryException;
049import org.ametys.plugins.repository.ModifiableTraversableAmetysObject;
050import org.ametys.plugins.workspaces.ObservationConstants;
051import org.ametys.plugins.workspaces.html.HTMLTransformer;
052import org.ametys.plugins.workspaces.members.ProjectMemberManager;
053import org.ametys.plugins.workspaces.project.objects.Project;
054import org.ametys.plugins.workspaces.tags.ProjectTagProviderExtensionPoint;
055import org.ametys.plugins.workspaces.tags.ProjectTagsDAO;
056import org.ametys.plugins.workspaces.tasks.Task.CheckItem;
057import org.ametys.plugins.workspaces.tasks.jcr.JCRTask;
058import org.ametys.plugins.workspaces.tasks.jcr.JCRTaskFactory;
059import org.ametys.plugins.workspaces.tasks.json.TaskJSONHelper;
060
061/**
062 * DAO for interacting with tasks of a project
063 */
064public class WorkspaceTaskDAO extends AbstractWorkspaceTaskDAO
065{
066    /** The Avalon role */
067    public static final String ROLE = WorkspaceTaskDAO.class.getName();
068    
069    /** The ametys object resolver */
070    protected AmetysObjectResolver _resolver;
071
072    /** The HTML transformer */
073    protected HTMLTransformer _htmlTransformer;
074    
075    /** The project member manager */
076    protected ProjectMemberManager _projectMemberManager;
077    
078    /** The tag provider extension point */
079    protected ProjectTagProviderExtensionPoint _tagProviderExtPt;
080    
081    /** The project tags DAO */
082    protected ProjectTagsDAO _projectTagsDAO;
083    
084    /** The user manager */
085    protected UserManager _userManager;
086    
087    /** The task JSON helper */
088    protected TaskJSONHelper _taskJSONHelper;
089    
090    @Override
091    public void service(ServiceManager manager) throws ServiceException
092    {
093        super.service(manager);
094        _resolver = (AmetysObjectResolver) manager.lookup(AmetysObjectResolver.ROLE);
095        _htmlTransformer = (HTMLTransformer) manager.lookup(HTMLTransformer.ROLE);
096        _projectMemberManager = (ProjectMemberManager) manager.lookup(ProjectMemberManager.ROLE);
097        _tagProviderExtPt = (ProjectTagProviderExtensionPoint) manager.lookup(ProjectTagProviderExtensionPoint.ROLE);
098        _projectTagsDAO = (ProjectTagsDAO) manager.lookup(ProjectTagsDAO.ROLE);
099        _userManager = (UserManager) manager.lookup(UserManager.ROLE);
100        _taskJSONHelper = (TaskJSONHelper) manager.lookup(TaskJSONHelper.ROLE);
101    }
102
103    /**
104     * Get the tasks from project
105     * @return the list of tasks
106     * @throws IllegalAccessException If an error occurs when checking the rights
107     */
108    @Callable
109    public List<Map<String, Object>> getTasks() throws IllegalAccessException
110    {
111        String projectName = _getProjectName();
112        
113        ModifiableResourceCollection moduleRoot = _getModuleRoot(projectName);
114        if (!_rightManager.currentUserHasReadAccess(moduleRoot))
115        {
116            throw new IllegalAccessException("User '" + _currentUserProvider.getUser() + "' tried to get tasks without reader right");
117        }
118        
119        List<Map<String, Object>> tasksInfo = new ArrayList<>();
120        Project project = _projectManager.getProject(projectName);
121        for (Task task : getProjectTasks(project))
122        {
123            tasksInfo.add(_taskJSONHelper.taskAsJSON(task, _getSitemapLanguage(), _getSiteName()));
124        }
125        
126        return tasksInfo;
127    }
128    
129    /**
130     * Add a new task to the tasks list
131     * @param tasksListId the tasks list id
132     * @param parameters The task parameters
133     * @param newFiles the files to add
134     * @param newFileNames the file names to add 
135     * @return The task data
136     * @throws IllegalAccessException If an error occurs when checking the rights
137     */
138    @Callable
139    public Map<String, Object> addTask(String tasksListId, Map<String, Object> parameters, List<Part> newFiles, List<String> newFileNames) throws IllegalAccessException
140    {
141        String projectName = _getProjectName();
142        
143        if (StringUtils.isBlank(tasksListId))
144        {
145            throw new IllegalArgumentException("Tasks list id is mandatory to create a new task");
146        }
147        
148        ModifiableTraversableAmetysObject tasksRoot = _getTasksRoot(projectName);
149        
150        // Check user right
151        if (_rightManager.hasRight(_currentUserProvider.getUser(), RIGHTS_HANDLE_TASK, tasksRoot) != RightResult.RIGHT_ALLOW)
152        {
153            throw new IllegalAccessException("User '" + _currentUserProvider.getUser() + "' tried to add task without convenient right [" + RIGHTS_HANDLE_TASK + "]");
154        }
155
156        int index = 1;
157        String name = "task-1";
158        while (tasksRoot.hasChild(name))
159        {
160            index++;
161            name = "task-" + index;
162        }
163        
164        JCRTask task = (JCRTask) tasksRoot.createChild(name, JCRTaskFactory.TASK_NODETYPE);
165        task.setTasksListId(tasksListId);
166        task.setPosition(Long.valueOf(WorkspaceTasksListDAO.getChildTask(tasksListId).size()));
167        
168        ZonedDateTime now = ZonedDateTime.now();
169        task.setCreationDate(now);
170        task.setLastModified(now);
171        task.setAuthor(_currentUserProvider.getUser());
172
173        Map<String, Object> attributesResults = _setTaskAttributes(task, parameters, newFiles, newFileNames, new ArrayList<>());
174        
175        tasksRoot.saveChanges();
176        
177        Map<String, Object> eventParams = new HashMap<>();
178        eventParams.put(ObservationConstants.ARGS_TASK, task);
179        eventParams.put(org.ametys.plugins.explorer.ObservationConstants.ARGS_ID, task.getId());
180        
181        _observationManager.notify(new Event(ObservationConstants.EVENT_TASK_CREATED, _currentUserProvider.getUser(), eventParams));
182
183        Map<String, Object> results = new HashMap<>();
184        results.put("task", _taskJSONHelper.taskAsJSON(task, _getSitemapLanguage(), _getSiteName()));
185        results.putAll(attributesResults);
186        return results;
187    }
188    
189    /**
190     * Edit a task
191     * @param taskId The id of the task to edit
192     * @param parameters The JS parameters
193     * @param newFiles the new file to add
194     * @param newFileNames the file names to add
195     * @param deleteFiles the file to delete
196     * @return The task data
197     * @throws IllegalAccessException If an error occurs when checking the rights
198     */
199    @Callable
200    public Map<String, Object> editTask(String taskId, Map<String, Object> parameters, List<Part> newFiles, List<String> newFileNames, List<String> deleteFiles) throws IllegalAccessException
201    {
202        AmetysObject object = _resolver.resolveById(taskId);
203        if (!(object instanceof JCRTask))
204        {
205            throw new IllegalClassException(JCRTask.class, object.getClass());
206        }
207        
208        // Check user right
209        ModifiableTraversableAmetysObject tasksRoot = object.getParent();
210        if (_rightManager.hasRight(_currentUserProvider.getUser(), RIGHTS_HANDLE_TASK, tasksRoot) != RightResult.RIGHT_ALLOW)
211        {
212            throw new IllegalAccessException("User '" + _currentUserProvider.getUser() + "' tried to edit task without convenient right [" + RIGHTS_HANDLE_TASK + "]");
213        }
214        
215        JCRTask task = (JCRTask) object;
216        Map<String, Object> attributesResults = _setTaskAttributes(task, parameters, newFiles, newFileNames, deleteFiles);
217        
218        ZonedDateTime now = ZonedDateTime.now();
219        task.setLastModified(now);
220            
221        task.saveChanges();
222            
223        Map<String, Object> eventParams = new HashMap<>();
224        eventParams.put(ObservationConstants.ARGS_TASK, task);
225        eventParams.put(org.ametys.plugins.explorer.ObservationConstants.ARGS_ID, taskId);
226        
227        _observationManager.notify(new Event(ObservationConstants.EVENT_TASK_UPDATED, _currentUserProvider.getUser(), eventParams));
228        
229        // Closed status has changed
230        if (attributesResults.containsKey("isClosed"))
231        {
232            _observationManager.notify(new Event(ObservationConstants.EVENT_TASK_CLOSED_STATUS_CHANGED, _currentUserProvider.getUser(), eventParams));
233        }
234
235        // Assigments have changed
236        if (attributesResults.containsKey("changedAssignments"))
237        {
238            _observationManager.notify(new Event(ObservationConstants.EVENT_TASK_ASSIGNED, _currentUserProvider.getUser(), eventParams));
239        }
240        
241         
242        Map<String, Object> results = new HashMap<>();
243        results.put("task", _taskJSONHelper.taskAsJSON(task, _getSitemapLanguage(), _getSiteName()));
244        results.putAll(attributesResults);
245        return results;
246    }
247    
248    /**
249     * Move task to new position
250     * @param tasksListId the tasks list id
251     * @param taskId the task id to move
252     * @param newPosition the new position
253     * @return The task data
254     * @throws IllegalAccessException If an error occurs when checking the rights
255     */
256    @Callable
257    public Map<String, Object> moveTask(String tasksListId, String taskId, long newPosition) throws IllegalAccessException
258    {
259        AmetysObject object = _resolver.resolveById(taskId);
260        if (!(object instanceof Task))
261        {
262            throw new IllegalClassException(Task.class, object.getClass());
263        }
264        
265        // Check user right
266        ModifiableTraversableAmetysObject tasksRoot = object.getParent();
267        if (_rightManager.hasRight(_currentUserProvider.getUser(), RIGHTS_HANDLE_TASK, tasksRoot) != RightResult.RIGHT_ALLOW)
268        {
269            throw new IllegalAccessException("User '" + _currentUserProvider.getUser() + "' tried to move task without convenient right [" + RIGHTS_HANDLE_TASK + "]");
270        }
271        
272        Task task = (Task) object;
273        if (tasksListId != task.getTaskListId())
274        {
275            List<Task> childTasks = WorkspaceTasksListDAO.getChildTask(task.getTaskListId());
276            long position = 0;
277            for (Task childTask : childTasks)
278            {
279                if (!childTask.getId().equals(taskId))
280                {
281                    childTask.setPosition(position);
282                    position++;
283                }
284            }
285        }
286        
287        task.setTasksListId(tasksListId);
288        List<Task> childTasks = WorkspaceTasksListDAO.getChildTask(tasksListId);
289        int size = childTasks.size();
290        if (newPosition > size)
291        {
292            throw new IllegalArgumentException("New position (" + newPosition + ") can't be greater than tasks child size (" + size + ")");
293        }
294        
295        long position = 0;
296        task.setPosition(newPosition);
297        for (Task childTask : childTasks)
298        {
299            if (position == newPosition)
300            {
301                position++;
302            }
303            
304            if (childTask.getId().equals(taskId))
305            {
306                childTask.setPosition(newPosition);
307            }
308            else
309            {
310                childTask.setPosition(position);
311                position++;
312            }
313        }
314        
315        tasksRoot.saveChanges();
316        
317        return _taskJSONHelper.taskAsJSON(task, _getSitemapLanguage(), _getSiteName());
318    }
319    
320    /**
321     * Remove a task
322     * @param taskId the task id to remove
323     * @return The task data
324     * @throws IllegalAccessException If an error occurs when checking the rights
325     */
326    @Callable
327    public Map<String, Object> deleteTask(String taskId) throws IllegalAccessException
328    {
329        AmetysObject object = _resolver.resolveById(taskId);
330        if (!(object instanceof Task))
331        {
332            throw new IllegalClassException(Task.class, object.getClass());
333        }
334        
335        // Check user right
336        ModifiableTraversableAmetysObject tasksRoot = object.getParent();
337        if (_rightManager.hasRight(_currentUserProvider.getUser(), RIGHTS_DELETE_TASK, tasksRoot) != RightResult.RIGHT_ALLOW)
338        {
339            throw new IllegalAccessException("User '" + _currentUserProvider.getUser() + "' tried to delete task without convenient right [" + RIGHTS_DELETE_TASK + "]");
340        }
341        
342        Map<String, Object> results = new HashMap<>();
343        Task jcrTask = (Task) object;
344        
345        Map<String, Object> eventParams = new HashMap<>();
346        eventParams.put(ObservationConstants.ARGS_TASK, jcrTask);
347        eventParams.put(org.ametys.plugins.explorer.ObservationConstants.ARGS_ID, taskId);
348        _observationManager.notify(new Event(ObservationConstants.EVENT_TASK_DELETING, _currentUserProvider.getUser(), eventParams));
349        
350        String tasksListId = jcrTask.getTaskListId();
351        jcrTask.remove();
352        
353        // Reorder tasks position
354        long position = 0;
355        for (Task childTask : WorkspaceTasksListDAO.getChildTask(tasksListId))
356        {
357            childTask.setPosition(position);
358            position++;
359        }
360        
361        tasksRoot.saveChanges();
362
363        eventParams = new HashMap<>();
364        eventParams.put(org.ametys.plugins.explorer.ObservationConstants.ARGS_ID, taskId);
365        _observationManager.notify(new Event(ObservationConstants.EVENT_TASK_DELETED, _currentUserProvider.getUser(), eventParams));
366
367        return results;
368    }
369    
370    /**
371     * Comment a task
372     * @param taskId the task id
373     * @param commentText the comment text
374     * @param authorURL the author URL
375     * @return The task data
376     * @throws IllegalAccessException If an error occurs when checking the rights
377     */
378    @Callable
379    public Map<String, Object> commentTask(String taskId, String commentText, String authorURL) throws IllegalAccessException
380    {
381        AmetysObject object = _resolver.resolveById(taskId);
382        if (!(object instanceof Task))
383        {
384            throw new IllegalClassException(Task.class, object.getClass());
385        }
386        
387        // Check user right
388        ModifiableTraversableAmetysObject tasksRoot = object.getParent();
389        if (_rightManager.hasRight(_currentUserProvider.getUser(), RIGHTS_COMMENT_TASK, tasksRoot) != RightResult.RIGHT_ALLOW)
390        {
391            throw new IllegalAccessException("User '" + _currentUserProvider.getUser() + "' tried to comment task without convenient right [" + RIGHTS_COMMENT_TASK + "]");
392        }
393        
394        Task task = (Task) object;
395        UserIdentity userIdentity = _currentUserProvider.getUser();
396        User user = _userManager.getUser(userIdentity);
397        
398        Comment comment = task.createComment();
399        comment.setAuthorName(user.getFullName());
400        comment.setAuthorEmail(user.getEmail());
401        comment.setEmailHiddenStatus(true);
402        comment.setAuthorURL(authorURL);
403        comment.setContent(commentText);
404        comment.setValidated(true);
405        
406        tasksRoot.saveChanges();
407        
408        return _taskJSONHelper.taskAsJSON(task, _getSitemapLanguage(), _getSiteName());
409    }
410    
411    /**
412     * Edit  a task
413     * @param taskId the task id
414     * @param commentId the comment Id
415     * @param commentText the comment text
416     * @return The task data
417     * @throws IllegalAccessException If an error occurs when checking the rights
418     */
419    @Callable
420    public Map<String, Object> editCommentTask(String taskId, String commentId, String commentText) throws IllegalAccessException
421    {
422        AmetysObject object = _resolver.resolveById(taskId);
423        if (!(object instanceof Task))
424        {
425            throw new IllegalClassException(Task.class, object.getClass());
426        }
427        
428        // Check user right
429        ModifiableTraversableAmetysObject tasksRoot = object.getParent();
430        if (_rightManager.hasRight(_currentUserProvider.getUser(), RIGHTS_COMMENT_TASK, tasksRoot) != RightResult.RIGHT_ALLOW)
431        {
432            throw new IllegalAccessException("User '" + _currentUserProvider.getUser() + "' tried to edit task without convenient right [" + RIGHTS_COMMENT_TASK + "]");
433        }
434        
435        UserIdentity userIdentity = _currentUserProvider.getUser();
436        User user = _userManager.getUser(userIdentity);
437        
438        Task task = (Task) object;
439        Comment comment = task.getComment(commentId);
440        String authorEmail = comment.getAuthorEmail();
441        if (!authorEmail.equals(user.getEmail()))
442        {
443            throw new IllegalAccessException("User '" + userIdentity + "' tried to edit an other user's comment task");
444        }
445
446        if (comment.getContent().equals(commentText))
447        {
448            return _taskJSONHelper.taskAsJSON(task, _getSitemapLanguage(), _getSiteName());
449        }
450        
451        comment.setContent(commentText);
452        comment.setEdited(true);
453        
454        tasksRoot.saveChanges();
455        
456        return _taskJSONHelper.taskAsJSON(task, _getSitemapLanguage(), _getSiteName());
457    }
458    
459    /**
460     * Answer to a task's comment
461     * @param taskId the task id
462     * @param commentId the comment id
463     * @param commentText the comment text
464     * @param authorURL the author URL
465     * @return The task data
466     * @throws IllegalAccessException If an error occurs when checking the rights
467     */
468    @Callable
469    public Map<String, Object> answerCommentTask(String taskId, String commentId, String commentText, String authorURL) throws IllegalAccessException
470    {
471        AmetysObject object = _resolver.resolveById(taskId);
472        if (!(object instanceof Task))
473        {
474            throw new IllegalClassException(Task.class, object.getClass());
475        }
476        
477        // Check user right
478        ModifiableTraversableAmetysObject tasksRoot = object.getParent();
479        if (_rightManager.hasRight(_currentUserProvider.getUser(), RIGHTS_COMMENT_TASK, tasksRoot) != RightResult.RIGHT_ALLOW)
480        {
481            throw new IllegalAccessException("User '" + _currentUserProvider.getUser() + "' tried to comment task without convenient right [" + RIGHTS_COMMENT_TASK + "]");
482        }
483        
484        Task task = (Task) object;
485        Comment comment = task.getComment(commentId);
486
487        UserIdentity userIdentity = _currentUserProvider.getUser();
488        User user = _userManager.getUser(userIdentity);
489        Comment subComment = comment.createSubComment();
490        subComment.setAuthorName(user.getFullName());
491        subComment.setAuthorEmail(user.getEmail());
492        subComment.setEmailHiddenStatus(true);
493        subComment.setAuthorURL(authorURL);
494        subComment.setContent(commentText);
495        subComment.setValidated(true);
496        
497        tasksRoot.saveChanges();
498        
499        return _taskJSONHelper.taskAsJSON(task, _getSitemapLanguage(), _getSiteName());
500    }
501    
502    /**
503     * Delete a task's comment
504     * @param taskId the task id
505     * @param commentId the comment id
506     * @return The task data
507     * @throws IllegalAccessException If an error occurs when checking the rights
508     */
509    @Callable
510    public Map<String, Object> deleteCommentTask(String taskId, String commentId) throws IllegalAccessException
511    {
512        AmetysObject object = _resolver.resolveById(taskId);
513        if (!(object instanceof Task))
514        {
515            throw new IllegalClassException(Task.class, object.getClass());
516        }
517        
518        // Check user right
519        ModifiableTraversableAmetysObject tasksRoot = object.getParent();
520        
521        UserIdentity userIdentity = _currentUserProvider.getUser();
522        User user = _userManager.getUser(userIdentity);
523        
524        Task task = (Task) object;
525        Comment comment = task.getComment(commentId);
526        String authorEmail = comment.getAuthorEmail();
527        if (!authorEmail.equals(user.getEmail()))
528        {
529            if (_rightManager.hasRight(_currentUserProvider.getUser(), RIGHTS_COMMENT_TASK, tasksRoot) != RightResult.RIGHT_ALLOW)
530            {
531                throw new IllegalAccessException("User '" + userIdentity + "' tried to delete an other user's comment task");
532            }
533        }
534        
535        boolean isSubComment = comment.getId().contains(Comment.ID_SEPARATOR);
536        if (isSubComment)
537        {
538            Comment parentComment = task.getComment(StringUtils.substringBeforeLast(comment.getId(), Comment.ID_SEPARATOR));
539            List<Comment> subComments = parentComment.getSubComment(true, true);
540            boolean hasAfterSubComments = subComments.stream()
541                .filter(c -> !c.getId().equals(comment.getId())) //Don't take current sub comment
542                .filter(c -> !c.isDeleted()) // Ignore alreay deleted sub comment
543                .filter(c -> c.getCreationDate().isAfter(comment.getCreationDate())) // Just take sub comment after current comment
544                .findAny()
545                .isPresent();
546            
547            if (hasAfterSubComments)
548            {
549                comment.setDeleted(true);
550            }
551            else
552            {
553                comment.remove();
554            }
555
556            // Sort comment by creation date (Recent creation date in first)
557            List<Comment> currentSubComments = parentComment.getSubComment(true, true);
558            Collections.sort(currentSubComments, (c1, c2) -> 
559            {
560                return c2.getCreationDate().compareTo(c1.getCreationDate());
561            });
562            
563            // Remove already deleted sub comment if no recent sub comment is present 
564            for (Comment subCom : currentSubComments)
565            {
566                if (subCom.isDeleted())
567                {
568                    subCom.remove();
569                }
570                else
571                {
572                    break;
573                }
574            }
575            
576            // Remove parent comment if there are no more sub comment
577            if (parentComment.isDeleted() && parentComment.getSubComment(true, true).isEmpty())
578            {
579                parentComment.remove();
580            }
581        }
582        else
583        {
584            List<Comment> subComment = comment.getSubComment(true, true);
585            boolean hasSubComments = subComment.stream()
586                .filter(c -> !c.isDeleted()) // Ignore alreay deleted sub comment
587                .findAny()
588                .isPresent();
589            if (hasSubComments)
590            {
591                comment.setDeleted(true);
592            }
593            else
594            {
595                comment.remove();
596            }
597        }
598        
599        tasksRoot.saveChanges();
600        
601        return _taskJSONHelper.taskAsJSON(task, _getSitemapLanguage(), _getSiteName());
602    }
603    
604    /**
605     * Like or unlike a task's comment
606     * @param taskId the task id
607     * @param commentId the comment id
608     * @param liked true if the comment is liked, otherwise the comment is unliked
609     * @return The task data
610     * @throws IllegalAccessException If an error occurs when checking the rights
611     */
612    @Callable
613    public Map<String, Object> likeOrUnlikeCommentTask(String taskId, String commentId, Boolean liked) throws IllegalAccessException
614    {
615        AmetysObject object = _resolver.resolveById(taskId);
616        if (!(object instanceof Task))
617        {
618            throw new IllegalClassException(Task.class, object.getClass());
619        }
620        
621        // Check user right
622        ModifiableTraversableAmetysObject tasksRoot = object.getParent();
623        if (_rightManager.hasRight(_currentUserProvider.getUser(), RIGHTS_COMMENT_TASK, tasksRoot) != RightResult.RIGHT_ALLOW)
624        {
625            throw new IllegalAccessException("User '" + _currentUserProvider.getUser() + "' tried to react a comment task without convenient right [" + RIGHTS_COMMENT_TASK + "]");
626        }
627        
628        Task task = (Task) object;
629        Comment comment = task.getComment(commentId);
630        
631        UserIdentity user = _currentUserProvider.getUser();
632        if (Boolean.FALSE.equals(liked)
633            || liked == null && comment.getReactionUsers(ReactionType.LIKE).contains(user))
634        {
635            comment.removeReaction(user, ReactionType.LIKE);
636        }
637        else
638        {
639            comment.addReaction(user, ReactionType.LIKE);
640        }
641        
642        tasksRoot.saveChanges();
643        
644        return _taskJSONHelper.taskAsJSON(task, _getSitemapLanguage(), _getSiteName());
645    }
646    
647    /**
648     * Set task's attributes
649     * @param task The task to edit
650     * @param parameters The JS parameters
651     * @param newFiles the new file to add to the task
652     * @param newFileNames the new file names to add to the task
653     * @param deleteFiles the file to remove from the task
654     * @return the map of results
655     */
656    protected Map<String, Object> _setTaskAttributes(JCRTask task, Map<String, Object> parameters, List<Part> newFiles, List<String> newFileNames, List<String> deleteFiles)
657    {
658        Map<String, Object> results = new HashMap<>();
659        
660        String label = (String) parameters.get(JCRTask.ATTRIBUTE_LABEL);
661        task.setLabel(label);
662
663        String description = (String) parameters.get(JCRTask.ATTRIBUTE_DESCRIPTION);
664        task.setDescription(description);
665        
666        _setTaskDates(task, parameters);
667        _setTaskCloseInfo(task, parameters, results);
668        _setTaskAttachments(task, newFiles, newFileNames, deleteFiles);
669        
670        @SuppressWarnings("unchecked")
671        List<Map<String, Object>> assignmentIds = (List<Map<String, Object>>) parameters.getOrDefault(JCRTask.ATTRIBUTE_ASSIGNMENTS, new ArrayList<>());
672        List<UserIdentity> users = assignmentIds.stream()
673            .map(m -> (String) m.get("id"))
674            .map(UserIdentity::stringToUserIdentity)
675            .collect(Collectors.toList());
676        
677        if (!task.getAssignments().equals(users))
678        {
679            task.setAssignments(users);
680            results.put("changedAssignments", true);
681        }
682        
683        @SuppressWarnings("unchecked")
684        List<Map<String, Object>> checkListItems = (List<Map<String, Object>>) parameters.getOrDefault(JCRTask.ATTRIBUTE_CHECKLIST, new ArrayList<>());
685        List<CheckItem> checkItems = checkListItems.stream()
686            .map(e -> new CheckItem((String) e.get(JCRTask.ATTRIBUTE_CHECKLIST_LABEL), (boolean) e.get(JCRTask.ATTRIBUTE_CHECKLIST_ISCHECKED)))
687            .collect(Collectors.toList());
688        task.setCheckListItem(checkItems);
689        
690        @SuppressWarnings("unchecked")
691        List<Object> tags = (List<Object>) parameters.getOrDefault("tags", new ArrayList<>());
692        List<String> createdTags = new ArrayList<>();
693        List<Map<String, Object>> createdTagsJson = new ArrayList<>();
694        // Tag new tags
695        for (Object tag : tags)
696        {
697            // Tag doesn't exist so create the tag
698            if (tag instanceof Map)
699            {
700                @SuppressWarnings("unchecked")
701                String tagText = (String) ((Map<String, Object>) tag).get("text");
702                List<Map<String, Object>> newTags = _projectTagsDAO.addTags(new String[] {tagText});
703                String newTag = (String) newTags.get(0).get("name");
704                task.tag(newTag);
705                createdTags.add(newTag);
706                createdTagsJson.addAll(newTags);
707            }
708            else
709            {
710                task.tag((String) tag);
711            }
712        }
713        // Untag unused tags
714        for (String tag : task.getTags())
715        {
716            if (!tags.contains(tag) && !createdTags.contains(tag))
717            {
718                task.untag(tag);
719            }
720        }
721        
722        results.put("newTags", createdTagsJson);
723        return results;
724    }
725    
726    private void _setTaskDates(JCRTask task, Map<String, Object> parameters)
727    {
728        String startDateAsStr = (String) parameters.get("startDate");
729        LocalDate startDate = Optional.ofNullable(startDateAsStr)
730                .map(date -> LocalDate.parse(date, DateTimeFormatter.ISO_LOCAL_DATE))
731                .orElse(null);
732        task.setStartDate(startDate);
733        
734        String dueDateAsStr = (String) parameters.get(JCRTask.ATTRIBUTE_DUEDATE);
735        LocalDate dueDate = Optional.ofNullable(dueDateAsStr)
736                .map(date -> LocalDate.parse(date, DateTimeFormatter.ISO_LOCAL_DATE))
737                .orElse(null);
738        task.setDueDate(dueDate);
739    }
740    
741    private void _setTaskCloseInfo(JCRTask task, Map<String, Object> parameters, Map<String, Object> results)
742    {
743        @SuppressWarnings("unchecked")
744        Map<String, Object> closeInfo = (Map<String, Object>) parameters.get("closeInfo");
745        if (closeInfo != null && !task.isClosed())
746        {
747            task.close(true);
748            task.setCloseAuthor(_currentUserProvider.getUser());
749            task.setCloseDate(LocalDate.now());
750
751            results.put("isClosed", true);
752        }
753        else if (closeInfo == null && task.isClosed())
754        {
755            task.close(false);
756            task.setCloseAuthor(null);
757            task.setCloseDate(null);
758
759            results.put("isClosed", false);
760        }
761    }
762    
763    private void _setTaskAttachments(JCRTask task, List<Part> newFiles, List<String> newFileNames, List<String> deleteFiles)
764    {
765        if (!newFiles.isEmpty() || !deleteFiles.isEmpty())
766        {
767            List<Binary> attachments = task.getAttachments()
768                .stream()
769                .filter(b -> !deleteFiles.contains(b.getName()))
770                .collect(Collectors.toList());
771            
772            List<String> fileNames = attachments.stream()
773                    .map(Binary::getFilename)
774                    .collect(Collectors.toList());
775            
776            int i = 0;
777            for (Part newPart : newFiles)
778            {
779                String newName = newFileNames.get(i);
780                fileNames.add(newName);
781                Binary newBinary = _partToBinary(newPart, newName);
782                if (newBinary != null)
783                {
784                    attachments.add(newBinary);
785                }
786                i++;
787            }
788            task.setAttachments(attachments);
789        }
790    }
791    
792    private Binary _partToBinary(Part part, String name)
793    {
794        if (part.isRejected())
795        {
796            getLogger().error("Part {} will not be uploaded because it's rejected", part.getFileName());
797            return null;
798        }
799        
800        try (InputStream is = part.getInputStream()) 
801        {
802            Binary binary = new Binary();
803            
804            binary.setFilename(name);
805            binary.setInputStream(is);
806            binary.setLastModificationDate(ZonedDateTime.now());
807            binary.setMimeType(part.getMimeType());
808            
809            return binary;
810        }
811        catch (Exception e) 
812        {
813            getLogger().error("An error occurred getting binary from part {}", part.getFileName(), e);
814        }
815        
816        return null;
817    }
818    
819    /**
820     * Get all tasks from given projets
821     * @param project the project
822     * @return All tasks as JSON
823     */
824    public List<Task> getProjectTasks(Project project)
825    {
826        TasksWorkspaceModule taskModule = _moduleEP.getModule(TasksWorkspaceModule.TASK_MODULE_ID);
827        ModifiableTraversableAmetysObject tasksRoot = taskModule.getTasksRoot(project, true);
828        return tasksRoot.getChildren()
829            .stream()
830            .filter(Task.class::isInstance)
831            .map(Task.class::cast)
832            .collect(Collectors.toList());
833    }
834    
835    /**
836     * Get the total number of tasks of the project
837     * @param project The project
838     * @return The number of tasks, or null if the module is not activated
839     */
840    public Long getTasksCount(Project project)
841    {
842        return Long.valueOf(getProjectTasks(project).size());
843    }
844    
845    /**
846     * Get project members 
847     * @return the project members
848     * @throws IllegalAccessException if an error occurred
849     * @throws AmetysRepositoryException if an error occurred
850     */
851    @Callable
852    public Map<String, Object> getProjectMembers() throws IllegalAccessException, AmetysRepositoryException 
853    {
854        String projectName = _getProjectName();
855        String lang = _getSitemapLanguage();
856        
857        return _projectMemberManager.getProjectMembers(projectName, lang, true);
858    }
859}