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);
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
236     * @param parameters The map of parameters to perform the action
237     * @return The map of results populated by the underlying workflow action
238     * @throws WorkflowException if an error occurred
239     * @throws IllegalAccessException  If the user does not have the right to add an event
240     */
241    @Callable
242    public Map<String, Object> addEvent(Map<String, Object> parameters) throws WorkflowException, IllegalAccessException
243    {
244        Map<String, Object> result = doWorkflowEventAction(parameters);
245        
246        //TODO Move to create event action (workflow) ?
247        String eventId = (String) result.get("id");
248        _messagingConnectorCalendarManager.addEventInvitation(parameters, eventId);
249        
250        _projectManager.getProjectsRoot().saveChanges();
251
252        JCRCalendarEvent event = _resolver.resolveById((String) result.get("id"));
253        Map<String, Object> eventDataWithFilteredOccurences = _calendarEventJSONHelper.eventAsJsonWithOccurrences(event, false, event.getStartDate(), event.getStartDate().plusYears(1));
254        
255        result.put("eventDataWithFilteredOccurences", eventDataWithFilteredOccurences);
256        
257        return result;
258    }
259    
260    /**
261     * Edit an event
262     * @param parameters The map of parameters to perform the action
263     * @return The map of results populated by the underlying workflow action
264     * @throws WorkflowException if an error occurred
265     */
266    @Callable
267    public Map<String, Object> editEvent(Map<String, Object> parameters) throws WorkflowException
268    {
269        String eventId = (String) parameters.get("id");
270        JCRCalendarEvent event = _resolver.resolveById(eventId);
271        
272        // handle event move if calendar has changed
273        String previousCalendarId = event.getParent().getId();
274        String parentId = (String) parameters.get("parentId");
275        
276        if (previousCalendarId != null && !StringUtils.equals(parentId, previousCalendarId))    
277        {
278            JCRCalendar parentCalendar = _resolver.resolveById(parentId);
279            move(event, parentCalendar);
280        }
281        
282        Map<String, Object> result = doWorkflowEventAction(parameters);
283        
284        //TODO Move to edit event action (workflow) ?
285        String choice = (String) parameters.get("choice");
286        if (!"unit".equals(choice))
287        {
288            _messagingConnectorCalendarManager.editEventInvitation(parameters, eventId);
289        }
290        
291        _projectManager.getProjectsRoot().saveChanges();
292
293        Map<String, Object> oldEventData = _calendarEventJSONHelper.eventAsJsonWithOccurrences(event, false, event.getStartDate(), event.getStartDate().plusYears(1));
294        JCRCalendarEvent newEvent = _resolver.resolveById((String) result.get("id"));
295        Map<String, Object> newEventData = _calendarEventJSONHelper.eventAsJsonWithOccurrences(newEvent, false, newEvent.getStartDate(), newEvent.getStartDate().plusYears(1));
296        
297        result.put("oldEventData", oldEventData);
298        result.put("newEventData", newEventData);
299        
300        return result;
301    }
302    
303    /**
304     * Move a event to another calendar
305     * @param event The event to move
306     * @param parent The new parent calendar
307     * @throws AmetysRepositoryException if an error occurred while moving
308     */
309    public void move(JCRCalendarEvent event, JCRCalendar parent) throws AmetysRepositoryException
310    {
311        try
312        {
313            event.getNode().getSession().move(event.getNode().getPath(), parent.getNode().getPath() + "/ametys:calendar-event");
314            
315            Workflow workflow = _workflowProvider.getAmetysObjectWorkflow(event);
316
317            String previousWorkflowName = workflow.getWorkflowName(event.getWorkflowId());
318            String workflowName = parent.getWorkflowName();
319
320            if (!StringUtils.equals(previousWorkflowName, workflowName))
321            {
322                // If both calendar have a different workflow, initialize a new workflow instance for the event
323                HashMap<String, Object> inputs = new HashMap<>();
324                inputs.put(AbstractNodeWorkflowComponent.EXPLORERNODE_KEY, parent);
325                workflow = _workflowProvider.getAmetysObjectWorkflow(event);
326                
327                long workflowId = workflow.initialize(workflowName, 0, inputs);
328                event.setWorkflowId(workflowId);
329            }
330        }
331        catch (WorkflowException | RepositoryException e)
332        {
333            String errorMsg = String.format("Fail to move the event '%s' to the calendar '%s'.", event.getId(), parent.getId());
334            throw new AmetysRepositoryException(errorMsg, e);
335        }
336    }
337    
338    /**
339     * Do an event workflow action
340     * @param parameters The map of action parameters
341     * @return The map of results populated by the workflow action
342     * @throws WorkflowException if an error occurred
343     */
344    @Callable
345    public Map<String, Object> doWorkflowEventAction(Map<String, Object> parameters) throws WorkflowException
346    {
347        Map<String, Object> result = new HashMap<>();
348        HashMap<String, Object> inputs = new HashMap<>();
349
350        inputs.put("parameters", parameters);
351        inputs.put("result", result);
352        
353        String eventId = (String) parameters.get("id");
354        Long workflowInstanceId = null;
355        CalendarEvent event = null;
356        if (StringUtils.isNotEmpty(eventId))
357        {
358            event = _resolver.resolveById(eventId);
359            workflowInstanceId = event.getWorkflowId();
360        }
361        
362        inputs.put("eventId", eventId);
363        
364        Calendar calendar = null; 
365        String calendarId = (String) parameters.get("parentId");
366        
367        if (StringUtils.isNotEmpty(calendarId))
368        {
369            calendar = _resolver.resolveById(calendarId);
370        }
371        // parentId can be not provided for some basic actions where the event already exists
372        else if (event != null)
373        {
374            calendar = event.getParent();
375        }
376        else
377        {
378            throw new WorkflowException("Unable to retrieve the current calendar");
379        }
380        
381        inputs.put(AbstractNodeWorkflowComponent.EXPLORERNODE_KEY, calendar);
382        
383        String workflowName = calendar.getWorkflowName();
384        if (workflowName == null)
385        {
386            throw new IllegalArgumentException("The workflow name is not specified");
387        }
388        
389        int actionId =  (int) parameters.get("actionId");
390        
391        boolean sendMail = true;
392        String choice = (String) parameters.get("choice");
393        if (actionId == 2 && "unit".equals(choice))
394        {
395            sendMail = false;
396        }
397        inputs.put("sendMail", sendMail);
398        
399        Workflow workflow = _workflowProvider.getAmetysObjectWorkflow(event != null ? event : null);
400        
401        if (workflowInstanceId == null)
402        {
403            try
404            {
405                workflow.initialize(workflowName, actionId, inputs);
406            }   
407            catch (WorkflowException e)
408            {
409                getLogger().error("An error occured while creating workflow '" + workflowName + "' with action '" + actionId, e);
410                throw e;
411            }
412        }
413        else
414        {
415            try
416            {
417                workflow.doAction(workflowInstanceId, actionId, inputs);
418            }
419            catch (WorkflowException e)
420            {
421                getLogger().error("An error occured while doing action '" + actionId + "'with the workflow '" + workflowName, e);
422                throw e;
423            }
424        }
425        
426        return result;
427    }
428    
429}