001/* 002 * Copyright 2016 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.tasks; 017 018import java.time.LocalDate; 019import java.time.ZonedDateTime; 020import java.time.format.DateTimeFormatter; 021import java.util.ArrayList; 022import java.util.HashMap; 023import java.util.List; 024import java.util.Map; 025import java.util.Optional; 026import java.util.stream.Collectors; 027 028import org.apache.avalon.framework.service.ServiceException; 029import org.apache.avalon.framework.service.ServiceManager; 030import org.apache.cocoon.servlet.multipart.Part; 031import org.apache.commons.lang3.StringUtils; 032 033import org.ametys.cms.repository.comment.Comment; 034import org.ametys.cms.repository.mentions.MentionUtils; 035import org.ametys.core.observation.Event; 036import org.ametys.core.ui.Callable; 037import org.ametys.core.user.User; 038import org.ametys.core.user.UserIdentity; 039import org.ametys.plugins.repository.AmetysRepositoryException; 040import org.ametys.plugins.repository.ModifiableTraversableAmetysObject; 041import org.ametys.plugins.workspaces.ObservationConstants; 042import org.ametys.plugins.workspaces.members.ProjectMemberManager; 043import org.ametys.plugins.workspaces.project.objects.Project; 044import org.ametys.plugins.workspaces.tags.ProjectTagProviderExtensionPoint; 045import org.ametys.plugins.workspaces.tasks.Task.CheckItem; 046import org.ametys.plugins.workspaces.tasks.jcr.JCRTask; 047import org.ametys.plugins.workspaces.tasks.jcr.JCRTaskFactory; 048import org.ametys.plugins.workspaces.tasks.json.TaskJSONHelper; 049import org.ametys.runtime.authentication.AccessDeniedException; 050 051/** 052 * DAO for interacting with tasks of a project 053 */ 054public class WorkspaceTaskDAO extends AbstractWorkspaceTaskDAO 055{ 056 /** The Avalon role */ 057 public static final String ROLE = WorkspaceTaskDAO.class.getName(); 058 059 /** The project member manager */ 060 protected ProjectMemberManager _projectMemberManager; 061 062 /** The tag provider extension point */ 063 protected ProjectTagProviderExtensionPoint _tagProviderExtPt; 064 065 /** The task JSON helper */ 066 protected TaskJSONHelper _taskJSONHelper; 067 068 /** The task list DAO */ 069 protected WorkspaceTasksListDAO _workspaceTasksListDAO; 070 071 /** The mentions helper */ 072 protected MentionUtils _mentionUtils; 073 074 @Override 075 public void service(ServiceManager manager) throws ServiceException 076 { 077 super.service(manager); 078 _projectMemberManager = (ProjectMemberManager) manager.lookup(ProjectMemberManager.ROLE); 079 _tagProviderExtPt = (ProjectTagProviderExtensionPoint) manager.lookup(ProjectTagProviderExtensionPoint.ROLE); 080 _taskJSONHelper = (TaskJSONHelper) manager.lookup(TaskJSONHelper.ROLE); 081 _workspaceTasksListDAO = (WorkspaceTasksListDAO) manager.lookup(WorkspaceTasksListDAO.ROLE); 082 _mentionUtils = (MentionUtils) manager.lookup(MentionUtils.ROLE); 083 } 084 085 /** 086 * Get the tasks from project 087 * @return the list of tasks 088 */ 089 @Callable (rights = Callable.CHECKED_BY_IMPLEMENTATION) 090 public List<Map<String, Object>> getTasks() 091 { 092 Project project = _workspaceHelper.getProjectFromRequest(); 093 094 if (!_projectRightHelper.hasReadAccessOnModule(project, TasksWorkspaceModule.TASK_MODULE_ID)) 095 { 096 throw new AccessDeniedException("User '" + _currentUserProvider.getUser() + "' tried to get tasks without reader right"); 097 } 098 099 List<Map<String, Object>> tasksInfo = new ArrayList<>(); 100 for (Task task : getProjectTasks(project)) 101 { 102 tasksInfo.add(_taskJSONHelper.taskAsJSON(task, getSitemapLanguage(), getSiteName())); 103 } 104 105 return tasksInfo; 106 } 107 108 /** 109 * Add a new task to the tasks list 110 * @param tasksListId the tasks list id 111 * @param parameters The task parameters 112 * @param newFiles the files to add 113 * @param newFileNames the file names to add 114 * @return The task data 115 */ 116 @Callable (rights = Callable.CHECKED_BY_IMPLEMENTATION) 117 public Map<String, Object> addTask(String tasksListId, Map<String, Object> parameters, List<Part> newFiles, List<String> newFileNames) 118 { 119 Project project = _workspaceHelper.getProjectFromRequest(); 120 121 if (StringUtils.isBlank(tasksListId)) 122 { 123 throw new IllegalArgumentException("Tasks list id is mandatory to create a new task"); 124 } 125 126 ModifiableTraversableAmetysObject tasksRoot = _getTasksRoot(project, true); 127 128 _checkUserRights(tasksRoot, RIGHTS_HANDLE_TASK); 129 130 int index = 1; 131 String name = "task-1"; 132 while (tasksRoot.hasChild(name)) 133 { 134 index++; 135 name = "task-" + index; 136 } 137 138 JCRTask task = (JCRTask) tasksRoot.createChild(name, JCRTaskFactory.TASK_NODETYPE); 139 task.setTasksListId(tasksListId); 140 task.setPosition(Long.valueOf(_workspaceTasksListDAO.getChildTask(tasksListId).size())); 141 142 ZonedDateTime now = ZonedDateTime.now(); 143 task.setCreationDate(now); 144 task.setLastModified(now); 145 task.setAuthor(_currentUserProvider.getUser()); 146 147 Map<String, Object> attributesResults = _setTaskAttributes(task, parameters, newFiles, newFileNames, new ArrayList<>()); 148 149 tasksRoot.saveChanges(); 150 151 Map<String, Object> eventParams = new HashMap<>(); 152 eventParams.put(ObservationConstants.ARGS_TASK, task); 153 eventParams.put(org.ametys.plugins.explorer.ObservationConstants.ARGS_ID, task.getId()); 154 155 _observationManager.notify(new Event(ObservationConstants.EVENT_TASK_CREATED, _currentUserProvider.getUser(), eventParams)); 156 157 Map<String, Object> results = new HashMap<>(); 158 results.put("task", _taskJSONHelper.taskAsJSON(task, getSitemapLanguage(), getSiteName())); 159 results.putAll(attributesResults); 160 return results; 161 } 162 163 /** 164 * Edit a task 165 * @param taskId The id of the task to edit 166 * @param parameters The JS parameters 167 * @param newFiles the new file to add 168 * @param newFileNames the file names to add 169 * @param deleteFiles the file to delete 170 * @return The task data 171 */ 172 @Callable (rights = Callable.CHECKED_BY_IMPLEMENTATION) 173 public Map<String, Object> editTask(String taskId, Map<String, Object> parameters, List<Part> newFiles, List<String> newFileNames, List<String> deleteFiles) 174 { 175 JCRTask task = _resolver.resolveById(taskId); 176 177 ModifiableTraversableAmetysObject tasksRoot = task.getParent(); 178 179 _checkUserRights(tasksRoot, RIGHTS_HANDLE_TASK); 180 181 Map<String, Object> attributesResults = _setTaskAttributes(task, parameters, newFiles, newFileNames, deleteFiles); 182 task.setLastModified(ZonedDateTime.now()); 183 task.saveChanges(); 184 185 Map<String, Object> eventParams = new HashMap<>(); 186 eventParams.put(ObservationConstants.ARGS_TASK, task); 187 eventParams.put(org.ametys.plugins.explorer.ObservationConstants.ARGS_ID, taskId); 188 189 _observationManager.notify(new Event(ObservationConstants.EVENT_TASK_UPDATED, _currentUserProvider.getUser(), eventParams)); 190 191 // Closed status has changed 192 if (attributesResults.containsKey("isClosed")) 193 { 194 _observationManager.notify(new Event(ObservationConstants.EVENT_TASK_CLOSED_STATUS_CHANGED, _currentUserProvider.getUser(), eventParams)); 195 } 196 197 // Assigments have changed 198 if (attributesResults.containsKey("changedAssignments")) 199 { 200 _observationManager.notify(new Event(ObservationConstants.EVENT_TASK_ASSIGNED, _currentUserProvider.getUser(), eventParams)); 201 } 202 203 Map<String, Object> results = new HashMap<>(); 204 results.put("task", _taskJSONHelper.taskAsJSON(task, getSitemapLanguage(), getSiteName())); 205 results.putAll(attributesResults); 206 return results; 207 } 208 209 /** 210 * Move task to new position 211 * @param tasksListId the tasks list id 212 * @param taskId the task id to move 213 * @param newPosition the new position 214 * @return The task data 215 */ 216 @Callable (rights = Callable.CHECKED_BY_IMPLEMENTATION) 217 public Map<String, Object> moveTask(String tasksListId, String taskId, long newPosition) 218 { 219 Task task = _resolver.resolveById(taskId); 220 221 ModifiableTraversableAmetysObject tasksRoot = task.getParent(); 222 _checkUserRights(task.getParent(), RIGHTS_HANDLE_TASK); 223 224 if (tasksListId != task.getTaskListId()) 225 { 226 List<Task> childTasks = _workspaceTasksListDAO.getChildTask(task.getTaskListId()); 227 long position = 0; 228 for (Task childTask : childTasks) 229 { 230 if (!childTask.getId().equals(taskId)) 231 { 232 childTask.setPosition(position); 233 position++; 234 } 235 } 236 } 237 238 task.setTasksListId(tasksListId); 239 List<Task> childTasks = _workspaceTasksListDAO.getChildTask(tasksListId); 240 int size = childTasks.size(); 241 if (newPosition > size) 242 { 243 throw new IllegalArgumentException("New position (" + newPosition + ") can't be greater than tasks child size (" + size + ")"); 244 } 245 246 long position = 0; 247 task.setPosition(newPosition); 248 for (Task childTask : childTasks) 249 { 250 if (position == newPosition) 251 { 252 position++; 253 } 254 255 if (childTask.getId().equals(taskId)) 256 { 257 childTask.setPosition(newPosition); 258 } 259 else 260 { 261 childTask.setPosition(position); 262 position++; 263 } 264 } 265 266 tasksRoot.saveChanges(); 267 268 return _taskJSONHelper.taskAsJSON(task, getSitemapLanguage(), getSiteName()); 269 } 270 271 /** 272 * Remove a task 273 * @param taskId the task id to remove 274 * @return The task data 275 */ 276 @Callable (rights = Callable.CHECKED_BY_IMPLEMENTATION) 277 public Map<String, Object> deleteTask(String taskId) 278 { 279 Task task = _resolver.resolveById(taskId); 280 281 // Check user right 282 ModifiableTraversableAmetysObject tasksRoot = task.getParent(); 283 _checkUserRights(tasksRoot, RIGHTS_DELETE_TASK); 284 285 Map<String, Object> results = new HashMap<>(); 286 287 Map<String, Object> eventParams = new HashMap<>(); 288 eventParams.put(ObservationConstants.ARGS_TASK, task); 289 eventParams.put(org.ametys.plugins.explorer.ObservationConstants.ARGS_ID, taskId); 290 _observationManager.notify(new Event(ObservationConstants.EVENT_TASK_DELETING, _currentUserProvider.getUser(), eventParams)); 291 292 String tasksListId = task.getTaskListId(); 293 task.remove(); 294 295 // Reorder tasks position 296 long position = 0; 297 for (Task childTask : _workspaceTasksListDAO.getChildTask(tasksListId)) 298 { 299 childTask.setPosition(position); 300 position++; 301 } 302 303 tasksRoot.saveChanges(); 304 305 eventParams = new HashMap<>(); 306 eventParams.put(org.ametys.plugins.explorer.ObservationConstants.ARGS_ID, taskId); 307 _observationManager.notify(new Event(ObservationConstants.EVENT_TASK_DELETED, _currentUserProvider.getUser(), eventParams)); 308 309 return results; 310 } 311 312 /** 313 * Comment a task 314 * @param taskId the task id 315 * @param commentText the comment text 316 * @return The task data 317 */ 318 @Callable (rights = Callable.CHECKED_BY_IMPLEMENTATION) 319 public Map<String, Object> commentTask(String taskId, String commentText) 320 { 321 Task task = _resolver.resolveById(taskId); 322 323 ModifiableTraversableAmetysObject tasksRoot = task.getParent(); 324 325 _checkUserRights(tasksRoot, RIGHTS_COMMENT_TASK); 326 327 Comment comment = createComment(task, commentText, tasksRoot); 328 329 // Notify listeners 330 Map<String, Object> eventParams = new HashMap<>(); 331 eventParams.put(org.ametys.plugins.explorer.ObservationConstants.ARGS_ID, task.getId()); 332 eventParams.put(ObservationConstants.ARGS_TASK_COMMENT_ID, comment.getId()); 333 eventParams.put(ObservationConstants.ARGS_TASK_COMMENT, _mentionUtils.transformTextToReadableText(commentText, null)); 334 335 eventParams.put(ObservationConstants.ARGS_TASK, task); 336 337 UserIdentity currentUser = _currentUserProvider.getUser(); 338 _observationManager.notify(new Event(ObservationConstants.EVENT_TASK_COMMENTED, currentUser, eventParams)); 339 340 341 return _taskJSONHelper.taskAsJSON(task, getSitemapLanguage(), getSiteName()); 342 } 343 344 /** 345 * Edit a task comment 346 * @param taskId the task id 347 * @param commentId the comment Id 348 * @param commentText the comment text 349 * @return The task data 350 */ 351 @Callable (rights = Callable.CHECKED_BY_IMPLEMENTATION) 352 public Map<String, Object> editCommentTask(String taskId, String commentId, String commentText) 353 { 354 Task task = _resolver.resolveById(taskId); 355 356 ModifiableTraversableAmetysObject tasksRoot = task.getParent(); 357 358 _checkUserRights(tasksRoot, RIGHTS_COMMENT_TASK); 359 360 editComment(task, commentId, commentText, tasksRoot); 361 362 return _taskJSONHelper.taskAsJSON(task, getSitemapLanguage(), getSiteName()); 363 } 364 365 /** 366 * Answer to a task's comment 367 * @param taskId the task id 368 * @param commentId the comment id 369 * @param commentText the comment text 370 * @return The task data 371 */ 372 @Callable (rights = Callable.CHECKED_BY_IMPLEMENTATION) 373 public Map<String, Object> answerCommentTask(String taskId, String commentId, String commentText) 374 { 375 Task task = _resolver.resolveById(taskId); 376 377 ModifiableTraversableAmetysObject tasksRoot = task.getParent(); 378 379 _checkUserRights(tasksRoot, RIGHTS_COMMENT_TASK); 380 381 Comment comment = answerComment(task, commentId, commentText, tasksRoot); 382 383 // Notify listeners 384 Map<String, Object> eventParams = new HashMap<>(); 385 eventParams.put(org.ametys.plugins.explorer.ObservationConstants.ARGS_ID, task.getId()); 386 eventParams.put(ObservationConstants.ARGS_TASK_COMMENT_ID, comment.getId()); 387 UserIdentity currentUser = _currentUserProvider.getUser(); 388 eventParams.put(ObservationConstants.ARGS_TASK_COMMENT, _mentionUtils.transformTextToReadableText(commentText, null)); 389 390 eventParams.put(ObservationConstants.ARGS_TASK, task); 391 _observationManager.notify(new Event(ObservationConstants.EVENT_TASK_COMMENTED, currentUser, eventParams)); 392 393 return _taskJSONHelper.taskAsJSON(task, getSitemapLanguage(), getSiteName()); 394 } 395 396 /** 397 * Delete a task's comment 398 * @param taskId the task id 399 * @param commentId the comment id 400 * @return The task data 401 */ 402 @Callable (rights = Callable.CHECKED_BY_IMPLEMENTATION) 403 public Map<String, Object> deleteCommentTask(String taskId, String commentId) 404 { 405 Task task = _resolver.resolveById(taskId); 406 407 // Check user right 408 ModifiableTraversableAmetysObject tasksRoot = task.getParent(); 409 410 UserIdentity userIdentity = _currentUserProvider.getUser(); 411 User user = _userManager.getUser(userIdentity); 412 413 Comment comment = task.getComment(commentId); 414 String authorEmail = comment.getAuthorEmail(); 415 if (!authorEmail.equals(user.getEmail())) 416 { 417 _checkUserRights(tasksRoot, RIGHTS_COMMENT_TASK); 418 } 419 420 deleteComment(task, commentId, tasksRoot); 421 422 return _taskJSONHelper.taskAsJSON(task, getSitemapLanguage(), getSiteName()); 423 } 424 425 /** 426 * Like or unlike a task's comment 427 * @param taskId the task id 428 * @param commentId the comment id 429 * @param liked true if the comment is liked, otherwise the comment is unliked 430 * @return The task data 431 */ 432 @Callable (rights = Callable.CHECKED_BY_IMPLEMENTATION) 433 public Map<String, Object> likeOrUnlikeCommentTask(String taskId, String commentId, Boolean liked) 434 { 435 Task task = _resolver.resolveById(taskId); 436 437 ModifiableTraversableAmetysObject tasksRoot = task.getParent(); 438 439 _checkUserRights(tasksRoot, RIGHTS_COMMENT_TASK); 440 441 likeOrUnlikeComment(task, commentId, liked, tasksRoot); 442 443 return _taskJSONHelper.taskAsJSON(task, getSitemapLanguage(), getSiteName()); 444 } 445 446 /** 447 * Set task's attributes 448 * @param task The task to edit 449 * @param parameters The JS parameters 450 * @param newFiles the new file to add to the task 451 * @param newFileNames the new file names to add to the task 452 * @param deleteFiles the file to remove from the task 453 * @return the map of results 454 */ 455 protected Map<String, Object> _setTaskAttributes(JCRTask task, Map<String, Object> parameters, List<Part> newFiles, List<String> newFileNames, List<String> deleteFiles) 456 { 457 Map<String, Object> results = new HashMap<>(); 458 459 String label = (String) parameters.get(JCRTask.ATTRIBUTE_LABEL); 460 task.setLabel(label); 461 462 String description = (String) parameters.get(JCRTask.ATTRIBUTE_DESCRIPTION); 463 task.setDescription(description); 464 465 _setTaskDates(task, parameters); 466 _setTaskCloseInfo(task, parameters, results); 467 _setAttachments(task, newFiles, newFileNames, deleteFiles); 468 469 @SuppressWarnings("unchecked") 470 List<Map<String, Object>> assignmentIds = (List<Map<String, Object>>) parameters.getOrDefault(JCRTask.ATTRIBUTE_ASSIGNMENTS, new ArrayList<>()); 471 List<UserIdentity> users = assignmentIds.stream() 472 .map(m -> (String) m.get("id")) 473 .map(UserIdentity::stringToUserIdentity) 474 .collect(Collectors.toList()); 475 476 if (!task.getAssignments().equals(users)) 477 { 478 task.setAssignments(users); 479 results.put("changedAssignments", true); 480 } 481 482 @SuppressWarnings("unchecked") 483 List<Map<String, Object>> checkListItems = (List<Map<String, Object>>) parameters.getOrDefault(JCRTask.ATTRIBUTE_CHECKLIST, new ArrayList<>()); 484 List<CheckItem> checkItems = checkListItems.stream() 485 .map(e -> new CheckItem((String) e.get(JCRTask.ATTRIBUTE_CHECKLIST_LABEL), (boolean) e.get(JCRTask.ATTRIBUTE_CHECKLIST_ISCHECKED))) 486 .collect(Collectors.toList()); 487 task.setCheckListItem(checkItems); 488 489 @SuppressWarnings("unchecked") 490 List<Object> tags = (List<Object>) parameters.getOrDefault(JCRTask.ATTRIBUTE_TAGS, new ArrayList<>()); 491 492 String projectName = getProjectName(); 493 Project project = _projectManager.getProject(projectName); 494 495 ModifiableTraversableAmetysObject tasksRoot = _getTasksRoot(project, true); 496 497 List<Map<String, Object>> createdTagsJson = _handleTags(task, tags, tasksRoot); 498 499 results.put("newTags", createdTagsJson); 500 return results; 501 } 502 503 private void _setTaskDates(JCRTask task, Map<String, Object> parameters) 504 { 505 String startDateAsStr = (String) parameters.get(JCRTask.ATTRIBUTE_STARTDATE); 506 LocalDate startDate = Optional.ofNullable(startDateAsStr) 507 .map(date -> LocalDate.parse(date, DateTimeFormatter.ISO_LOCAL_DATE)) 508 .orElse(null); 509 task.setStartDate(startDate); 510 511 String dueDateAsStr = (String) parameters.get(JCRTask.ATTRIBUTE_DUEDATE); 512 LocalDate dueDate = Optional.ofNullable(dueDateAsStr) 513 .map(date -> LocalDate.parse(date, DateTimeFormatter.ISO_LOCAL_DATE)) 514 .orElse(null); 515 task.setDueDate(dueDate); 516 } 517 518 private void _setTaskCloseInfo(JCRTask task, Map<String, Object> parameters, Map<String, Object> results) 519 { 520 @SuppressWarnings("unchecked") 521 Map<String, Object> closeInfo = (Map<String, Object>) parameters.get("closeInfo"); 522 if (closeInfo != null && !task.isClosed()) 523 { 524 task.close(true); 525 task.setCloseAuthor(_currentUserProvider.getUser()); 526 task.setCloseDate(LocalDate.now()); 527 528 results.put("isClosed", true); 529 } 530 else if (closeInfo == null && task.isClosed()) 531 { 532 task.close(false); 533 task.setCloseAuthor(null); 534 task.setCloseDate(null); 535 536 results.put("isClosed", false); 537 } 538 } 539 540 /** 541 * Get all tasks from given projets 542 * @param project the project 543 * @return All tasks as JSON 544 */ 545 public List<Task> getProjectTasks(Project project) 546 { 547 TasksWorkspaceModule taskModule = _workspaceModuleEP.getModule(TasksWorkspaceModule.TASK_MODULE_ID); 548 ModifiableTraversableAmetysObject tasksRoot = taskModule.getTasksRoot(project, true); 549 return tasksRoot.getChildren() 550 .stream() 551 .filter(Task.class::isInstance) 552 .map(Task.class::cast) 553 .collect(Collectors.toList()); 554 } 555 556 /** 557 * Get the total number of tasks of the project 558 * @param project The project 559 * @return The number of tasks, or null if the module is not activated 560 */ 561 public Long getTasksCount(Project project) 562 { 563 return Long.valueOf(getProjectTasks(project).size()); 564 } 565 566 /** 567 * Get project members 568 * @return the project members 569 * @throws AmetysRepositoryException if an error occurred 570 */ 571 @Callable (rights = Callable.CHECKED_BY_IMPLEMENTATION) 572 public Map<String, Object> getProjectMembers() throws AmetysRepositoryException 573 { 574 String projectName = getProjectName(); 575 String lang = getSitemapLanguage(); 576 577 return _projectMemberManager.getProjectMembers(projectName, lang, true); 578 } 579}