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