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}