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.resources;
018
019import java.time.ZoneId;
020import java.time.ZonedDateTime;
021import java.time.temporal.ChronoUnit;
022import java.util.ArrayList;
023import java.util.HashMap;
024import java.util.HashSet;
025import java.util.List;
026import java.util.Map;
027import java.util.Optional;
028import java.util.Set;
029import java.util.stream.Collectors;
030
031import org.apache.commons.lang3.BooleanUtils;
032import org.apache.commons.lang3.StringUtils;
033import org.apache.jackrabbit.util.Text;
034
035import org.ametys.cms.fo.ForceDefaultRepositoryWorkspaceCallableDecorator;
036import org.ametys.core.observation.Event;
037import org.ametys.core.ui.Callable;
038import org.ametys.core.util.DateUtils;
039import org.ametys.plugins.explorer.ModifiableExplorerNode;
040import org.ametys.plugins.explorer.ObservationConstants;
041import org.ametys.plugins.explorer.resources.ModifiableResourceCollection;
042import org.ametys.plugins.messagingconnector.EventRecurrenceTypeEnum;
043import org.ametys.plugins.repository.ModifiableTraversableAmetysObject;
044import org.ametys.plugins.workspaces.calendars.AbstractCalendarDAO;
045import org.ametys.plugins.workspaces.calendars.Calendar;
046import org.ametys.plugins.workspaces.calendars.CalendarWorkspaceModule;
047import org.ametys.plugins.workspaces.calendars.events.CalendarEvent;
048import org.ametys.plugins.workspaces.calendars.events.CalendarEventOccurrence;
049import org.ametys.plugins.workspaces.calendars.events.ModifiableCalendarEvent;
050import org.ametys.plugins.workspaces.calendars.helper.RecurrentEventHelper;
051import org.ametys.plugins.workspaces.calendars.jcr.JCRCalendarResource;
052import org.ametys.plugins.workspaces.calendars.jcr.JCRCalendarResourceFactory;
053import org.ametys.plugins.workspaces.project.objects.Project;
054
055/**
056 * Calendar Resource DAO
057 *
058 */
059public class CalendarResourceDAO extends AbstractCalendarDAO
060{
061
062    /** Avalon Role */
063    public static final String ROLE = CalendarResourceDAO.class.getName();
064        
065    /**
066     * Delete a calendar
067     * @param id The id of the calendar
068     * @return The result map with id, parent id and message keys
069     */
070    @Callable (rights = Callable.CHECKED_BY_IMPLEMENTATION, decorators = ForceDefaultRepositoryWorkspaceCallableDecorator.DECORATOR_ID)
071    public Map<String, Object> deleteResource(String id)
072    {
073        Map<String, Object> result = new HashMap<>();
074
075        assert id != null;
076        
077        CalendarResource resource = _resolver.resolveById(id);
078        ModifiableExplorerNode resourcesRoot = resource.getParent();
079        
080        _checkUserRights(resourcesRoot, RIGHTS_HANDLE_RESOURCE);
081        
082        if (!_explorerResourcesDAO.checkLock(resource))
083        {
084            getLogger().warn("User '" + _currentUserProvider.getUser() + "' try to delete resource'" + resource.getName() + "' but it is locked by another user");
085            result.put("message", "locked");
086            return result;
087        }
088        
089        String parentId = resourcesRoot.getId();
090        String name = resource.getName();
091        String path = resource.getPath();
092        
093        resource.remove();
094        resourcesRoot.saveChanges();
095
096        CalendarWorkspaceModule calendarModule = (CalendarWorkspaceModule) _workspaceModuleEP.getModule(CalendarWorkspaceModule.CALENDAR_MODULE_ID);
097
098        Project project = _workspaceHelper.getProjectFromRequest();
099        for (Calendar calendar : calendarModule.getCalendars(project, true))
100        {
101            _removeResource(calendar, id, calendarModule.getCalendarsRoot(project, true));
102        }
103      
104        Calendar resourceCalendar = calendarModule.getResourceCalendar(project);
105        _removeResource(resourceCalendar, id, calendarModule.getResourceCalendarRoot(project, true));
106     
107        // Notify listeners
108        Map<String, Object> eventParams = new HashMap<>();
109        eventParams.put(ObservationConstants.ARGS_ID, id);
110        eventParams.put(ObservationConstants.ARGS_PARENT_ID, parentId);
111        eventParams.put(ObservationConstants.ARGS_NAME, name);
112        eventParams.put(ObservationConstants.ARGS_PATH, path);
113        
114        _observationManager.notify(new Event(org.ametys.plugins.workspaces.calendars.ObservationConstants.EVENT_CALENDAR_RESOURCE_DELETED, _currentUserProvider.getUser(), eventParams));
115        
116        result.put("id", id);
117        result.put("parentId", parentId);
118        
119        return result;
120    }
121
122    private void _removeResource(Calendar calendar, String id, ModifiableResourceCollection modifiableResourceCollection)
123    {
124        boolean saveChanges = false;
125        List<ModifiableCalendarEvent> events = calendar.getAllEvents()
126            .stream()
127            .filter(ModifiableCalendarEvent.class::isInstance)
128            .map(ModifiableCalendarEvent.class::cast)
129            .toList();
130        
131        for (ModifiableCalendarEvent event : events)
132        {
133            List<String> resourceIds = event.getResources();
134            if (resourceIds.contains(id))
135            {
136                List<String> resourcesWithoutDeletedResource = new ArrayList<>(resourceIds);
137                resourcesWithoutDeletedResource.remove(id);
138                event.setResources(resourcesWithoutDeletedResource);
139                saveChanges = true;
140            }
141        }
142        
143        if (saveChanges)
144        {
145            modifiableResourceCollection.saveChanges();
146        }
147    }
148    
149    /**
150     * Get the resources from project
151     * @return the list of resources
152     */
153    @Callable (rights = Callable.CHECKED_BY_IMPLEMENTATION, decorators = ForceDefaultRepositoryWorkspaceCallableDecorator.DECORATOR_ID)
154    public List<Map<String, Object>> getResources()
155    {
156        List<Map<String, Object>> resourcesInfo = new ArrayList<>();
157        Project project = _workspaceHelper.getProjectFromRequest();
158        
159        _checkReadAccess(project, CalendarWorkspaceModule.CALENDAR_MODULE_ID);
160        
161        for (CalendarResource resource : getProjectResources(project))
162        {
163            resourcesInfo.add(getCalendarResourceData(resource));
164        }
165        
166        return resourcesInfo;
167    }
168
169    
170    /**
171     * Get all resources from given projets
172     * @param project the project
173     * @return All resources as JSON
174     */
175    public List<CalendarResource> getProjectResources(Project project)
176    {
177        CalendarWorkspaceModule calendarModule = (CalendarWorkspaceModule) _workspaceModuleEP.getModule(CalendarWorkspaceModule.CALENDAR_MODULE_ID);
178        ModifiableResourceCollection resourcesRoot = calendarModule.getCalendarResourcesRoot(project, true);
179        
180        return resourcesRoot.getChildren()
181            .stream()
182            .filter(CalendarResource.class::isInstance)
183            .map(CalendarResource.class::cast)
184            .collect(Collectors.toList());
185    }
186
187    /**
188     * Get calendar info
189     * @param calendarResource The calendar
190     * @return the calendar data in a map
191     */
192    public Map<String, Object> getCalendarResourceData(CalendarResource calendarResource)
193    {
194        Map<String, Object> result = new HashMap<>();
195        
196        result.put("id", calendarResource.getId());
197        result.put("title", Text.unescapeIllegalJcrChars(calendarResource.getTitle()));
198        result.put("icon", calendarResource.getIcon());
199        result.put("instructions", calendarResource.getInstructions());
200        return result;
201    }
202
203    /**
204     * Add a calendar
205     * @param title The resource title
206     * @param icon The resource icon
207     * @param instructions The resource instructions
208     * @param renameIfExists True to rename if existing
209     * @return The result map with id, parentId and name keys
210     */
211    @Callable (rights = Callable.CHECKED_BY_IMPLEMENTATION, decorators = ForceDefaultRepositoryWorkspaceCallableDecorator.DECORATOR_ID)
212    public Map<String, Object> addResource(String title, String icon, String instructions, boolean renameIfExists)
213    {
214        Map<String, Object> result = new HashMap<>();
215        
216        String originalName = Text.escapeIllegalJcrChars(title);
217        
218        Project project = _workspaceHelper.getProjectFromRequest();
219        CalendarWorkspaceModule calendarModule = (CalendarWorkspaceModule) _workspaceModuleEP.getModule(CalendarWorkspaceModule.CALENDAR_MODULE_ID);
220        ModifiableTraversableAmetysObject resourcesRoot = calendarModule.getCalendarResourcesRoot(project, true);
221        
222        _checkUserRights(resourcesRoot, RIGHTS_HANDLE_RESOURCE);
223        
224        if (BooleanUtils.isNotTrue(renameIfExists) && resourcesRoot.hasChild(originalName))
225        {
226            getLogger().warn("Cannot create the calendar with name '" + originalName + "', an object with same name already exists.");
227            result.put("message", "already-exist");
228            return result;
229        }
230        
231        if (!_explorerResourcesDAO.checkLock(resourcesRoot))
232        {
233            getLogger().warn("User '" + _currentUserProvider.getUser() + "' try to modify the object '" + resourcesRoot.getName() + "' but it is locked by another user");
234            result.put("message", "locked");
235            return result;
236        }
237        
238        int index = 2;
239        String name = originalName;
240        while (resourcesRoot.hasChild(name))
241        {
242            name = originalName + " (" + index + ")";
243            index++;
244        }
245        
246        JCRCalendarResource resource = resourcesRoot.createChild(name, JCRCalendarResourceFactory.CALENDAR_RESOURCE_NODETYPE);
247        resource.setTitle(title);
248        resource.setIcon(icon);
249        resource.setInstructions(instructions);
250        resourcesRoot.saveChanges();
251        
252        // Notify listeners
253        Map<String, Object> eventParams = new HashMap<>();
254        eventParams.put(ObservationConstants.ARGS_ID, resource.getId());
255        eventParams.put(ObservationConstants.ARGS_PARENT_ID, resourcesRoot.getId());
256        eventParams.put(ObservationConstants.ARGS_NAME, title);
257        eventParams.put(ObservationConstants.ARGS_PATH, resource.getPath());
258        
259        _observationManager.notify(new Event(org.ametys.plugins.workspaces.calendars.ObservationConstants.EVENT_CALENDAR_RESOURCE_CREATED, _currentUserProvider.getUser(), eventParams));
260        
261        result.put("id", resource.getId());
262        result.put("title", Text.unescapeIllegalJcrChars(name));
263        result.put("icon", resource.getIcon());
264        result.put("instructions", resource.getInstructions());
265
266        return result;
267    }
268
269    /**
270     * Edit a resource
271     * @param id The id of the resource
272     * @param title The resource title
273     * @param icon The resource icon
274     * @param instructions The resource instructions
275     * @return The result map with id, parentId and name keys
276     */
277    @Callable (rights = Callable.CHECKED_BY_IMPLEMENTATION, decorators = ForceDefaultRepositoryWorkspaceCallableDecorator.DECORATOR_ID)
278    public Map<String, Object> editResource(String id, String title, String icon, String instructions)
279    {
280        CalendarResource resource = _resolver.resolveById(id);
281        
282        Map<String, Object> result = new HashMap<>();
283        
284        String name = Text.escapeIllegalJcrChars(title);
285        
286        Project project = _workspaceHelper.getProjectFromRequest();
287        CalendarWorkspaceModule calendarModule = (CalendarWorkspaceModule) _workspaceModuleEP.getModule(CalendarWorkspaceModule.CALENDAR_MODULE_ID);
288        ModifiableTraversableAmetysObject resourcesRoot = calendarModule.getCalendarResourcesRoot(project, true);
289        
290        _checkUserRights(resourcesRoot, RIGHTS_HANDLE_RESOURCE);
291                
292        if (!_explorerResourcesDAO.checkLock(resourcesRoot))
293        {
294            getLogger().warn("User '" + _currentUserProvider.getUser() + "' try to modify the object '" + resourcesRoot.getName() + "' but it is locked by another user");
295            result.put("message", "locked");
296            return result;
297        }
298        
299        resource.setTitle(name);
300        resource.setIcon(icon);
301        resource.setInstructions(instructions);
302        resourcesRoot.saveChanges();
303        
304        // Notify listeners
305        Map<String, Object> eventParams = new HashMap<>();
306        eventParams.put(ObservationConstants.ARGS_ID, resource.getId());
307        eventParams.put(ObservationConstants.ARGS_PARENT_ID, resourcesRoot.getId());
308        eventParams.put(ObservationConstants.ARGS_NAME, name);
309        eventParams.put(ObservationConstants.ARGS_PATH, resource.getPath());
310        
311        _observationManager.notify(new Event(org.ametys.plugins.workspaces.calendars.ObservationConstants.EVENT_CALENDAR_RESOURCE_UPDATED, _currentUserProvider.getUser(), eventParams));
312        
313        result.put("id", resource.getId());
314        result.put("title", Text.unescapeIllegalJcrChars(name));
315        result.put("icon", resource.getIcon());
316        result.put("instructions", resource.getInstructions());
317
318        return result;
319    }
320    
321    /**
322     * Get all available resources between two dates for a given id
323     * @param eventId the event id
324     * @param startDateAsStr The start date.
325     * @param endDateAsStr The end date.
326     * @param eventStartDateAsStr The start date.
327     * @param eventEndDateAsStr The end date.
328     * @param recurrenceType The recurrence type.
329     * @param isFullDay Is the event full day
330     * @param originalOccurrenceStartAsStr original occurrence start date
331     * @param zoneIdAsString The zone ID used for the dates
332     * @return All available resources as JSON
333     */
334    @Callable (rights = Callable.CHECKED_BY_IMPLEMENTATION, decorators = ForceDefaultRepositoryWorkspaceCallableDecorator.DECORATOR_ID)
335    public List<Map<String, Object>> loadResourcesWithAvailability(String eventId, String startDateAsStr, String endDateAsStr, String eventStartDateAsStr, String eventEndDateAsStr, String recurrenceType, boolean isFullDay, String originalOccurrenceStartAsStr, String zoneIdAsString)
336    {
337        EventRecurrenceTypeEnum recurrenceEnum = EventRecurrenceTypeEnum.valueOf(recurrenceType);
338        CalendarEvent event = StringUtils.isNotEmpty(eventId) ? _resolver.resolveById(eventId) : null;
339
340        Project project = _workspaceHelper.getProjectFromRequest();
341        
342        _checkReadAccess(project, CalendarWorkspaceModule.CALENDAR_MODULE_ID);
343        
344        List<Map<String, Object>> resourcesInfo = new ArrayList<>();
345        Set<String> collideEventResources = new HashSet<>();
346        
347        ZonedDateTime startDate = DateUtils.parseZonedDateTime(startDateAsStr);
348        ZonedDateTime eventStartDate = DateUtils.parseZonedDateTime(eventStartDateAsStr);
349        ZonedDateTime eventEndDate = DateUtils.parseZonedDateTime(eventEndDateAsStr);
350        ZonedDateTime originalOccurrenceStartDate = DateUtils.parseZonedDateTime(originalOccurrenceStartAsStr);
351        ZonedDateTime endDate = DateUtils.parseZonedDateTime(endDateAsStr);
352        long diffInSeconds = ChronoUnit.SECONDS.between(eventStartDate, eventEndDate);
353
354        ZoneId zoneId = ZoneId.of(zoneIdAsString);
355
356        List<ZonedDateTime> occurencesfromDAO = RecurrentEventHelper.getOccurrences(startDate, endDate, eventStartDate, originalOccurrenceStartDate, recurrenceEnum, event != null ? event.getExcludedOccurences()  : new ArrayList<>(), zoneId, endDate);
357        
358        if (occurencesfromDAO.size() > 0)
359        {
360            
361            ZonedDateTime newStartDate = occurencesfromDAO.get(0);
362            ZonedDateTime newEndDate = occurencesfromDAO.get(occurencesfromDAO.size() - 1).plusSeconds(diffInSeconds);
363            
364            
365            List<CalendarEvent> events = _getEvents(newStartDate, newEndDate);
366            
367            // Check if any other event collide with any occurrence of the event we create/edit
368            for (CalendarEvent calendarEvent : events)
369            {
370                // Don't compute next occurrences for events without resources or the event that is edited
371                if (!calendarEvent.getResources().isEmpty() && !calendarEvent.getId().equals(eventId) && _eventCollide(calendarEvent, newStartDate, newEndDate, occurencesfromDAO, diffInSeconds, isFullDay))
372                {
373                    // Store the resources used by the event that collide with the event we create/edit
374                    collideEventResources.addAll(calendarEvent.getResources());
375                }
376            }
377        }
378
379        for (CalendarResource resource : getProjectResources(project))
380        {
381            Map<String, Object> resourceMap = getCalendarResourceData(resource);
382            
383            // The resource is available only if it is not used by any of the event that collides with the event we create/edit
384            resourceMap.put("available", !collideEventResources.contains(resource.getId()));
385            
386            resourcesInfo.add(resourceMap);
387        }
388
389        return resourcesInfo;
390    }
391    
392    private boolean _eventCollide(CalendarEvent calendarEvent, ZonedDateTime startDate, ZonedDateTime endDate, List<ZonedDateTime> occurencesfromDAO, long diffInSeconds, boolean isFullDay)
393    {
394
395        Optional<CalendarEventOccurrence> firstOccurrence = calendarEvent.getFirstOccurrence(isFullDay ? startDate : startDate.truncatedTo(ChronoUnit.DAYS));
396        
397        if (firstOccurrence.isEmpty())
398        {
399            return false;
400        }
401        
402        List<ZonedDateTime> excludedOccurences = calendarEvent.getExcludedOccurences();
403        
404        
405        ZonedDateTime firstDateCalendar = firstOccurrence.get().getStartDate();
406        
407        if (!excludedOccurences.contains(firstDateCalendar) && !_isAvailable(firstOccurrence.get(), occurencesfromDAO, diffInSeconds, isFullDay))
408        {
409            return true;
410        }
411        
412        Optional<CalendarEventOccurrence> nextOccurrence = calendarEvent.getNextOccurrence(firstOccurrence.get());
413        while (nextOccurrence.isPresent() && nextOccurrence.get().before(endDate))
414        {
415
416            ZonedDateTime nextDateCalendar = nextOccurrence.get().getStartDate();
417            
418            if (!excludedOccurences.contains(nextDateCalendar) && !_isAvailable(firstOccurrence.get(), occurencesfromDAO, diffInSeconds, isFullDay))
419            {
420                return true;
421            }
422            nextOccurrence = calendarEvent.getNextOccurrence(nextOccurrence.get());
423        }
424        
425        return false;
426    }
427
428    /**
429     * Get the events between two dates
430     * @param startDate Begin date
431     * @param endDate End date
432     * @return the list of events
433     */
434    protected List<CalendarEvent> _getEvents(ZonedDateTime startDate, ZonedDateTime endDate)
435    {
436        CalendarWorkspaceModule calendarModule = (CalendarWorkspaceModule) _workspaceModuleEP.getModule(CalendarWorkspaceModule.CALENDAR_MODULE_ID);
437        Project project = _workspaceHelper.getProjectFromRequest();
438
439        List<CalendarEvent> eventList = new ArrayList<>();
440        for (Calendar calendar : calendarModule.getCalendars(project, true))
441        {
442            if (calendarModule.canView(calendar))
443            {
444                for (Map.Entry<CalendarEvent, List<CalendarEventOccurrence>> entry : calendar.getEvents(startDate, endDate).entrySet())
445                {
446                    CalendarEvent event = entry.getKey();
447                    eventList.add(event);
448                }
449            }
450        }
451      
452        Calendar resourceCalendar = calendarModule.getResourceCalendar(project);
453        for (Map.Entry<CalendarEvent, List<CalendarEventOccurrence>> entry : resourceCalendar.getEvents(startDate, endDate).entrySet())
454        {
455            CalendarEvent event = entry.getKey();
456            eventList.add(event);
457        }
458        
459        return eventList;
460    }
461
462    
463    private boolean _isAvailable(CalendarEventOccurrence eventOccurrence, List<ZonedDateTime> occurencesfromDAO, long diffInSeconds, boolean isFullDay)
464    {
465        ZonedDateTime occurrenceStartDate = eventOccurrence.getStartDate();
466        ZonedDateTime occurrenceEndDate = eventOccurrence.getEndDate();
467        
468        
469        if (eventOccurrence.isFullDay())
470        {
471            occurrenceEndDate = occurrenceEndDate.plusDays(1);
472        }
473        
474        // Compute all occurrence of the event we create/edit
475        for (ZonedDateTime occurenceDate : occurencesfromDAO)
476        {
477            
478            ZonedDateTime startDateEvent = occurenceDate;
479            ZonedDateTime endDateEvent = occurenceDate.plusSeconds(diffInSeconds);
480
481            if (isFullDay)
482            {
483               // startDateEvent = startDateEvent.truncatedTo(ChronoUnit.DAYS);
484                
485               // endDateEvent = endDateEvent.truncatedTo(ChronoUnit.DAYS).plusDays(1);
486            }
487            if (startDateEvent.isBefore(occurrenceEndDate) && occurrenceStartDate.isBefore(endDateEvent))
488            {
489                return false;
490            }
491        }
492        
493        return true;
494    }
495    
496}