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}