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