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}