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