001/*
002 *  Copyright 2015 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.calendars;
017
018import java.util.ArrayList;
019import java.util.Arrays;
020import java.util.Collection;
021import java.util.HashMap;
022import java.util.HashSet;
023import java.util.LinkedList;
024import java.util.List;
025import java.util.Map;
026import java.util.Optional;
027import java.util.Set;
028import java.util.stream.Collectors;
029import java.util.stream.Stream;
030
031import org.ametys.core.right.RightManager.RightResult;
032import org.ametys.core.ui.Callable;
033import org.ametys.core.user.UserIdentity;
034import org.ametys.plugins.explorer.calendars.Calendar;
035import org.ametys.plugins.explorer.calendars.CalendarEvent;
036import org.ametys.plugins.explorer.calendars.actions.CalendarDAO;
037import org.ametys.plugins.explorer.calendars.jcr.JCRCalendar;
038import org.ametys.plugins.explorer.calendars.jcr.JCRCalendarEvent;
039import org.ametys.plugins.explorer.workflow.AbstractExplorerNodeWorkflowComponent;
040import org.ametys.plugins.workspaces.project.ProjectManager;
041import org.ametys.runtime.i18n.I18nizableText;
042import org.apache.avalon.framework.service.ServiceException;
043import org.apache.avalon.framework.service.ServiceManager;
044import org.apache.commons.lang3.BooleanUtils;
045import org.apache.commons.lang3.StringUtils;
046
047import com.google.common.primitives.Ints;
048import com.opensymphony.workflow.Workflow;
049import com.opensymphony.workflow.WorkflowException;
050import com.opensymphony.workflow.loader.ActionDescriptor;
051import com.opensymphony.workflow.loader.StepDescriptor;
052import com.opensymphony.workflow.loader.WorkflowDescriptor;
053import com.opensymphony.workflow.spi.Step;
054
055/**
056 * DAO for manipulating calendars of a project
057 *
058 */
059public class WorkspaceCalendarDAO extends CalendarDAO
060{
061    /** Avalon Role */
062    @SuppressWarnings("hiding")
063    public static final String ROLE = WorkspaceCalendarDAO.class.getName();
064    
065    /** Right to add a tag */
066    public static final String RIGHTS_TAG_ADD = "Plugins_Workspaces_Rights_Project_Add_Tag";
067    /** Right to delete a tag */
068    public static final String RIGHTS_TAG_DELETE = "Plugins_Workspaces_Rights_Project_Delete_Tag";
069    /** Right to add a place */
070    public static final String RIGHTS_PLACE_ADD = "Plugins_Workspaces_Rights_Project_Add_Place";
071    /** Right to delete a place */
072    public static final String RIGHTS_PLACE_DELETE = "Plugins_Workspaces_Rights_Project_Delete_Place";
073
074    private ProjectManager _projectManager;
075    
076    private MessagingConnectorCalendarManager _messagingConnectorCalendarManager;
077   
078    
079    @Override
080    public void service(ServiceManager manager) throws ServiceException
081    {
082        super.service(manager);
083        _projectManager = (ProjectManager) manager.lookup(ProjectManager.ROLE);
084        _messagingConnectorCalendarManager = (MessagingConnectorCalendarManager) manager.lookup(MessagingConnectorCalendarManager.ROLE);
085        
086    }
087    
088    /**
089     * Add an event
090     * @param parameters The map of parameters to perform the action
091     * @return The map of results populated by the underlying workflow action
092     * @throws WorkflowException if an error occurred
093     * @throws IllegalAccessException  If the user does not have the right to add an event
094     */
095    @Callable
096    public Map<String, Object> addEvent(Map<String, Object> parameters) throws WorkflowException, IllegalAccessException
097    {
098        // Sanitize keywords and location (respectively tags and places in the client parameters)
099        @SuppressWarnings("unchecked")
100        Collection<String> keywords = (Collection<String>) parameters.get("tags");
101        keywords = _sanitizeEventKeywordsInput(keywords);
102        parameters.put("keywords", keywords);
103        
104        @SuppressWarnings("unchecked")
105        Collection<String> locations = (Collection<String>) parameters.get("places");
106        String location = _sanitizeEventLocationsInput(locations);
107        parameters.put("location", location);
108        
109        Map<String, Object> result = doWorkflowEventAction(parameters);
110        
111        //TODO Move to create event action (workflow) ?
112        String eventId = (String) result.get("id");
113        _messagingConnectorCalendarManager.addEventInvitation(parameters, eventId);
114        
115        _projectManager.addTags(keywords);
116        _projectManager.addPlaces(Arrays.asList(location.split(",")));
117        
118        _projectManager.getProjectsRoot().saveChanges();
119        
120        return result;
121    }
122    
123    /**
124     * Edit an event
125     * @param parameters The map of parameters to perform the action
126     * @return The map of results populated by the underlying workflow action
127     * @throws WorkflowException if an error occurred
128     */
129    @Callable
130    public Map<String, Object> editEvent(Map<String, Object> parameters) throws WorkflowException
131    {
132        // Sanitize keywords and location (respectively tags and places in the client parameters)
133        @SuppressWarnings("unchecked")
134        Collection<String> keywords = (Collection<String>) parameters.get("tags");
135        keywords = _sanitizeEventKeywordsInput(keywords);
136        parameters.put("keywords", keywords);
137        
138        @SuppressWarnings("unchecked")
139        Collection<String> locations = (Collection<String>) parameters.get("places");
140        String location = _sanitizeEventLocationsInput(locations);
141        parameters.put("location", location);
142        
143        String eventId = (String) parameters.get("id");
144        JCRCalendarEvent event = _resolver.resolveById(eventId);
145        
146        // handle event move if calendar has changed
147        String previousCalendarId = event.getParent().getId();
148        String parentId = (String) parameters.get("parentId");
149        
150        if (previousCalendarId != null && !StringUtils.equals(parentId, previousCalendarId))    
151        {
152            JCRCalendar parentCalendar = _resolver.resolveById(parentId);
153            move(event, parentCalendar);
154        }
155        
156        Map<String, Object> result = doWorkflowEventAction(parameters);
157        
158        //TODO Move to edit event action (workflow) ?
159        String choice = (String) parameters.get("choice");
160        if (!"unit".equals(choice))
161        {
162            _messagingConnectorCalendarManager.editEventInvitation(parameters, eventId);
163        }
164        
165        _projectManager.addTags(keywords);
166        _projectManager.addPlaces(Arrays.asList(location.split(",")));
167        
168        _projectManager.getProjectsRoot().saveChanges();
169        
170        return result;
171    }
172    
173    @Override
174    @Callable
175    public Map<String, Object> deleteEvent(String id, String occurrence, String choice) throws IllegalAccessException
176    {
177        if (!"unit".equals(choice))
178        {
179            JCRCalendarEvent event = _resolver.resolveById(id);
180            _messagingConnectorCalendarManager.deleteEvent(event);
181        }
182        
183        return super.deleteEvent(id, occurrence, choice);
184    }
185    
186    @Override
187    @Callable
188    public Map<String, Object> getCalendarData(Calendar calendar, boolean recursive, boolean includeEvents)
189    {
190        Map<String, Object> data = super.getCalendarData(calendar, recursive, includeEvents);
191        data.put("rights", _extractCalendarRightData(calendar));
192        return data;
193    }
194    
195    @Override
196    public Map<String, Object> getEventData(CalendarEvent event, boolean fullInfo)
197    {
198        Map<String, Object> eventData = super.getEventData(event, fullInfo);
199        
200        eventData.put("calendar", event.getParent().getName()); 
201        
202        eventData.put("isStillSynchronized", _messagingConnectorCalendarManager.isEventStillSynchronized(event.getId()));
203        
204        // tags and places are expected by the client (respectively keywords and location on the server side)
205        eventData.put("tags", event.getKeywords());
206        
207        String location = StringUtils.defaultString(event.getLocation());
208        eventData.put("places", Stream.of(location.split(",")).filter(StringUtils::isNotEmpty).collect(Collectors.toList()));
209        
210        // add event rights
211        eventData.put("rights", _extractEventRightData(event));
212        
213        return eventData;
214    }
215    
216    /**
217     * Retrieves the available workflows for the calendars 
218     * @param withSteps true to include the list of steps and corresponding actions
219     * @return The list of workflow. Each entry is a map of data (id, label)
220     */
221    @Callable
222    public List<Map<String, Object>> getWorkflows(boolean withSteps)
223    {
224        List<Map<String, Object>> workflows = new ArrayList<>();
225        
226        for (String workflowName : _workflowHelper.getWorkflowNames())
227        {
228            if (StringUtils.startsWith(workflowName, "calendar-"))
229            {
230                Map<String, Object> workflow = new HashMap<>();
231                workflows.add(workflow);
232                
233                workflow.put("id", workflowName);
234                workflow.put("isDefault", StringUtils.contains(workflowName, "default"));
235                
236                String i18nKey = "WORKFLOW_" + workflowName;
237                workflow.put("label", new I18nizableText("application", i18nKey));
238                
239                if (withSteps)
240                {
241                    WorkflowDescriptor workflowDescriptor = _workflowHelper.getWorkflowDescriptor(workflowName);
242                    workflow.put("steps", _getSteps(workflowDescriptor));
243                    workflow.put("actions", _getActions(workflowDescriptor));
244                }
245            }
246        }
247        
248        return workflows;
249    }
250    
251    /**
252     * Get the workflow state of an event
253     * @param eventId The id of the event
254     * @return A map containing information about the workflow state
255     */
256    @Callable
257    public Map<String, Object> getWorkflowState(String eventId)
258    {
259        Map<String, Object> state = new HashMap<>();
260        
261        CalendarEvent event = _resolver.resolveById(eventId);
262        Workflow workflow = _workflowProvider.getAmetysObjectWorkflow(event);
263        
264        long workflowId = event.getWorkflowId();
265        state.put("workflowName", workflow.getWorkflowName(workflowId));
266        
267        Step step = (Step) workflow.getCurrentSteps(workflowId).stream().findFirst().get();
268        state.put("step", step.getStepId());
269        
270        Map<String, Object> inputs = new HashMap<>();
271        inputs.put(AbstractExplorerNodeWorkflowComponent.EXPLORERNODE_KEY, event.getParent()); // event's parent is the calendar
272        state.put("actions", workflow.getAvailableActions(workflowId, inputs));
273        
274        return state;
275    }
276    
277    /**
278     * Get the steps of a workflow
279     * @param workflowDescriptor workflow descriptor
280     * @return The list of steps
281     */
282    protected List<Map<String, Object>> _getSteps(WorkflowDescriptor workflowDescriptor)
283    {
284        List<StepDescriptor> stepDescriptors = workflowDescriptor.getSteps();
285                
286        return stepDescriptors.stream()
287            .map(stepDescriptor -> _getStep(workflowDescriptor, stepDescriptor))
288            .collect(Collectors.toList());
289    }
290    
291    /**
292     * Get a map of data about a step
293     * @param workflowDescriptor workflow descriptor
294     * @param stepDescriptor step descriptor
295     * @return The map of data
296     */
297    protected Map<String, Object> _getStep(WorkflowDescriptor workflowDescriptor, StepDescriptor stepDescriptor)
298    {
299        Map<String, Object> step = new HashMap<>();
300        
301        int stepId = stepDescriptor.getId();
302        String stepName = stepDescriptor.getName();
303        String[] nameParts = stepName.split(":");
304        
305        step.put("id", stepId);
306        step.put("name", nameParts[nameParts.length - 1]); // step name without the possible catalog name
307        
308        I18nizableText i18nStepName = new I18nizableText("application", stepName);
309        step.put("label", i18nStepName);
310        step.put("description", new I18nizableText("application", stepName + "_DESCRIPTION"));
311        
312        // Default icons
313        String[] icons = new String[]{"-small", "-medium", "-large"};
314        for (String icon : icons)
315        {
316            if ("application".equals(i18nStepName.getCatalogue()))
317            {
318                step.put("icon" + icon, new I18nizableText("/plugins/explorer/resources_workflow/" + i18nStepName.getKey() + icon + ".png"));
319            }
320            else
321            {
322                String pluginName = i18nStepName.getCatalogue().substring("plugin.".length());
323                step.put("icon" + icon, new I18nizableText("/plugins/" + pluginName + "/resources/img/workflow/" + i18nStepName.getKey() + icon + ".png"));
324            }
325        }
326        
327        List<Integer> actionsId = new LinkedList<>();
328        step.put("actions", actionsId);
329        
330        for (int actionId : _workflowHelper.getAllActionsFromStep(workflowDescriptor.getName(), stepId))
331        {
332            actionsId.add(actionId);
333        }
334        
335        return step;
336    }
337    
338    /**
339     * Get the actions of a workflow
340     * @param workflowDescriptor workflow descriptor
341     * @return The list of steps
342     */
343    protected List<Map<String, Object>> _getActions(WorkflowDescriptor workflowDescriptor)
344    {
345        int[] allActions = _workflowHelper.getAllActions(workflowDescriptor.getName());
346        
347        return Ints.asList(allActions).stream()
348            .map(workflowDescriptor::getAction)
349            .map(actionDescriptor -> _getAction(workflowDescriptor, actionDescriptor))
350            .collect(Collectors.toList());
351    }
352    
353    /**
354     * Get a map of data about an action
355     * @param workflowDescriptor workflow descriptor
356     * @param actionDescriptor action descriptor
357     * @return The map of data
358     */
359    protected Map<String, Object> _getAction(WorkflowDescriptor workflowDescriptor, ActionDescriptor actionDescriptor)
360    {
361        Map<String, Object> action = new HashMap<>();
362        
363        int actionId = actionDescriptor.getId();
364        String actionName = actionDescriptor.getName();
365        String[] nameParts = actionName.split(":");
366        
367        action.put("id", actionId);
368        action.put("name", nameParts[nameParts.length - 1]); // action name without the possible catalog name
369        
370        action.put("label", new I18nizableText("application", actionName));
371        action.put("description", new I18nizableText("application", actionName + "_DESCRIPTION"));
372        
373        action.put("internal", BooleanUtils.toBoolean((String) actionDescriptor.getMetaAttributes().get("internal")));
374        
375        return action;
376    }
377    
378    /**
379     * Internal method to extract the data concerning the right of the current user for an event
380     * @param event The event
381     * @return The map of right data. Keys are the rights id, and values indicates whether the current user has the right or not.
382     */
383    protected  Map<String, Object> _extractEventRightData(CalendarEvent event)
384    {
385        Map<String, Object> rightsData = new HashMap<>();
386        UserIdentity user = _currentUserProvider.getUser();
387        Calendar calendar = event.getParent();
388        
389        rightsData.put("edit", _rightManager.hasRight(user, RIGHTS_EVENT_EDIT, calendar) == RightResult.RIGHT_ALLOW);
390        rightsData.put("delete", _rightManager.hasRight(user, RIGHTS_EVENT_DELETE, calendar) == RightResult.RIGHT_ALLOW);
391        rightsData.put("delete-own", _rightManager.hasRight(user, RIGHTS_EVENT_DELETE_OWN, calendar) == RightResult.RIGHT_ALLOW);
392        
393        return rightsData;
394    }
395    
396    /**
397     * Internal method to extract the data concerning the right of the current user for a calendar
398     * @param calendar The calendar
399     * @return The map of right data. Keys are the rights id, and values indicates whether the current user has the right or not.
400     */
401    protected  Map<String, Object> _extractCalendarRightData(Calendar calendar)
402    {
403        Map<String, Object> rightsData = new HashMap<>();
404        UserIdentity user = _currentUserProvider.getUser();
405        
406        // Add
407        rightsData.put("add-event", _rightManager.hasRight(user, RIGHTS_EVENT_ADD, calendar) == RightResult.RIGHT_ALLOW);
408        
409        // edit - delete
410        rightsData.put("edit", _rightManager.hasRight(user, RIGHTS_CALENDAR_EDIT, calendar) == RightResult.RIGHT_ALLOW);
411        rightsData.put("delete", _rightManager.hasRight(user, RIGHTS_CALENDAR_DELETE, calendar) == RightResult.RIGHT_ALLOW);
412        
413        return rightsData;
414    }
415    
416    /**
417     * Sanitize the locations parameters received as input
418     * @param locations collection of locations passed as an input
419     * @return The sanitized location, a single string that represent a comma-separated list of locations 
420     */
421    protected String _sanitizeEventLocationsInput(Collection<String> locations)
422    {
423        Set<String> lowercasedPlaces = new HashSet<>();
424        
425        // duplicates are filtered out
426        // and result is returned as a single string joined with a comma delimiter  
427        return Optional.ofNullable(locations).orElseGet(ArrayList::new).stream()
428                .map(String::trim)
429                .filter(StringUtils::isNotEmpty)
430                .filter(p -> lowercasedPlaces.add(p.toLowerCase()))
431                .collect(Collectors.joining(","));
432    }
433    
434    /**
435     * Sanitize the keyword parameters received as input
436     * @param keywords collection of keywords passed as an input
437     * @return The sanitized collection
438     */
439    protected Collection<String> _sanitizeEventKeywordsInput(Collection<String> keywords)
440    {
441        // Enforce lowercase and remove possible duplicate tags
442        return Optional.ofNullable(keywords).orElseGet(ArrayList::new).stream()
443                .map(String::trim)
444                .map(String::toLowerCase)
445                .filter(StringUtils::isNotEmpty)
446                .distinct()
447                .collect(Collectors.toList());
448    }
449}