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        if (StringUtils.isNotBlank(choice) && choice.equals("unit"))
205        {
206            ArrayList<ZonedDateTime> excludedOccurrences = new ArrayList<>();
207            excludedOccurrences.addAll(event.getExcludedOccurences());
208            ZonedDateTime occurrenceDate = DateUtils.parseZonedDateTime(occurrence).withZoneSameInstant(event.getZone());
209            excludedOccurrences.add(occurrenceDate.truncatedTo(ChronoUnit.DAYS));
210            
211            event.setExcludedOccurrences(excludedOccurrences);
212        }
213        else
214        {
215            event.remove();
216        }
217        
218        calendar.saveChanges();
219        
220        // Notify listeners
221        Map<String, Object> eventParams = new HashMap<>();
222        eventParams.put(org.ametys.plugins.workspaces.calendars.ObservationConstants.ARGS_CALENDAR, calendar);
223        eventParams.put(ObservationConstants.ARGS_ID, id);
224        eventParams.put(ObservationConstants.ARGS_NAME, name);
225        eventParams.put(ObservationConstants.ARGS_PATH, path);
226        _observationManager.notify(new Event(org.ametys.plugins.workspaces.calendars.ObservationConstants.EVENT_CALENDAR_EVENT_DELETED, _currentUserProvider.getUser(), eventParams));
227        
228        result.put("id", id);
229        result.put("parentId", parentId);
230        
231        return result;
232    }
233
234    /**
235     * Add an event and return it. Use the calendar view dates to compute occurrences between those dates.
236     * @param parameters The map of parameters to perform the action
237     * @param calendarViewStartDateAsStr The calendar view start date, compute occurrences after this date.
238     * @param calendarViewEndDateAsStr The calendar view end date, compute occurrences before this date.
239     * @return The map of results populated by the underlying workflow action
240     * @throws WorkflowException if an error occurred
241     * @throws IllegalAccessException  If the user does not have the right to add an event
242     */
243    @Callable
244    public Map<String, Object> addEvent(Map<String, Object> parameters, String calendarViewStartDateAsStr, String calendarViewEndDateAsStr) throws WorkflowException, IllegalAccessException
245    {
246        ZonedDateTime calendarViewStartDate = calendarViewStartDateAsStr != null ? DateUtils.parseZonedDateTime(calendarViewStartDateAsStr) : null;
247        ZonedDateTime calendarViewEndDate = calendarViewEndDateAsStr != null ? DateUtils.parseZonedDateTime(calendarViewEndDateAsStr) : null;
248        
249        Map<String, Object> result = doWorkflowEventAction(parameters);
250        
251        //TODO Move to create event action (workflow) ?
252        String eventId = (String) result.get("id");
253        _messagingConnectorCalendarManager.addEventInvitation(parameters, eventId);
254        
255        _projectManager.getProjectsRoot().saveChanges();
256
257        JCRCalendarEvent event = _resolver.resolveById((String) result.get("id"));
258        Map<String, Object> eventDataWithFilteredOccurences = _calendarEventJSONHelper.eventAsJsonWithOccurrences(event, false, calendarViewStartDate, calendarViewEndDate);
259        
260        result.put("eventDataWithFilteredOccurences", eventDataWithFilteredOccurences);
261        
262        return result;
263    }
264    
265    /**
266     * Edit an event
267     * @param parameters The map of parameters to perform the action
268     * @param calendarViewStartDateAsStr The calendar view start date, compute occurrences after this date.
269     * @param calendarViewEndDateAsStr The calendar view end date, compute occurrences before this date.
270     * @return The map of results populated by the underlying workflow action
271     * @throws WorkflowException if an error occurred
272     */
273    @Callable
274    public Map<String, Object> editEvent(Map<String, Object> parameters, String calendarViewStartDateAsStr, String calendarViewEndDateAsStr) throws WorkflowException
275    {
276        ZonedDateTime calendarViewStartDate = calendarViewStartDateAsStr != null ? DateUtils.parseZonedDateTime(calendarViewStartDateAsStr) : null;
277        ZonedDateTime calendarViewEndDate = calendarViewEndDateAsStr != null ? DateUtils.parseZonedDateTime(calendarViewEndDateAsStr) : null;
278        
279        String eventId = (String) parameters.get("id");
280        JCRCalendarEvent event = _resolver.resolveById(eventId);
281        
282        // handle event move if calendar has changed
283        String previousCalendarId = event.getParent().getId();
284        String parentId = (String) parameters.get("parentId");
285        
286        if (previousCalendarId != null && !StringUtils.equals(parentId, previousCalendarId))
287        {
288            JCRCalendar parentCalendar = _resolver.resolveById(parentId);
289            move(event, parentCalendar);
290        }
291        
292        Map<String, Object> result = doWorkflowEventAction(parameters);
293        
294        //TODO Move to edit event action (workflow) ?
295        String choice = (String) parameters.get("choice");
296        if (!"unit".equals(choice))
297        {
298            _messagingConnectorCalendarManager.editEventInvitation(parameters, eventId);
299        }
300        
301        _projectManager.getProjectsRoot().saveChanges();
302
303        Map<String, Object> oldEventData = _calendarEventJSONHelper.eventAsJsonWithOccurrences(event, false, calendarViewStartDate, calendarViewEndDate);
304        JCRCalendarEvent newEvent = _resolver.resolveById((String) result.get("id"));
305        Map<String, Object> newEventData = _calendarEventJSONHelper.eventAsJsonWithOccurrences(newEvent, false, calendarViewStartDate, calendarViewEndDate);
306        
307        result.put("oldEventData", oldEventData);
308        result.put("newEventData", newEventData);
309        
310        return result;
311    }
312    
313    /**
314     * Move a event to another calendar
315     * @param event The event to move
316     * @param parent The new parent calendar
317     * @throws AmetysRepositoryException if an error occurred while moving
318     */
319    public void move(JCRCalendarEvent event, JCRCalendar parent) throws AmetysRepositoryException
320    {
321        try
322        {
323            event.getNode().getSession().move(event.getNode().getPath(), parent.getNode().getPath() + "/ametys:calendar-event");
324            
325            Workflow workflow = _workflowProvider.getAmetysObjectWorkflow(event);
326
327            String previousWorkflowName = workflow.getWorkflowName(event.getWorkflowId());
328            String workflowName = parent.getWorkflowName();
329
330            if (!StringUtils.equals(previousWorkflowName, workflowName))
331            {
332                // If both calendar have a different workflow, initialize a new workflow instance for the event
333                HashMap<String, Object> inputs = new HashMap<>();
334                inputs.put(AbstractNodeWorkflowComponent.EXPLORERNODE_KEY, parent);
335                workflow = _workflowProvider.getAmetysObjectWorkflow(event);
336                
337                long workflowId = workflow.initialize(workflowName, 0, inputs);
338                event.setWorkflowId(workflowId);
339            }
340        }
341        catch (WorkflowException | RepositoryException e)
342        {
343            String errorMsg = String.format("Fail to move the event '%s' to the calendar '%s'.", event.getId(), parent.getId());
344            throw new AmetysRepositoryException(errorMsg, e);
345        }
346    }
347    
348    /**
349     * Do an event workflow action
350     * @param parameters The map of action parameters
351     * @return The map of results populated by the workflow action
352     * @throws WorkflowException if an error occurred
353     */
354    @Callable
355    public Map<String, Object> doWorkflowEventAction(Map<String, Object> parameters) throws WorkflowException
356    {
357        Map<String, Object> result = new HashMap<>();
358        HashMap<String, Object> inputs = new HashMap<>();
359
360        inputs.put("parameters", parameters);
361        inputs.put("result", result);
362        
363        String eventId = (String) parameters.get("id");
364        Long workflowInstanceId = null;
365        CalendarEvent event = null;
366        if (StringUtils.isNotEmpty(eventId))
367        {
368            event = _resolver.resolveById(eventId);
369            workflowInstanceId = event.getWorkflowId();
370        }
371        
372        inputs.put("eventId", eventId);
373        
374        Calendar calendar = null;
375        String calendarId = (String) parameters.get("parentId");
376        
377        if (StringUtils.isNotEmpty(calendarId))
378        {
379            calendar = _resolver.resolveById(calendarId);
380        }
381        // parentId can be not provided for some basic actions where the event already exists
382        else if (event != null)
383        {
384            calendar = event.getParent();
385        }
386        else
387        {
388            throw new WorkflowException("Unable to retrieve the current calendar");
389        }
390        
391        inputs.put(AbstractNodeWorkflowComponent.EXPLORERNODE_KEY, calendar);
392        
393        String workflowName = calendar.getWorkflowName();
394        if (workflowName == null)
395        {
396            throw new IllegalArgumentException("The workflow name is not specified");
397        }
398        
399        int actionId =  (int) parameters.get("actionId");
400        
401        boolean sendMail = true;
402        String choice = (String) parameters.get("choice");
403        if (actionId == 2 && "unit".equals(choice))
404        {
405            sendMail = false;
406        }
407        inputs.put("sendMail", sendMail);
408        
409        Workflow workflow = _workflowProvider.getAmetysObjectWorkflow(event != null ? event : null);
410        
411        if (workflowInstanceId == null)
412        {
413            try
414            {
415                workflow.initialize(workflowName, actionId, inputs);
416            }
417            catch (WorkflowException e)
418            {
419                getLogger().error("An error occured while creating workflow '" + workflowName + "' with action '" + actionId, e);
420                throw e;
421            }
422        }
423        else
424        {
425            try
426            {
427                workflow.doAction(workflowInstanceId, actionId, inputs);
428            }
429            catch (WorkflowException e)
430            {
431                getLogger().error("An error occured while doing action '" + actionId + "'with the workflow '" + workflowName, e);
432                throw e;
433            }
434        }
435        
436        return result;
437    }
438    
439}