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