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 if (StringUtils.isNotBlank(choice) && choice.equals("unit")) 205 { 206 ArrayList<ZonedDateTime> excludedOccurrences = new ArrayList<>(); 207 excludedOccurrences.addAll(event.getExcludedOccurences()); 208 ZonedDateTime occurrenceDate = DateUtils.parseZonedDateTime(occurrence).withZoneSameInstant(event.getZone()); 209 excludedOccurrences.add(occurrenceDate.truncatedTo(ChronoUnit.DAYS)); 210 211 event.setExcludedOccurrences(excludedOccurrences); 212 } 213 else 214 { 215 event.remove(); 216 } 217 218 calendar.saveChanges(); 219 220 // Notify listeners 221 Map<String, Object> eventParams = new HashMap<>(); 222 eventParams.put(org.ametys.plugins.workspaces.calendars.ObservationConstants.ARGS_CALENDAR, calendar); 223 eventParams.put(ObservationConstants.ARGS_ID, id); 224 eventParams.put(ObservationConstants.ARGS_NAME, name); 225 eventParams.put(ObservationConstants.ARGS_PATH, path); 226 _observationManager.notify(new Event(org.ametys.plugins.workspaces.calendars.ObservationConstants.EVENT_CALENDAR_EVENT_DELETED, _currentUserProvider.getUser(), eventParams)); 227 228 result.put("id", id); 229 result.put("parentId", parentId); 230 231 return result; 232 } 233 234 /** 235 * Add an event and return it. Use the calendar view dates to compute occurrences between those dates. 236 * @param parameters The map of parameters to perform the action 237 * @param calendarViewStartDateAsStr The calendar view start date, compute occurrences after this date. 238 * @param calendarViewEndDateAsStr The calendar view end date, compute occurrences before this date. 239 * @return The map of results populated by the underlying workflow action 240 * @throws WorkflowException if an error occurred 241 * @throws IllegalAccessException If the user does not have the right to add an event 242 */ 243 @Callable 244 public Map<String, Object> addEvent(Map<String, Object> parameters, String calendarViewStartDateAsStr, String calendarViewEndDateAsStr) throws WorkflowException, IllegalAccessException 245 { 246 ZonedDateTime calendarViewStartDate = calendarViewStartDateAsStr != null ? DateUtils.parseZonedDateTime(calendarViewStartDateAsStr) : null; 247 ZonedDateTime calendarViewEndDate = calendarViewEndDateAsStr != null ? DateUtils.parseZonedDateTime(calendarViewEndDateAsStr) : null; 248 249 Map<String, Object> result = doWorkflowEventAction(parameters); 250 251 //TODO Move to create event action (workflow) ? 252 String eventId = (String) result.get("id"); 253 _messagingConnectorCalendarManager.addEventInvitation(parameters, eventId); 254 255 _projectManager.getProjectsRoot().saveChanges(); 256 257 JCRCalendarEvent event = _resolver.resolveById((String) result.get("id")); 258 Map<String, Object> eventDataWithFilteredOccurences = _calendarEventJSONHelper.eventAsJsonWithOccurrences(event, false, calendarViewStartDate, calendarViewEndDate); 259 260 result.put("eventDataWithFilteredOccurences", eventDataWithFilteredOccurences); 261 262 return result; 263 } 264 265 /** 266 * Edit an event 267 * @param parameters The map of parameters to perform the action 268 * @param calendarViewStartDateAsStr The calendar view start date, compute occurrences after this date. 269 * @param calendarViewEndDateAsStr The calendar view end date, compute occurrences before this date. 270 * @return The map of results populated by the underlying workflow action 271 * @throws WorkflowException if an error occurred 272 */ 273 @Callable 274 public Map<String, Object> editEvent(Map<String, Object> parameters, String calendarViewStartDateAsStr, String calendarViewEndDateAsStr) throws WorkflowException 275 { 276 ZonedDateTime calendarViewStartDate = calendarViewStartDateAsStr != null ? DateUtils.parseZonedDateTime(calendarViewStartDateAsStr) : null; 277 ZonedDateTime calendarViewEndDate = calendarViewEndDateAsStr != null ? DateUtils.parseZonedDateTime(calendarViewEndDateAsStr) : null; 278 279 String eventId = (String) parameters.get("id"); 280 JCRCalendarEvent event = _resolver.resolveById(eventId); 281 282 // handle event move if calendar has changed 283 String previousCalendarId = event.getParent().getId(); 284 String parentId = (String) parameters.get("parentId"); 285 286 if (previousCalendarId != null && !StringUtils.equals(parentId, previousCalendarId)) 287 { 288 JCRCalendar parentCalendar = _resolver.resolveById(parentId); 289 move(event, parentCalendar); 290 } 291 292 Map<String, Object> result = doWorkflowEventAction(parameters); 293 294 //TODO Move to edit event action (workflow) ? 295 String choice = (String) parameters.get("choice"); 296 if (!"unit".equals(choice)) 297 { 298 _messagingConnectorCalendarManager.editEventInvitation(parameters, eventId); 299 } 300 301 _projectManager.getProjectsRoot().saveChanges(); 302 303 Map<String, Object> oldEventData = _calendarEventJSONHelper.eventAsJsonWithOccurrences(event, false, calendarViewStartDate, calendarViewEndDate); 304 JCRCalendarEvent newEvent = _resolver.resolveById((String) result.get("id")); 305 Map<String, Object> newEventData = _calendarEventJSONHelper.eventAsJsonWithOccurrences(newEvent, false, calendarViewStartDate, calendarViewEndDate); 306 307 result.put("oldEventData", oldEventData); 308 result.put("newEventData", newEventData); 309 310 return result; 311 } 312 313 /** 314 * Move a event to another calendar 315 * @param event The event to move 316 * @param parent The new parent calendar 317 * @throws AmetysRepositoryException if an error occurred while moving 318 */ 319 public void move(JCRCalendarEvent event, JCRCalendar parent) throws AmetysRepositoryException 320 { 321 try 322 { 323 event.getNode().getSession().move(event.getNode().getPath(), parent.getNode().getPath() + "/ametys:calendar-event"); 324 325 Workflow workflow = _workflowProvider.getAmetysObjectWorkflow(event); 326 327 String previousWorkflowName = workflow.getWorkflowName(event.getWorkflowId()); 328 String workflowName = parent.getWorkflowName(); 329 330 if (!StringUtils.equals(previousWorkflowName, workflowName)) 331 { 332 // If both calendar have a different workflow, initialize a new workflow instance for the event 333 HashMap<String, Object> inputs = new HashMap<>(); 334 inputs.put(AbstractNodeWorkflowComponent.EXPLORERNODE_KEY, parent); 335 workflow = _workflowProvider.getAmetysObjectWorkflow(event); 336 337 long workflowId = workflow.initialize(workflowName, 0, inputs); 338 event.setWorkflowId(workflowId); 339 } 340 } 341 catch (WorkflowException | RepositoryException e) 342 { 343 String errorMsg = String.format("Fail to move the event '%s' to the calendar '%s'.", event.getId(), parent.getId()); 344 throw new AmetysRepositoryException(errorMsg, e); 345 } 346 } 347 348 /** 349 * Do an event workflow action 350 * @param parameters The map of action parameters 351 * @return The map of results populated by the workflow action 352 * @throws WorkflowException if an error occurred 353 */ 354 @Callable 355 public Map<String, Object> doWorkflowEventAction(Map<String, Object> parameters) throws WorkflowException 356 { 357 Map<String, Object> result = new HashMap<>(); 358 HashMap<String, Object> inputs = new HashMap<>(); 359 360 inputs.put("parameters", parameters); 361 inputs.put("result", result); 362 363 String eventId = (String) parameters.get("id"); 364 Long workflowInstanceId = null; 365 CalendarEvent event = null; 366 if (StringUtils.isNotEmpty(eventId)) 367 { 368 event = _resolver.resolveById(eventId); 369 workflowInstanceId = event.getWorkflowId(); 370 } 371 372 inputs.put("eventId", eventId); 373 374 Calendar calendar = null; 375 String calendarId = (String) parameters.get("parentId"); 376 377 if (StringUtils.isNotEmpty(calendarId)) 378 { 379 calendar = _resolver.resolveById(calendarId); 380 } 381 // parentId can be not provided for some basic actions where the event already exists 382 else if (event != null) 383 { 384 calendar = event.getParent(); 385 } 386 else 387 { 388 throw new WorkflowException("Unable to retrieve the current calendar"); 389 } 390 391 inputs.put(AbstractNodeWorkflowComponent.EXPLORERNODE_KEY, calendar); 392 393 String workflowName = calendar.getWorkflowName(); 394 if (workflowName == null) 395 { 396 throw new IllegalArgumentException("The workflow name is not specified"); 397 } 398 399 int actionId = (int) parameters.get("actionId"); 400 401 boolean sendMail = true; 402 String choice = (String) parameters.get("choice"); 403 if (actionId == 2 && "unit".equals(choice)) 404 { 405 sendMail = false; 406 } 407 inputs.put("sendMail", sendMail); 408 409 Workflow workflow = _workflowProvider.getAmetysObjectWorkflow(event != null ? event : null); 410 411 if (workflowInstanceId == null) 412 { 413 try 414 { 415 workflow.initialize(workflowName, actionId, inputs); 416 } 417 catch (WorkflowException e) 418 { 419 getLogger().error("An error occured while creating workflow '" + workflowName + "' with action '" + actionId, e); 420 throw e; 421 } 422 } 423 else 424 { 425 try 426 { 427 workflow.doAction(workflowInstanceId, actionId, inputs); 428 } 429 catch (WorkflowException e) 430 { 431 getLogger().error("An error occured while doing action '" + actionId + "'with the workflow '" + workflowName, e); 432 throw e; 433 } 434 } 435 436 return result; 437 } 438 439}