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