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); 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 236 * @param parameters The map of parameters to perform the action 237 * @return The map of results populated by the underlying workflow action 238 * @throws WorkflowException if an error occurred 239 * @throws IllegalAccessException If the user does not have the right to add an event 240 */ 241 @Callable 242 public Map<String, Object> addEvent(Map<String, Object> parameters) throws WorkflowException, IllegalAccessException 243 { 244 Map<String, Object> result = doWorkflowEventAction(parameters); 245 246 //TODO Move to create event action (workflow) ? 247 String eventId = (String) result.get("id"); 248 _messagingConnectorCalendarManager.addEventInvitation(parameters, eventId); 249 250 _projectManager.getProjectsRoot().saveChanges(); 251 252 JCRCalendarEvent event = _resolver.resolveById((String) result.get("id")); 253 Map<String, Object> eventDataWithFilteredOccurences = _calendarEventJSONHelper.eventAsJsonWithOccurrences(event, false, event.getStartDate(), event.getStartDate().plusYears(1)); 254 255 result.put("eventDataWithFilteredOccurences", eventDataWithFilteredOccurences); 256 257 return result; 258 } 259 260 /** 261 * Edit an event 262 * @param parameters The map of parameters to perform the action 263 * @return The map of results populated by the underlying workflow action 264 * @throws WorkflowException if an error occurred 265 */ 266 @Callable 267 public Map<String, Object> editEvent(Map<String, Object> parameters) throws WorkflowException 268 { 269 String eventId = (String) parameters.get("id"); 270 JCRCalendarEvent event = _resolver.resolveById(eventId); 271 272 // handle event move if calendar has changed 273 String previousCalendarId = event.getParent().getId(); 274 String parentId = (String) parameters.get("parentId"); 275 276 if (previousCalendarId != null && !StringUtils.equals(parentId, previousCalendarId)) 277 { 278 JCRCalendar parentCalendar = _resolver.resolveById(parentId); 279 move(event, parentCalendar); 280 } 281 282 Map<String, Object> result = doWorkflowEventAction(parameters); 283 284 //TODO Move to edit event action (workflow) ? 285 String choice = (String) parameters.get("choice"); 286 if (!"unit".equals(choice)) 287 { 288 _messagingConnectorCalendarManager.editEventInvitation(parameters, eventId); 289 } 290 291 _projectManager.getProjectsRoot().saveChanges(); 292 293 Map<String, Object> oldEventData = _calendarEventJSONHelper.eventAsJsonWithOccurrences(event, false, event.getStartDate(), event.getStartDate().plusYears(1)); 294 JCRCalendarEvent newEvent = _resolver.resolveById((String) result.get("id")); 295 Map<String, Object> newEventData = _calendarEventJSONHelper.eventAsJsonWithOccurrences(newEvent, false, newEvent.getStartDate(), newEvent.getStartDate().plusYears(1)); 296 297 result.put("oldEventData", oldEventData); 298 result.put("newEventData", newEventData); 299 300 return result; 301 } 302 303 /** 304 * Move a event to another calendar 305 * @param event The event to move 306 * @param parent The new parent calendar 307 * @throws AmetysRepositoryException if an error occurred while moving 308 */ 309 public void move(JCRCalendarEvent event, JCRCalendar parent) throws AmetysRepositoryException 310 { 311 try 312 { 313 event.getNode().getSession().move(event.getNode().getPath(), parent.getNode().getPath() + "/ametys:calendar-event"); 314 315 Workflow workflow = _workflowProvider.getAmetysObjectWorkflow(event); 316 317 String previousWorkflowName = workflow.getWorkflowName(event.getWorkflowId()); 318 String workflowName = parent.getWorkflowName(); 319 320 if (!StringUtils.equals(previousWorkflowName, workflowName)) 321 { 322 // If both calendar have a different workflow, initialize a new workflow instance for the event 323 HashMap<String, Object> inputs = new HashMap<>(); 324 inputs.put(AbstractNodeWorkflowComponent.EXPLORERNODE_KEY, parent); 325 workflow = _workflowProvider.getAmetysObjectWorkflow(event); 326 327 long workflowId = workflow.initialize(workflowName, 0, inputs); 328 event.setWorkflowId(workflowId); 329 } 330 } 331 catch (WorkflowException | RepositoryException e) 332 { 333 String errorMsg = String.format("Fail to move the event '%s' to the calendar '%s'.", event.getId(), parent.getId()); 334 throw new AmetysRepositoryException(errorMsg, e); 335 } 336 } 337 338 /** 339 * Do an event workflow action 340 * @param parameters The map of action parameters 341 * @return The map of results populated by the workflow action 342 * @throws WorkflowException if an error occurred 343 */ 344 @Callable 345 public Map<String, Object> doWorkflowEventAction(Map<String, Object> parameters) throws WorkflowException 346 { 347 Map<String, Object> result = new HashMap<>(); 348 HashMap<String, Object> inputs = new HashMap<>(); 349 350 inputs.put("parameters", parameters); 351 inputs.put("result", result); 352 353 String eventId = (String) parameters.get("id"); 354 Long workflowInstanceId = null; 355 CalendarEvent event = null; 356 if (StringUtils.isNotEmpty(eventId)) 357 { 358 event = _resolver.resolveById(eventId); 359 workflowInstanceId = event.getWorkflowId(); 360 } 361 362 inputs.put("eventId", eventId); 363 364 Calendar calendar = null; 365 String calendarId = (String) parameters.get("parentId"); 366 367 if (StringUtils.isNotEmpty(calendarId)) 368 { 369 calendar = _resolver.resolveById(calendarId); 370 } 371 // parentId can be not provided for some basic actions where the event already exists 372 else if (event != null) 373 { 374 calendar = event.getParent(); 375 } 376 else 377 { 378 throw new WorkflowException("Unable to retrieve the current calendar"); 379 } 380 381 inputs.put(AbstractNodeWorkflowComponent.EXPLORERNODE_KEY, calendar); 382 383 String workflowName = calendar.getWorkflowName(); 384 if (workflowName == null) 385 { 386 throw new IllegalArgumentException("The workflow name is not specified"); 387 } 388 389 int actionId = (int) parameters.get("actionId"); 390 391 boolean sendMail = true; 392 String choice = (String) parameters.get("choice"); 393 if (actionId == 2 && "unit".equals(choice)) 394 { 395 sendMail = false; 396 } 397 inputs.put("sendMail", sendMail); 398 399 Workflow workflow = _workflowProvider.getAmetysObjectWorkflow(event != null ? event : null); 400 401 if (workflowInstanceId == null) 402 { 403 try 404 { 405 workflow.initialize(workflowName, actionId, inputs); 406 } 407 catch (WorkflowException e) 408 { 409 getLogger().error("An error occured while creating workflow '" + workflowName + "' with action '" + actionId, e); 410 throw e; 411 } 412 } 413 else 414 { 415 try 416 { 417 workflow.doAction(workflowInstanceId, actionId, inputs); 418 } 419 catch (WorkflowException e) 420 { 421 getLogger().error("An error occured while doing action '" + actionId + "'with the workflow '" + workflowName, e); 422 throw e; 423 } 424 } 425 426 return result; 427 } 428 429}