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.lang.IllegalClassException;
032import org.apache.commons.lang3.StringUtils;
033
034import org.ametys.core.observation.Event;
035import org.ametys.core.right.RightManager.RightResult;
036import org.ametys.core.ui.Callable;
037import org.ametys.core.user.UserIdentity;
038import org.ametys.core.util.DateUtils;
039import org.ametys.plugins.explorer.ObservationConstants;
040import org.ametys.plugins.repository.AmetysObject;
041import org.ametys.plugins.repository.AmetysObjectIterable;
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.CalendarWorkspaceModule;
046import org.ametys.plugins.workspaces.calendars.ModifiableCalendar;
047import org.ametys.plugins.workspaces.calendars.jcr.JCRCalendar;
048import org.ametys.plugins.workspaces.calendars.jcr.JCRCalendarEvent;
049import org.ametys.plugins.workspaces.project.objects.Project;
050import org.ametys.plugins.workspaces.workflow.AbstractNodeWorkflowComponent;
051
052import com.opensymphony.workflow.Workflow;
053import com.opensymphony.workflow.WorkflowException;
054
055/**
056 * Calendar event DAO
057 */
058public class CalendarEventDAO extends AbstractCalendarDAO
059{
060
061    /** Avalon Role */
062    public static final String ROLE = CalendarEventDAO.class.getName();
063
064    /** The tasks list JSON helper */
065    protected CalendarEventJSONHelper _calendarEventJSONHelper;
066    
067    @Override
068    public void service(ServiceManager manager) throws ServiceException
069    {
070        super.service(manager);
071        _calendarEventJSONHelper = (CalendarEventJSONHelper) manager.lookup(CalendarEventJSONHelper.ROLE);
072    }
073    
074
075    /**
076     * Get the events between two dates
077     * @param startDateAsStr The start date.
078     * @param endDateAsStr The end date.
079     * @return the events between two dates
080     */
081    @Callable
082    public List<Map<String, Object>> getEvents(String startDateAsStr, String endDateAsStr)
083    {
084        ZonedDateTime startDate = startDateAsStr != null ? DateUtils.parseZonedDateTime(startDateAsStr) : null;
085        ZonedDateTime endDate = endDateAsStr != null ? DateUtils.parseZonedDateTime(endDateAsStr) : null;
086
087        return getEvents(startDate, endDate)
088                .stream()
089                .map(event -> {
090                    Map<String, Object> eventData = _calendarEventJSONHelper.eventAsJson(event, false, false);
091                    
092                    List<Object> occurrencesDataList = new ArrayList<>();
093                    eventData.put("occurrences", occurrencesDataList);
094                    
095                    List<CalendarEventOccurrence> occurrences = event.getOccurrences(startDate, endDate);
096                    for (CalendarEventOccurrence occurrence : occurrences)
097                    {
098                        occurrencesDataList.add(occurrence.toJSON());
099                    }
100                    return eventData;
101                })
102                .collect(Collectors.toList());
103    }
104    
105    /**
106     * Get the events between two dates
107     * @param startDate Begin date
108     * @param endDate End date
109     * @return the list of events
110     */
111    public List<CalendarEvent> getEvents(ZonedDateTime startDate, ZonedDateTime endDate)
112    {
113
114        CalendarWorkspaceModule calendarModule = (CalendarWorkspaceModule) _workspaceModuleEP.getModule(CalendarWorkspaceModule.CALENDAR_MODULE_ID);
115        Project project = _getProject();
116        AmetysObjectIterable<Calendar> calendars = calendarModule.getCalendars(project);
117
118        List<CalendarEvent> eventList = new ArrayList<>();
119        if (calendars != null)
120        {
121            for (Calendar calendar : calendars)
122            {
123                if (calendarModule.canView(calendar))
124                {
125                    for (Map.Entry<CalendarEvent, List<CalendarEventOccurrence>> entry : calendar.getEvents(startDate, endDate).entrySet())
126                    {
127                        CalendarEvent event = entry.getKey();
128                        eventList.add(event);
129                    }
130                }
131            }
132        }
133      
134        Calendar resourceCalendar = calendarModule.getResourceCalendar(_getProject());
135
136        for (Map.Entry<CalendarEvent, List<CalendarEventOccurrence>> entry : resourceCalendar.getEvents(startDate, endDate).entrySet())
137        {
138            CalendarEvent event = entry.getKey();
139            eventList.add(event);
140        }
141        
142        return eventList;
143    }
144    
145    /**
146     * Delete an event
147     * @param id The id of the event
148     * @param occurrence a string representing the occurrence date (ISO format).
149     * @param choice The type of modification
150     * @return The result map with id, parent id and message keys
151     * @throws IllegalAccessException If the user has no sufficient rights
152     */
153    @Callable
154    public Map<String, Object> deleteEvent(String id, String occurrence, String choice) throws IllegalAccessException
155    {
156        if (!"unit".equals(choice))
157        {
158            JCRCalendarEvent event = _resolver.resolveById(id);
159            _messagingConnectorCalendarManager.deleteEvent(event);
160        }
161        
162        Map<String, Object> result = new HashMap<>();
163
164        assert id != null;
165        
166        AmetysObject object = _resolver.resolveById(id);
167        if (!(object instanceof ModifiableCalendarEvent))
168        {
169            throw new IllegalClassException(ModifiableCalendarEvent.class, object.getClass());
170        }
171        
172        ModifiableCalendarEvent event = (ModifiableCalendarEvent) object;
173        ModifiableCalendar calendar = event.getParent();
174        
175        // Check user right
176        try
177        {
178            _explorerResourcesDAO.checkUserRight(calendar, RIGHTS_EVENT_DELETE);
179        }
180        catch (IllegalAccessException e)
181        {
182            UserIdentity user = _currentUserProvider.getUser();
183            UserIdentity creator = event.getCreator();
184            RightResult rightCreator = _rightManager.hasRight(user, RIGHTS_EVENT_DELETE_OWN, calendar);
185            boolean hasOwnDeleteRight = rightCreator == RightResult.RIGHT_ALLOW && creator.equals(user);
186            if (!hasOwnDeleteRight)
187            {
188                // rethrow exception
189                throw e;
190            }
191        }
192        
193        if (!_explorerResourcesDAO.checkLock(event))
194        {
195            getLogger().warn("User '" + _currentUserProvider.getUser() + "' try to delete event'" + object.getName() + "' but it is locked by another user");
196            result.put("message", "locked");
197            return result;
198        }
199        
200        String parentId = calendar.getId();
201        String name = event.getName();
202        String path = event.getPath();
203        
204        // Notify listeners
205        Map<String, Object> eventParams = new HashMap<>();
206        eventParams.put(org.ametys.plugins.workspaces.calendars.ObservationConstants.ARGS_CALENDAR, calendar);
207        eventParams.put(ObservationConstants.ARGS_ID, id);
208        eventParams.put(ObservationConstants.ARGS_NAME, name);
209        eventParams.put(ObservationConstants.ARGS_PATH, path);
210        eventParams.put(org.ametys.plugins.workspaces.calendars.ObservationConstants.ARGS_CALENDAR_EVENT, event);
211       
212        if (StringUtils.isNotBlank(choice) && choice.equals("unit"))
213        {
214            ArrayList<ZonedDateTime> excludedOccurrences = new ArrayList<>();
215            excludedOccurrences.addAll(event.getExcludedOccurences());
216            ZonedDateTime occurrenceDate = DateUtils.parseZonedDateTime(occurrence).withZoneSameInstant(event.getZone());
217            excludedOccurrences.add(occurrenceDate.truncatedTo(ChronoUnit.DAYS));
218
219            _observationManager.notify(new Event(org.ametys.plugins.workspaces.calendars.ObservationConstants.EVENT_CALENDAR_EVENT_DELETING, _currentUserProvider.getUser(), eventParams));
220            
221            event.setExcludedOccurrences(excludedOccurrences);
222        }
223        else
224        {
225            _observationManager.notify(new Event(org.ametys.plugins.workspaces.calendars.ObservationConstants.EVENT_CALENDAR_EVENT_DELETING, _currentUserProvider.getUser(), eventParams));
226            
227            event.remove();
228        }
229        
230        calendar.saveChanges();
231        
232        result.put("id", id);
233        result.put("parentId", parentId);
234        
235        eventParams = new HashMap<>();
236        eventParams.put(ObservationConstants.ARGS_ID, id);
237        _observationManager.notify(new Event(org.ametys.plugins.workspaces.calendars.ObservationConstants.EVENT_CALENDAR_EVENT_DELETED, _currentUserProvider.getUser(), eventParams));
238        
239        return result;
240    }
241
242    /**
243     * Add an event and return it. Use the calendar view dates to compute occurrences between those dates.
244     * @param parameters The map of parameters to perform the action
245     * @param calendarViewStartDateAsStr The calendar view start date, compute occurrences after this date.
246     * @param calendarViewEndDateAsStr The calendar view end date, compute occurrences before this date.
247     * @return The map of results populated by the underlying workflow action
248     * @throws WorkflowException if an error occurred
249     * @throws IllegalAccessException  If the user does not have the right to add an event
250     */
251    @Callable
252    public Map<String, Object> addEvent(Map<String, Object> parameters, String calendarViewStartDateAsStr, String calendarViewEndDateAsStr) throws WorkflowException, IllegalAccessException
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, false, 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
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 && !StringUtils.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, false, calendarViewStartDate, calendarViewEndDate);
312        JCRCalendarEvent newEvent = _resolver.resolveById((String) result.get("id"));
313        Map<String, Object> newEventData = _calendarEventJSONHelper.eventAsJsonWithOccurrences(newEvent, false, 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 (!StringUtils.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
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        CalendarEvent 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        Calendar 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}