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.explorer.tasks.jcr;
017
018import java.io.ByteArrayInputStream;
019import java.io.IOException;
020import java.time.LocalDate;
021import java.time.format.DateTimeFormatter;
022import java.util.ArrayList;
023import java.util.Date;
024import java.util.HashMap;
025import java.util.HashSet;
026import java.util.List;
027import java.util.Map;
028import java.util.Set;
029import java.util.stream.Collectors;
030
031import javax.jcr.NodeIterator;
032import javax.jcr.RepositoryException;
033import javax.jcr.Session;
034import javax.jcr.query.Query;
035
036import org.apache.avalon.framework.component.Component;
037import org.apache.avalon.framework.service.ServiceException;
038import org.apache.avalon.framework.service.ServiceManager;
039import org.apache.avalon.framework.service.Serviceable;
040import org.apache.cocoon.ProcessingException;
041import org.apache.commons.io.IOUtils;
042import org.apache.commons.lang.IllegalClassException;
043import org.apache.commons.lang3.BooleanUtils;
044import org.apache.commons.lang3.StringUtils;
045
046import org.ametys.core.observation.Event;
047import org.ametys.core.observation.ObservationManager;
048import org.ametys.core.right.RightManager;
049import org.ametys.core.right.RightManager.RightResult;
050import org.ametys.core.ui.Callable;
051import org.ametys.core.user.CurrentUserProvider;
052import org.ametys.core.user.UserIdentity;
053import org.ametys.core.user.UserManager;
054import org.ametys.core.util.DateUtils;
055import org.ametys.plugins.core.user.UserHelper;
056import org.ametys.plugins.explorer.ModifiableExplorerNode;
057import org.ametys.plugins.explorer.ObservationConstants;
058import org.ametys.plugins.explorer.resources.ModifiableResourceCollection;
059import org.ametys.plugins.explorer.resources.actions.ExplorerResourcesDAO;
060import org.ametys.plugins.explorer.tasks.ModifiableTask;
061import org.ametys.plugins.explorer.tasks.Task;
062import org.ametys.plugins.explorer.tasks.Task.TaskPriority;
063import org.ametys.plugins.explorer.tasks.Task.TaskStatus;
064import org.ametys.plugins.repository.AmetysObject;
065import org.ametys.plugins.repository.AmetysObjectResolver;
066import org.ametys.plugins.repository.AmetysRepositoryException;
067import org.ametys.plugins.repository.ModifiableTraversableAmetysObject;
068import org.ametys.plugins.repository.jcr.JCRAmetysObject;
069import org.ametys.plugins.repository.metadata.ModifiableRichText;
070import org.ametys.plugins.repository.metadata.RichText;
071import org.ametys.plugins.repository.query.SortCriteria;
072import org.ametys.plugins.repository.query.expression.AndExpression;
073import org.ametys.plugins.repository.query.expression.Expression;
074import org.ametys.plugins.repository.query.expression.Expression.Operator;
075import org.ametys.plugins.repository.query.expression.OrExpression;
076import org.ametys.plugins.repository.query.expression.StringExpression;
077import org.ametys.runtime.parameter.ParameterHelper;
078import org.ametys.runtime.plugin.component.AbstractLogEnabled;
079
080/**
081 * DAO for interacting with JCRTasks
082 */
083public class JCRTasksDAO  extends AbstractLogEnabled implements Serviceable, Component
084{
085    /** Avalon Role */
086    public static final String ROLE = JCRTasksDAO.class.getName();
087    
088    /** Rights to view the tasks */
089    public static final String RIGHTS_VIEW_TASKS = "Plugin_Explorer_Task_View";
090    
091    /** Rights to add a task */
092    public static final String RIGHTS_ADD_TASK = "Plugin_Explorer_Task_Add";
093    
094    /** Rights to edit a task */
095    public static final String RIGHTS_EDIT_TASK = "Plugin_Explorer_Task_Edit";
096    
097    /** Rights to delete a task */
098    public static final String RIGHTS_DELETE_TASK = "Plugin_Explorer_Task_Delete";
099    
100    /** Rights to delete_all the tasks */
101    public static final String RIGHTS_DELETE_ALL_TASK = "Plugin_Explorer_Task_Delete_All";
102    
103    /** Ametys object resolver */
104    protected AmetysObjectResolver _resolver;
105
106    /** DAO for the explorer resources */
107    protected ExplorerResourcesDAO _explorerResourcesDAO;
108
109    /** Current user provider */
110    protected CurrentUserProvider _currentUserProvider;
111
112    /** The user manager */
113    protected UserManager _userManager;
114    
115    /** The observation manager */
116    protected ObservationManager _observationManager;
117    
118    /** The rights manager */
119    protected RightManager _rightManager;
120    /** The user helper */
121    protected UserHelper _userHelper;
122    
123
124    public void service(ServiceManager manager) throws ServiceException
125    {
126        _explorerResourcesDAO = (ExplorerResourcesDAO) manager.lookup(ExplorerResourcesDAO.ROLE);
127        _resolver = (AmetysObjectResolver) manager.lookup(AmetysObjectResolver.ROLE);
128        _currentUserProvider = (CurrentUserProvider) manager.lookup(CurrentUserProvider.ROLE);
129        _userManager = (UserManager) manager.lookup(UserManager.ROLE);
130        _userHelper = (UserHelper) manager.lookup(UserHelper.ROLE);
131        _observationManager = (ObservationManager) manager.lookup(ObservationManager.ROLE);
132        _rightManager = (RightManager) manager.lookup(RightManager.ROLE);
133    }
134    
135    /**
136     * Add a new task
137     * @param parentId The parent node id
138     * @param parameters The task parameters
139     * @return The task data
140     * @throws IllegalAccessException If an error occurs when checking the rights
141     */
142    @Callable
143    public Map<String, Object> addTask(String parentId, Map<String, Object> parameters) throws IllegalAccessException
144    {
145        Map<String, Object> result = new HashMap<>();
146        
147        AmetysObject object = _resolver.resolveById(parentId);
148        if (!(object instanceof ModifiableResourceCollection || object instanceof JCRTasksList))
149        {
150            throw new IllegalClassException(ModifiableResourceCollection.class, object.getClass());
151        }
152        
153        ModifiableTraversableAmetysObject parent = (ModifiableTraversableAmetysObject) object;
154        
155        // Check user right
156        _explorerResourcesDAO.checkUserRight(object, RIGHTS_ADD_TASK);
157        
158        if (!_explorerResourcesDAO.checkLock(parent))
159        {
160            getLogger().warn("User '" + _currentUserProvider.getUser() + "' try to modify task list '" + object.getName() + "' but it is locked by another user");
161            result.put("message", "locked");
162            return result;
163        }
164        
165        int index = 1;
166        String name = "task-1";
167        while (parent.hasChild(name))
168        {
169            index++;
170            name = "task-" + index;
171        }
172        
173        JCRTask task = parent.createChild(name, JCRTaskFactory.TASK_NODETYPE);
174        
175        _setTaskParameters(task, parameters, index, true);
176        
177        Date now = new Date();
178        task.setCreationDate(now);
179        task.setLastModified(now);
180        task.setAuthor(_currentUserProvider.getUser());
181        parent.saveChanges();
182        
183        result.put("task", getTask(task, false));
184        
185        // Notify listeners
186        Map<String, Object> eventParams = new HashMap<>();
187        eventParams.put(ObservationConstants.ARGS_TASK, task);
188        eventParams.put(ObservationConstants.ARGS_ID, task.getId());
189        _observationManager.notify(new Event(ObservationConstants.EVENT_TASK_CREATED, _currentUserProvider.getUser(), eventParams));
190        
191        return result;
192    }
193    
194
195    /**
196     * Edit a task
197     * @param id The id of the task to edit
198     * @param parameters The task parameters
199     * @return The task data
200     * @throws IllegalAccessException If an error occurs when checking the rights
201     */
202    @Callable
203    public Map<String, Object> editTask(String id, Map<String, Object> parameters) throws IllegalAccessException
204    {
205        Map<String, Object> result = new HashMap<>();
206        
207        AmetysObject object = _resolver.resolveById(id);
208        if (!(object instanceof JCRTask))
209        {
210            throw new IllegalClassException(ModifiableResourceCollection.class, object.getClass());
211        }
212        
213        JCRTask task = (JCRTask) object;
214        
215        // Check user right
216        if (!_currentUserProvider.getUser().equals(task.getAuthor()))
217        {
218            _explorerResourcesDAO.checkUserRight(object.getParent(), RIGHTS_EDIT_TASK);
219        }
220        
221        if (!_explorerResourcesDAO.checkLock(task))
222        {
223            getLogger().warn("User '" + _currentUserProvider.getUser() + "' try to modify task '" + object.getName() + "' but it is locked by another user");
224            result.put("message", "locked");
225            return result;
226        }
227        
228        Map<String, Object> updatedValues = _setTaskParameters(task, parameters, null, parameters.containsKey("description"));
229        
230        Date now = new Date();
231        task.setLastModified(now);
232        task.saveChanges();
233        
234        result.put("task", getTask(task, false));
235        
236        // Notify listeners
237        Map<String, Object> eventParams = new HashMap<>();
238        eventParams.put(ObservationConstants.ARGS_TASK, task);
239        eventParams.put(ObservationConstants.ARGS_ID, task.getId());
240        
241        if (updatedValues.size() == 1 && updatedValues.containsKey("status"))
242        {
243            eventParams.put("status", updatedValues.get("status"));
244            _observationManager.notify(new Event(ObservationConstants.EVENT_TASK_STATUS_CHANGED, _currentUserProvider.getUser(), eventParams));
245        }
246        else
247        {
248            _observationManager.notify(new Event(ObservationConstants.EVENT_TASK_UPDATED, _currentUserProvider.getUser(), eventParams));
249        }
250        
251        return result;
252    }
253
254    /**
255     * Set task's properties
256     * @param task The task to edit
257     * @param parameters The JS parameters
258     * @param index The index of task
259     * @param setDescription <code>true</code> to edit description
260     * @return The map of updated values
261     */
262    @SuppressWarnings("unchecked")
263    protected Map<String, Object> _setTaskParameters(JCRTask task, Map<String, Object> parameters, Integer index, boolean setDescription)
264    {
265        Map<String, Object> updatedValues = new HashMap<>();
266        
267        String label = (String) parameters.get("title");
268        if (!_isTaskParameterEquals(label, task.getLabel(), "label", updatedValues))
269        {
270            task.setLabel(label);
271        }
272        
273        if (index != null)
274        {
275            task.setTaskId(Integer.toString(index));
276        }
277        
278        if (setDescription)
279        {
280            String description = (String) parameters.getOrDefault("description", StringUtils.EMPTY);
281            setTaskDescription(task, description);
282        }
283        
284        String startDateAsStr = (String) parameters.getOrDefault("start", null);
285        Date startDate = null;
286        if (startDateAsStr != null)
287        {
288            LocalDate localDate = LocalDate.parse(startDateAsStr, DateTimeFormatter.ISO_LOCAL_DATE);
289            startDate = DateUtils.asDate(localDate);
290        }
291        if (!_isTaskParameterEquals(startDate, task.getStartDate(), "startDate", updatedValues))
292        {
293            task.setStartDate(startDate);
294        }
295        
296        String endDateAsStr = (String) parameters.getOrDefault("end", null);
297        Date endDate = null;
298        if (endDateAsStr != null)
299        {
300            LocalDate localDate = LocalDate.parse(endDateAsStr, DateTimeFormatter.ISO_LOCAL_DATE);
301            endDate = DateUtils.asDate(localDate);
302        }
303        if (!_isTaskParameterEquals(endDate, task.getEndDate(), "endDate", updatedValues))
304        {
305            task.setEndDate(endDate);
306        }
307        
308        TaskStatus status = TaskStatus.createsFromString((String) parameters.getOrDefault("status", null));
309        if (!_isTaskParameterEquals(status, task.getStatus(), "status", updatedValues))
310        {
311            task.setStatus(status);
312        }
313        
314        TaskPriority priority = TaskPriority.createsFromString((String) parameters.getOrDefault("priority", null));
315        if (!_isTaskParameterEquals(priority, task.getPriority(), "priority", updatedValues))
316        {
317            task.setPriority(priority);
318        }
319        
320        Object initialLoad = parameters.getOrDefault("load", null);
321        if (initialLoad != null)
322        {
323            if (initialLoad instanceof Integer)
324            {
325                task.setInitialLoad(new Double((Integer) initialLoad));
326            }
327            else if (initialLoad instanceof Double)
328            {
329                task.setInitialLoad((Double) initialLoad);
330            }
331        }
332        
333        Double progress = ((Integer) parameters.get("progress")).doubleValue();
334        if (!_isTaskParameterEquals(progress, task.getProgress(), "progress", updatedValues))
335        {
336            task.setProgress(progress);
337        }
338        
339        List<String> assignment = (List<String>) parameters.getOrDefault("assignmentIds", new ArrayList<>());
340        task.setAssignment(assignment.stream().map(user -> UserIdentity.stringToUserIdentity(user)).collect(Collectors.toList()));
341        
342        List<String> subscribers = (List<String>) parameters.getOrDefault("subscribersIds", new ArrayList<>());
343        task.setSubscribers(subscribers.stream().map(user -> UserIdentity.stringToUserIdentity(user)).collect(Collectors.toList()));
344        
345        return updatedValues;
346    }
347    
348    private boolean _isTaskParameterEquals(Object newValue, Object oldValue, String valueName, Map<String, Object> updatedValues)
349    {
350        if ((newValue != null && !newValue.equals(oldValue)) || (newValue == null && oldValue != null))
351        {
352            updatedValues.put(valueName, newValue);
353            return false;
354        }
355        
356        return true;
357    }
358
359    /**
360     * Update the description of a task
361     * @param task The task to update
362     * @param description The description as string
363     */
364    protected void setTaskDescription(ModifiableTask task, String description)
365    {
366        try
367        {
368            ModifiableRichText richText = task.getDescription();
369            
370            richText.setMimeType("text/plain");
371            richText.setLastModified(new Date());
372            richText.setInputStream(new ByteArrayInputStream(description.getBytes("UTF-8")));
373        }
374        catch (IOException e)
375        {
376            throw new AmetysRepositoryException("Failed to set task's description as rich text", e);
377        }
378    }
379    
380    /**
381     * Get the description of a task as a String
382     * @param task the task
383     * @return The content as String
384     * @throws AmetysRepositoryException if failed to parse description
385     */
386    protected String getTaskDescription(Task task) throws AmetysRepositoryException
387    {
388        try
389        {
390            RichText richText = task.getDescription();
391            return IOUtils.toString(richText.getInputStream(), "UTF-8");
392        }
393        catch (IOException e)
394        {
395            throw new AmetysRepositoryException("Failed to get task's description", e);
396        }
397    }
398    
399    /**
400     * Get the description of a task  to edit as a String
401     * @param task the task
402     * @return The content as String
403     * @throws AmetysRepositoryException if failed to parse description
404     */
405    protected String getTaskDescriptionForEdition(Task task) throws AmetysRepositoryException
406    {
407        return getTaskDescription(task);
408    }
409    
410    /**
411     * Get the data of a task
412     * @param taskId the task id
413     * @param isEdition true to get the task in edit mode
414     * @return The task data
415     */
416    @Callable
417    public Map<String, Object> getTask(String taskId, boolean isEdition)
418    {
419        Task task = _resolver.resolveById(taskId);
420        return getTask(task, isEdition);
421    }
422    
423    /**
424     * Get the list of tasks
425     * @param parentIds The tasks parents
426     * @param assignedToUser Filter only the tasks assigned to the current user
427     * @param userSubscribed Filter only the tasks for which current user subscribed
428     * @param offset Offset the list of results
429     * @param limit  The maximum number of results to return
430     * @param filter Return only tasks matching the filter
431     * @param orderBy Order the list by this property. Default to the creation date
432     * @param orderAsc Sort the list by order ascendant or descendant. Default to ascendant. 
433     * @return The list as a {@link TaskListResult} object
434     * @throws ProcessingException If an error occurred
435     */
436    public TaskListResult getTaskList(List<String> parentIds, boolean assignedToUser, boolean userSubscribed, Integer offset, Integer limit, String filter, String orderBy, Boolean orderAsc) throws ProcessingException
437    {
438        List<Task> tasksList = new ArrayList<>();
439        List<Expression> andExprs = new ArrayList<>();
440
441        UserIdentity currentUser = _currentUserProvider.getUser();
442        if (currentUser == null && (assignedToUser || userSubscribed))
443        {
444            // Not authenticated, but looking for tasks assigned or subscribed to the current user.
445            return new TaskListResult(tasksList, 0);
446        }
447        
448        _handleAssignedOrSubscribedExprs(andExprs, assignedToUser, userSubscribed, currentUser);
449        
450        if (StringUtils.isNotEmpty(filter))
451        {
452            StringExpression labelExpr = new StringExpression(JCRTask.METADATA_LABEL, Operator.WD, filter);
453            StringExpression taskIdExpr = new StringExpression(JCRTask.METADATA_TASKID, Operator.WD, filter);
454            andExprs.add(new OrExpression(labelExpr, taskIdExpr));
455        }
456        
457        SortCriteria sortCriteria = new SortCriteria();
458        String realOrderBy = StringUtils.defaultIfEmpty(orderBy, JCRTask.METADATA_CREATIONDATE);
459        boolean realOrderAsc = BooleanUtils.isNotFalse(orderAsc);
460        sortCriteria.addCriterion(realOrderBy, realOrderAsc, true);
461        
462        long size = 0;
463        try
464        {
465            for (String parentId : parentIds)
466            {
467                JCRAmetysObject object = _resolver.resolveById(parentId);
468                
469                if (_rightManager.hasRight(_currentUserProvider.getUser(), RIGHTS_VIEW_TASKS, object) == RightResult.RIGHT_ALLOW)
470                {
471                    Expression expr = andExprs.size() > 0 ? new AndExpression(andExprs.toArray(new Expression[andExprs.size()])) : null;
472                    String xPathQuery = getTasksXpathQuery(object.getNode().getPath(), expr, sortCriteria);
473                    
474                    Session session = object.getNode().getSession();
475                    
476                    @SuppressWarnings("deprecation")
477                    Query query = session.getWorkspace().getQueryManager().createQuery(xPathQuery, Query.XPATH);
478                    size += query.execute().getNodes().getSize();
479                    if (offset != null)
480                    {
481                        query.setOffset(offset);
482                    }
483                    if (limit != null)
484                    {
485                        query.setLimit(limit);
486                    }
487                    NodeIterator nodes = query.execute().getNodes();
488                    while (nodes.hasNext())
489                    {
490                        tasksList.add(_resolver.resolve(nodes.nextNode(), false));
491                    }
492                }
493            }
494        }
495        catch (RepositoryException e)
496        {
497            throw new ProcessingException("Unable to retrieve the list of tasks", e);
498        }
499        
500        // FIXME INTRANET-196
501        // tasks are not sorted and limit is wrong in case of multiple parent ids
502        return new TaskListResult(tasksList, Math.toIntExact(size));
503    }
504
505    private void _handleAssignedOrSubscribedExprs(List<Expression> andExprs, boolean assignedToUser, boolean userSubscribed, UserIdentity currentUser)
506    {
507        if (assignedToUser || userSubscribed)
508        {
509            List<Expression> exprs = new ArrayList<>();
510            
511            if (assignedToUser)
512            {
513                exprs.add(new StringExpression(JCRTask.METADATA_ASSIGNMENT, Operator.EQ, UserIdentity.userIdentityToString(currentUser)));
514            }
515            
516            if (userSubscribed)
517            {
518                exprs.add(new StringExpression(JCRTask.METADATA_SUBSCRIBERS, Operator.EQ, UserIdentity.userIdentityToString(currentUser)));
519            }
520            
521            andExprs.add(new OrExpression(exprs.toArray(new Expression[exprs.size()])));
522        }
523    }
524    
525    /**
526     * Creates the XPath query corresponding to specified {@link Expression}.
527     * @param rootPath the path to the node containing the tasks
528     * @param tasksExpression the query predicates.
529     * @param sortCriteria the sort criteria.
530     * @return the created XPath query.
531     * @throws RepositoryException if an error occurred
532     */
533    public static String getTasksXpathQuery(String rootPath, Expression tasksExpression, SortCriteria sortCriteria) throws RepositoryException
534    {
535        String predicats = null;
536        
537        if (tasksExpression != null)
538        {
539            predicats = StringUtils.trimToNull(tasksExpression.build());
540        }
541        
542        String xpathQuery = "/jcr:root"
543            + rootPath
544            + "//element(*, ametys:task)" 
545            + (predicats != null ? "[" + predicats + "]" : "") 
546            + ((sortCriteria != null) ? (" " + sortCriteria.build()) : "");
547        return xpathQuery;
548    }
549    
550    
551    /**
552     * Get the list of tasks
553     * @param parentIds The tasks parents
554     * @param assignedToUser Filter only the tasks assigned to the current user
555     * @param userSubscribed Filter only the tasks for which current user subscribed
556     * @param offset Offset the list of results
557     * @param limit  The maximum number of results to return
558     * @param filter Return only tasks matching the filter
559     * @param orderBy Order the list by this property. Default to the creation date
560     * @param orderAsc Sort the list by order ascendant or descendant. Default to ascendant. 
561     * @return The list of tasks
562     * @throws ProcessingException If an error occurred
563     */
564    @Callable
565    public Map<String, Object> getTasks(List<String> parentIds, boolean assignedToUser, boolean userSubscribed, Integer offset, Integer limit, String filter, String orderBy, Boolean orderAsc) throws ProcessingException
566    {
567        TaskListResult taskListResult = getTaskList(parentIds, assignedToUser, userSubscribed, offset, limit, filter, orderBy, orderAsc);
568        
569        Map<String, Object> result = new HashMap<>();
570        
571        result.put("total", taskListResult.getTotal());
572        result.put("tasks", _tasksToJson(taskListResult.getTasks()));
573        
574        return result;
575    }
576    
577    /**
578     * Simple structure used to store a list of task and some metadata.
579     * Used when retrieving a list of task as a result of a search operation.
580     */
581    public static class TaskListResult
582    {
583        /** task list */
584        protected List<Task> _tasks;
585        /** number of result before applying the filter and the limit */
586        protected int _total;
587        
588        /**
589         * Task list result constructor
590         * @param task The task list to reference
591         * @param total number of result before applying the filter and the limit
592         */
593        protected TaskListResult(List<Task> task, int total)
594        {
595            _tasks = task;
596            _total = total;
597        }
598
599        /**
600         * Retrieves the tasks
601         * @return the tasks
602         */
603        public List<Task> getTasks()
604        {
605            return _tasks;
606        }
607        
608        /**
609         * Retrieves the total
610         * @return the total
611         */
612        public int getTotal()
613        {
614            return _total;
615        }
616    }
617    
618    private List<Map<String, Object>> _tasksToJson(List<Task> tasks)
619    {
620        List<Map<String, Object>> result = new ArrayList<>();
621        
622        for (Task task : tasks)
623        {
624            result.add(getTask(task, false));
625        }
626        
627        return result;
628    }
629    
630    /**
631     * Assign one or more task to one or more users
632     * @param taskIds The tasks ids
633     * @param users The users
634     * @return The tasks data, updated
635     * @throws IllegalAccessException If an error occurs
636     */
637    @Callable
638    public Map<String, Object> assignTasks(List<String> taskIds, List<String> users) throws IllegalAccessException
639    {
640        Map<String, Object> result = new HashMap<>();
641        List<JCRTask> tasks = _getTasksById(taskIds, result, RIGHTS_EDIT_TASK);
642        
643        if (result.containsKey("message"))
644        {
645            return result;
646        }
647        
648        List<UserIdentity> userIdentities = new ArrayList<>();
649        for (String user : users)
650        {
651            userIdentities.add(UserIdentity.stringToUserIdentity(user));
652        }
653        
654        ArrayList<Map<String, Object>> tasksData = new ArrayList<>();
655        result.put("tasks", tasksData); 
656        for (JCRTask task : tasks)
657        {
658            boolean modified = false;
659            List<UserIdentity> assignment = task.getAssignment();
660            for (UserIdentity identity : userIdentities)
661            {
662                if (!assignment.contains(identity))
663                {
664                    modified = true;
665                    assignment.add(identity);
666                }
667            }
668            
669            if (modified)
670            {
671                task.setAssignment(assignment);
672                
673                Date now = new Date();
674                task.setLastModified(now);
675                task.saveChanges();
676
677                if (!assignment.isEmpty())
678                {
679                    // Notify listeners
680                    Map<String, Object> eventParams = new HashMap<>();
681                    eventParams.put(ObservationConstants.ARGS_TASK, task);
682                    eventParams.put(ObservationConstants.ARGS_ID, task.getId());
683                    _observationManager.notify(new Event(ObservationConstants.EVENT_TASK_ASSIGNED, _currentUserProvider.getUser(), eventParams));
684                }
685            }
686            
687            tasksData.add(getTask(task, false));
688        }
689        
690        return result;
691    }
692    
693    /**
694     * Update the status of one or multiple tasks
695     * @param taskIds The tasks ids
696     * @param statusString The status
697     * @return The tasks data updated
698     * @throws IllegalAccessException If an error occurs
699     */
700    @Callable
701    public Map<String, Object> setTasksStatus(List<String> taskIds, String statusString) throws IllegalAccessException
702    {
703        Map<String, Object> result = new HashMap<>();
704        List<JCRTask> tasks = _getTasksById(taskIds, result, RIGHTS_EDIT_TASK);
705        
706        if (result.containsKey("message"))
707        {
708            return result;
709        }
710        
711        TaskStatus status = TaskStatus.createsFromString(statusString);
712        if (status == null)
713        {
714            getLogger().warn("User '" + _currentUserProvider.getUser() + "' try to set the task status '" + statusString + "' which is not a valid value.");
715            result.put("message", "unknown_status");
716            return result;
717        }
718        
719        ArrayList<Map<String, Object>> tasksData = new ArrayList<>();
720        result.put("tasks", tasksData); 
721        for (JCRTask task : tasks)
722        {
723            if (!task.getStatus().equals(status))
724            {
725                task.setStatus(status);
726                
727                Date now = new Date();
728                task.setLastModified(now);
729                task.saveChanges();
730                
731                // Notify listeners
732                Map<String, Object> eventParams = new HashMap<>();
733                eventParams.put(ObservationConstants.ARGS_TASK, task);
734                eventParams.put(ObservationConstants.ARGS_ID, task.getId());
735                eventParams.put("status", status.toString());
736
737                _observationManager.notify(new Event(ObservationConstants.EVENT_TASK_STATUS_CHANGED, _currentUserProvider.getUser(), eventParams));
738            }
739            
740            tasksData.add(getTask(task, false));
741        }
742        
743        return result;
744    }
745    
746    /**
747     * Update the progress of one or multiple tasks
748     * @param taskIds The tasks ids
749     * @param progress The progress
750     * @return The tasks data updated
751     * @throws IllegalAccessException If an error occurs
752     */
753    @Callable
754    public Map<String, Object> setTasksProgress(List<String> taskIds, Integer progress) throws IllegalAccessException
755    {
756        Map<String, Object> result = new HashMap<>();
757        List<JCRTask> tasks = _getTasksById(taskIds, result, RIGHTS_EDIT_TASK);
758        
759        if (result.containsKey("message"))
760        {
761            return result;
762        }
763        
764        ArrayList<Map<String, Object>> tasksData = new ArrayList<>();
765        result.put("tasks", tasksData); 
766        for (JCRTask task : tasks)
767        {
768            if (!task.getProgress().equals(progress))
769            {
770                task.setProgress(progress.doubleValue());
771                
772                Date now = new Date();
773                task.setLastModified(now);
774                task.saveChanges();
775            }
776            
777            tasksData.add(getTask(task, false));
778        }
779        
780        return result;
781    }
782    
783    /**
784     * Delete one or more tasks
785     * @param taskIds The tasks ids
786     * @return The list of tasks ids
787     * @throws IllegalAccessException If an error occurs
788     */
789    @Callable
790    public Map<String, Object> deleteTasks(List<String> taskIds) throws IllegalAccessException
791    {
792        Map<String, Object> result = new HashMap<>();
793        List<JCRTask> tasks = new ArrayList<>();
794        
795        UserIdentity currentUser = _currentUserProvider.getUser();
796        for (String id : taskIds)
797        {
798            AmetysObject object = _resolver.resolveById(id);
799            if (!(object instanceof JCRTask))
800            {
801                throw new IllegalClassException(ModifiableResourceCollection.class, object.getClass());
802            }
803            
804            JCRTask task = (JCRTask) object;
805            
806            // Check user right
807            _explorerResourcesDAO.checkUserRight(object.getParent(), task.getAuthor().equals(currentUser) ? RIGHTS_DELETE_TASK : RIGHTS_DELETE_ALL_TASK);
808            
809            if (!_explorerResourcesDAO.checkLock(task))
810            {
811                getLogger().warn("User '" + currentUser + "' try to modify task '" + object.getName() + "' but it is locked by another user");
812                result.put("message", "locked");
813            }
814            
815            tasks.add(task);
816        }
817        
818        if (result.containsKey("message"))
819        {
820            return result;
821        }
822        
823        Set<ModifiableExplorerNode> parents = new HashSet<>();
824        
825        for (JCRTask task : tasks)
826        {
827            ModifiableExplorerNode parent = task.getParent();
828            
829            Map<String, Object> eventParams = new HashMap<>();
830            eventParams.put(ObservationConstants.ARGS_TASK, task);
831            eventParams.put(ObservationConstants.ARGS_ID, task.getId());
832            _observationManager.notify(new Event(ObservationConstants.EVENT_TASK_DELETING, currentUser, eventParams));
833            
834            eventParams.put(ObservationConstants.ARGS_PARENT_ID, parent.getId());
835            eventParams.remove(ObservationConstants.ARGS_TASK);
836            eventParams.put(ObservationConstants.ARGS_PATH, task.getPath());
837            
838            task.remove();
839            parents.add(parent);
840            
841            _observationManager.notify(new Event(ObservationConstants.EVENT_TASK_DELETED, currentUser, eventParams));
842        }
843        
844        for (ModifiableExplorerNode parent : parents)
845        {
846            parent.saveChanges();
847        }
848        
849        result.put("tasks", taskIds);
850        
851        return result;
852    }
853    
854
855    private List<JCRTask> _getTasksById(List<String> taskIds, Map<String, Object> result, String right) throws IllegalAccessException
856    {
857        List<JCRTask> tasks = new ArrayList<>();
858        
859        for (String id : taskIds)
860        {
861            AmetysObject object = _resolver.resolveById(id);
862            if (!(object instanceof JCRTask))
863            {
864                throw new IllegalClassException(ModifiableResourceCollection.class, object.getClass());
865            }
866            
867            JCRTask task = (JCRTask) object;
868            
869            // Check user right
870            _explorerResourcesDAO.checkUserRight(object.getParent(), right);
871            
872            if (!_explorerResourcesDAO.checkLock(task))
873            {
874                getLogger().warn("User '" + _currentUserProvider.getUser() + "' try to modify task '" + object.getName() + "' but it is locked by another user");
875                result.put("message", "locked");
876            }
877            
878            tasks.add(task);
879        }
880        return tasks;
881    }
882    
883    /**
884     * Transform a task to JSON data
885     * @param task The task
886     * @param isEdition true to get the task in edit mode
887     * @return The JSON data
888     */
889    protected Map<String, Object> getTask(Task task, boolean isEdition)
890    {
891        Map<String, Object> result = new HashMap<>();
892        
893        Date start = task.getStartDate();
894        Date end = task.getEndDate();
895        
896        result.put("id", task.getId());
897        result.put("taskId", task.getTaskId());
898        result.put("title", task.getLabel());
899        result.put("description", isEdition ? getTaskDescriptionForEdition(task) : getTaskDescription(task));
900        result.put("startDate", ParameterHelper.valueToString(start));
901        result.put("endDate", ParameterHelper.valueToString(end));
902        result.put("status", task.getStatus().toString());
903        result.put("priority", task.getPriority().toString());
904        result.put("load", task.getInitialLoad());
905        result.put("progress", task.getProgress());
906        result.put("assignment", _assignmentToJSON(task));
907        result.put("subscribers", _subscribersToJSON(task));
908        result.put("creationDate", task.getCreationDate());
909        result.put("lastModified", task.getLastModified());
910        
911        Map<String, Object> rights = new HashMap<>();
912        
913        UserIdentity currentUser = _currentUserProvider.getUser();
914        rights.put("edit", _explorerResourcesDAO.getUserRight(currentUser, RIGHTS_EDIT_TASK, task));
915        boolean canDelete = _explorerResourcesDAO.getUserRight(currentUser, RIGHTS_DELETE_ALL_TASK, task);
916        if (!canDelete && (task instanceof JCRTask))
917        {
918            JCRTask jcrtask = (JCRTask) task;
919            if (jcrtask.getAuthor().equals(currentUser))
920            {
921                canDelete = _explorerResourcesDAO.getUserRight(currentUser, RIGHTS_DELETE_TASK, task);
922            }
923        }
924        rights.put("delete", canDelete);
925        result.put("rights", rights);
926        
927        return result;
928    }
929    
930    /**
931     * Transform assignments of a task to JSON data
932     * @param task The task
933     * @return The JSON data
934     */
935    protected List<Map<String, Object>> _assignmentToJSON(Task task)
936    {
937        return _userHelper.userIdentities2json(task.getAssignment());
938    }
939    
940    /**
941     * Transform subscribers of a task to JSON data
942     * @param task The task
943     * @return The JSON data
944     */
945    protected List<Map<String, Object>> _subscribersToJSON(Task task)
946    {
947        return _userHelper.userIdentities2json(task.getSubscribers());
948    }
949}