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