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.io.InputStream; 019import java.time.LocalDate; 020import java.time.ZonedDateTime; 021import java.time.format.DateTimeFormatter; 022import java.util.ArrayList; 023import java.util.Collections; 024import java.util.HashMap; 025import java.util.List; 026import java.util.Map; 027import java.util.Optional; 028import java.util.stream.Collectors; 029 030import org.apache.avalon.framework.service.ServiceException; 031import org.apache.avalon.framework.service.ServiceManager; 032import org.apache.cocoon.servlet.multipart.Part; 033import org.apache.commons.lang.IllegalClassException; 034import org.apache.commons.lang3.StringUtils; 035 036import org.ametys.cms.data.Binary; 037import org.ametys.cms.repository.ReactionableObject.ReactionType; 038import org.ametys.cms.repository.comment.Comment; 039import org.ametys.core.observation.Event; 040import org.ametys.core.right.RightManager.RightResult; 041import org.ametys.core.ui.Callable; 042import org.ametys.core.user.User; 043import org.ametys.core.user.UserIdentity; 044import org.ametys.core.user.UserManager; 045import org.ametys.plugins.explorer.resources.ModifiableResourceCollection; 046import org.ametys.plugins.repository.AmetysObject; 047import org.ametys.plugins.repository.AmetysObjectResolver; 048import org.ametys.plugins.repository.AmetysRepositoryException; 049import org.ametys.plugins.repository.ModifiableTraversableAmetysObject; 050import org.ametys.plugins.workspaces.ObservationConstants; 051import org.ametys.plugins.workspaces.html.HTMLTransformer; 052import org.ametys.plugins.workspaces.members.ProjectMemberManager; 053import org.ametys.plugins.workspaces.project.objects.Project; 054import org.ametys.plugins.workspaces.tags.ProjectTagProviderExtensionPoint; 055import org.ametys.plugins.workspaces.tags.ProjectTagsDAO; 056import org.ametys.plugins.workspaces.tasks.Task.CheckItem; 057import org.ametys.plugins.workspaces.tasks.jcr.JCRTask; 058import org.ametys.plugins.workspaces.tasks.jcr.JCRTaskFactory; 059import org.ametys.plugins.workspaces.tasks.json.TaskJSONHelper; 060 061/** 062 * DAO for interacting with tasks of a project 063 */ 064public class WorkspaceTaskDAO extends AbstractWorkspaceTaskDAO 065{ 066 /** The Avalon role */ 067 public static final String ROLE = WorkspaceTaskDAO.class.getName(); 068 069 /** The ametys object resolver */ 070 protected AmetysObjectResolver _resolver; 071 072 /** The HTML transformer */ 073 protected HTMLTransformer _htmlTransformer; 074 075 /** The project member manager */ 076 protected ProjectMemberManager _projectMemberManager; 077 078 /** The tag provider extension point */ 079 protected ProjectTagProviderExtensionPoint _tagProviderExtPt; 080 081 /** The project tags DAO */ 082 protected ProjectTagsDAO _projectTagsDAO; 083 084 /** The user manager */ 085 protected UserManager _userManager; 086 087 /** The task JSON helper */ 088 protected TaskJSONHelper _taskJSONHelper; 089 090 @Override 091 public void service(ServiceManager manager) throws ServiceException 092 { 093 super.service(manager); 094 _resolver = (AmetysObjectResolver) manager.lookup(AmetysObjectResolver.ROLE); 095 _htmlTransformer = (HTMLTransformer) manager.lookup(HTMLTransformer.ROLE); 096 _projectMemberManager = (ProjectMemberManager) manager.lookup(ProjectMemberManager.ROLE); 097 _tagProviderExtPt = (ProjectTagProviderExtensionPoint) manager.lookup(ProjectTagProviderExtensionPoint.ROLE); 098 _projectTagsDAO = (ProjectTagsDAO) manager.lookup(ProjectTagsDAO.ROLE); 099 _userManager = (UserManager) manager.lookup(UserManager.ROLE); 100 _taskJSONHelper = (TaskJSONHelper) manager.lookup(TaskJSONHelper.ROLE); 101 } 102 103 /** 104 * Get the tasks from project 105 * @return the list of tasks 106 * @throws IllegalAccessException If an error occurs when checking the rights 107 */ 108 @Callable 109 public List<Map<String, Object>> getTasks() throws IllegalAccessException 110 { 111 String projectName = _getProjectName(); 112 113 ModifiableResourceCollection moduleRoot = _getModuleRoot(projectName); 114 if (!_rightManager.currentUserHasReadAccess(moduleRoot)) 115 { 116 throw new IllegalAccessException("User '" + _currentUserProvider.getUser() + "' tried to get tasks without reader right"); 117 } 118 119 List<Map<String, Object>> tasksInfo = new ArrayList<>(); 120 Project project = _projectManager.getProject(projectName); 121 for (Task task : getProjectTasks(project)) 122 { 123 tasksInfo.add(_taskJSONHelper.taskAsJSON(task, _getSitemapLanguage(), _getSiteName())); 124 } 125 126 return tasksInfo; 127 } 128 129 /** 130 * Add a new task to the tasks list 131 * @param tasksListId the tasks list id 132 * @param parameters The task parameters 133 * @param newFiles the files to add 134 * @param newFileNames the file names to add 135 * @return The task data 136 * @throws IllegalAccessException If an error occurs when checking the rights 137 */ 138 @Callable 139 public Map<String, Object> addTask(String tasksListId, Map<String, Object> parameters, List<Part> newFiles, List<String> newFileNames) throws IllegalAccessException 140 { 141 String projectName = _getProjectName(); 142 143 if (StringUtils.isBlank(tasksListId)) 144 { 145 throw new IllegalArgumentException("Tasks list id is mandatory to create a new task"); 146 } 147 148 ModifiableTraversableAmetysObject tasksRoot = _getTasksRoot(projectName); 149 150 // Check user right 151 if (_rightManager.hasRight(_currentUserProvider.getUser(), RIGHTS_HANDLE_TASK, tasksRoot) != RightResult.RIGHT_ALLOW) 152 { 153 throw new IllegalAccessException("User '" + _currentUserProvider.getUser() + "' tried to add task without convenient right [" + RIGHTS_HANDLE_TASK + "]"); 154 } 155 156 int index = 1; 157 String name = "task-1"; 158 while (tasksRoot.hasChild(name)) 159 { 160 index++; 161 name = "task-" + index; 162 } 163 164 JCRTask task = (JCRTask) tasksRoot.createChild(name, JCRTaskFactory.TASK_NODETYPE); 165 task.setTasksListId(tasksListId); 166 task.setPosition(Long.valueOf(WorkspaceTasksListDAO.getChildTask(tasksListId).size())); 167 168 ZonedDateTime now = ZonedDateTime.now(); 169 task.setCreationDate(now); 170 task.setLastModified(now); 171 task.setAuthor(_currentUserProvider.getUser()); 172 173 Map<String, Object> attributesResults = _setTaskAttributes(task, parameters, newFiles, newFileNames, new ArrayList<>()); 174 175 tasksRoot.saveChanges(); 176 177 Map<String, Object> eventParams = new HashMap<>(); 178 eventParams.put(ObservationConstants.ARGS_TASK, task); 179 eventParams.put(org.ametys.plugins.explorer.ObservationConstants.ARGS_ID, task.getId()); 180 181 _observationManager.notify(new Event(ObservationConstants.EVENT_TASK_CREATED, _currentUserProvider.getUser(), eventParams)); 182 183 Map<String, Object> results = new HashMap<>(); 184 results.put("task", _taskJSONHelper.taskAsJSON(task, _getSitemapLanguage(), _getSiteName())); 185 results.putAll(attributesResults); 186 return results; 187 } 188 189 /** 190 * Edit a task 191 * @param taskId The id of the task to edit 192 * @param parameters The JS parameters 193 * @param newFiles the new file to add 194 * @param newFileNames the file names to add 195 * @param deleteFiles the file to delete 196 * @return The task data 197 * @throws IllegalAccessException If an error occurs when checking the rights 198 */ 199 @Callable 200 public Map<String, Object> editTask(String taskId, Map<String, Object> parameters, List<Part> newFiles, List<String> newFileNames, List<String> deleteFiles) throws IllegalAccessException 201 { 202 AmetysObject object = _resolver.resolveById(taskId); 203 if (!(object instanceof JCRTask)) 204 { 205 throw new IllegalClassException(JCRTask.class, object.getClass()); 206 } 207 208 // Check user right 209 ModifiableTraversableAmetysObject tasksRoot = object.getParent(); 210 if (_rightManager.hasRight(_currentUserProvider.getUser(), RIGHTS_HANDLE_TASK, tasksRoot) != RightResult.RIGHT_ALLOW) 211 { 212 throw new IllegalAccessException("User '" + _currentUserProvider.getUser() + "' tried to edit task without convenient right [" + RIGHTS_HANDLE_TASK + "]"); 213 } 214 215 JCRTask task = (JCRTask) object; 216 Map<String, Object> attributesResults = _setTaskAttributes(task, parameters, newFiles, newFileNames, deleteFiles); 217 218 ZonedDateTime now = ZonedDateTime.now(); 219 task.setLastModified(now); 220 221 task.saveChanges(); 222 223 Map<String, Object> eventParams = new HashMap<>(); 224 eventParams.put(ObservationConstants.ARGS_TASK, task); 225 eventParams.put(org.ametys.plugins.explorer.ObservationConstants.ARGS_ID, taskId); 226 227 _observationManager.notify(new Event(ObservationConstants.EVENT_TASK_UPDATED, _currentUserProvider.getUser(), eventParams)); 228 229 // Closed status has changed 230 if (attributesResults.containsKey("isClosed")) 231 { 232 _observationManager.notify(new Event(ObservationConstants.EVENT_TASK_CLOSED_STATUS_CHANGED, _currentUserProvider.getUser(), eventParams)); 233 } 234 235 // Assigments have changed 236 if (attributesResults.containsKey("changedAssignments")) 237 { 238 _observationManager.notify(new Event(ObservationConstants.EVENT_TASK_ASSIGNED, _currentUserProvider.getUser(), eventParams)); 239 } 240 241 242 Map<String, Object> results = new HashMap<>(); 243 results.put("task", _taskJSONHelper.taskAsJSON(task, _getSitemapLanguage(), _getSiteName())); 244 results.putAll(attributesResults); 245 return results; 246 } 247 248 /** 249 * Move task to new position 250 * @param tasksListId the tasks list id 251 * @param taskId the task id to move 252 * @param newPosition the new position 253 * @return The task data 254 * @throws IllegalAccessException If an error occurs when checking the rights 255 */ 256 @Callable 257 public Map<String, Object> moveTask(String tasksListId, String taskId, long newPosition) throws IllegalAccessException 258 { 259 AmetysObject object = _resolver.resolveById(taskId); 260 if (!(object instanceof Task)) 261 { 262 throw new IllegalClassException(Task.class, object.getClass()); 263 } 264 265 // Check user right 266 ModifiableTraversableAmetysObject tasksRoot = object.getParent(); 267 if (_rightManager.hasRight(_currentUserProvider.getUser(), RIGHTS_HANDLE_TASK, tasksRoot) != RightResult.RIGHT_ALLOW) 268 { 269 throw new IllegalAccessException("User '" + _currentUserProvider.getUser() + "' tried to move task without convenient right [" + RIGHTS_HANDLE_TASK + "]"); 270 } 271 272 Task task = (Task) object; 273 if (tasksListId != task.getTaskListId()) 274 { 275 List<Task> childTasks = WorkspaceTasksListDAO.getChildTask(task.getTaskListId()); 276 long position = 0; 277 for (Task childTask : childTasks) 278 { 279 if (!childTask.getId().equals(taskId)) 280 { 281 childTask.setPosition(position); 282 position++; 283 } 284 } 285 } 286 287 task.setTasksListId(tasksListId); 288 List<Task> childTasks = WorkspaceTasksListDAO.getChildTask(tasksListId); 289 int size = childTasks.size(); 290 if (newPosition > size) 291 { 292 throw new IllegalArgumentException("New position (" + newPosition + ") can't be greater than tasks child size (" + size + ")"); 293 } 294 295 long position = 0; 296 task.setPosition(newPosition); 297 for (Task childTask : childTasks) 298 { 299 if (position == newPosition) 300 { 301 position++; 302 } 303 304 if (childTask.getId().equals(taskId)) 305 { 306 childTask.setPosition(newPosition); 307 } 308 else 309 { 310 childTask.setPosition(position); 311 position++; 312 } 313 } 314 315 tasksRoot.saveChanges(); 316 317 return _taskJSONHelper.taskAsJSON(task, _getSitemapLanguage(), _getSiteName()); 318 } 319 320 /** 321 * Remove a task 322 * @param taskId the task id to remove 323 * @return The task data 324 * @throws IllegalAccessException If an error occurs when checking the rights 325 */ 326 @Callable 327 public Map<String, Object> deleteTask(String taskId) throws IllegalAccessException 328 { 329 AmetysObject object = _resolver.resolveById(taskId); 330 if (!(object instanceof Task)) 331 { 332 throw new IllegalClassException(Task.class, object.getClass()); 333 } 334 335 // Check user right 336 ModifiableTraversableAmetysObject tasksRoot = object.getParent(); 337 if (_rightManager.hasRight(_currentUserProvider.getUser(), RIGHTS_DELETE_TASK, tasksRoot) != RightResult.RIGHT_ALLOW) 338 { 339 throw new IllegalAccessException("User '" + _currentUserProvider.getUser() + "' tried to delete task without convenient right [" + RIGHTS_DELETE_TASK + "]"); 340 } 341 342 Map<String, Object> results = new HashMap<>(); 343 Task jcrTask = (Task) object; 344 345 Map<String, Object> eventParams = new HashMap<>(); 346 eventParams.put(ObservationConstants.ARGS_TASK, jcrTask); 347 eventParams.put(org.ametys.plugins.explorer.ObservationConstants.ARGS_ID, taskId); 348 _observationManager.notify(new Event(ObservationConstants.EVENT_TASK_DELETING, _currentUserProvider.getUser(), eventParams)); 349 350 String tasksListId = jcrTask.getTaskListId(); 351 jcrTask.remove(); 352 353 // Reorder tasks position 354 long position = 0; 355 for (Task childTask : WorkspaceTasksListDAO.getChildTask(tasksListId)) 356 { 357 childTask.setPosition(position); 358 position++; 359 } 360 361 tasksRoot.saveChanges(); 362 363 eventParams = new HashMap<>(); 364 eventParams.put(org.ametys.plugins.explorer.ObservationConstants.ARGS_ID, taskId); 365 _observationManager.notify(new Event(ObservationConstants.EVENT_TASK_DELETED, _currentUserProvider.getUser(), eventParams)); 366 367 return results; 368 } 369 370 /** 371 * Comment a task 372 * @param taskId the task id 373 * @param commentText the comment text 374 * @param authorURL the author URL 375 * @return The task data 376 * @throws IllegalAccessException If an error occurs when checking the rights 377 */ 378 @Callable 379 public Map<String, Object> commentTask(String taskId, String commentText, String authorURL) throws IllegalAccessException 380 { 381 AmetysObject object = _resolver.resolveById(taskId); 382 if (!(object instanceof Task)) 383 { 384 throw new IllegalClassException(Task.class, object.getClass()); 385 } 386 387 // Check user right 388 ModifiableTraversableAmetysObject tasksRoot = object.getParent(); 389 if (_rightManager.hasRight(_currentUserProvider.getUser(), RIGHTS_COMMENT_TASK, tasksRoot) != RightResult.RIGHT_ALLOW) 390 { 391 throw new IllegalAccessException("User '" + _currentUserProvider.getUser() + "' tried to comment task without convenient right [" + RIGHTS_COMMENT_TASK + "]"); 392 } 393 394 Task task = (Task) object; 395 UserIdentity userIdentity = _currentUserProvider.getUser(); 396 User user = _userManager.getUser(userIdentity); 397 398 Comment comment = task.createComment(); 399 comment.setAuthorName(user.getFullName()); 400 comment.setAuthorEmail(user.getEmail()); 401 comment.setEmailHiddenStatus(true); 402 comment.setAuthorURL(authorURL); 403 comment.setContent(commentText); 404 comment.setValidated(true); 405 406 tasksRoot.saveChanges(); 407 408 return _taskJSONHelper.taskAsJSON(task, _getSitemapLanguage(), _getSiteName()); 409 } 410 411 /** 412 * Edit a task 413 * @param taskId the task id 414 * @param commentId the comment Id 415 * @param commentText the comment text 416 * @return The task data 417 * @throws IllegalAccessException If an error occurs when checking the rights 418 */ 419 @Callable 420 public Map<String, Object> editCommentTask(String taskId, String commentId, String commentText) throws IllegalAccessException 421 { 422 AmetysObject object = _resolver.resolveById(taskId); 423 if (!(object instanceof Task)) 424 { 425 throw new IllegalClassException(Task.class, object.getClass()); 426 } 427 428 // Check user right 429 ModifiableTraversableAmetysObject tasksRoot = object.getParent(); 430 if (_rightManager.hasRight(_currentUserProvider.getUser(), RIGHTS_COMMENT_TASK, tasksRoot) != RightResult.RIGHT_ALLOW) 431 { 432 throw new IllegalAccessException("User '" + _currentUserProvider.getUser() + "' tried to edit task without convenient right [" + RIGHTS_COMMENT_TASK + "]"); 433 } 434 435 UserIdentity userIdentity = _currentUserProvider.getUser(); 436 User user = _userManager.getUser(userIdentity); 437 438 Task task = (Task) object; 439 Comment comment = task.getComment(commentId); 440 String authorEmail = comment.getAuthorEmail(); 441 if (!authorEmail.equals(user.getEmail())) 442 { 443 throw new IllegalAccessException("User '" + userIdentity + "' tried to edit an other user's comment task"); 444 } 445 446 if (comment.getContent().equals(commentText)) 447 { 448 return _taskJSONHelper.taskAsJSON(task, _getSitemapLanguage(), _getSiteName()); 449 } 450 451 comment.setContent(commentText); 452 comment.setEdited(true); 453 454 tasksRoot.saveChanges(); 455 456 return _taskJSONHelper.taskAsJSON(task, _getSitemapLanguage(), _getSiteName()); 457 } 458 459 /** 460 * Answer to a task's comment 461 * @param taskId the task id 462 * @param commentId the comment id 463 * @param commentText the comment text 464 * @param authorURL the author URL 465 * @return The task data 466 * @throws IllegalAccessException If an error occurs when checking the rights 467 */ 468 @Callable 469 public Map<String, Object> answerCommentTask(String taskId, String commentId, String commentText, String authorURL) throws IllegalAccessException 470 { 471 AmetysObject object = _resolver.resolveById(taskId); 472 if (!(object instanceof Task)) 473 { 474 throw new IllegalClassException(Task.class, object.getClass()); 475 } 476 477 // Check user right 478 ModifiableTraversableAmetysObject tasksRoot = object.getParent(); 479 if (_rightManager.hasRight(_currentUserProvider.getUser(), RIGHTS_COMMENT_TASK, tasksRoot) != RightResult.RIGHT_ALLOW) 480 { 481 throw new IllegalAccessException("User '" + _currentUserProvider.getUser() + "' tried to comment task without convenient right [" + RIGHTS_COMMENT_TASK + "]"); 482 } 483 484 Task task = (Task) object; 485 Comment comment = task.getComment(commentId); 486 487 UserIdentity userIdentity = _currentUserProvider.getUser(); 488 User user = _userManager.getUser(userIdentity); 489 Comment subComment = comment.createSubComment(); 490 subComment.setAuthorName(user.getFullName()); 491 subComment.setAuthorEmail(user.getEmail()); 492 subComment.setEmailHiddenStatus(true); 493 subComment.setAuthorURL(authorURL); 494 subComment.setContent(commentText); 495 subComment.setValidated(true); 496 497 tasksRoot.saveChanges(); 498 499 return _taskJSONHelper.taskAsJSON(task, _getSitemapLanguage(), _getSiteName()); 500 } 501 502 /** 503 * Delete a task's comment 504 * @param taskId the task id 505 * @param commentId the comment id 506 * @return The task data 507 * @throws IllegalAccessException If an error occurs when checking the rights 508 */ 509 @Callable 510 public Map<String, Object> deleteCommentTask(String taskId, String commentId) throws IllegalAccessException 511 { 512 AmetysObject object = _resolver.resolveById(taskId); 513 if (!(object instanceof Task)) 514 { 515 throw new IllegalClassException(Task.class, object.getClass()); 516 } 517 518 // Check user right 519 ModifiableTraversableAmetysObject tasksRoot = object.getParent(); 520 521 UserIdentity userIdentity = _currentUserProvider.getUser(); 522 User user = _userManager.getUser(userIdentity); 523 524 Task task = (Task) object; 525 Comment comment = task.getComment(commentId); 526 String authorEmail = comment.getAuthorEmail(); 527 if (!authorEmail.equals(user.getEmail())) 528 { 529 if (_rightManager.hasRight(_currentUserProvider.getUser(), RIGHTS_COMMENT_TASK, tasksRoot) != RightResult.RIGHT_ALLOW) 530 { 531 throw new IllegalAccessException("User '" + userIdentity + "' tried to delete an other user's comment task"); 532 } 533 } 534 535 boolean isSubComment = comment.getId().contains(Comment.ID_SEPARATOR); 536 if (isSubComment) 537 { 538 Comment parentComment = task.getComment(StringUtils.substringBeforeLast(comment.getId(), Comment.ID_SEPARATOR)); 539 List<Comment> subComments = parentComment.getSubComment(true, true); 540 boolean hasAfterSubComments = subComments.stream() 541 .filter(c -> !c.getId().equals(comment.getId())) //Don't take current sub comment 542 .filter(c -> !c.isDeleted()) // Ignore alreay deleted sub comment 543 .filter(c -> c.getCreationDate().isAfter(comment.getCreationDate())) // Just take sub comment after current comment 544 .findAny() 545 .isPresent(); 546 547 if (hasAfterSubComments) 548 { 549 comment.setDeleted(true); 550 } 551 else 552 { 553 comment.remove(); 554 } 555 556 // Sort comment by creation date (Recent creation date in first) 557 List<Comment> currentSubComments = parentComment.getSubComment(true, true); 558 Collections.sort(currentSubComments, (c1, c2) -> 559 { 560 return c2.getCreationDate().compareTo(c1.getCreationDate()); 561 }); 562 563 // Remove already deleted sub comment if no recent sub comment is present 564 for (Comment subCom : currentSubComments) 565 { 566 if (subCom.isDeleted()) 567 { 568 subCom.remove(); 569 } 570 else 571 { 572 break; 573 } 574 } 575 576 // Remove parent comment if there are no more sub comment 577 if (parentComment.isDeleted() && parentComment.getSubComment(true, true).isEmpty()) 578 { 579 parentComment.remove(); 580 } 581 } 582 else 583 { 584 List<Comment> subComment = comment.getSubComment(true, true); 585 boolean hasSubComments = subComment.stream() 586 .filter(c -> !c.isDeleted()) // Ignore alreay deleted sub comment 587 .findAny() 588 .isPresent(); 589 if (hasSubComments) 590 { 591 comment.setDeleted(true); 592 } 593 else 594 { 595 comment.remove(); 596 } 597 } 598 599 tasksRoot.saveChanges(); 600 601 return _taskJSONHelper.taskAsJSON(task, _getSitemapLanguage(), _getSiteName()); 602 } 603 604 /** 605 * Like or unlike a task's comment 606 * @param taskId the task id 607 * @param commentId the comment id 608 * @param liked true if the comment is liked, otherwise the comment is unliked 609 * @return The task data 610 * @throws IllegalAccessException If an error occurs when checking the rights 611 */ 612 @Callable 613 public Map<String, Object> likeOrUnlikeCommentTask(String taskId, String commentId, Boolean liked) throws IllegalAccessException 614 { 615 AmetysObject object = _resolver.resolveById(taskId); 616 if (!(object instanceof Task)) 617 { 618 throw new IllegalClassException(Task.class, object.getClass()); 619 } 620 621 // Check user right 622 ModifiableTraversableAmetysObject tasksRoot = object.getParent(); 623 if (_rightManager.hasRight(_currentUserProvider.getUser(), RIGHTS_COMMENT_TASK, tasksRoot) != RightResult.RIGHT_ALLOW) 624 { 625 throw new IllegalAccessException("User '" + _currentUserProvider.getUser() + "' tried to react a comment task without convenient right [" + RIGHTS_COMMENT_TASK + "]"); 626 } 627 628 Task task = (Task) object; 629 Comment comment = task.getComment(commentId); 630 631 UserIdentity user = _currentUserProvider.getUser(); 632 if (Boolean.FALSE.equals(liked) 633 || liked == null && comment.getReactionUsers(ReactionType.LIKE).contains(user)) 634 { 635 comment.removeReaction(user, ReactionType.LIKE); 636 } 637 else 638 { 639 comment.addReaction(user, ReactionType.LIKE); 640 } 641 642 tasksRoot.saveChanges(); 643 644 return _taskJSONHelper.taskAsJSON(task, _getSitemapLanguage(), _getSiteName()); 645 } 646 647 /** 648 * Set task's attributes 649 * @param task The task to edit 650 * @param parameters The JS parameters 651 * @param newFiles the new file to add to the task 652 * @param newFileNames the new file names to add to the task 653 * @param deleteFiles the file to remove from the task 654 * @return the map of results 655 */ 656 protected Map<String, Object> _setTaskAttributes(JCRTask task, Map<String, Object> parameters, List<Part> newFiles, List<String> newFileNames, List<String> deleteFiles) 657 { 658 Map<String, Object> results = new HashMap<>(); 659 660 String label = (String) parameters.get(JCRTask.ATTRIBUTE_LABEL); 661 task.setLabel(label); 662 663 String description = (String) parameters.get(JCRTask.ATTRIBUTE_DESCRIPTION); 664 task.setDescription(description); 665 666 _setTaskDates(task, parameters); 667 _setTaskCloseInfo(task, parameters, results); 668 _setTaskAttachments(task, newFiles, newFileNames, deleteFiles); 669 670 @SuppressWarnings("unchecked") 671 List<Map<String, Object>> assignmentIds = (List<Map<String, Object>>) parameters.getOrDefault(JCRTask.ATTRIBUTE_ASSIGNMENTS, new ArrayList<>()); 672 List<UserIdentity> users = assignmentIds.stream() 673 .map(m -> (String) m.get("id")) 674 .map(UserIdentity::stringToUserIdentity) 675 .collect(Collectors.toList()); 676 677 if (!task.getAssignments().equals(users)) 678 { 679 task.setAssignments(users); 680 results.put("changedAssignments", true); 681 } 682 683 @SuppressWarnings("unchecked") 684 List<Map<String, Object>> checkListItems = (List<Map<String, Object>>) parameters.getOrDefault(JCRTask.ATTRIBUTE_CHECKLIST, new ArrayList<>()); 685 List<CheckItem> checkItems = checkListItems.stream() 686 .map(e -> new CheckItem((String) e.get(JCRTask.ATTRIBUTE_CHECKLIST_LABEL), (boolean) e.get(JCRTask.ATTRIBUTE_CHECKLIST_ISCHECKED))) 687 .collect(Collectors.toList()); 688 task.setCheckListItem(checkItems); 689 690 @SuppressWarnings("unchecked") 691 List<Object> tags = (List<Object>) parameters.getOrDefault("tags", new ArrayList<>()); 692 List<String> createdTags = new ArrayList<>(); 693 List<Map<String, Object>> createdTagsJson = new ArrayList<>(); 694 // Tag new tags 695 for (Object tag : tags) 696 { 697 // Tag doesn't exist so create the tag 698 if (tag instanceof Map) 699 { 700 @SuppressWarnings("unchecked") 701 String tagText = (String) ((Map<String, Object>) tag).get("text"); 702 List<Map<String, Object>> newTags = _projectTagsDAO.addTags(new String[] {tagText}); 703 String newTag = (String) newTags.get(0).get("name"); 704 task.tag(newTag); 705 createdTags.add(newTag); 706 createdTagsJson.addAll(newTags); 707 } 708 else 709 { 710 task.tag((String) tag); 711 } 712 } 713 // Untag unused tags 714 for (String tag : task.getTags()) 715 { 716 if (!tags.contains(tag) && !createdTags.contains(tag)) 717 { 718 task.untag(tag); 719 } 720 } 721 722 results.put("newTags", createdTagsJson); 723 return results; 724 } 725 726 private void _setTaskDates(JCRTask task, Map<String, Object> parameters) 727 { 728 String startDateAsStr = (String) parameters.get("startDate"); 729 LocalDate startDate = Optional.ofNullable(startDateAsStr) 730 .map(date -> LocalDate.parse(date, DateTimeFormatter.ISO_LOCAL_DATE)) 731 .orElse(null); 732 task.setStartDate(startDate); 733 734 String dueDateAsStr = (String) parameters.get(JCRTask.ATTRIBUTE_DUEDATE); 735 LocalDate dueDate = Optional.ofNullable(dueDateAsStr) 736 .map(date -> LocalDate.parse(date, DateTimeFormatter.ISO_LOCAL_DATE)) 737 .orElse(null); 738 task.setDueDate(dueDate); 739 } 740 741 private void _setTaskCloseInfo(JCRTask task, Map<String, Object> parameters, Map<String, Object> results) 742 { 743 @SuppressWarnings("unchecked") 744 Map<String, Object> closeInfo = (Map<String, Object>) parameters.get("closeInfo"); 745 if (closeInfo != null && !task.isClosed()) 746 { 747 task.close(true); 748 task.setCloseAuthor(_currentUserProvider.getUser()); 749 task.setCloseDate(LocalDate.now()); 750 751 results.put("isClosed", true); 752 } 753 else if (closeInfo == null && task.isClosed()) 754 { 755 task.close(false); 756 task.setCloseAuthor(null); 757 task.setCloseDate(null); 758 759 results.put("isClosed", false); 760 } 761 } 762 763 private void _setTaskAttachments(JCRTask task, List<Part> newFiles, List<String> newFileNames, List<String> deleteFiles) 764 { 765 if (!newFiles.isEmpty() || !deleteFiles.isEmpty()) 766 { 767 List<Binary> attachments = task.getAttachments() 768 .stream() 769 .filter(b -> !deleteFiles.contains(b.getName())) 770 .collect(Collectors.toList()); 771 772 List<String> fileNames = attachments.stream() 773 .map(Binary::getFilename) 774 .collect(Collectors.toList()); 775 776 int i = 0; 777 for (Part newPart : newFiles) 778 { 779 String newName = newFileNames.get(i); 780 fileNames.add(newName); 781 Binary newBinary = _partToBinary(newPart, newName); 782 if (newBinary != null) 783 { 784 attachments.add(newBinary); 785 } 786 i++; 787 } 788 task.setAttachments(attachments); 789 } 790 } 791 792 private Binary _partToBinary(Part part, String name) 793 { 794 if (part.isRejected()) 795 { 796 getLogger().error("Part {} will not be uploaded because it's rejected", part.getFileName()); 797 return null; 798 } 799 800 try (InputStream is = part.getInputStream()) 801 { 802 Binary binary = new Binary(); 803 804 binary.setFilename(name); 805 binary.setInputStream(is); 806 binary.setLastModificationDate(ZonedDateTime.now()); 807 binary.setMimeType(part.getMimeType()); 808 809 return binary; 810 } 811 catch (Exception e) 812 { 813 getLogger().error("An error occurred getting binary from part {}", part.getFileName(), e); 814 } 815 816 return null; 817 } 818 819 /** 820 * Get all tasks from given projets 821 * @param project the project 822 * @return All tasks as JSON 823 */ 824 public List<Task> getProjectTasks(Project project) 825 { 826 TasksWorkspaceModule taskModule = _moduleEP.getModule(TasksWorkspaceModule.TASK_MODULE_ID); 827 ModifiableTraversableAmetysObject tasksRoot = taskModule.getTasksRoot(project, true); 828 return tasksRoot.getChildren() 829 .stream() 830 .filter(Task.class::isInstance) 831 .map(Task.class::cast) 832 .collect(Collectors.toList()); 833 } 834 835 /** 836 * Get the total number of tasks of the project 837 * @param project The project 838 * @return The number of tasks, or null if the module is not activated 839 */ 840 public Long getTasksCount(Project project) 841 { 842 return Long.valueOf(getProjectTasks(project).size()); 843 } 844 845 /** 846 * Get project members 847 * @return the project members 848 * @throws IllegalAccessException if an error occurred 849 * @throws AmetysRepositoryException if an error occurred 850 */ 851 @Callable 852 public Map<String, Object> getProjectMembers() throws IllegalAccessException, AmetysRepositoryException 853 { 854 String projectName = _getProjectName(); 855 String lang = _getSitemapLanguage(); 856 857 return _projectMemberManager.getProjectMembers(projectName, lang, true); 858 } 859}