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.events; 018 019import java.time.ZonedDateTime; 020import java.time.temporal.ChronoUnit; 021import java.util.ArrayList; 022import java.util.HashMap; 023import java.util.List; 024import java.util.Map; 025import java.util.stream.Collectors; 026 027import javax.jcr.RepositoryException; 028 029import org.apache.avalon.framework.service.ServiceException; 030import org.apache.avalon.framework.service.ServiceManager; 031import org.apache.commons.lang3.StringUtils; 032 033import org.ametys.core.observation.Event; 034import org.ametys.core.right.RightManager.RightResult; 035import org.ametys.core.ui.Callable; 036import org.ametys.core.user.UserIdentity; 037import org.ametys.core.util.DateUtils; 038import org.ametys.plugins.explorer.ObservationConstants; 039import org.ametys.plugins.repository.AmetysObject; 040import org.ametys.plugins.repository.AmetysRepositoryException; 041import org.ametys.plugins.workspaces.calendars.AbstractCalendarDAO; 042import org.ametys.plugins.workspaces.calendars.Calendar; 043import org.ametys.plugins.workspaces.calendars.CalendarDAO; 044import org.ametys.plugins.workspaces.calendars.CalendarWorkspaceModule; 045import org.ametys.plugins.workspaces.calendars.jcr.JCRCalendar; 046import org.ametys.plugins.workspaces.calendars.jcr.JCRCalendarEvent; 047import org.ametys.plugins.workspaces.calendars.task.TaskCalendar; 048import org.ametys.plugins.workspaces.calendars.task.TaskCalendarEvent; 049import org.ametys.plugins.workspaces.project.objects.Project; 050import org.ametys.plugins.workspaces.tasks.Task; 051import org.ametys.plugins.workspaces.workflow.AbstractNodeWorkflowComponent; 052import org.ametys.runtime.authentication.AccessDeniedException; 053 054import com.opensymphony.workflow.Workflow; 055import com.opensymphony.workflow.WorkflowException; 056 057/** 058 * Calendar event DAO 059 */ 060public class CalendarEventDAO extends AbstractCalendarDAO 061{ 062 063 /** Avalon Role */ 064 public static final String ROLE = CalendarEventDAO.class.getName(); 065 066 /** The tasks list JSON helper */ 067 protected CalendarEventJSONHelper _calendarEventJSONHelper; 068 069 /** The calendar DAO */ 070 protected CalendarDAO _calendarDAO; 071 072 @Override 073 public void service(ServiceManager manager) throws ServiceException 074 { 075 super.service(manager); 076 _calendarEventJSONHelper = (CalendarEventJSONHelper) manager.lookup(CalendarEventJSONHelper.ROLE); 077 _calendarDAO = (CalendarDAO) manager.lookup(CalendarDAO.ROLE); 078 } 079 080 /** 081 * Get the events between two dates 082 * @param startDateAsStr The start date. 083 * @param endDateAsStr The end date. 084 * @return the events between two dates 085 */ 086 @Callable (rights = Callable.CHECKED_BY_IMPLEMENTATION) 087 public List<Map<String, Object>> getEvents(String startDateAsStr, String endDateAsStr) 088 { 089 ZonedDateTime startDate = startDateAsStr != null ? DateUtils.parseZonedDateTime(startDateAsStr) : null; 090 ZonedDateTime endDate = endDateAsStr != null ? DateUtils.parseZonedDateTime(endDateAsStr) : null; 091 092 return getEvents(startDate, endDate) 093 .stream() 094 .map(event -> { 095 Map<String, Object> eventData = _calendarEventJSONHelper.eventAsJson(event, false, false); 096 097 List<Object> occurrencesDataList = new ArrayList<>(); 098 eventData.put("occurrences", occurrencesDataList); 099 100 List<CalendarEventOccurrence> occurrences = event.getOccurrences(startDate, endDate); 101 for (CalendarEventOccurrence occurrence : occurrences) 102 { 103 occurrencesDataList.add(occurrence.toJSON()); 104 } 105 return eventData; 106 }) 107 .collect(Collectors.toList()); 108 } 109 110 /** 111 * Get the events between two dates 112 * @param startDate Begin date 113 * @param endDate End date 114 * @return the list of events 115 */ 116 public List<CalendarEvent> getEvents(ZonedDateTime startDate, ZonedDateTime endDate) 117 { 118 CalendarWorkspaceModule calendarModule = (CalendarWorkspaceModule) _workspaceModuleEP.getModule(CalendarWorkspaceModule.CALENDAR_MODULE_ID); 119 Project project = _workspaceHelper.getProjectFromRequest(); 120 121 _checkReadAccess(project, CalendarWorkspaceModule.CALENDAR_MODULE_ID); 122 123 List<CalendarEvent> eventList = new ArrayList<>(); 124 for (Calendar calendar : calendarModule.getCalendars(project, true)) 125 { 126 if (calendarModule.canView(calendar)) 127 { 128 for (Map.Entry<CalendarEvent, List<CalendarEventOccurrence>> entry : calendar.getEvents(startDate, endDate).entrySet()) 129 { 130 CalendarEvent event = entry.getKey(); 131 eventList.add(event); 132 } 133 } 134 } 135 136 Calendar resourceCalendar = calendarModule.getResourceCalendar(project); 137 138 for (Map.Entry<CalendarEvent, List<CalendarEventOccurrence>> entry : resourceCalendar.getEvents(startDate, endDate).entrySet()) 139 { 140 CalendarEvent event = entry.getKey(); 141 eventList.add(event); 142 } 143 144 return eventList; 145 } 146 147 /** 148 * Delete an event 149 * @param id The id of the event 150 * @param occurrence a string representing the occurrence date (ISO format). 151 * @param choice The type of modification 152 * @return The result map with id, parent id and message keys 153 */ 154 @Callable (rights = Callable.CHECKED_BY_IMPLEMENTATION) 155 public Map<String, Object> deleteEvent(String id, String occurrence, String choice) 156 { 157 if (!"unit".equals(choice)) 158 { 159 JCRCalendarEvent event = _resolver.resolveById(id); 160 _messagingConnectorCalendarManager.deleteEvent(event); 161 } 162 163 Map<String, Object> result = new HashMap<>(); 164 165 assert id != null; 166 167 CalendarEvent event = _resolver.resolveById(id); 168 if (!(event instanceof JCRCalendarEvent)) 169 { 170 throw new IllegalArgumentException("Cannot delete a non modifiable event"); 171 } 172 173 JCRCalendarEvent mevent = (JCRCalendarEvent) event; 174 JCRCalendar calendar = mevent.getParent(); 175 176 try 177 { 178 // Check user right 179 _checkUserRights(calendar, RIGHTS_EVENT_DELETE); 180 } 181 catch (AccessDeniedException e) 182 { 183 // Check if user is event's author and has right to delete its own events 184 UserIdentity user = _currentUserProvider.getUser(); 185 boolean hasOwnDeleteRight = mevent.getCreator().equals(user) && _rightManager.hasRight(user, RIGHTS_EVENT_DELETE_OWN, calendar) == RightResult.RIGHT_ALLOW; 186 if (!hasOwnDeleteRight) 187 { 188 throw e; // not authorized, rethrow exception 189 } 190 } 191 192 if (!_explorerResourcesDAO.checkLock(mevent)) 193 { 194 getLogger().warn("User '" + _currentUserProvider.getUser() + "' try to delete event'" + mevent.getName() + "' but it is locked by another user"); 195 result.put("message", "locked"); 196 return result; 197 } 198 199 String parentId = calendar.getId(); 200 String name = mevent.getName(); 201 String path = event.getPath(); 202 203 // Notify listeners 204 Map<String, Object> eventParams = new HashMap<>(); 205 eventParams.put(org.ametys.plugins.workspaces.calendars.ObservationConstants.ARGS_CALENDAR, calendar); 206 eventParams.put(ObservationConstants.ARGS_ID, id); 207 eventParams.put(ObservationConstants.ARGS_NAME, name); 208 eventParams.put(ObservationConstants.ARGS_PATH, path); 209 eventParams.put(org.ametys.plugins.workspaces.calendars.ObservationConstants.ARGS_CALENDAR_EVENT, event); 210 211 if (StringUtils.isNotBlank(choice) && choice.equals("unit")) 212 { 213 ArrayList<ZonedDateTime> excludedOccurrences = new ArrayList<>(); 214 excludedOccurrences.addAll(event.getExcludedOccurences()); 215 ZonedDateTime occurrenceDate = DateUtils.parseZonedDateTime(occurrence).withZoneSameInstant(event.getZone()); 216 excludedOccurrences.add(occurrenceDate.truncatedTo(ChronoUnit.DAYS)); 217 218 _observationManager.notify(new Event(org.ametys.plugins.workspaces.calendars.ObservationConstants.EVENT_CALENDAR_EVENT_DELETING, _currentUserProvider.getUser(), eventParams)); 219 220 mevent.setExcludedOccurrences(excludedOccurrences); 221 } 222 else 223 { 224 _observationManager.notify(new Event(org.ametys.plugins.workspaces.calendars.ObservationConstants.EVENT_CALENDAR_EVENT_DELETING, _currentUserProvider.getUser(), eventParams)); 225 226 mevent.remove(); 227 } 228 229 calendar.saveChanges(); 230 231 result.put("id", id); 232 result.put("parentId", parentId); 233 234 eventParams = new HashMap<>(); 235 eventParams.put(ObservationConstants.ARGS_ID, id); 236 _observationManager.notify(new Event(org.ametys.plugins.workspaces.calendars.ObservationConstants.EVENT_CALENDAR_EVENT_DELETED, _currentUserProvider.getUser(), eventParams)); 237 238 return result; 239 } 240 241 /** 242 * Add an event and return it. Use the calendar view dates to compute occurrences between those dates. 243 * @param parameters The map of parameters to perform the action 244 * @param calendarViewStartDateAsStr The calendar view start date, compute occurrences after this date. 245 * @param calendarViewEndDateAsStr The calendar view end date, compute occurrences before this date. 246 * @return The map of results populated by the underlying workflow action 247 * @throws WorkflowException if an error occurred 248 */ 249 @Callable (rights = Callable.NO_CHECK_REQUIRED) // right protection is provided by events' workflow itself 250 public Map<String, Object> addEvent(Map<String, Object> parameters, String calendarViewStartDateAsStr, String calendarViewEndDateAsStr) throws WorkflowException 251 { 252 ZonedDateTime calendarViewStartDate = calendarViewStartDateAsStr != null ? DateUtils.parseZonedDateTime(calendarViewStartDateAsStr) : null; 253 ZonedDateTime calendarViewEndDate = calendarViewEndDateAsStr != null ? DateUtils.parseZonedDateTime(calendarViewEndDateAsStr) : null; 254 255 Map<String, Object> result = doWorkflowEventAction(parameters); 256 257 //TODO Move to create event action (workflow) ? 258 String eventId = (String) result.get("id"); 259 _messagingConnectorCalendarManager.addEventInvitation(parameters, eventId); 260 261 _projectManager.getProjectsRoot().saveChanges(); 262 263 JCRCalendarEvent event = _resolver.resolveById((String) result.get("id")); 264 Map<String, Object> eventDataWithFilteredOccurences = _calendarEventJSONHelper.eventAsJsonWithOccurrences(event, false, calendarViewStartDate, calendarViewEndDate); 265 266 result.put("eventDataWithFilteredOccurences", eventDataWithFilteredOccurences); 267 268 return result; 269 } 270 271 /** 272 * Edit an event 273 * @param parameters The map of parameters to perform the action 274 * @param calendarViewStartDateAsStr The calendar view start date, compute occurrences after this date. 275 * @param calendarViewEndDateAsStr The calendar view end date, compute occurrences before this date. 276 * @return The map of results populated by the underlying workflow action 277 * @throws WorkflowException if an error occurred 278 */ 279 @Callable (rights = Callable.NO_CHECK_REQUIRED) // right protection is provided by events' workflow itself 280 public Map<String, Object> editEvent(Map<String, Object> parameters, String calendarViewStartDateAsStr, String calendarViewEndDateAsStr) throws WorkflowException 281 { 282 ZonedDateTime calendarViewStartDate = calendarViewStartDateAsStr != null ? DateUtils.parseZonedDateTime(calendarViewStartDateAsStr) : null; 283 ZonedDateTime calendarViewEndDate = calendarViewEndDateAsStr != null ? DateUtils.parseZonedDateTime(calendarViewEndDateAsStr) : null; 284 285 String eventId = (String) parameters.get("id"); 286 JCRCalendarEvent event = _resolver.resolveById(eventId); 287 288 // handle event move if calendar has changed 289 String previousCalendarId = event.getParent().getId(); 290 String parentId = (String) parameters.get("parentId"); 291 292 if (previousCalendarId != null && !StringUtils.equals(parentId, previousCalendarId)) 293 { 294 JCRCalendar parentCalendar = _resolver.resolveById(parentId); 295 move(event, parentCalendar); 296 } 297 298 Map<String, Object> result = doWorkflowEventAction(parameters); 299 300 //TODO Move to edit event action (workflow) ? 301 String choice = (String) parameters.get("choice"); 302 if (!"unit".equals(choice)) 303 { 304 _messagingConnectorCalendarManager.editEventInvitation(parameters, eventId); 305 } 306 307 _projectManager.getProjectsRoot().saveChanges(); 308 309 Map<String, Object> oldEventData = _calendarEventJSONHelper.eventAsJsonWithOccurrences(event, false, calendarViewStartDate, calendarViewEndDate); 310 JCRCalendarEvent newEvent = _resolver.resolveById((String) result.get("id")); 311 Map<String, Object> newEventData = _calendarEventJSONHelper.eventAsJsonWithOccurrences(newEvent, false, calendarViewStartDate, calendarViewEndDate); 312 313 result.put("oldEventData", oldEventData); 314 result.put("newEventData", newEventData); 315 316 return result; 317 } 318 319 /** 320 * Move a event to another calendar 321 * @param event The event to move 322 * @param parent The new parent calendar 323 * @throws AmetysRepositoryException if an error occurred while moving 324 */ 325 public void move(JCRCalendarEvent event, JCRCalendar parent) throws AmetysRepositoryException 326 { 327 try 328 { 329 event.getNode().getSession().move(event.getNode().getPath(), parent.getNode().getPath() + "/ametys:calendar-event"); 330 331 Workflow workflow = _workflowProvider.getAmetysObjectWorkflow(event); 332 333 String previousWorkflowName = workflow.getWorkflowName(event.getWorkflowId()); 334 String workflowName = parent.getWorkflowName(); 335 336 if (!StringUtils.equals(previousWorkflowName, workflowName)) 337 { 338 // If both calendar have a different workflow, initialize a new workflow instance for the event 339 HashMap<String, Object> inputs = new HashMap<>(); 340 inputs.put(AbstractNodeWorkflowComponent.EXPLORERNODE_KEY, parent); 341 workflow = _workflowProvider.getAmetysObjectWorkflow(event); 342 343 long workflowId = workflow.initialize(workflowName, 0, inputs); 344 event.setWorkflowId(workflowId); 345 } 346 } 347 catch (WorkflowException | RepositoryException e) 348 { 349 String errorMsg = String.format("Fail to move the event '%s' to the calendar '%s'.", event.getId(), parent.getId()); 350 throw new AmetysRepositoryException(errorMsg, e); 351 } 352 } 353 354 /** 355 * Do an event workflow action 356 * @param parameters The map of action parameters 357 * @return The map of results populated by the workflow action 358 * @throws WorkflowException if an error occurred 359 */ 360 @Callable (rights = Callable.NO_CHECK_REQUIRED) // right protection is provided by events' workflow itself 361 public Map<String, Object> doWorkflowEventAction(Map<String, Object> parameters) throws WorkflowException 362 { 363 Map<String, Object> result = new HashMap<>(); 364 HashMap<String, Object> inputs = new HashMap<>(); 365 366 inputs.put("parameters", parameters); 367 inputs.put("result", result); 368 369 String eventId = (String) parameters.get("id"); 370 Long workflowInstanceId = null; 371 JCRCalendarEvent event = null; 372 if (StringUtils.isNotEmpty(eventId)) 373 { 374 event = _resolver.resolveById(eventId); 375 workflowInstanceId = event.getWorkflowId(); 376 } 377 378 inputs.put("eventId", eventId); 379 380 JCRCalendar calendar = null; 381 String calendarId = (String) parameters.get("parentId"); 382 383 if (StringUtils.isNotEmpty(calendarId)) 384 { 385 calendar = _resolver.resolveById(calendarId); 386 } 387 // parentId can be not provided for some basic actions where the event already exists 388 else if (event != null) 389 { 390 calendar = event.getParent(); 391 } 392 else 393 { 394 throw new WorkflowException("Unable to retrieve the current calendar"); 395 } 396 397 inputs.put(AbstractNodeWorkflowComponent.EXPLORERNODE_KEY, calendar); 398 399 String workflowName = calendar.getWorkflowName(); 400 if (workflowName == null) 401 { 402 throw new IllegalArgumentException("The workflow name is not specified"); 403 } 404 405 int actionId = (int) parameters.get("actionId"); 406 407 boolean sendMail = true; 408 String choice = (String) parameters.get("choice"); 409 if (actionId == 2 && "unit".equals(choice)) 410 { 411 sendMail = false; 412 } 413 inputs.put("sendMail", sendMail); 414 415 Workflow workflow = _workflowProvider.getAmetysObjectWorkflow(event != null ? event : null); 416 417 if (workflowInstanceId == null) 418 { 419 try 420 { 421 workflow.initialize(workflowName, actionId, inputs); 422 } 423 catch (WorkflowException e) 424 { 425 getLogger().error("An error occured while creating workflow '" + workflowName + "' with action '" + actionId, e); 426 throw e; 427 } 428 } 429 else 430 { 431 try 432 { 433 workflow.doAction(workflowInstanceId, actionId, inputs); 434 } 435 catch (WorkflowException e) 436 { 437 getLogger().error("An error occured while doing action '" + actionId + "'with the workflow '" + workflowName, e); 438 throw e; 439 } 440 } 441 442 return result; 443 } 444 445 /** 446 * Get an event by id 447 * @param id The id of the event 448 * @return The event as a JSON map 449 */ 450 @Callable (rights = Callable.CHECKED_BY_IMPLEMENTATION) 451 public Map<String, Object> getEventByID(String id) 452 { 453 Project project = _workspaceHelper.getProjectFromRequest(); 454 _checkReadAccess(project, CalendarWorkspaceModule.CALENDAR_MODULE_ID); 455 456 if (!_resolver.hasAmetysObjectForId(id)) 457 { 458 return null; 459 } 460 CalendarEvent event = getCalenderEventById(id); 461 462 CalendarWorkspaceModule calendarModule = (CalendarWorkspaceModule) _workspaceModuleEP.getModule(CalendarWorkspaceModule.CALENDAR_MODULE_ID); 463 464 if (!calendarModule.canView(event.getCalendar())) 465 { 466 throw new AccessDeniedException("User '" + _currentUserProvider.getUser() + "' tried to access to calendar module of project '" + project.getName() + "' without convenient right or calandar module does not exist."); 467 } 468 return _calendarEventJSONHelper.eventAsJsonWithOccurrences(event, true, event.getStartDate(), event.getFullDay() ? event.getEndDate().plusDays(1) : event.getEndDate()); 469 } 470 471 /** 472 * Get an event by id 473 * @param eventId The id of the event 474 * @return The event 475 */ 476 public CalendarEvent getCalenderEventById(String eventId) 477 { 478 AmetysObject object = _resolver.resolveById(eventId); 479 if (object instanceof CalendarEvent event) 480 { 481 return event; 482 } 483 else if (object instanceof Task task) 484 { 485 Project project = _projectManager.getParentProject(task); 486 TaskCalendar taskCalendar = _calendarDAO.getTaskCalendar(project, true); 487 return taskCalendar != null ? new TaskCalendarEvent(taskCalendar, task) : null; 488 } 489 490 return null; 491 } 492}