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.lang.IllegalClassException; 032import org.apache.commons.lang3.StringUtils; 033 034import org.ametys.core.observation.Event; 035import org.ametys.core.right.RightManager.RightResult; 036import org.ametys.core.ui.Callable; 037import org.ametys.core.user.UserIdentity; 038import org.ametys.core.util.DateUtils; 039import org.ametys.plugins.explorer.ObservationConstants; 040import org.ametys.plugins.repository.AmetysObject; 041import org.ametys.plugins.repository.AmetysObjectIterable; 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.CalendarWorkspaceModule; 046import org.ametys.plugins.workspaces.calendars.ModifiableCalendar; 047import org.ametys.plugins.workspaces.calendars.jcr.JCRCalendar; 048import org.ametys.plugins.workspaces.calendars.jcr.JCRCalendarEvent; 049import org.ametys.plugins.workspaces.project.objects.Project; 050import org.ametys.plugins.workspaces.workflow.AbstractNodeWorkflowComponent; 051 052import com.opensymphony.workflow.Workflow; 053import com.opensymphony.workflow.WorkflowException; 054 055/** 056 * Calendar event DAO 057 */ 058public class CalendarEventDAO extends AbstractCalendarDAO 059{ 060 061 /** Avalon Role */ 062 public static final String ROLE = CalendarEventDAO.class.getName(); 063 064 /** The tasks list JSON helper */ 065 protected CalendarEventJSONHelper _calendarEventJSONHelper; 066 067 @Override 068 public void service(ServiceManager manager) throws ServiceException 069 { 070 super.service(manager); 071 _calendarEventJSONHelper = (CalendarEventJSONHelper) manager.lookup(CalendarEventJSONHelper.ROLE); 072 } 073 074 075 /** 076 * Get the events between two dates 077 * @param startDateAsStr The start date. 078 * @param endDateAsStr The end date. 079 * @return the events between two dates 080 */ 081 @Callable 082 public List<Map<String, Object>> getEvents(String startDateAsStr, String endDateAsStr) 083 { 084 ZonedDateTime startDate = startDateAsStr != null ? DateUtils.parseZonedDateTime(startDateAsStr) : null; 085 ZonedDateTime endDate = endDateAsStr != null ? DateUtils.parseZonedDateTime(endDateAsStr) : null; 086 087 return getEvents(startDate, endDate) 088 .stream() 089 .map(event -> { 090 Map<String, Object> eventData = _calendarEventJSONHelper.eventAsJson(event, false, false); 091 092 List<Object> occurrencesDataList = new ArrayList<>(); 093 eventData.put("occurrences", occurrencesDataList); 094 095 List<CalendarEventOccurrence> occurrences = event.getOccurrences(startDate, endDate); 096 for (CalendarEventOccurrence occurrence : occurrences) 097 { 098 occurrencesDataList.add(occurrence.toJSON()); 099 } 100 return eventData; 101 }) 102 .collect(Collectors.toList()); 103 } 104 105 /** 106 * Get the events between two dates 107 * @param startDate Begin date 108 * @param endDate End date 109 * @return the list of events 110 */ 111 public List<CalendarEvent> getEvents(ZonedDateTime startDate, ZonedDateTime endDate) 112 { 113 114 CalendarWorkspaceModule calendarModule = (CalendarWorkspaceModule) _workspaceModuleEP.getModule(CalendarWorkspaceModule.CALENDAR_MODULE_ID); 115 Project project = _getProject(); 116 AmetysObjectIterable<Calendar> calendars = calendarModule.getCalendars(project); 117 118 List<CalendarEvent> eventList = new ArrayList<>(); 119 if (calendars != null) 120 { 121 for (Calendar calendar : calendars) 122 { 123 if (calendarModule.canView(calendar)) 124 { 125 for (Map.Entry<CalendarEvent, List<CalendarEventOccurrence>> entry : calendar.getEvents(startDate, endDate).entrySet()) 126 { 127 CalendarEvent event = entry.getKey(); 128 eventList.add(event); 129 } 130 } 131 } 132 } 133 134 Calendar resourceCalendar = calendarModule.getResourceCalendar(_getProject()); 135 136 for (Map.Entry<CalendarEvent, List<CalendarEventOccurrence>> entry : resourceCalendar.getEvents(startDate, endDate).entrySet()) 137 { 138 CalendarEvent event = entry.getKey(); 139 eventList.add(event); 140 } 141 142 return eventList; 143 } 144 145 /** 146 * Delete an event 147 * @param id The id of the event 148 * @param occurrence a string representing the occurrence date (ISO format). 149 * @param choice The type of modification 150 * @return The result map with id, parent id and message keys 151 * @throws IllegalAccessException If the user has no sufficient rights 152 */ 153 @Callable 154 public Map<String, Object> deleteEvent(String id, String occurrence, String choice) throws IllegalAccessException 155 { 156 if (!"unit".equals(choice)) 157 { 158 JCRCalendarEvent event = _resolver.resolveById(id); 159 _messagingConnectorCalendarManager.deleteEvent(event); 160 } 161 162 Map<String, Object> result = new HashMap<>(); 163 164 assert id != null; 165 166 AmetysObject object = _resolver.resolveById(id); 167 if (!(object instanceof ModifiableCalendarEvent)) 168 { 169 throw new IllegalClassException(ModifiableCalendarEvent.class, object.getClass()); 170 } 171 172 ModifiableCalendarEvent event = (ModifiableCalendarEvent) object; 173 ModifiableCalendar calendar = event.getParent(); 174 175 // Check user right 176 try 177 { 178 _explorerResourcesDAO.checkUserRight(calendar, RIGHTS_EVENT_DELETE); 179 } 180 catch (IllegalAccessException e) 181 { 182 UserIdentity user = _currentUserProvider.getUser(); 183 UserIdentity creator = event.getCreator(); 184 RightResult rightCreator = _rightManager.hasRight(user, RIGHTS_EVENT_DELETE_OWN, calendar); 185 boolean hasOwnDeleteRight = rightCreator == RightResult.RIGHT_ALLOW && creator.equals(user); 186 if (!hasOwnDeleteRight) 187 { 188 // rethrow exception 189 throw e; 190 } 191 } 192 193 if (!_explorerResourcesDAO.checkLock(event)) 194 { 195 getLogger().warn("User '" + _currentUserProvider.getUser() + "' try to delete event'" + object.getName() + "' but it is locked by another user"); 196 result.put("message", "locked"); 197 return result; 198 } 199 200 String parentId = calendar.getId(); 201 String name = event.getName(); 202 String path = event.getPath(); 203 204 // Notify listeners 205 Map<String, Object> eventParams = new HashMap<>(); 206 eventParams.put(org.ametys.plugins.workspaces.calendars.ObservationConstants.ARGS_CALENDAR, calendar); 207 eventParams.put(ObservationConstants.ARGS_ID, id); 208 eventParams.put(ObservationConstants.ARGS_NAME, name); 209 eventParams.put(ObservationConstants.ARGS_PATH, path); 210 eventParams.put(org.ametys.plugins.workspaces.calendars.ObservationConstants.ARGS_CALENDAR_EVENT, event); 211 212 if (StringUtils.isNotBlank(choice) && choice.equals("unit")) 213 { 214 ArrayList<ZonedDateTime> excludedOccurrences = new ArrayList<>(); 215 excludedOccurrences.addAll(event.getExcludedOccurences()); 216 ZonedDateTime occurrenceDate = DateUtils.parseZonedDateTime(occurrence).withZoneSameInstant(event.getZone()); 217 excludedOccurrences.add(occurrenceDate.truncatedTo(ChronoUnit.DAYS)); 218 219 _observationManager.notify(new Event(org.ametys.plugins.workspaces.calendars.ObservationConstants.EVENT_CALENDAR_EVENT_DELETING, _currentUserProvider.getUser(), eventParams)); 220 221 event.setExcludedOccurrences(excludedOccurrences); 222 } 223 else 224 { 225 _observationManager.notify(new Event(org.ametys.plugins.workspaces.calendars.ObservationConstants.EVENT_CALENDAR_EVENT_DELETING, _currentUserProvider.getUser(), eventParams)); 226 227 event.remove(); 228 } 229 230 calendar.saveChanges(); 231 232 result.put("id", id); 233 result.put("parentId", parentId); 234 235 eventParams = new HashMap<>(); 236 eventParams.put(ObservationConstants.ARGS_ID, id); 237 _observationManager.notify(new Event(org.ametys.plugins.workspaces.calendars.ObservationConstants.EVENT_CALENDAR_EVENT_DELETED, _currentUserProvider.getUser(), eventParams)); 238 239 return result; 240 } 241 242 /** 243 * Add an event and return it. Use the calendar view dates to compute occurrences between those dates. 244 * @param parameters The map of parameters to perform the action 245 * @param calendarViewStartDateAsStr The calendar view start date, compute occurrences after this date. 246 * @param calendarViewEndDateAsStr The calendar view end date, compute occurrences before this date. 247 * @return The map of results populated by the underlying workflow action 248 * @throws WorkflowException if an error occurred 249 * @throws IllegalAccessException If the user does not have the right to add an event 250 */ 251 @Callable 252 public Map<String, Object> addEvent(Map<String, Object> parameters, String calendarViewStartDateAsStr, String calendarViewEndDateAsStr) throws WorkflowException, IllegalAccessException 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, false, 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 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 && !StringUtils.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, false, calendarViewStartDate, calendarViewEndDate); 312 JCRCalendarEvent newEvent = _resolver.resolveById((String) result.get("id")); 313 Map<String, Object> newEventData = _calendarEventJSONHelper.eventAsJsonWithOccurrences(newEvent, false, 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 (!StringUtils.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 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 CalendarEvent 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 Calendar 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}