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}