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