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