001/* 002 * Copyright 2015 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 */ 016package org.ametys.plugins.workspaces.calendars; 017 018import java.util.ArrayList; 019import java.util.Arrays; 020import java.util.Collection; 021import java.util.HashMap; 022import java.util.HashSet; 023import java.util.LinkedList; 024import java.util.List; 025import java.util.Map; 026import java.util.Optional; 027import java.util.Set; 028import java.util.UUID; 029import java.util.stream.Collectors; 030import java.util.stream.Stream; 031 032import org.apache.avalon.framework.service.ServiceException; 033import org.apache.avalon.framework.service.ServiceManager; 034import org.apache.commons.lang3.BooleanUtils; 035import org.apache.commons.lang3.StringUtils; 036 037import org.ametys.core.right.RightManager.RightResult; 038import org.ametys.core.ui.Callable; 039import org.ametys.core.user.UserIdentity; 040import org.ametys.plugins.explorer.calendars.Calendar; 041import org.ametys.plugins.explorer.calendars.CalendarEvent; 042import org.ametys.plugins.explorer.calendars.ModifiableCalendar; 043import org.ametys.plugins.explorer.calendars.actions.CalendarDAO; 044import org.ametys.plugins.explorer.calendars.jcr.JCRCalendar; 045import org.ametys.plugins.explorer.calendars.jcr.JCRCalendarEvent; 046import org.ametys.plugins.explorer.calendars.jcr.JCRCalendarFactory; 047import org.ametys.plugins.explorer.workflow.AbstractExplorerNodeWorkflowComponent; 048import org.ametys.plugins.repository.AmetysObjectIterable; 049import org.ametys.plugins.repository.AmetysObjectIterator; 050import org.ametys.plugins.repository.query.QueryHelper; 051import org.ametys.plugins.repository.query.expression.Expression; 052import org.ametys.plugins.repository.query.expression.Expression.Operator; 053import org.ametys.plugins.repository.query.expression.StringExpression; 054import org.ametys.plugins.workspaces.project.ProjectManager; 055import org.ametys.runtime.i18n.I18nizableText; 056 057import com.google.common.primitives.Ints; 058import com.opensymphony.workflow.Workflow; 059import com.opensymphony.workflow.WorkflowException; 060import com.opensymphony.workflow.loader.ActionDescriptor; 061import com.opensymphony.workflow.loader.StepDescriptor; 062import com.opensymphony.workflow.loader.WorkflowDescriptor; 063import com.opensymphony.workflow.spi.Step; 064 065/** 066 * DAO for manipulating calendars of a project 067 * 068 */ 069public class WorkspaceCalendarDAO extends CalendarDAO 070{ 071 /** Avalon Role */ 072 @SuppressWarnings("hiding") 073 public static final String ROLE = WorkspaceCalendarDAO.class.getName(); 074 075 private ProjectManager _projectManager; 076 077 private MessagingConnectorCalendarManager _messagingConnectorCalendarManager; 078 079 080 @Override 081 public void service(ServiceManager manager) throws ServiceException 082 { 083 super.service(manager); 084 _projectManager = (ProjectManager) manager.lookup(ProjectManager.ROLE); 085 _messagingConnectorCalendarManager = (MessagingConnectorCalendarManager) manager.lookup(MessagingConnectorCalendarManager.ROLE); 086 087 } 088 089 /** 090 * Add an event 091 * @param parameters The map of parameters to perform the action 092 * @return The map of results populated by the underlying workflow action 093 * @throws WorkflowException if an error occurred 094 * @throws IllegalAccessException If the user does not have the right to add an event 095 */ 096 @Callable 097 public Map<String, Object> addEvent(Map<String, Object> parameters) throws WorkflowException, IllegalAccessException 098 { 099 // Sanitize keywords and location (respectively tags and places in the client parameters) 100 @SuppressWarnings("unchecked") 101 Collection<String> keywords = (Collection<String>) parameters.get("tags"); 102 keywords = _sanitizeEventKeywordsInput(keywords); 103 parameters.put("keywords", keywords); 104 105 @SuppressWarnings("unchecked") 106 Collection<String> locations = (Collection<String>) parameters.get("places"); 107 String location = _sanitizeEventLocationsInput(locations); 108 parameters.put("location", location); 109 110 Map<String, Object> result = doWorkflowEventAction(parameters); 111 112 //TODO Move to create event action (workflow) ? 113 String eventId = (String) result.get("id"); 114 _messagingConnectorCalendarManager.addEventInvitation(parameters, eventId); 115 116 _projectManager.addTags(keywords); 117 _projectManager.addPlaces(Arrays.asList(location.split(","))); 118 119 _projectManager.getProjectsRoot().saveChanges(); 120 121 return result; 122 } 123 124 /** 125 * Edit an event 126 * @param parameters The map of parameters to perform the action 127 * @return The map of results populated by the underlying workflow action 128 * @throws WorkflowException if an error occurred 129 */ 130 @Callable 131 public Map<String, Object> editEvent(Map<String, Object> parameters) throws WorkflowException 132 { 133 // Sanitize keywords and location (respectively tags and places in the client parameters) 134 @SuppressWarnings("unchecked") 135 Collection<String> keywords = (Collection<String>) parameters.get("tags"); 136 keywords = _sanitizeEventKeywordsInput(keywords); 137 parameters.put("keywords", keywords); 138 139 @SuppressWarnings("unchecked") 140 Collection<String> locations = (Collection<String>) parameters.get("places"); 141 String location = _sanitizeEventLocationsInput(locations); 142 parameters.put("location", location); 143 144 String eventId = (String) parameters.get("id"); 145 JCRCalendarEvent event = _resolver.resolveById(eventId); 146 147 // handle event move if calendar has changed 148 String previousCalendarId = event.getParent().getId(); 149 String parentId = (String) parameters.get("parentId"); 150 151 if (previousCalendarId != null && !StringUtils.equals(parentId, previousCalendarId)) 152 { 153 JCRCalendar parentCalendar = _resolver.resolveById(parentId); 154 move(event, parentCalendar); 155 } 156 157 Map<String, Object> result = doWorkflowEventAction(parameters); 158 159 //TODO Move to edit event action (workflow) ? 160 String choice = (String) parameters.get("choice"); 161 if (!"unit".equals(choice)) 162 { 163 _messagingConnectorCalendarManager.editEventInvitation(parameters, eventId); 164 } 165 166 _projectManager.addTags(keywords); 167 _projectManager.addPlaces(Arrays.asList(location.split(","))); 168 169 _projectManager.getProjectsRoot().saveChanges(); 170 171 return result; 172 } 173 174 @Override 175 @Callable 176 public Map<String, Object> deleteEvent(String id, String occurrence, String choice) throws IllegalAccessException 177 { 178 if (!"unit".equals(choice)) 179 { 180 JCRCalendarEvent event = _resolver.resolveById(id); 181 _messagingConnectorCalendarManager.deleteEvent(event); 182 } 183 184 return super.deleteEvent(id, occurrence, choice); 185 } 186 187 @Override 188 @Callable 189 public Map<String, Object> getCalendarData(Calendar calendar, boolean recursive, boolean includeEvents) 190 { 191 Map<String, Object> data = super.getCalendarData(calendar, recursive, includeEvents); 192 data.put("rights", _extractCalendarRightData(calendar)); 193 return data; 194 } 195 196 @Override 197 public Map<String, Object> getEventData(CalendarEvent event, boolean fullInfo) 198 { 199 Map<String, Object> eventData = super.getEventData(event, fullInfo); 200 201 eventData.put("calendar", event.getParent().getName()); 202 203 eventData.put("isStillSynchronized", _messagingConnectorCalendarManager.isEventStillSynchronized(event.getId())); 204 205 // tags and places are expected by the client (respectively keywords and location on the server side) 206 eventData.put("tags", event.getKeywords()); 207 208 String location = StringUtils.defaultString(event.getLocation()); 209 eventData.put("places", Stream.of(location.split(",")).filter(StringUtils::isNotEmpty).collect(Collectors.toList())); 210 211 // add event rights 212 eventData.put("rights", _extractEventRightData(event)); 213 214 return eventData; 215 } 216 217 /** 218 * Retrieves the available workflows for the calendars 219 * @param withSteps true to include the list of steps and corresponding actions 220 * @return The list of workflow. Each entry is a map of data (id, label) 221 */ 222 @Callable 223 public List<Map<String, Object>> getWorkflows(boolean withSteps) 224 { 225 List<Map<String, Object>> workflows = new ArrayList<>(); 226 227 for (String workflowName : _workflowHelper.getWorkflowNames()) 228 { 229 if (StringUtils.startsWith(workflowName, "calendar-")) 230 { 231 Map<String, Object> workflow = new HashMap<>(); 232 workflows.add(workflow); 233 234 workflow.put("id", workflowName); 235 workflow.put("isDefault", StringUtils.contains(workflowName, "default")); 236 237 String i18nKey = "WORKFLOW_" + workflowName; 238 workflow.put("label", new I18nizableText("application", i18nKey)); 239 240 if (withSteps) 241 { 242 WorkflowDescriptor workflowDescriptor = _workflowHelper.getWorkflowDescriptor(workflowName); 243 workflow.put("steps", _getSteps(workflowDescriptor)); 244 workflow.put("actions", _getActions(workflowDescriptor)); 245 } 246 } 247 } 248 249 return workflows; 250 } 251 252 /** 253 * Get the workflow state of an event 254 * @param eventId The id of the event 255 * @return A map containing information about the workflow state 256 */ 257 @Callable 258 public Map<String, Object> getWorkflowState(String eventId) 259 { 260 Map<String, Object> state = new HashMap<>(); 261 262 CalendarEvent event = _resolver.resolveById(eventId); 263 Workflow workflow = _workflowProvider.getAmetysObjectWorkflow(event); 264 265 long workflowId = event.getWorkflowId(); 266 state.put("workflowName", workflow.getWorkflowName(workflowId)); 267 268 Step step = (Step) workflow.getCurrentSteps(workflowId).stream().findFirst().get(); 269 state.put("step", step.getStepId()); 270 271 Map<String, Object> inputs = new HashMap<>(); 272 inputs.put(AbstractExplorerNodeWorkflowComponent.EXPLORERNODE_KEY, event.getParent()); // event's parent is the calendar 273 state.put("actions", workflow.getAvailableActions(workflowId, inputs)); 274 275 return state; 276 } 277 278 /** 279 * Get the ICS token for the given calendar 280 * @param calendarId The calendar id 281 * @return The ICS token 282 */ 283 @Callable 284 public Map<String, Object> getCalendarIcsToken(String calendarId) 285 { 286 Map<String, Object> result = new HashMap<>(); 287 288 Calendar calendar = (Calendar) _resolver.resolveById(calendarId); 289 String token = getCalendarIcsToken(calendar, true); 290 291 if (StringUtils.isNotEmpty(token)) 292 { 293 result.put("token", token); 294 } 295 296 return result; 297 } 298 299 300 /** 301 * Get or create the calendar ICS token 302 * @param calendar The calendar 303 * @param createIfNotExisting Create the token if none exists for the given calendar 304 * @return The token 305 */ 306 public String getCalendarIcsToken(Calendar calendar, boolean createIfNotExisting) 307 { 308 String token = calendar.getIcsUrlToken(); 309 310 if (createIfNotExisting && token == null && calendar instanceof ModifiableCalendar) 311 { 312 token = UUID.randomUUID().toString(); 313 ((ModifiableCalendar) calendar).setIcsUrlToken(token); 314 ((ModifiableCalendar) calendar).saveChanges(); 315 } 316 317 return token; 318 } 319 320 /** 321 * Retrieve the calendar for the matching ICS token 322 * @param token The ICS token 323 * @return The calendar, or null if not found 324 */ 325 public Calendar getCalendarFromIcsToken(String token) 326 { 327 if (StringUtils.isEmpty(token)) 328 { 329 return null; 330 } 331 332 Expression expr = new StringExpression(JCRCalendar.CALENDAR_ICS_TOKEN, Operator.EQ, token); 333 String calendarsQuery = QueryHelper.getXPathQuery(null, JCRCalendarFactory.CALENDAR_NODETYPE, expr); 334 AmetysObjectIterable<Calendar> calendars = _resolver.query(calendarsQuery); 335 AmetysObjectIterator<Calendar> calendarsIterator = calendars.iterator(); 336 337 if (calendarsIterator.getSize() > 0) 338 { 339 return calendarsIterator.next(); 340 } 341 342 return null; 343 } 344 345 346 /** 347 * Get the steps of a workflow 348 * @param workflowDescriptor workflow descriptor 349 * @return The list of steps 350 */ 351 protected List<Map<String, Object>> _getSteps(WorkflowDescriptor workflowDescriptor) 352 { 353 List<StepDescriptor> stepDescriptors = workflowDescriptor.getSteps(); 354 355 return stepDescriptors.stream() 356 .map(stepDescriptor -> _getStep(workflowDescriptor, stepDescriptor)) 357 .collect(Collectors.toList()); 358 } 359 360 /** 361 * Get a map of data about a step 362 * @param workflowDescriptor workflow descriptor 363 * @param stepDescriptor step descriptor 364 * @return The map of data 365 */ 366 protected Map<String, Object> _getStep(WorkflowDescriptor workflowDescriptor, StepDescriptor stepDescriptor) 367 { 368 Map<String, Object> step = new HashMap<>(); 369 370 int stepId = stepDescriptor.getId(); 371 String stepName = stepDescriptor.getName(); 372 String[] nameParts = stepName.split(":"); 373 374 step.put("id", stepId); 375 step.put("name", nameParts[nameParts.length - 1]); // step name without the possible catalog name 376 377 I18nizableText i18nStepName = new I18nizableText("application", stepName); 378 step.put("label", i18nStepName); 379 step.put("description", new I18nizableText("application", stepName + "_DESCRIPTION")); 380 381 // Default icons 382 String[] icons = new String[]{"-small", "-medium", "-large"}; 383 for (String icon : icons) 384 { 385 if ("application".equals(i18nStepName.getCatalogue())) 386 { 387 step.put("icon" + icon, new I18nizableText("/plugins/explorer/resources_workflow/" + i18nStepName.getKey() + icon + ".png")); 388 } 389 else 390 { 391 String pluginName = i18nStepName.getCatalogue().substring("plugin.".length()); 392 step.put("icon" + icon, new I18nizableText("/plugins/" + pluginName + "/resources/img/workflow/" + i18nStepName.getKey() + icon + ".png")); 393 } 394 } 395 396 List<Integer> actionsId = new LinkedList<>(); 397 step.put("actions", actionsId); 398 399 for (int actionId : _workflowHelper.getAllActionsFromStep(workflowDescriptor.getName(), stepId)) 400 { 401 actionsId.add(actionId); 402 } 403 404 return step; 405 } 406 407 /** 408 * Get the actions of a workflow 409 * @param workflowDescriptor workflow descriptor 410 * @return The list of steps 411 */ 412 protected List<Map<String, Object>> _getActions(WorkflowDescriptor workflowDescriptor) 413 { 414 int[] allActions = _workflowHelper.getAllActions(workflowDescriptor.getName()); 415 416 return Ints.asList(allActions).stream() 417 .map(workflowDescriptor::getAction) 418 .map(actionDescriptor -> _getAction(workflowDescriptor, actionDescriptor)) 419 .collect(Collectors.toList()); 420 } 421 422 /** 423 * Get a map of data about an action 424 * @param workflowDescriptor workflow descriptor 425 * @param actionDescriptor action descriptor 426 * @return The map of data 427 */ 428 protected Map<String, Object> _getAction(WorkflowDescriptor workflowDescriptor, ActionDescriptor actionDescriptor) 429 { 430 Map<String, Object> action = new HashMap<>(); 431 432 int actionId = actionDescriptor.getId(); 433 String actionName = actionDescriptor.getName(); 434 String[] nameParts = actionName.split(":"); 435 436 action.put("id", actionId); 437 action.put("name", nameParts[nameParts.length - 1]); // action name without the possible catalog name 438 439 action.put("label", new I18nizableText("application", actionName)); 440 action.put("description", new I18nizableText("application", actionName + "_DESCRIPTION")); 441 442 action.put("internal", BooleanUtils.toBoolean((String) actionDescriptor.getMetaAttributes().get("internal"))); 443 444 return action; 445 } 446 447 /** 448 * Internal method to extract the data concerning the right of the current user for an event 449 * @param event The event 450 * @return The map of right data. Keys are the rights id, and values indicates whether the current user has the right or not. 451 */ 452 protected Map<String, Object> _extractEventRightData(CalendarEvent event) 453 { 454 Map<String, Object> rightsData = new HashMap<>(); 455 UserIdentity user = _currentUserProvider.getUser(); 456 Calendar calendar = event.getParent(); 457 458 rightsData.put("edit", _rightManager.hasRight(user, RIGHTS_EVENT_EDIT, calendar) == RightResult.RIGHT_ALLOW); 459 rightsData.put("delete", _rightManager.hasRight(user, RIGHTS_EVENT_DELETE, calendar) == RightResult.RIGHT_ALLOW); 460 rightsData.put("delete-own", _rightManager.hasRight(user, RIGHTS_EVENT_DELETE_OWN, calendar) == RightResult.RIGHT_ALLOW); 461 462 return rightsData; 463 } 464 465 /** 466 * Internal method to extract the data concerning the right of the current user for a calendar 467 * @param calendar The calendar 468 * @return The map of right data. Keys are the rights id, and values indicates whether the current user has the right or not. 469 */ 470 protected Map<String, Object> _extractCalendarRightData(Calendar calendar) 471 { 472 Map<String, Object> rightsData = new HashMap<>(); 473 UserIdentity user = _currentUserProvider.getUser(); 474 475 // Add 476 rightsData.put("add-event", _rightManager.hasRight(user, RIGHTS_EVENT_ADD, calendar) == RightResult.RIGHT_ALLOW); 477 478 // edit - delete 479 rightsData.put("edit", _rightManager.hasRight(user, RIGHTS_CALENDAR_EDIT, calendar) == RightResult.RIGHT_ALLOW); 480 rightsData.put("delete", _rightManager.hasRight(user, RIGHTS_CALENDAR_DELETE, calendar) == RightResult.RIGHT_ALLOW); 481 482 return rightsData; 483 } 484 485 /** 486 * Sanitize the locations parameters received as input 487 * @param locations collection of locations passed as an input 488 * @return The sanitized location, a single string that represent a comma-separated list of locations 489 */ 490 protected String _sanitizeEventLocationsInput(Collection<String> locations) 491 { 492 Set<String> lowercasedPlaces = new HashSet<>(); 493 494 // duplicates are filtered out 495 // and result is returned as a single string joined with a comma delimiter 496 return Optional.ofNullable(locations).orElseGet(ArrayList::new).stream() 497 .map(String::trim) 498 .filter(StringUtils::isNotEmpty) 499 .filter(p -> lowercasedPlaces.add(p.toLowerCase())) 500 .collect(Collectors.joining(",")); 501 } 502 503 /** 504 * Sanitize the keyword parameters received as input 505 * @param keywords collection of keywords passed as an input 506 * @return The sanitized collection 507 */ 508 protected Collection<String> _sanitizeEventKeywordsInput(Collection<String> keywords) 509 { 510 // Enforce lowercase and remove possible duplicate tags 511 return Optional.ofNullable(keywords).orElseGet(ArrayList::new).stream() 512 .map(String::trim) 513 .map(String::toLowerCase) 514 .filter(StringUtils::isNotEmpty) 515 .distinct() 516 .collect(Collectors.toList()); 517 } 518}