001/*
002 *  Copyright 2016 Anyware Services
003 *
004 *  Licensed under the Apache License, Version 2.0 (the "License");
005 *  you may not use this file except in compliance with the License.
006 *  You may obtain a copy of the License at
007 *
008 *      http://www.apache.org/licenses/LICENSE-2.0
009 *
010 *  Unless required by applicable law or agreed to in writing, software
011 *  distributed under the License is distributed on an "AS IS" BASIS,
012 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 *  See the License for the specific language governing permissions and
014 *  limitations under the License.
015 */
016package org.ametys.plugins.workspaces.tasks;
017
018import java.io.IOException;
019import java.util.Collection;
020import java.util.Collections;
021import java.util.HashMap;
022import java.util.List;
023import java.util.Map;
024import java.util.stream.Collectors;
025
026import javax.jcr.Node;
027import javax.jcr.RepositoryException;
028import javax.jcr.Session;
029import javax.jcr.query.Query;
030
031import org.apache.avalon.framework.context.Context;
032import org.apache.avalon.framework.context.ContextException;
033import org.apache.avalon.framework.context.Contextualizable;
034import org.apache.avalon.framework.service.ServiceException;
035import org.apache.avalon.framework.service.ServiceManager;
036import org.apache.cocoon.ProcessingException;
037import org.apache.cocoon.components.ContextHelper;
038import org.apache.cocoon.environment.Request;
039import org.apache.commons.io.IOUtils;
040import org.apache.commons.lang3.StringUtils;
041import org.apache.excalibur.source.Source;
042import org.apache.excalibur.source.SourceResolver;
043
044import org.ametys.core.ui.Callable;
045import org.ametys.plugins.explorer.tasks.ModifiableTask;
046import org.ametys.plugins.explorer.tasks.Task;
047import org.ametys.plugins.explorer.tasks.TasksList;
048import org.ametys.plugins.explorer.tasks.jcr.JCRTasksDAO;
049import org.ametys.plugins.repository.AmetysObject;
050import org.ametys.plugins.repository.AmetysRepositoryException;
051import org.ametys.plugins.repository.jcr.DefaultTraversableAmetysObject;
052import org.ametys.plugins.workspaces.html.HTMLTransformer;
053import org.ametys.plugins.workspaces.project.ProjectManager;
054import org.ametys.plugins.workspaces.project.modules.WorkspaceModuleExtensionPoint;
055import org.ametys.plugins.workspaces.project.objects.Project;
056import org.ametys.runtime.plugin.component.PluginAware;
057
058/**
059 * DAO for interacting with tasks of a project
060 */
061public class WorkspaceTaskDAO extends JCRTasksDAO implements PluginAware, Contextualizable
062{
063    /** The Avalon role */
064    @SuppressWarnings("hiding")
065    public static final String ROLE = WorkspaceTaskDAO.class.getName();
066    
067    private HTMLTransformer _htmlTransformer;
068    
069    private SourceResolver _sourceResolver;
070
071    private ProjectManager _projectManager;
072
073    private String _pluginName;
074
075    private Context _context;
076
077    private WorkspaceModuleExtensionPoint _moduleEP;
078
079    @Override
080    public void service(ServiceManager manager) throws ServiceException
081    {
082        super.service(manager);
083        _htmlTransformer = (HTMLTransformer) manager.lookup(HTMLTransformer.ROLE);
084        _sourceResolver = (SourceResolver) manager.lookup(SourceResolver.ROLE);
085        _projectManager = (ProjectManager) manager.lookup(ProjectManager.ROLE);
086        _moduleEP = (WorkspaceModuleExtensionPoint) manager.lookup(WorkspaceModuleExtensionPoint.ROLE);
087    }
088
089    @Override
090    public void setPluginInfo(String pluginName, String featureName, String id)
091    {
092        _pluginName = pluginName;
093    }
094    
095    @Override
096    public void contextualize(Context context) throws ContextException
097    {
098        _context = context;
099    }
100    
101    /**
102     * Add a new task to the current project
103     * @param parameters The task parameters
104     * @return The task data
105     * @throws IllegalAccessException If an error occurs
106     */
107    @Callable
108    public Map<String, Object> addTask(Map<String, Object> parameters) throws IllegalAccessException
109    {
110        String projectName = (String) parameters.get("project");
111        
112        if (StringUtils.isEmpty(projectName))
113        {
114            projectName = _getProjectFromRequest();
115        }
116        
117        Project project = _projectManager.getProject(projectName);
118        
119        TasksWorkspaceModule taskModule = _moduleEP.getModule(TasksWorkspaceModule.TASK_MODULE_ID);
120        DefaultTraversableAmetysObject tasksRoot = taskModule.getTasksRoot(project, true);
121        
122        return addTask(tasksRoot.getId(), parameters);
123    }
124    
125    /**
126     * Get the list of tasks
127     * @param projectNames The project that contain the tasks. Can be null to search in all project.
128     * @param assignedToUser Filter only the tasks assigned to the current user
129     * @param userSubscribed Filter only the tasks for which current user subscribed
130     * @param offset Offset the list of results
131     * @param limit  The maximum number of results to return
132     * @param filter Return only tasks matching the filter
133     * @param orderBy Order the list by this property. Default to the creation date
134     * @param orderAsc Sort the list by order ascendant or descendant. Default to ascendant. 
135     * @return The list as a {@link org.ametys.plugins.explorer.tasks.jcr.JCRTasksDAO.TaskListResult} object
136     * @throws ProcessingException If an error occurred
137     */
138    public TaskListResult getProjectTaskList(List<String> projectNames, boolean assignedToUser, boolean userSubscribed, Integer offset, Integer limit, String filter, String orderBy, Boolean orderAsc) throws ProcessingException
139    {
140        List<String> parentIds = _projectToTaskList(projectNames);
141        return getTaskList(parentIds, assignedToUser, userSubscribed, offset, limit, filter, orderBy, orderAsc);
142    }
143    
144    /**
145     * Get the list of tasks
146     * @param projectNames The project that contain the tasks. Can be null to search in all project.
147     * @param assignedToUser Filter only the tasks assigned to the current user
148     * @param userSubscribed Filter only the tasks for which current user subscribed
149     * @param offset Offset the list of results
150     * @param limit  The maximum number of results to return
151     * @param filter Return only tasks matching the filter
152     * @param orderBy Order the list by this property. Default to the creation date
153     * @param orderAsc Sort the list by order ascendant or descendant. Default to ascendant. 
154     * @return The list of tasks
155     * @throws ProcessingException If an error occurred
156     */
157    @Callable
158    public Map<String, Object> getProjectTasks(List<String> projectNames, boolean assignedToUser, boolean userSubscribed, Integer offset, Integer limit, String filter, String orderBy, Boolean orderAsc) throws ProcessingException
159    {
160        List<String> parentIds = _projectToTaskList(projectNames);
161        return getTasks(parentIds, assignedToUser, userSubscribed, offset, limit, filter, orderBy, orderAsc);
162    }
163    
164    /**
165     * Retrieves the list of {@link TasksList}
166     * @param projectNames The project names
167     * @return list of task list identifiers
168     */
169    private List<String> _projectToTaskList(List<String> projectNames)
170    {
171        Collection<String> finalProjectNames = projectNames;
172        
173        // All project if no project names provided
174        if (finalProjectNames == null)
175        {
176            finalProjectNames = _projectManager.getProjectNames();
177        }
178        
179        TasksWorkspaceModule taskModule = _moduleEP.getModule(TasksWorkspaceModule.TASK_MODULE_ID);
180        
181        return finalProjectNames.stream()
182                                     .map(_projectManager::getProject)
183                                     .map(project -> taskModule.getTasksRoot(project, true).getId())
184                                     .collect(Collectors.toList());
185    }
186    
187    /**
188     * Get the tasks of the current project
189     * @param assignedToUser True to filter only the tasks assigned to the current user
190     * @param userSubscribed Filter only the tasks for which current user subscribed
191     * @param offset Start the list of tasks at the Nth task, used for pagination
192     * @param limit Only return a maximum of N tasks, used for pagination
193     * @param filter Return only tasks matching the filter
194     * @param orderBy Order the list by this property. Default to the creation date
195     * @param orderAsc Sort the list by order ascendant or descendant. Default to ascendant.
196     * @return The list of tasks
197     * @throws IllegalAccessException If the user is not authorized
198     * @throws ProcessingException If an error occurred
199     */
200    @Callable
201    public Map<String, Object> getProjectTasks(boolean assignedToUser, boolean userSubscribed, Integer offset, Integer limit, String filter, String orderBy, Boolean orderAsc) throws IllegalAccessException, ProcessingException
202    {
203        String projectName = _getProjectFromRequest();
204        Project project = _projectManager.getProject(projectName);
205        
206        TasksWorkspaceModule taskModule = _moduleEP.getModule(TasksWorkspaceModule.TASK_MODULE_ID);
207        DefaultTraversableAmetysObject tasksRoot = taskModule.getTasksRoot(project, true);
208        
209        return getTasks(Collections.singletonList(tasksRoot.getId()), assignedToUser, userSubscribed, offset, limit, filter, orderBy, orderAsc);
210    }
211    
212    @Override
213    protected void setTaskDescription(ModifiableTask task, String description)
214    {
215        try
216        {
217            _htmlTransformer.transform(description, task.getDescription());
218        }
219        catch (IOException e)
220        {
221            throw new AmetysRepositoryException("Failed to transform task's description into rich text", e);
222        }
223    }
224    
225    @Override
226    protected String getTaskDescription(Task task) throws AmetysRepositoryException
227    {
228        Source contentSource = null;
229        try
230        {
231            Map<String, Object> parameters = new HashMap<>();
232            parameters.put("source", task.getDescription().getInputStream());
233            contentSource = _sourceResolver.resolveURI("cocoon://_plugins/" + _pluginName + "/convert/html2html", null, parameters);
234            return IOUtils.toString(contentSource.getInputStream(), "UTF-8");
235        }
236        catch (IOException e)
237        {
238            throw new AmetysRepositoryException("Failed to transform rich text into string", e);
239        }
240        finally
241        {
242            _sourceResolver.release(contentSource);
243        }
244    }
245    
246    @Override
247    protected String getTaskDescriptionForEdition(Task task) throws AmetysRepositoryException
248    {
249        try
250        {
251            if (!(task instanceof ModifiableTask))
252            {
253                throw new AmetysRepositoryException("Can not transform description of a non modifiable task");
254            }
255            
256            StringBuilder sb = new StringBuilder();
257            _htmlTransformer.transformForEditing(((ModifiableTask) task).getDescription(), sb);
258            return sb.toString();
259        }
260        catch (IOException e)
261        {
262            throw new AmetysRepositoryException("Failed to transform rich text into string", e);
263        }
264    }
265    
266    @Override
267    protected Map<String, Object> getTask(Task task, boolean isEdition)
268    {
269        Map<String, Object> data = super.getTask(task, isEdition);
270        
271        Project project = getParentProject(task);
272        if (project != null)
273        {
274            data.put("project", _projectToJson(project));
275        }
276        
277        return data;
278    }
279    
280    /**
281     * Get the parent project of a task
282     * @param task The task
283     * @return the project or <code>null</code> if not found
284     */
285    protected Project getParentProject(Task task)
286    {
287        AmetysObject parent = task.getParent();
288        while (parent != null)
289        {
290            if (parent instanceof Project)
291            {
292                return (Project) parent;
293            }
294            parent = parent.getParent();
295        }
296        
297        return null;
298    }
299    
300    /**
301     * Retrieve the project JSON data
302     * @param project The project
303     * @return The data
304     */
305    protected Map<String, String> _projectToJson(Project project)
306    {
307        Map<String, String> projectData = new HashMap<>();
308        projectData.put("title", project.getTitle());
309        projectData.put("name", project.getName());
310        return projectData;
311    }
312    
313    private String _getProjectFromRequest()
314    {
315        Request request = ContextHelper.getRequest(_context);
316        return (String) request.getAttribute("projectName");
317    }
318
319    /**
320     * Get the total number of tasks of the project
321     * @param project The project
322     * @return The number of tasks, or null if the module is not activated
323     */
324    public Long getTasksCount(Project project)
325    {
326        TasksWorkspaceModule taskModule = _moduleEP.getModule(TasksWorkspaceModule.TASK_MODULE_ID);
327        DefaultTraversableAmetysObject tasksRoot = taskModule.getTasksRoot(project, true);
328        
329        if (tasksRoot != null)
330        {
331            try
332            {
333                Node node = tasksRoot.getNode();
334                String xPathQuery = getTasksXpathQuery(node.getPath(), null, null);
335                Session session = node.getSession();
336                Query query = session.getWorkspace().getQueryManager().createQuery(xPathQuery, Query.XPATH);
337                
338                return query.execute().getNodes().getSize();
339            }
340            catch (RepositoryException e)
341            {
342                getLogger().error("Unable to retrieve the total count of tasks for the project " + project, e);
343            }
344        }
345        return null;
346    }
347}