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