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        List<Map<String, Object>> resourcesInfo = new ArrayList<>();
186        Project project = _getProject();
187        for (CalendarResource resource : getProjectResources(project))
188        {
189            resourcesInfo.add(getCalendarResourceData(resource));
190        }
191        
192        return resourcesInfo;
193    }
194
195    
196    /**
197     * Get all resources from given projets
198     * @param project the project
199     * @return All resources as JSON
200     */
201    public List<CalendarResource> getProjectResources(Project project)
202    {
203        CalendarWorkspaceModule calendarModule = (CalendarWorkspaceModule) _workspaceModuleEP.getModule(CalendarWorkspaceModule.CALENDAR_MODULE_ID);
204        ModifiableTraversableAmetysObject resourcesRoot = calendarModule.getCalendarResourcesRoot(project, true);
205        return resourcesRoot.getChildren()
206            .stream()
207            .filter(CalendarResource.class::isInstance)
208            .map(CalendarResource.class::cast)
209            .collect(Collectors.toList());
210    }
211
212    /**
213     * Get calendar info
214     * @param calendarResource The calendar
215     * @return the calendar data in a map
216     */
217    public Map<String, Object> getCalendarResourceData(CalendarResource calendarResource)
218    {
219        Map<String, Object> result = new HashMap<>();
220        
221        result.put("id", calendarResource.getId());
222        result.put("title", Text.unescapeIllegalJcrChars(calendarResource.getTitle()));
223        result.put("icon", calendarResource.getIcon());
224        result.put("instructions", calendarResource.getInstructions());
225        return result;
226    }
227
228    /**
229     * Add a calendar
230     * @param title The resource title
231     * @param icon The resource icon
232     * @param instructions The resource instructions
233     * @param renameIfExists True to rename if existing
234     * @return The result map with id, parentId and name keys
235     * @throws IllegalAccessException If the user has no sufficient rights
236     */
237    @Callable
238    public Map<String, Object> addResource(String title, String icon, String instructions, boolean renameIfExists) throws IllegalAccessException
239    {
240        Map<String, Object> result = new HashMap<>();
241        
242        String originalName = Text.escapeIllegalJcrChars(title);
243        
244        CalendarWorkspaceModule calendarModule = (CalendarWorkspaceModule) _workspaceModuleEP.getModule(CalendarWorkspaceModule.CALENDAR_MODULE_ID);
245        ModifiableTraversableAmetysObject resourcesRoot = calendarModule.getCalendarResourcesRoot(_getProject(), true);
246        
247        // Check user right
248        _explorerResourcesDAO.checkUserRight(resourcesRoot, RIGHTS_HANDLE_RESOURCE);
249        
250        if (BooleanUtils.isNotTrue(renameIfExists) && resourcesRoot.hasChild(originalName))
251        {
252            getLogger().warn("Cannot create the calendar with name '" + originalName + "', an object with same name already exists.");
253            result.put("message", "already-exist");
254            return result;
255        }
256        
257        if (!_explorerResourcesDAO.checkLock(resourcesRoot))
258        {
259            getLogger().warn("User '" + _currentUserProvider.getUser() + "' try to modify the object '" + resourcesRoot.getName() + "' but it is locked by another user");
260            result.put("message", "locked");
261            return result;
262        }
263        
264        int index = 2;
265        String name = originalName;
266        while (resourcesRoot.hasChild(name))
267        {
268            name = originalName + " (" + index + ")";
269            index++;
270        }
271        
272        JCRCalendarResource resource = resourcesRoot.createChild(name, JCRCalendarResourceFactory.CALENDAR_RESOURCE_NODETYPE);
273        resource.setTitle(title);
274        resource.setIcon(icon);
275        resource.setInstructions(instructions);
276        resourcesRoot.saveChanges();
277        
278        // Notify listeners
279        Map<String, Object> eventParams = new HashMap<>();
280        eventParams.put(ObservationConstants.ARGS_ID, resource.getId());
281        eventParams.put(ObservationConstants.ARGS_PARENT_ID, resourcesRoot.getId());
282        eventParams.put(ObservationConstants.ARGS_NAME, title);
283        eventParams.put(ObservationConstants.ARGS_PATH, resource.getPath());
284        
285        _observationManager.notify(new Event(org.ametys.plugins.workspaces.calendars.ObservationConstants.EVENT_CALENDAR_RESOURCE_CREATED, _currentUserProvider.getUser(), eventParams));
286        
287        result.put("id", resource.getId());
288        result.put("title", Text.unescapeIllegalJcrChars(name));
289        result.put("icon", resource.getIcon());
290        result.put("instructions", resource.getInstructions());
291
292        return result;
293    }
294
295    /**
296     * Edit a resource
297     * @param id The id of the resource
298     * @param title The resource title
299     * @param icon The resource icon
300     * @param instructions The resource instructions
301     * @return The result map with id, parentId and name keys
302     * @throws IllegalAccessException If the user has no sufficient rights
303     */
304    @Callable
305    public Map<String, Object> editResource(String id, String title, String icon, String instructions) throws IllegalAccessException
306    {
307
308        AmetysObject object = _resolver.resolveById(id);
309        if (!(object instanceof CalendarResource))
310        {
311            throw new IllegalClassException(CalendarResource.class, object.getClass());
312        }
313        
314        CalendarResource resource = (CalendarResource) object;
315        
316        Map<String, Object> result = new HashMap<>();
317        
318        String name = Text.escapeIllegalJcrChars(title);
319        
320        CalendarWorkspaceModule calendarModule = (CalendarWorkspaceModule) _workspaceModuleEP.getModule(CalendarWorkspaceModule.CALENDAR_MODULE_ID);
321        ModifiableTraversableAmetysObject resourcesRoot = calendarModule.getCalendarResourcesRoot(_getProject(), true);
322        
323        // Check user right
324        _explorerResourcesDAO.checkUserRight(resourcesRoot, RIGHTS_HANDLE_RESOURCE);
325                
326        if (!_explorerResourcesDAO.checkLock(resourcesRoot))
327        {
328            getLogger().warn("User '" + _currentUserProvider.getUser() + "' try to modify the object '" + resourcesRoot.getName() + "' but it is locked by another user");
329            result.put("message", "locked");
330            return result;
331        }
332        
333        resource.setTitle(name);
334        resource.setIcon(icon);
335        resource.setInstructions(instructions);
336        resourcesRoot.saveChanges();
337        
338        // Notify listeners
339        Map<String, Object> eventParams = new HashMap<>();
340        eventParams.put(ObservationConstants.ARGS_ID, resource.getId());
341        eventParams.put(ObservationConstants.ARGS_PARENT_ID, resourcesRoot.getId());
342        eventParams.put(ObservationConstants.ARGS_NAME, name);
343        eventParams.put(ObservationConstants.ARGS_PATH, resource.getPath());
344        
345        _observationManager.notify(new Event(org.ametys.plugins.workspaces.calendars.ObservationConstants.EVENT_CALENDAR_RESOURCE_UPDATED, _currentUserProvider.getUser(), eventParams));
346        
347        result.put("id", resource.getId());
348        result.put("title", Text.unescapeIllegalJcrChars(name));
349        result.put("icon", resource.getIcon());
350        result.put("instructions", resource.getInstructions());
351
352        return result;
353    }
354    
355    /**
356     * Get all available resources between two dates for a given id
357     * @param eventId the event id
358     * @param startDateAsStr The start date.
359     * @param endDateAsStr The end date.
360     * @param eventStartDateAsStr The start date.
361     * @param eventEndDateAsStr The end date.
362     * @param recurrenceType The recurrence type.
363     * @param isFullDay Is the event full day
364     * @param originalOccurrenceStartAsStr original occurrence start date
365     * @param zoneIdAsString The zone ID used for the dates
366     * @return All available resources as JSON
367     */
368    @Callable
369    public List<Map<String, Object>> loadResourcesWithAvailability(String eventId, String startDateAsStr, String endDateAsStr, String eventStartDateAsStr, String eventEndDateAsStr, String recurrenceType, boolean isFullDay, String originalOccurrenceStartAsStr, String zoneIdAsString)
370    {
371        String projectName = _getProjectName();
372        EventRecurrenceTypeEnum recurrenceEnum = EventRecurrenceTypeEnum.valueOf(recurrenceType);
373        CalendarEvent event = StringUtils.isNotEmpty(eventId) ? _resolver.resolveById(eventId) : null;
374
375        Project project = _projectManager.getProject(projectName);
376        List<Map<String, Object>> resourcesInfo = new ArrayList<>();
377        Set<String> collideEventResources = new HashSet<>();
378        
379        ZonedDateTime startDate = DateUtils.parseZonedDateTime(startDateAsStr);
380        ZonedDateTime eventStartDate = DateUtils.parseZonedDateTime(eventStartDateAsStr);
381        ZonedDateTime eventEndDate = DateUtils.parseZonedDateTime(eventEndDateAsStr);
382        ZonedDateTime originalOccurrenceStartDate = DateUtils.parseZonedDateTime(originalOccurrenceStartAsStr);
383        ZonedDateTime endDate = DateUtils.parseZonedDateTime(endDateAsStr);
384        long diffInSeconds = ChronoUnit.SECONDS.between(eventStartDate, eventEndDate);
385
386        ZoneId zoneId = ZoneId.of(zoneIdAsString);
387
388        List<ZonedDateTime> occurencesfromDAO = RecurrentEventHelper.getOccurrences(startDate, endDate, eventStartDate, originalOccurrenceStartDate, recurrenceEnum, event != null ? event.getExcludedOccurences()  : new ArrayList<>(), zoneId, endDate);
389        
390        if (occurencesfromDAO.size() > 0)
391        {
392            
393            ZonedDateTime newStartDate = occurencesfromDAO.get(0);
394            ZonedDateTime newEndDate = occurencesfromDAO.get(occurencesfromDAO.size() - 1).plusSeconds(diffInSeconds);
395            
396            
397            List<CalendarEvent> events = _getEvents(newStartDate, newEndDate);
398            
399            // Check if any other event collide with any occurrence of the event we create/edit
400            for (CalendarEvent calendarEvent : events)
401            {
402                // Don't compute next occurrences for events without resources or the event that is edited
403                if (!calendarEvent.getResources().isEmpty() && !calendarEvent.getId().equals(eventId) && _eventCollide(calendarEvent, newStartDate, newEndDate, occurencesfromDAO, diffInSeconds, isFullDay))
404                {
405                    // Store the resources used by the event that collide with the event we create/edit
406                    collideEventResources.addAll(calendarEvent.getResources());
407                }
408            }
409        }
410
411        for (CalendarResource resource : getProjectResources(project))
412        {
413            Map<String, Object> resourceMap = getCalendarResourceData(resource);
414            
415            // The resource is available only if it is not used by any of the event that collides with the event we create/edit
416            resourceMap.put("available", !collideEventResources.contains(resource.getId()));
417            
418            resourcesInfo.add(resourceMap);
419        }
420
421        return resourcesInfo;
422    }
423    
424    private boolean _eventCollide(CalendarEvent calendarEvent, ZonedDateTime startDate, ZonedDateTime endDate, List<ZonedDateTime> occurencesfromDAO, long diffInSeconds, boolean isFullDay)
425    {
426
427        Optional<CalendarEventOccurrence> firstOccurrence = calendarEvent.getFirstOccurrence(isFullDay ? startDate : startDate.truncatedTo(ChronoUnit.DAYS));
428        
429        if (firstOccurrence.isEmpty())
430        {
431            return false;
432        }
433        
434        List<ZonedDateTime> excludedOccurences = calendarEvent.getExcludedOccurences();
435        
436        
437        ZonedDateTime firstDateCalendar = firstOccurrence.get().getStartDate();
438        
439        if (!excludedOccurences.contains(firstDateCalendar) && !_isAvailable(firstOccurrence.get(), occurencesfromDAO, diffInSeconds, isFullDay))
440        {
441            return true;
442        }
443        
444        Optional<CalendarEventOccurrence> nextOccurrence = calendarEvent.getNextOccurrence(firstOccurrence.get());
445        while (nextOccurrence.isPresent() && nextOccurrence.get().before(endDate))
446        {
447
448            ZonedDateTime nextDateCalendar = nextOccurrence.get().getStartDate();
449            
450            if (!excludedOccurences.contains(nextDateCalendar) && !_isAvailable(firstOccurrence.get(), occurencesfromDAO, diffInSeconds, isFullDay))
451            {
452                return true;
453            }
454            nextOccurrence = calendarEvent.getNextOccurrence(nextOccurrence.get());
455        }
456        
457        return false;
458    }
459
460    /**
461     * Get the events between two dates
462     * @param startDate Begin date
463     * @param endDate End date
464     * @return the list of events
465     */
466    protected List<CalendarEvent> _getEvents(ZonedDateTime startDate, ZonedDateTime endDate)
467    {
468
469        CalendarWorkspaceModule calendarModule = (CalendarWorkspaceModule) _workspaceModuleEP.getModule(CalendarWorkspaceModule.CALENDAR_MODULE_ID);
470        Project project = _getProject();
471        AmetysObjectIterable<Calendar> calendars = calendarModule.getCalendars(project);
472
473        List<CalendarEvent> eventList = new ArrayList<>();
474        if (calendars != null)
475        {
476            for (Calendar calendar : calendars)
477            {
478                if (calendarModule.canView(calendar))
479                {
480                    for (Map.Entry<CalendarEvent, List<CalendarEventOccurrence>> entry : calendar.getEvents(startDate, endDate).entrySet())
481                    {
482                        CalendarEvent event = entry.getKey();
483                        eventList.add(event);
484                    }
485                }
486            }
487        }
488      
489        Calendar resourceCalendar = calendarModule.getResourceCalendar(_getProject());
490
491        for (Map.Entry<CalendarEvent, List<CalendarEventOccurrence>> entry : resourceCalendar.getEvents(startDate, endDate).entrySet())
492        {
493            CalendarEvent event = entry.getKey();
494            eventList.add(event);
495        }
496        
497        return eventList; 
498    }
499
500    
501    private boolean _isAvailable(CalendarEventOccurrence eventOccurrence, List<ZonedDateTime> occurencesfromDAO, long diffInSeconds, boolean isFullDay)
502    {
503        ZonedDateTime occurrenceStartDate = eventOccurrence.getStartDate();
504        ZonedDateTime occurrenceEndDate = eventOccurrence.getEndDate();
505        
506        
507        if (eventOccurrence.isFullDay())
508        {                
509            occurrenceEndDate = occurrenceEndDate.plusDays(1);
510        }
511        
512        // Compute all occurrence of the event we create/edit
513        for (ZonedDateTime occurenceDate : occurencesfromDAO)
514        {
515            
516            ZonedDateTime startDateEvent = occurenceDate;
517            ZonedDateTime endDateEvent = occurenceDate.plusSeconds(diffInSeconds);
518
519            if (isFullDay)
520            {
521               // startDateEvent = startDateEvent.truncatedTo(ChronoUnit.DAYS);
522                
523               // endDateEvent = endDateEvent.truncatedTo(ChronoUnit.DAYS).plusDays(1);
524            }
525            if (startDateEvent.isBefore(occurrenceEndDate) && occurrenceStartDate.isBefore(endDateEvent))
526            {
527                return false;
528            }
529        }
530        
531        return true;
532    }
533    
534}