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