001/*
002 *  Copyright 2022 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 */
016
017package org.ametys.plugins.workspaces.calendars.events;
018
019import java.time.ZonedDateTime;
020import java.time.temporal.ChronoUnit;
021import java.util.ArrayList;
022import java.util.HashMap;
023import java.util.List;
024import java.util.Map;
025import java.util.stream.Collectors;
026
027import javax.jcr.RepositoryException;
028
029import org.apache.avalon.framework.service.ServiceException;
030import org.apache.avalon.framework.service.ServiceManager;
031import org.apache.commons.lang3.StringUtils;
032import org.apache.commons.lang3.Strings;
033
034import org.ametys.cms.fo.ForceDefaultRepositoryWorkspaceCallableDecorator;
035import org.ametys.core.observation.Event;
036import org.ametys.core.right.RightManager.RightResult;
037import org.ametys.core.ui.Callable;
038import org.ametys.core.user.UserIdentity;
039import org.ametys.core.util.DateUtils;
040import org.ametys.plugins.explorer.ObservationConstants;
041import org.ametys.plugins.repository.AmetysObject;
042import org.ametys.plugins.repository.AmetysRepositoryException;
043import org.ametys.plugins.workspaces.calendars.AbstractCalendarDAO;
044import org.ametys.plugins.workspaces.calendars.Calendar;
045import org.ametys.plugins.workspaces.calendars.CalendarDAO;
046import org.ametys.plugins.workspaces.calendars.CalendarWorkspaceModule;
047import org.ametys.plugins.workspaces.calendars.jcr.JCRCalendar;
048import org.ametys.plugins.workspaces.calendars.jcr.JCRCalendarEvent;
049import org.ametys.plugins.workspaces.calendars.task.TaskCalendar;
050import org.ametys.plugins.workspaces.calendars.task.TaskCalendarEvent;
051import org.ametys.plugins.workspaces.project.objects.Project;
052import org.ametys.plugins.workspaces.tasks.Task;
053import org.ametys.plugins.workspaces.workflow.AbstractNodeWorkflowComponent;
054import org.ametys.runtime.authentication.AccessDeniedException;
055
056import com.opensymphony.workflow.Workflow;
057import com.opensymphony.workflow.WorkflowException;
058
059/**
060 * Calendar event DAO
061 */
062public class CalendarEventDAO extends AbstractCalendarDAO
063{
064
065    /** Avalon Role */
066    public static final String ROLE = CalendarEventDAO.class.getName();
067
068    /** The tasks list JSON helper */
069    protected CalendarEventJSONHelper _calendarEventJSONHelper;
070    
071    /** The calendar DAO */
072    protected CalendarDAO _calendarDAO;
073    
074    @Override
075    public void service(ServiceManager manager) throws ServiceException
076    {
077        super.service(manager);
078        _calendarEventJSONHelper = (CalendarEventJSONHelper) manager.lookup(CalendarEventJSONHelper.ROLE);
079        _calendarDAO = (CalendarDAO) manager.lookup(CalendarDAO.ROLE);
080    }
081
082    /**
083     * Get the events between two dates
084     * @param startDateAsStr The start date.
085     * @param endDateAsStr The end date.
086     * @return the events between two dates
087     */
088    @Callable (rights = Callable.CHECKED_BY_IMPLEMENTATION, decorators = ForceDefaultRepositoryWorkspaceCallableDecorator.DECORATOR_ID)
089    public List<Map<String, Object>> getEvents(String startDateAsStr, String endDateAsStr)
090    {
091        ZonedDateTime startDate = startDateAsStr != null ? DateUtils.parseZonedDateTime(startDateAsStr) : null;
092        ZonedDateTime endDate = endDateAsStr != null ? DateUtils.parseZonedDateTime(endDateAsStr) : null;
093
094        return getEvents(startDate, endDate)
095                .stream()
096                .map(event -> {
097                    Map<String, Object> eventData = _calendarEventJSONHelper.eventAsJson(event, false);
098                    
099                    List<Object> occurrencesDataList = new ArrayList<>();
100                    eventData.put("occurrences", occurrencesDataList);
101                    
102                    List<CalendarEventOccurrence> occurrences = event.getOccurrences(startDate, endDate);
103                    for (CalendarEventOccurrence occurrence : occurrences)
104                    {
105                        occurrencesDataList.add(occurrence.toJSON());
106                    }
107                    return eventData;
108                })
109                .collect(Collectors.toList());
110    }
111    
112    /**
113     * Get the events between two dates
114     * @param startDate Begin date
115     * @param endDate End date
116     * @return the list of events
117     */
118    public List<CalendarEvent> getEvents(ZonedDateTime startDate, ZonedDateTime endDate)
119    {
120        CalendarWorkspaceModule calendarModule = (CalendarWorkspaceModule) _workspaceModuleEP.getModule(CalendarWorkspaceModule.CALENDAR_MODULE_ID);
121        Project project = _workspaceHelper.getProjectFromRequest();
122        
123        _checkReadAccess(project, CalendarWorkspaceModule.CALENDAR_MODULE_ID);
124        
125        List<CalendarEvent> eventList = new ArrayList<>();
126        for (Calendar calendar : calendarModule.getCalendars(project, true))
127        {
128            if (calendarModule.canView(calendar))
129            {
130                for (Map.Entry<CalendarEvent, List<CalendarEventOccurrence>> entry : calendar.getEvents(startDate, endDate).entrySet())
131                {
132                    CalendarEvent event = entry.getKey();
133                    eventList.add(event);
134                }
135            }
136        }
137      
138        Calendar resourceCalendar = calendarModule.getResourceCalendar(project);
139
140        for (Map.Entry<CalendarEvent, List<CalendarEventOccurrence>> entry : resourceCalendar.getEvents(startDate, endDate).entrySet())
141        {
142            CalendarEvent event = entry.getKey();
143            eventList.add(event);
144        }
145        
146        return eventList;
147    }
148    
149    /**
150     * Delete an event
151     * @param id The id of the event
152     * @param occurrence a string representing the occurrence date (ISO format).
153     * @param choice The type of modification
154     * @return The result map with id, parent id and message keys
155     */
156    @Callable (rights = Callable.CHECKED_BY_IMPLEMENTATION, decorators = ForceDefaultRepositoryWorkspaceCallableDecorator.DECORATOR_ID)
157    public Map<String, Object> deleteEvent(String id, String occurrence, String choice)
158    {
159        if (!"unit".equals(choice))
160        {
161            JCRCalendarEvent event = _resolver.resolveById(id);
162            _messagingConnectorCalendarManager.deleteEvent(event);
163        }
164        
165        Map<String, Object> result = new HashMap<>();
166
167        assert id != null;
168        
169        CalendarEvent event = _resolver.resolveById(id);
170        if (!(event instanceof JCRCalendarEvent))
171        {
172            throw new IllegalArgumentException("Cannot delete a non modifiable event");
173        }
174        
175        JCRCalendarEvent mevent = (JCRCalendarEvent) event;
176        JCRCalendar calendar = mevent.getParent();
177        
178        try
179        {
180            // Check user right
181            _checkUserRights(calendar, RIGHTS_EVENT_DELETE);
182        }
183        catch (AccessDeniedException e)
184        {
185            // Check if user is event's author and has right to delete its own events
186            UserIdentity user = _currentUserProvider.getUser();
187            boolean hasOwnDeleteRight = mevent.getCreator().equals(user) && _rightManager.hasRight(user, RIGHTS_EVENT_DELETE_OWN, calendar) == RightResult.RIGHT_ALLOW;
188            if (!hasOwnDeleteRight)
189            {
190                throw e; // not authorized, rethrow exception
191            }
192        }
193        
194        if (!_explorerResourcesDAO.checkLock(mevent))
195        {
196            getLogger().warn("User '" + _currentUserProvider.getUser() + "' try to delete event'" + mevent.getName() + "' but it is locked by another user");
197            result.put("message", "locked");
198            return result;
199        }
200        
201        String parentId = calendar.getId();
202        String name = mevent.getName();
203        String path = event.getPath();
204        
205        // Notify listeners
206        Map<String, Object> eventParams = new HashMap<>();
207        eventParams.put(org.ametys.plugins.workspaces.calendars.ObservationConstants.ARGS_CALENDAR, calendar);
208        eventParams.put(ObservationConstants.ARGS_ID, id);
209        eventParams.put(ObservationConstants.ARGS_NAME, name);
210        eventParams.put(ObservationConstants.ARGS_PATH, path);
211        eventParams.put(org.ametys.plugins.workspaces.calendars.ObservationConstants.ARGS_CALENDAR_EVENT, event);
212       
213        if (StringUtils.isNotBlank(choice) && choice.equals("unit"))
214        {
215            ArrayList<ZonedDateTime> excludedOccurrences = new ArrayList<>();
216            excludedOccurrences.addAll(event.getExcludedOccurences());
217            ZonedDateTime occurrenceDate = DateUtils.parseZonedDateTime(occurrence).withZoneSameInstant(event.getZone());
218            excludedOccurrences.add(occurrenceDate.truncatedTo(ChronoUnit.DAYS));
219
220            _observationManager.notify(new Event(org.ametys.plugins.workspaces.calendars.ObservationConstants.EVENT_CALENDAR_EVENT_DELETING, _currentUserProvider.getUser(), eventParams));
221            
222            mevent.setExcludedOccurrences(excludedOccurrences);
223        }
224        else
225        {
226            _observationManager.notify(new Event(org.ametys.plugins.workspaces.calendars.ObservationConstants.EVENT_CALENDAR_EVENT_DELETING, _currentUserProvider.getUser(), eventParams));
227            
228            mevent.remove();
229        }
230        
231        calendar.saveChanges();
232        
233        result.put("id", id);
234        result.put("parentId", parentId);
235        
236        eventParams = new HashMap<>();
237        eventParams.put(ObservationConstants.ARGS_ID, id);
238        _observationManager.notify(new Event(org.ametys.plugins.workspaces.calendars.ObservationConstants.EVENT_CALENDAR_EVENT_DELETED, _currentUserProvider.getUser(), eventParams));
239        
240        return result;
241    }
242
243    /**
244     * Add an event and return it. Use the calendar view dates to compute occurrences between those dates.
245     * @param parameters The map of parameters to perform the action
246     * @param calendarViewStartDateAsStr The calendar view start date, compute occurrences after this date.
247     * @param calendarViewEndDateAsStr The calendar view end date, compute occurrences before this date.
248     * @return The map of results populated by the underlying workflow action
249     * @throws WorkflowException if an error occurred
250     */
251    @Callable (rights = Callable.NO_CHECK_REQUIRED, decorators = ForceDefaultRepositoryWorkspaceCallableDecorator.DECORATOR_ID) // right protection is provided by events' workflow itself
252    public Map<String, Object> addEvent(Map<String, Object> parameters, String calendarViewStartDateAsStr, String calendarViewEndDateAsStr) throws WorkflowException
253    {
254        ZonedDateTime calendarViewStartDate = calendarViewStartDateAsStr != null ? DateUtils.parseZonedDateTime(calendarViewStartDateAsStr) : null;
255        ZonedDateTime calendarViewEndDate = calendarViewEndDateAsStr != null ? DateUtils.parseZonedDateTime(calendarViewEndDateAsStr) : null;
256        
257        Map<String, Object> result = doWorkflowEventAction(parameters);
258        
259        //TODO Move to create event action (workflow) ?
260        String eventId = (String) result.get("id");
261        _messagingConnectorCalendarManager.addEventInvitation(parameters, eventId);
262        
263        _projectManager.getProjectsRoot().saveChanges();
264
265        JCRCalendarEvent event = _resolver.resolveById((String) result.get("id"));
266        Map<String, Object> eventDataWithFilteredOccurences = _calendarEventJSONHelper.eventAsJsonWithOccurrences(event, calendarViewStartDate, calendarViewEndDate);
267        
268        result.put("eventDataWithFilteredOccurences", eventDataWithFilteredOccurences);
269        
270        return result;
271    }
272    
273    /**
274     * Edit an event
275     * @param parameters The map of parameters to perform the action
276     * @param calendarViewStartDateAsStr The calendar view start date, compute occurrences after this date.
277     * @param calendarViewEndDateAsStr The calendar view end date, compute occurrences before this date.
278     * @return The map of results populated by the underlying workflow action
279     * @throws WorkflowException if an error occurred
280     */
281    @Callable (rights = Callable.NO_CHECK_REQUIRED, decorators = ForceDefaultRepositoryWorkspaceCallableDecorator.DECORATOR_ID) // right protection is provided by events' workflow itself
282    public Map<String, Object> editEvent(Map<String, Object> parameters, String calendarViewStartDateAsStr, String calendarViewEndDateAsStr) throws WorkflowException
283    {
284        ZonedDateTime calendarViewStartDate = calendarViewStartDateAsStr != null ? DateUtils.parseZonedDateTime(calendarViewStartDateAsStr) : null;
285        ZonedDateTime calendarViewEndDate = calendarViewEndDateAsStr != null ? DateUtils.parseZonedDateTime(calendarViewEndDateAsStr) : null;
286        
287        String eventId = (String) parameters.get("id");
288        JCRCalendarEvent event = _resolver.resolveById(eventId);
289        
290        // handle event move if calendar has changed
291        String previousCalendarId = event.getParent().getId();
292        String parentId = (String) parameters.get("parentId");
293        
294        if (previousCalendarId != null && !Strings.CS.equals(parentId, previousCalendarId))
295        {
296            JCRCalendar parentCalendar = _resolver.resolveById(parentId);
297            move(event, parentCalendar);
298        }
299        
300        Map<String, Object> result = doWorkflowEventAction(parameters);
301        
302        //TODO Move to edit event action (workflow) ?
303        String choice = (String) parameters.get("choice");
304        if (!"unit".equals(choice))
305        {
306            _messagingConnectorCalendarManager.editEventInvitation(parameters, eventId);
307        }
308        
309        _projectManager.getProjectsRoot().saveChanges();
310
311        Map<String, Object> oldEventData = _calendarEventJSONHelper.eventAsJsonWithOccurrences(event, calendarViewStartDate, calendarViewEndDate);
312        JCRCalendarEvent newEvent = _resolver.resolveById((String) result.get("id"));
313        Map<String, Object> newEventData = _calendarEventJSONHelper.eventAsJsonWithOccurrences(newEvent, calendarViewStartDate, calendarViewEndDate);
314        
315        result.put("oldEventData", oldEventData);
316        result.put("newEventData", newEventData);
317        
318        return result;
319    }
320    
321    /**
322     * Move a event to another calendar
323     * @param event The event to move
324     * @param parent The new parent calendar
325     * @throws AmetysRepositoryException if an error occurred while moving
326     */
327    public void move(JCRCalendarEvent event, JCRCalendar parent) throws AmetysRepositoryException
328    {
329        try
330        {
331            event.getNode().getSession().move(event.getNode().getPath(), parent.getNode().getPath() + "/ametys:calendar-event");
332            
333            Workflow workflow = _workflowProvider.getAmetysObjectWorkflow(event);
334
335            String previousWorkflowName = workflow.getWorkflowName(event.getWorkflowId());
336            String workflowName = parent.getWorkflowName();
337
338            if (!Strings.CS.equals(previousWorkflowName, workflowName))
339            {
340                // If both calendar have a different workflow, initialize a new workflow instance for the event
341                HashMap<String, Object> inputs = new HashMap<>();
342                inputs.put(AbstractNodeWorkflowComponent.EXPLORERNODE_KEY, parent);
343                workflow = _workflowProvider.getAmetysObjectWorkflow(event);
344                
345                long workflowId = workflow.initialize(workflowName, 0, inputs);
346                event.setWorkflowId(workflowId);
347            }
348        }
349        catch (WorkflowException | RepositoryException e)
350        {
351            String errorMsg = String.format("Fail to move the event '%s' to the calendar '%s'.", event.getId(), parent.getId());
352            throw new AmetysRepositoryException(errorMsg, e);
353        }
354    }
355    
356    /**
357     * Do an event workflow action
358     * @param parameters The map of action parameters
359     * @return The map of results populated by the workflow action
360     * @throws WorkflowException if an error occurred
361     */
362    @Callable (rights = Callable.NO_CHECK_REQUIRED, decorators = ForceDefaultRepositoryWorkspaceCallableDecorator.DECORATOR_ID) // right protection is provided by events' workflow itself
363    public Map<String, Object> doWorkflowEventAction(Map<String, Object> parameters) throws WorkflowException
364    {
365        Map<String, Object> result = new HashMap<>();
366        HashMap<String, Object> inputs = new HashMap<>();
367
368        inputs.put("parameters", parameters);
369        inputs.put("result", result);
370        
371        String eventId = (String) parameters.get("id");
372        Long workflowInstanceId = null;
373        JCRCalendarEvent event = null;
374        if (StringUtils.isNotEmpty(eventId))
375        {
376            event = _resolver.resolveById(eventId);
377            workflowInstanceId = event.getWorkflowId();
378        }
379        
380        inputs.put("eventId", eventId);
381        
382        JCRCalendar calendar = null;
383        String calendarId = (String) parameters.get("parentId");
384        
385        if (StringUtils.isNotEmpty(calendarId))
386        {
387            calendar = _resolver.resolveById(calendarId);
388        }
389        // parentId can be not provided for some basic actions where the event already exists
390        else if (event != null)
391        {
392            calendar = event.getParent();
393        }
394        else
395        {
396            throw new WorkflowException("Unable to retrieve the current calendar");
397        }
398        
399        inputs.put(AbstractNodeWorkflowComponent.EXPLORERNODE_KEY, calendar);
400        
401        String workflowName = calendar.getWorkflowName();
402        if (workflowName == null)
403        {
404            throw new IllegalArgumentException("The workflow name is not specified");
405        }
406        
407        int actionId =  (int) parameters.get("actionId");
408        
409        boolean sendMail = true;
410        String choice = (String) parameters.get("choice");
411        if (actionId == 2 && "unit".equals(choice))
412        {
413            sendMail = false;
414        }
415        inputs.put("sendMail", sendMail);
416        
417        Workflow workflow = _workflowProvider.getAmetysObjectWorkflow(event != null ? event : null);
418        
419        if (workflowInstanceId == null)
420        {
421            try
422            {
423                workflow.initialize(workflowName, actionId, inputs);
424            }
425            catch (WorkflowException e)
426            {
427                getLogger().error("An error occured while creating workflow '" + workflowName + "' with action '" + actionId, e);
428                throw e;
429            }
430        }
431        else
432        {
433            try
434            {
435                workflow.doAction(workflowInstanceId, actionId, inputs);
436            }
437            catch (WorkflowException e)
438            {
439                getLogger().error("An error occured while doing action '" + actionId + "'with the workflow '" + workflowName, e);
440                throw e;
441            }
442        }
443        
444        return result;
445    }
446    
447    /**
448     * Get an event by id
449     * @param id The id of the event
450     * @return The event as a JSON map
451     */
452    @Callable (rights = Callable.CHECKED_BY_IMPLEMENTATION, decorators = ForceDefaultRepositoryWorkspaceCallableDecorator.DECORATOR_ID)
453    public Map<String, Object> getEventByID(String id)
454    {
455        Project project = _workspaceHelper.getProjectFromRequest();
456        _checkReadAccess(project, CalendarWorkspaceModule.CALENDAR_MODULE_ID);
457
458        if (!_resolver.hasAmetysObjectForId(id))
459        {
460            return null;
461        }
462        CalendarEvent event = getCalenderEventById(id);
463        
464        CalendarWorkspaceModule calendarModule = (CalendarWorkspaceModule) _workspaceModuleEP.getModule(CalendarWorkspaceModule.CALENDAR_MODULE_ID);
465        
466        if (!calendarModule.canView(event.getCalendar()))
467        {
468            throw new AccessDeniedException("User '" + _currentUserProvider.getUser() + "' tried to access to calendar module of project '" + project.getName() + "' without convenient right or calandar module does not exist.");
469        }
470        return _calendarEventJSONHelper.eventAsJsonWithOccurrences(event, event.getStartDate(), event.getFullDay() ? event.getEndDate().plusDays(1) : event.getEndDate());
471    }
472    
473    /**
474     * Get an event by id
475     * @param eventId The id of the event
476     * @return The event
477     */
478    public CalendarEvent getCalenderEventById(String eventId)
479    {
480        AmetysObject object = _resolver.resolveById(eventId);
481        if (object instanceof CalendarEvent event)
482        {
483            return event;
484        }
485        else if (object instanceof Task task)
486        {
487            Project project = _projectManager.getParentProject(task);
488            TaskCalendar taskCalendar = _calendarDAO.getTaskCalendar(project, true);
489            return taskCalendar != null ? new TaskCalendarEvent(taskCalendar, task) : null;
490        }
491        
492        return null;
493    }
494}