diff --git a/TaskService.java b/TaskService.java new file mode 100644 index 000000000..812288f66 --- /dev/null +++ b/TaskService.java @@ -0,0 +1,993 @@ +package pro.taskana.task.api; + +import java.time.Instant; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import pro.taskana.classification.api.exceptions.ClassificationNotFoundException; +import pro.taskana.classification.api.models.Classification; +import pro.taskana.common.api.BulkOperationResults; +import pro.taskana.common.api.TaskanaRole; +import pro.taskana.common.api.exceptions.ConcurrencyException; +import pro.taskana.common.api.exceptions.InvalidArgumentException; +import pro.taskana.common.api.exceptions.NotAuthorizedException; +import pro.taskana.common.api.exceptions.TaskanaException; +import pro.taskana.spi.routing.api.TaskRoutingProvider; +import pro.taskana.task.api.exceptions.AttachmentPersistenceException; +import pro.taskana.task.api.exceptions.InvalidCallbackStateException; +import pro.taskana.task.api.exceptions.InvalidOwnerException; +import pro.taskana.task.api.exceptions.InvalidTaskStateException; +import pro.taskana.task.api.exceptions.NotAuthorizedOnTaskCommentException; +import pro.taskana.task.api.exceptions.ObjectReferencePersistenceException; +import pro.taskana.task.api.exceptions.TaskAlreadyExistException; +import pro.taskana.task.api.exceptions.TaskCommentNotFoundException; +import pro.taskana.task.api.exceptions.TaskNotFoundException; +import pro.taskana.task.api.models.Attachment; +import pro.taskana.task.api.models.ObjectReference; +import pro.taskana.task.api.models.Task; +import pro.taskana.task.api.models.TaskComment; +import pro.taskana.workbasket.api.WorkbasketPermission; +import pro.taskana.workbasket.api.exceptions.NotAuthorizedOnWorkbasketException; +import pro.taskana.workbasket.api.exceptions.WorkbasketNotFoundException; +import pro.taskana.workbasket.api.models.Workbasket; + +/** The TaskService manages all operations on {@linkplain Task Tasks}. */ +public interface TaskService { + + // region Task + + // region CREATE + + /** + * Instantiates a non-persistent/non-inserted {@linkplain Task}. + * + *

Since a {@linkplain Task} doesn't allow setting a {@linkplain Task#getWorkbasketSummary() + * workbasketSummary}, please either provide an implementation of the {@linkplain + * TaskRoutingProvider} or use the referenced methods to create a {@linkplain Task} within a + * specific {@linkplain Workbasket}. + * + * @return the instantiated {@linkplain Task} + * @see #newTask(String) + * @see #newTask(String, String) + */ + Task newTask(); + + /** + * Instantiates a non-persistent/non-inserted {@linkplain Task}. + * + * @param workbasketId the {@linkplain Workbasket#getId() id} of the {@linkplain Workbasket} to + * which the {@linkplain Task} belongs + * @return the instantiated {@linkplain Task} + * @see #newTask() + * @see #newTask(String, String) + */ + Task newTask(String workbasketId); + + /** + * Instantiates a non-persistent/non-inserted {@linkplain Task}. + * + * @param workbasketKey the {@linkplain Workbasket#getKey() key} of the {@linkplain Workbasket} to + * which the {@linkplain Task} belongs + * @param domain the {@linkplain Workbasket#getDomain() domain} of the {@linkplain Workbasket} to + * which the {@linkplain Task} belongs + * @return the instantiated {@linkplain Task} + * @see #newTask() + * @see #newTask(String) + */ + Task newTask(String workbasketKey, String domain); + + /** + * Inserts a {@linkplain Task} that doesn't exist in the database yet. + * + *

If the {@linkplain Task#getWorkbasketSummary() workbasketSummary} of the given {@linkplain + * Task} is NULL, TaskService will call the {@linkplain TaskRoutingProvider} to determine a + * {@linkplain Workbasket} for the {@linkplain Task}. If the {@linkplain TaskRoutingProvider} is + * not active, e.g. because no {@linkplain TaskRoutingProvider} is registered, or the {@linkplain + * TaskRoutingProvider} doesn't find a {@linkplain Workbasket}, the {@linkplain Task} will not be + * inserted. + * + *

The default values of the created {@linkplain Task} are: + * + *

+ * + * @param taskToCreate the transient {@linkplain Task} to be inserted + * @return the created and inserted {@linkplain Task} + * @throws TaskAlreadyExistException if the {@linkplain Task} already exists + * @throws NotAuthorizedOnWorkbasketException if the current user has no {@linkplain + * WorkbasketPermission#APPEND} for the {@linkplain Workbasket} the {@linkplain Task} is in + * @throws WorkbasketNotFoundException if the {@linkplain Workbasket} referenced by the + * {@linkplain Task#getWorkbasketSummary() workbasketSummary} of the {@linkplain Task} isn't + * found + * @throws ClassificationNotFoundException if the {@linkplain Classification} referenced by + * {@linkplain Task#getClassificationSummary() classificationSummary} of the {@linkplain Task} + * isn't found + * @throws InvalidArgumentException if the {@linkplain Task#getPrimaryObjRef() primaryObjRef} is + * invalid + * @throws AttachmentPersistenceException if an {@linkplain Attachment} with the same {@linkplain + * Attachment#getId() id} was added to the {@linkplain Task} multiple times without using + * {@linkplain Task#addAttachment(Attachment)} + * @throws ObjectReferencePersistenceException if an {@linkplain ObjectReference} with the same + * {@linkplain ObjectReference#getId() id} was added to the {@linkplain Task} multiple times + * without using {@linkplain Task#addSecondaryObjectReference(ObjectReference)} + */ + Task createTask(Task taskToCreate) + throws WorkbasketNotFoundException, + ClassificationNotFoundException, + TaskAlreadyExistException, + InvalidArgumentException, + AttachmentPersistenceException, + ObjectReferencePersistenceException, + NotAuthorizedOnWorkbasketException; + + // endregion + + // region READ + + /** + * Fetches a {@linkplain Task} from the database by the specified {@linkplain Task#getId() id}. + * + * @param taskId the {@linkplain Task#getId() id} of the {@linkplain Task} + * @return the {@linkplain Task} with the specified taskId + * @throws TaskNotFoundException if the {@linkplain Task} with taskId wasn't found + * @throws NotAuthorizedOnWorkbasketException if the current user has no {@linkplain + * WorkbasketPermission#READ} for the {@linkplain Workbasket} the {@linkplain Task} is in + */ + Task getTask(String taskId) throws TaskNotFoundException, NotAuthorizedOnWorkbasketException; + + // endregion + + // region UPDATE + + /** + * Claim an existing {@linkplain Task} for the current user. + * + * @param taskId the {@linkplain Task#getId() id} of the {@linkplain Task} to be claimed + * @return the claimed {@linkplain Task} + * @throws TaskNotFoundException if the {@linkplain Task} with taskId was not found + * @throws InvalidTaskStateException if the {@linkplain Task#getState() state} of the {@linkplain + * Task} with taskId isn't {@linkplain TaskState#READY} + * @throws InvalidOwnerException if the {@linkplain Task} with taskId is claimed by some else + * @throws NotAuthorizedOnWorkbasketException if the current user has no {@linkplain + * WorkbasketPermission#READ} for the {@linkplain Workbasket} the {@linkplain Task} is in + */ + Task claim(String taskId) + throws TaskNotFoundException, + InvalidOwnerException, + NotAuthorizedOnWorkbasketException, + InvalidTaskStateException; + + /** + * Claim an existing {@linkplain Task} for the current user even if it is already claimed by + * someone else. + * + * @param taskId the {@linkplain Task#getId() id} of the {@linkplain Task} to be claimed + * @return the claimed {@linkplain Task} + * @throws TaskNotFoundException if the {@linkplain Task} with taskId was not found + * @throws InvalidTaskStateException if the state of Task with taskId is in {@linkplain + * TaskState#END_STATES} + * @throws InvalidOwnerException cannot be thrown + * @throws NotAuthorizedOnWorkbasketException if the current user has no {@linkplain + * WorkbasketPermission#READ} for the {@linkplain Workbasket} the {@linkplain Task} is in + */ + Task forceClaim(String taskId) + throws TaskNotFoundException, + InvalidOwnerException, + NotAuthorizedOnWorkbasketException, + InvalidTaskStateException; + + /** + * Selects and claims the first {@linkplain Task} which is returned by the {@linkplain TaskQuery}. + * + * @param taskQuery the {@linkplain TaskQuery} + * @return the {@linkplain Task} that got selected and claimed + * @throws InvalidOwnerException if the {@linkplain Task} is claimed by someone else + * @throws NotAuthorizedOnWorkbasketException if the current user has no {@linkplain + * WorkbasketPermission#READ} for the {@linkplain Workbasket} the {@linkplain Task} is in + */ + Optional selectAndClaim(TaskQuery taskQuery) + throws InvalidOwnerException, NotAuthorizedOnWorkbasketException; + + /** + * Cancel the claim of an existing {@linkplain Task} if it was claimed by the current user before. + * + * @param taskId the {@linkplain Task#getId() id} of the {@linkplain Task} which should be + * unclaimed + * @return the unclaimed {@linkplain Task} + * @throws TaskNotFoundException if the {@linkplain Task} with taskId was not found + * @throws InvalidTaskStateException if the {@linkplain Task} is already in one of the {@linkplain + * TaskState#END_STATES} + * @throws InvalidOwnerException if the {@linkplain Task} is claimed by another user + * @throws NotAuthorizedOnWorkbasketException if the current user has no {@linkplain + * WorkbasketPermission#READ} for the {@linkplain Workbasket} the {@linkplain Task} is in + */ + Task cancelClaim(String taskId) + throws TaskNotFoundException, + InvalidOwnerException, + NotAuthorizedOnWorkbasketException, + InvalidTaskStateException; + + /** + * Cancel the claim of an existing {@linkplain Task} even if it was claimed by another user. + * + * @param taskId the {@linkplain Task#getId() id} of the {@linkplain Task} which should be + * unclaimed + * @return the unclaimed {@linkplain Task} + * @throws TaskNotFoundException if the {@linkplain Task} with taskId was not found + * @throws InvalidTaskStateException if the {@linkplain Task} is already in one of the {@linkplain + * TaskState#END_STATES} + * @throws InvalidOwnerException cannot be thrown + * @throws NotAuthorizedOnWorkbasketException if the current user has no {@linkplain + * WorkbasketPermission#READ} for the {@linkplain Workbasket} the {@linkplain Task} is in + */ + Task forceCancelClaim(String taskId) + throws TaskNotFoundException, + InvalidOwnerException, + InvalidTaskStateException { + try { + return this.cancelClaim(taskId, true); + } catch (NotAuthorizedOnWorkbasketException e) { + throw new SystemException( + "this should not have happened. You've discovered a new bug! :D", e); + } + } + + /** + * Request review for an existing {@linkplain Task} that is in {@linkplain TaskState#CLAIMED}. + * + * @param taskId the {@linkplain Task#getId() id} of the specified {@linkplain Task} + * @return the {@linkplain Task} after a review has been requested + * @throws InvalidTaskStateException if the {@linkplain Task#getState() state} of the {@linkplain + * Task} with taskId is not in {@linkplain TaskState#CLAIMED} + * @throws TaskNotFoundException if the {@linkplain Task} with taskId wasn't found + * @throws InvalidOwnerException if the {@linkplain Task} is claimed by another user + * @throws NotAuthorizedOnWorkbasketException if the current user has no {@linkplain + * WorkbasketPermission#READ} for the {@linkplain Workbasket} the {@linkplain Task} is in + */ + Task requestReview(String taskId) + throws InvalidTaskStateException, + TaskNotFoundException, + InvalidOwnerException, + NotAuthorizedOnWorkbasketException; + + /** + * Request review for an existing {@linkplain Task} even if the current user is not the + * {@linkplain Task#getOwner() owner} or the Task is not in {@linkplain TaskState#CLAIMED} yet. + * + * @param taskId the {@linkplain Task#getId() id} of the specified {@linkplain Task} + * @return the {@linkplain Task} after a review has been requested + * @throws InvalidTaskStateException if the {@linkplain Task#getState() state} of the {@linkplain + * Task} with taskId is one of the {@linkplain TaskState#END_STATES} + * @throws TaskNotFoundException if the {@linkplain Task} with taskId wasn't found + * @throws InvalidOwnerException cannot be thrown + * @throws NotAuthorizedOnWorkbasketException if the current user has no {@linkplain + * WorkbasketPermission#READ} for the {@linkplain Workbasket} the {@linkplain Task} is in + */ + Task forceRequestReview(String taskId) + throws InvalidTaskStateException, + TaskNotFoundException, + InvalidOwnerException, + NotAuthorizedOnWorkbasketException; + + /** + * Request changes for an existing {@linkplain Task} that is in {@linkplain TaskState#IN_REVIEW}. + * The {@linkplain TaskState} is changed to {@linkplain TaskState#READY} after changes have been + * requested. + * + * @param taskId the {@linkplain Task#getId() id} of the specified {@linkplain Task} + * @return the {@linkplain Task} after changes have been requested + * @throws InvalidTaskStateException if the {@linkplain Task#getState() state} of the {@linkplain + * Task} with taskId is not in {@linkplain TaskState#IN_REVIEW} + * @throws TaskNotFoundException if the {@linkplain Task} with taskId wasn't found + * @throws InvalidOwnerException if the {@linkplain Task} is claimed by another user + * @throws NotAuthorizedOnWorkbasketException if the current user has no {@linkplain + * WorkbasketPermission#READ} for the {@linkplain Workbasket} the {@linkplain Task} is in + */ + Task requestChanges(String taskId) + throws InvalidTaskStateException, + TaskNotFoundException, + InvalidOwnerException, + NotAuthorizedOnWorkbasketException; + + /** + * Request changes for an existing {@linkplain Task} even if the current user is not the + * {@linkplain Task#getOwner() owner} or the Task is not in {@linkplain TaskState#IN_REVIEW} yet. + * The {@linkplain TaskState} is changed to {@linkplain TaskState#READY} after changes have been + * requested. + * + * @param taskId the {@linkplain Task#getId() id} of the specified {@linkplain Task} + * @return the {@linkplain Task} after changes have been requested + * @throws InvalidTaskStateException if the {@linkplain Task#getState() state} of the {@linkplain + * Task} with taskId is one of the {@linkplain TaskState#END_STATES} + * @throws TaskNotFoundException if the {@linkplain Task} with taskId wasn't found + * @throws InvalidOwnerException cannot be thrown + * @throws NotAuthorizedOnWorkbasketException if the current user has no {@linkplain + * WorkbasketPermission#READ} for the {@linkplain Workbasket} the {@linkplain Task} is in + */ + Task forceRequestChanges(String taskId) + throws InvalidTaskStateException, + TaskNotFoundException, + InvalidOwnerException, + NotAuthorizedOnWorkbasketException; + + /** + * Complete a claimed {@linkplain Task} as {@linkplain Task#getOwner() owner} or {@linkplain + * TaskanaRole#ADMIN} and update {@linkplain Task#getState() state} and timestamps. + * + *

If the {@linkplain Task} is already completed, the {@linkplain Task} is returned unchanged. + * + * @param taskId the {@linkplain Task#getId() id} of the {@linkplain Task} which should be + * completed + * @return the completed {@linkplain Task} + * @throws InvalidTaskStateException if the {@linkplain Task#getState() state} of the {@linkplain + * Task} with taskId is neither {@linkplain TaskState#CLAIMED} nor {@linkplain + * TaskState#COMPLETED} + * @throws TaskNotFoundException if the {@linkplain Task} with taskId wasn't found + * @throws InvalidOwnerException if current user isn't the {@linkplain Task#getOwner() owner} of + * the {@linkplain Task} or {@linkplain TaskanaRole#ADMIN} + * @throws NotAuthorizedOnWorkbasketException if the current user has no {@linkplain + * WorkbasketPermission#READ} for the {@linkplain Workbasket} the {@linkplain Task} is in + */ + Task completeTask(String taskId) + throws TaskNotFoundException, + InvalidOwnerException, + NotAuthorizedOnWorkbasketException, + InvalidTaskStateException; + + /** + * Completes a {@linkplain Task} and updates {@linkplain Task#getState() state} and timestamps in + * every case if the {@linkplain Task} exists. + * + *

If the {@linkplain Task} is already completed, the {@linkplain Task} is returned unchanged. + * + * @param taskId the {@linkplain Task#getId() id} of the {@linkplain Task} which should be + * completed + * @return the updated {@linkplain Task} after completion + * @throws InvalidTaskStateException if the {@linkplain Task#getState() state} of the {@linkplain + * Task} with taskId is with taskId is {@linkplain TaskState#TERMINATED} or {@linkplain + * TaskState#CANCELLED} + * @throws TaskNotFoundException if the {@linkplain Task} with taskId wasn't found + * @throws InvalidOwnerException if current user isn't the {@linkplain Task#getOwner() owner} of + * the {@linkplain Task} or {@linkplain TaskanaRole#ADMIN} + * @throws NotAuthorizedOnWorkbasketException if the current user has no {@linkplain + * WorkbasketPermission#READ} for the {@linkplain Workbasket} the {@linkplain Task} is in + */ + Task forceCompleteTask(String taskId) + throws TaskNotFoundException, + InvalidOwnerException, + NotAuthorizedOnWorkbasketException, + InvalidTaskStateException; + + /** + * Completes a List of {@linkplain Task Tasks}. + * + * @param taskIds {@linkplain Task#getId() ids} of the {@linkplain Task Tasks} which should be + * completed + * @return the result of the operations with each {@linkplain Task#getId() id} and Exception for + * each failed completion + * @throws InvalidArgumentException If the taskIds parameter is NULL + */ + BulkOperationResults completeTasks(List taskIds) + throws InvalidArgumentException; + + /** + * Completes each existing {@linkplain Task} in the given List in every case, independent of the + * {@linkplain Task#getOwner() owner} or {@linkplain Task#getState() state} of the {@linkplain + * Task}. + * + *

If the {@linkplain Task} is already {@linkplain TaskState#COMPLETED completed}, the + * {@linkplain Task} stays unchanged. + * + * @param taskIds {@linkplain Task#getId() id} of the {@linkplain Task Tasks} which should be + * completed + * @return the result of the operations with {@linkplain Task#getId() ids} and Exception for each + * failed completion + * @throws InvalidArgumentException If the taskIds parameter is NULL + */ + BulkOperationResults forceCompleteTasks(List taskIds) + throws InvalidArgumentException; + + /** + * Cancels the {@linkplain Task} with the given {@linkplain Task#getId() id}. + * + *

Cancellation means a {@linkplain Task} is obsolete from a business perspective and doesn't + * need to be completed anymore. + * + * @param taskId the {@linkplain Task#getId() id} of the {@linkplain Task} to cancel + * @return the updated {@linkplain Task} + * @throws TaskNotFoundException if the {@linkplain Task} with taskId wasn't found + * @throws InvalidTaskStateException if the {@linkplain Task} isn't in {@linkplain + * TaskState#READY} or {@linkplain TaskState#CLAIMED} + * @throws NotAuthorizedOnWorkbasketException if the current user has no {@linkplain + * WorkbasketPermission#READ} for the {@linkplain Workbasket} the {@linkplain Task} is in + */ + Task cancelTask(String taskId) + throws TaskNotFoundException, NotAuthorizedOnWorkbasketException, InvalidTaskStateException; + + /** + * Terminates a {@linkplain Task}. Termination is an administrative action to complete a + * {@linkplain Task}. + * + *

This is typically done by administration to correct any technical issue. + * + * @param taskId the {@linkplain Task#getId() id} of the {@linkplain Task} to terminate + * @return the updated {@linkplain Task} + * @throws TaskNotFoundException if the {@linkplain Task} with taskId wasn't found + * @throws InvalidTaskStateException if the {@linkplain Task} isn't in {@linkplain + * TaskState#READY} or {@linkplain TaskState#CLAIMED} + * @throws NotAuthorizedException if the current user isn't member of {@linkplain + * TaskanaRole#ADMIN} or {@linkplain TaskanaRole#TASK_ADMIN} + * @throws NotAuthorizedOnWorkbasketException If the current user doesn't have the correct + * permission + */ + Task terminateTask(String taskId) + throws TaskNotFoundException, + NotAuthorizedException, + NotAuthorizedOnWorkbasketException, + InvalidTaskStateException; + + /** + * Transfers a {@linkplain Task} to another {@linkplain Workbasket} while always setting + * {@linkplain Task#isTransferred() isTransferred} to true. + * + * @see #transfer(String, String, boolean) + */ + @SuppressWarnings("checkstyle:JavadocMethod") + default Task transfer(String taskId, String destinationWorkbasketId) + throws TaskNotFoundException, + WorkbasketNotFoundException, + NotAuthorizedOnWorkbasketException, + InvalidTaskStateException { + return transfer(taskId, destinationWorkbasketId, true); + } + + /** + * Transfers a {@linkplain Task} to another {@linkplain Workbasket}. + * + *

The transfer resets {@linkplain Task#isRead() isRead} and sets {@linkplain + * Task#isTransferred() isTransferred} if setTransferFlag is true. + * + * @param taskId the {@linkplain Task#getId() id} of the {@linkplain Task} which should be + * transferred + * @param destinationWorkbasketId the {@linkplain Workbasket#getId() id} of the target {@linkplain + * Workbasket} + * @param setTransferFlag controls whether to set {@linkplain Task#isTransferred() isTransferred} + * to true or not + * @return the transferred {@linkplain Task} + * @throws TaskNotFoundException if the {@linkplain Task} with taskId wasn't found + * @throws WorkbasketNotFoundException if the target {@linkplain Workbasket} was not found + * @throws NotAuthorizedOnWorkbasketException if the current user has no {@linkplain + * WorkbasketPermission#READ} for the source {@linkplain Workbasket} or no {@linkplain + * WorkbasketPermission#TRANSFER} for the target {@linkplain Workbasket} + * @throws InvalidTaskStateException if the {@linkplain Task} is in one of the {@linkplain + * TaskState#END_STATES} + */ + Task transfer(String taskId, String destinationWorkbasketId, boolean setTransferFlag) + throws TaskNotFoundException, + WorkbasketNotFoundException, + NotAuthorizedOnWorkbasketException, + InvalidTaskStateException; + + /** + * Transfers a {@linkplain Task} to another {@linkplain Workbasket} while always setting + * {@linkplain Task#isTransferred isTransferred} . + * + * @see #transfer(String, String, String, boolean) + */ + @SuppressWarnings("checkstyle:JavadocMethod") + default Task transfer(String taskId, String workbasketKey, String domain) + throws TaskNotFoundException, + WorkbasketNotFoundException, + NotAuthorizedOnWorkbasketException, + InvalidTaskStateException { + return transfer(taskId, workbasketKey, domain, true); + } + + /** + * Transfers a {@linkplain Task} to another {@linkplain Workbasket}. + * + *

The transfer resets {@linkplain Task#isRead() isRead} and sets {@linkplain + * Task#isTransferred() isTransferred} if setTransferFlag is true. + * + * @param taskId the {@linkplain Task#getId() id} of the {@linkplain Task} which should be + * transferred + * @param workbasketKey the {@linkplain Workbasket#getKey() key} of the target {@linkplain + * Workbasket} + * @param domain the {@linkplain Workbasket#getDomain() domain} of the target {@linkplain + * Workbasket} + * @param setTransferFlag controls whether to set {@linkplain Task#isTransferred() isTransferred} + * or not + * @return the transferred {@linkplain Task} + * @throws TaskNotFoundException if the {@linkplain Task} with taskId was not found + * @throws WorkbasketNotFoundException if the target {@linkplain Workbasket} was not found + * @throws NotAuthorizedOnWorkbasketException if the current user has no {@linkplain + * WorkbasketPermission#READ} for the source {@linkplain Workbasket} or no {@linkplain + * WorkbasketPermission#TRANSFER} for the target {@linkplain Workbasket} + * @throws InvalidTaskStateException if the {@linkplain Task} is in one of the {@linkplain + * TaskState#END_STATES} + */ + Task transfer(String taskId, String workbasketKey, String domain, boolean setTransferFlag) + throws TaskNotFoundException, + WorkbasketNotFoundException, + NotAuthorizedOnWorkbasketException, + InvalidTaskStateException; + + /** + * Transfers a List of {@linkplain Task Tasks} to another {@linkplain Workbasket} while always + * setting {@linkplain Task#isTransferred isTransferred} to true. + * + * @see #transferTasks(String, List, boolean) + */ + @SuppressWarnings("checkstyle:JavadocMethod") + default BulkOperationResults transferTasks( + String destinationWorkbasketId, List taskIds) + throws InvalidArgumentException, + WorkbasketNotFoundException, + NotAuthorizedOnWorkbasketException { + return transferTasks(destinationWorkbasketId, taskIds, true); + } + + /** + * Transfers a List of {@linkplain Task Tasks} to another {@linkplain Workbasket}. + * + *

The transfer resets {@linkplain Task#isRead() isRead} and sets {@linkplain + * Task#isTransferred() isTransferred} if setTransferFlag is true. Exceptions will be thrown if + * the caller got no {@linkplain WorkbasketPermission} on the target or if the target {@linkplain + * Workbasket} doesn't exist. Other Exceptions will be stored and returned in the end. + * + * @param destinationWorkbasketId {@linkplain Workbasket#getId() id} of the target {@linkplain + * Workbasket} + * @param taskIds List of source {@linkplain Task Tasks} which will be moved + * @param setTransferFlag controls whether to set {@linkplain Task#isTransferred() isTransferred} + * or not + * @return Bulkresult with {@linkplain Task#getId() ids} and Error for each failed transactions + * @throws NotAuthorizedOnWorkbasketException if the current user has no {@linkplain + * WorkbasketPermission#READ} for the source {@linkplain Workbasket} or no {@linkplain + * WorkbasketPermission#TRANSFER} for the target {@linkplain Workbasket} + * @throws InvalidArgumentException if the method parameters are empty or NULL + * @throws WorkbasketNotFoundException if the target {@linkplain Workbasket} can't be found + */ + BulkOperationResults transferTasks( + String destinationWorkbasketId, List taskIds, boolean setTransferFlag) + throws InvalidArgumentException, + WorkbasketNotFoundException, + NotAuthorizedOnWorkbasketException; + + /** + * Transfers a List of {@linkplain Task Tasks} to another {@linkplain Workbasket} while always + * setting {@linkplain Task#isTransferred() isTransferred} to true. + * + * @see #transferTasks(String, String, List, boolean) + */ + @SuppressWarnings("checkstyle:JavadocMethod") + default BulkOperationResults transferTasks( + String destinationWorkbasketKey, String destinationWorkbasketDomain, List taskIds) + throws InvalidArgumentException, + WorkbasketNotFoundException, + NotAuthorizedOnWorkbasketException { + return transferTasks(destinationWorkbasketKey, destinationWorkbasketDomain, taskIds, true); + } + + /** + * Transfers a List of {@linkplain Task Tasks} to another {@linkplain Workbasket}. + * + *

The transfer resets {@linkplain Task#isRead() isRead} and sets {@linkplain + * Task#isTransferred() isTransferred} if setTransferFlag is true. Exceptions will be thrown if + * the caller got no {@linkplain WorkbasketPermission} on the target {@linkplain Workbasket} or if + * it doesn't exist. Other Exceptions will be stored and returned in the end. + * + * @param destinationWorkbasketKey target {@linkplain Workbasket#getKey() key} + * @param destinationWorkbasketDomain target {@linkplain Workbasket#getDomain() domain} + * @param taskIds List of {@linkplain Task#getId() ids} of source {@linkplain Task Tasks} which + * will be moved + * @param setTransferFlag controls whether to set {@linkplain Task#isTransferred() isTransferred} + * or not + * @return BulkResult with {@linkplain Task#getId() ids} and Error for each failed transactions + * @throws NotAuthorizedOnWorkbasketException if the current user has no {@linkplain + * WorkbasketPermission#READ} for the source {@linkplain Workbasket} or no {@linkplain + * WorkbasketPermission#TRANSFER} for the target {@linkplain Workbasket} + * @throws InvalidArgumentException if the method parameters are empty or NULL + * @throws WorkbasketNotFoundException if the target {@linkplain Workbasket} can't be found + */ + BulkOperationResults transferTasks( + String destinationWorkbasketKey, + String destinationWorkbasketDomain, + List taskIds, + boolean setTransferFlag) + throws InvalidArgumentException, + WorkbasketNotFoundException, + NotAuthorizedOnWorkbasketException; + + /** + * Update a {@linkplain Task}. + * + * @param task the {@linkplain Task} to be updated + * @return the updated {@linkplain Task} + * @throws InvalidArgumentException if the {@linkplain Task} to be updated contains invalid + * properties like e.g. invalid {@linkplain Task#getSecondaryObjectReferences() + * secondaryObjectReferences} + * @throws TaskNotFoundException if the {@linkplain Task} isn't found in the database by its + * {@linkplain Task#getId() id} + * @throws ConcurrencyException if the {@linkplain Task} has been updated by another user in the + * meantime; that's the case if the given {@linkplain Task#getModified() modified} timestamp + * differs from the one in the database + * @throws ClassificationNotFoundException if the {@linkplain Task#getClassificationSummary() + * classificationSummary} of the updated {@linkplain Task} refers to a {@link Classification} + * that can't be found + * @throws NotAuthorizedOnWorkbasketException if the current user has no {@linkplain + * WorkbasketPermission#READ} for the {@linkplain Workbasket} the {@linkplain Task} is in + * @throws AttachmentPersistenceException if an {@linkplain Attachment} with the same {@linkplain + * Attachment#getId() id} was added to the {@linkplain Task} multiple times without using + * {@linkplain Task#addAttachment(Attachment)} + * @throws ObjectReferencePersistenceException if an {@linkplain ObjectReference} with the same + * {@linkplain ObjectReference#getId() id} was added to the {@linkplain Task} multiple times + * without using {@linkplain Task#addSecondaryObjectReference(ObjectReference)} + * @throws InvalidTaskStateException if an attempt is made to change the {@linkplain + * Task#getOwner() owner} of the {@linkplain Task} that {@linkplain Task#getState() state} + * isn't {@linkplain TaskState#READY} + */ + Task updateTask(Task task) + throws InvalidArgumentException, + TaskNotFoundException, + ConcurrencyException, + ClassificationNotFoundException, + AttachmentPersistenceException, + ObjectReferencePersistenceException, + NotAuthorizedOnWorkbasketException, + InvalidTaskStateException; + + /** + * Updates specified {@linkplain TaskCustomField TaskCustomFields} of {@linkplain Task Tasks} + * associated with the given {@linkplain Task#getPrimaryObjRef() primaryObjRef}. + * + * @param selectionCriteria the {@linkplain Task#getPrimaryObjRef() primaryObjRef} of the + * searched-for {@linkplain Task Tasks}. + * @param customFieldsToUpdate a Map that contains as key the identification of the {@linkplain + * TaskCustomField} and as value the corresponding new value of that {@linkplain + * TaskCustomField} + * @return a List of the {@linkplain Task#getId() ids} of all modified {@linkplain Task Tasks} + * @throws InvalidArgumentException if the given selectionCriteria is invalid or the given + * customFieldsToUpdate are NULL or empty + * @see #updateTasks(List, Map) + */ + List updateTasks( + ObjectReference selectionCriteria, Map customFieldsToUpdate) + throws InvalidArgumentException; + + /** + * Updates specified {@linkplain TaskCustomField TaskCustomFields} for all given {@linkplain Task + * Tasks}. + * + * @param taskIds the {@linkplain Task#getId() taskIds} that are used to select the {@linkplain + * Task Tasks} + * @param customFieldsToUpdate a Map that contains as key the identification of the {@linkplain + * TaskCustomField} and as value the corresponding new value of that {@linkplain + * TaskCustomField} + * @return a list of the {@linkplain Task#getId() ids} of all modified {@linkplain Task Tasks} + * @throws InvalidArgumentException if the given customFieldsToUpdate are NULL or empty + * @see #updateTasks(ObjectReference, Map) + */ + List updateTasks(List taskIds, Map customFieldsToUpdate) + throws InvalidArgumentException; + + /** + * Sets the value of {@linkplain Task#isRead() isRead} of the specified {@linkplain Task}. + * + * @param taskId the {@linkplain Task#getId() id} of the {@linkplain Task} to be updated + * @param isRead the new status of {@linkplain Task#isRead() isRead} + * @return the updated {@linkplain Task} + * @throws TaskNotFoundException if the {@linkplain Task} with taskId wasn't found + * @throws NotAuthorizedOnWorkbasketException if the current user has no {@linkplain + * WorkbasketPermission#READ} for the {@linkplain Workbasket} the {@linkplain Task} is in + */ + Task setTaskRead(String taskId, boolean isRead) + throws TaskNotFoundException, NotAuthorizedOnWorkbasketException; + + /** + * Sets the specified {@linkplain CallbackState} on a List of {@linkplain Task Tasks}. + * + *

Note: this method is primarily intended to be used by the TaskanaAdapter + * + * @param externalIds the {@linkplain Task#getExternalId() externalIds} of the {@linkplain Task + * Tasks} on which the {@linkplain CallbackState} is set + * @param state the {@linkplain CallbackState} that is to be set on the {@linkplain Task Tasks} + * @return the result of the operations with {@linkplain Task#getId() ids} and Exception for each + * failed operation + */ + BulkOperationResults setCallbackStateForTasks( + List externalIds, CallbackState state); + + /** + * Sets the {@linkplain Task#getOwner() owner} on a List of {@linkplain Task Tasks}. + * + *

The {@linkplain Task#getOwner() owner} will only be set on {@linkplain Task Tasks} that are + * in {@linkplain TaskState#READY}. + * + * @param owner the new {@linkplain Task#getOwner() owner} of the {@linkplain Task Tasks} + * @param taskIds the {@linkplain Task#getId() ids} of the {@linkplain Task Tasks} on which the + * {@linkplain Task#getOwner() owner} is to be set + * @return the result of the operations with {@linkplain Task#getId() ids} and Exception for each + * failed {@linkplain Task}-update + */ + BulkOperationResults setOwnerOfTasks( + String owner, List taskIds); + + /** + * Sets the {@linkplain Task#getPlanned() planned} Instant on a List of {@linkplain Task Tasks}. + * + *

Only {@linkplain Task Tasks} in state {@linkplain TaskState#READY} and {@linkplain + * TaskState#CLAIMED} will be affected by this method. On each {@linkplain Task}, the + * corresponding {@linkplain Task#getDue() due} Instant is set according to the shortest + * serviceLevel in the {@linkplain Task#getClassificationSummary() Classification} of the + * {@linkplain Task} and its {@linkplain Task#getAttachments() Attachments}. + * + * @param planned the new {@linkplain Task#getPlanned() planned} Instant of the {@linkplain Task + * Tasks} + * @param taskIds the {@linkplain Task#getId() ids} of the {@linkplain Task Tasks} on which the + * new {@linkplain Task#getPlanned() planned} Instant is to be set + * @return the result of the operations with {@linkplain Task#getId() ids} and Exception for each + * failed {@linkplain Task} update + */ + BulkOperationResults setPlannedPropertyOfTasks( + Instant planned, List taskIds); + + // endregion + + // region DELETE + + /** + * Deletes the {@linkplain Task} with the given {@linkplain Task#getId() id}. + * + * @param taskId The {@linkplain Task#getId() id} of the {@linkplain Task} to delete + * @throws NotAuthorizedOnWorkbasketException If the current user doesn't have correct permission + * @throws TaskNotFoundException If the given {@linkplain Task#getId() id} doesn't refer to an + * existing {@linkplain Task} + * @throws InvalidTaskStateException If the {@linkplain Task#getState() state} of the referenced + * {@linkplain Task} isn't one of the {@linkplain TaskState#END_STATES} + * @throws NotAuthorizedException if the current user isn't member of {@linkplain + * TaskanaRole#ADMIN} + * @throws InvalidCallbackStateException the Callback State of the Task is {@linkplain + * CallbackState#CALLBACK_PROCESSING_REQUIRED} + */ + void deleteTask(String taskId) + throws TaskNotFoundException, + NotAuthorizedException, + NotAuthorizedOnWorkbasketException, + InvalidTaskStateException, + InvalidCallbackStateException; + + /** + * Deletes the {@linkplain Task} with the given {@linkplain Task#getId() id} even if it isn't + * completed. + * + * @param taskId The {@linkplain Task#getId() id} of the {@linkplain Task} to delete + * @throws TaskNotFoundException if the given {@linkplain Task#getId() id} doesn't refer to an + * existing {@linkplain Task} + * @throws InvalidTaskStateException if the {@linkplain Task#getState() state} of the referenced + * {@linkplain Task} isn't {@linkplain TaskState#TERMINATED} or {@linkplain + * TaskState#CANCELLED} + * @throws NotAuthorizedOnWorkbasketException If the current user doesn't have correct permissions + * @throws NotAuthorizedException if the current user isn't member of {@linkplain + * TaskanaRole#ADMIN} + * @throws InvalidCallbackStateException the Callback State of the Task is {@linkplain + * CallbackState#CALLBACK_PROCESSING_REQUIRED} + */ + void forceDeleteTask(String taskId) + throws TaskNotFoundException, + NotAuthorizedException, + NotAuthorizedOnWorkbasketException, + InvalidTaskStateException, + InvalidCallbackStateException; + + /** + * Deletes a List of {@linkplain Task Tasks}. + * + * @param tasks the {@linkplain Task#getId() ids} of the {@linkplain Task Tasks} to delete + * @return the result of the operations with each {@linkplain Task#getId() id} and Exception for + * each failed deletion + * @throws InvalidArgumentException if the tasks parameter contains NULL values + * @throws NotAuthorizedException if the current user isn't member of {@linkplain + * TaskanaRole#ADMIN} + */ + BulkOperationResults deleteTasks(List tasks) + throws InvalidArgumentException, NotAuthorizedException; + + // endregion + + // endregion + + // region TaskComment + + // region CREATE + /** + * Instantiates a non-persistent/non-inserted {@linkplain TaskComment}. + * + * @param taskId the {@linkplain Task#getId() id} of the {@linkplain Task} to which the + * {@linkplain TaskComment} belongs + * @return the instantiated {@linkplain TaskComment} + */ + TaskComment newTaskComment(String taskId); + + /** + * Inserts the specified {@linkplain TaskComment} into the database. + * + * @param taskComment the {@linkplain TaskComment} to be created + * @return the created {@linkplain TaskComment} + * @throws NotAuthorizedOnWorkbasketException if the current user has no {@linkplain + * WorkbasketPermission#READ} for the {@linkplain Workbasket} of the commented {@linkplain + * Task}. + * @throws TaskNotFoundException if the given {@linkplain TaskComment#getTaskId() taskId} doesn't + * refer to an existing {@linkplain Task} + * @throws InvalidArgumentException if the {@linkplain TaskComment#getId() id} of the provided + * {@link TaskComment} is neither NULL nor empty + */ + TaskComment createTaskComment(TaskComment taskComment) + throws TaskNotFoundException, InvalidArgumentException, NotAuthorizedOnWorkbasketException; + + // endregion + + // region READ + + /** + * Retrieves the {@linkplain TaskComment} with the given {@linkplain TaskComment#getId() id}. + * + * @param taskCommentId the {@linkplain TaskComment#getId() id} of the {@linkplain TaskComment} + * which should be retrieved + * @return the {@linkplain TaskComment} identified by taskCommentId + * @throws TaskCommentNotFoundException if the given taskCommentId doesn't refer to an existing + * {@linkplain TaskComment} + * @throws NotAuthorizedOnWorkbasketException if the current user has no {@linkplain + * WorkbasketPermission#READ} for the {@linkplain Workbasket} of the commented {@linkplain + * Task} + * @throws TaskNotFoundException if the {@linkplain TaskComment#getTaskId() taskId} of the + * TaskComment doesn't refer to an existing {@linkplain Task} + * @throws InvalidArgumentException if the given taskCommentId is NULL or empty + */ + TaskComment getTaskComment(String taskCommentId) + throws TaskCommentNotFoundException, + TaskNotFoundException, + InvalidArgumentException, + NotAuthorizedOnWorkbasketException; + + /** + * Retrieves the List of {@linkplain TaskComment TaskComments} for the {@linkplain Task} with + * given {@linkplain Task#getId() id}. + * + * @param taskId the {@linkplain Task#getId() id} of the {@linkplain Task} for which all + * {@linkplain TaskComment TaskComments} should be retrieved + * @return the List of {@linkplain TaskComment TaskComments} attached to the specified {@linkplain + * Task} + * @throws NotAuthorizedOnWorkbasketException if the current user has no {@linkplain + * WorkbasketPermission#READ} for the {@linkplain Workbasket} of the commented {@linkplain + * Task} + * @throws TaskNotFoundException if the given taskId doesn't refer to an existing {@linkplain + * Task} + */ + List getTaskComments(String taskId) + throws TaskNotFoundException, NotAuthorizedOnWorkbasketException; + + // endregion + + // region UPDATE + + /** + * Updates the specified {@linkplain TaskComment}. + * + * @param taskComment the {@linkplain TaskComment} to be updated in the database + * @return the updated {@linkplain TaskComment} + * @throws NotAuthorizedOnWorkbasketException if the current user has no {@linkplain + * WorkbasketPermission#READ} for the {@linkplain Workbasket} of the commented {@linkplain + * Task}. + * @throws ConcurrencyException if an attempt is made to update the {@linkplain TaskComment} and + * another user updated it already; that's the case if the given {@linkplain + * Task#getModified() modified} timestamp differs from the one in the database + * @throws TaskCommentNotFoundException if the {@linkplain TaskComment#getId() id} of the + * specified {@linkplain TaskComment} doesn't refer to an existing {@linkplain TaskComment} + * @throws TaskNotFoundException if the {@linkplain TaskComment#getTaskId() taskId} doesn't refer + * to an existing {@linkplain Task} + * @throws InvalidArgumentException if the {@linkplain TaskComment#getId() id} of the specified + * {@linkplain TaskComment} is NULL or empty + * @throws NotAuthorizedOnTaskCommentException If the current user doesn't have correct + * permissions + */ + TaskComment updateTaskComment(TaskComment taskComment) + throws ConcurrencyException, + TaskCommentNotFoundException, + TaskNotFoundException, + InvalidArgumentException, + NotAuthorizedOnTaskCommentException, + NotAuthorizedOnWorkbasketException; + + // endregion + + // region DELETE + + /** + * Deletes the {@linkplain TaskComment} with the given {@linkplain TaskComment#getId() id}. + * + * @param taskCommentId the {@linkplain TaskComment#getId() id} of the {@linkplain TaskComment} to + * delete + * @throws InvalidArgumentException if the taskCommentId is NULL or empty + * @throws TaskCommentNotFoundException if the given taskCommentId doesn't refer to an existing + * {@linkplain TaskComment} + * @throws TaskNotFoundException if the {@linkplain TaskComment#getTaskId() taskId} of the + * TaskComment doesn't refer to an existing {@linkplain Task} + * @throws InvalidArgumentException if the given taskCommentId is NULL or empty + * @throws NotAuthorizedOnWorkbasketException if the current user has no {@linkplain + * WorkbasketPermission#READ} for the {@linkplain Workbasket} the commented {@linkplain Task} + * is in + * @throws NotAuthorizedOnTaskCommentException if the current user is not the {@linkplain + * TaskComment#getCreator() creator} of the {@linkplain TaskComment}. + */ + void deleteTaskComment(String taskCommentId) + throws TaskCommentNotFoundException, + TaskNotFoundException, + InvalidArgumentException, + NotAuthorizedOnTaskCommentException, + NotAuthorizedOnWorkbasketException; + + // endregion + + // endregion + + /** + * Instantiates a non-persistent/non-inserted {@linkplain Attachment}. + * + * @return the instantiated {@linkplain Attachment} + */ + Attachment newAttachment(); + + /** + * Instantiates a non-persistent/non-inserted {@linkplain ObjectReference}. + * + * @return the instantiated {@linkplain ObjectReference} + * @see #newObjectReference(String, String, String, String, String) + */ + ObjectReference newObjectReference(); + + /** + * Instantiates a non-persistent/non-inserted {@linkplain ObjectReference}. + * + * @param company the {@linkplain ObjectReference#getCompany() company} of the new {@linkplain + * ObjectReference} + * @param system the {@linkplain ObjectReference#getSystem() system} of the new {@linkplain + * ObjectReference} + * @param systemInstance the {@linkplain ObjectReference#getSystemInstance() systemInstance} of + * the new {@linkplain ObjectReference} + * @param type the {@linkplain ObjectReference#getType() type} of the new {@linkplain + * ObjectReference} + * @param value the {@linkplain ObjectReference#getValue() value} of the new {@linkplain + * ObjectReference} + * @return the instantiated {@linkplain ObjectReference} + * @see #newObjectReference() + */ + ObjectReference newObjectReference( + String company, String system, String systemInstance, String type, String value); + + /** + * Creates an empty {@linkplain TaskQuery}. + * + * @return a {@linkplain TaskQuery} + */ + TaskQuery createTaskQuery(); + + /** + * Creates an empty {@linkplain TaskCommentQuery}. + * + * @return a {@linkplain TaskCommentQuery} + */ + TaskCommentQuery createTaskCommentQuery(); +} diff --git a/TaskServiceImpl.java b/TaskServiceImpl.java new file mode 100644 index 000000000..5361f2a18 --- /dev/null +++ b/TaskServiceImpl.java @@ -0,0 +1,2144 @@ +package pro.taskana.task.internal; + +import static java.util.function.Predicate.not; +import static pro.taskana.common.internal.util.CheckedFunction.wrap; + +import java.time.Instant; +import java.time.temporal.ChronoUnit; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import org.apache.ibatis.exceptions.PersistenceException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import pro.taskana.classification.api.ClassificationService; +import pro.taskana.classification.api.exceptions.ClassificationNotFoundException; +import pro.taskana.classification.api.models.Classification; +import pro.taskana.classification.api.models.ClassificationSummary; +import pro.taskana.common.api.BulkOperationResults; +import pro.taskana.common.api.TaskanaRole; +import pro.taskana.common.api.exceptions.ConcurrencyException; +import pro.taskana.common.api.exceptions.InvalidArgumentException; +import pro.taskana.common.api.exceptions.NotAuthorizedException; +import pro.taskana.common.api.exceptions.SystemException; +import pro.taskana.common.api.exceptions.TaskanaException; +import pro.taskana.common.internal.InternalTaskanaEngine; +import pro.taskana.common.internal.util.CheckedConsumer; +import pro.taskana.common.internal.util.CollectionUtil; +import pro.taskana.common.internal.util.EnumUtil; +import pro.taskana.common.internal.util.IdGenerator; +import pro.taskana.common.internal.util.ObjectAttributeChangeDetector; +import pro.taskana.common.internal.util.Pair; +import pro.taskana.spi.history.api.events.task.TaskCancelledEvent; +import pro.taskana.spi.history.api.events.task.TaskClaimCancelledEvent; +import pro.taskana.spi.history.api.events.task.TaskClaimedEvent; +import pro.taskana.spi.history.api.events.task.TaskCompletedEvent; +import pro.taskana.spi.history.api.events.task.TaskCreatedEvent; +import pro.taskana.spi.history.api.events.task.TaskRequestChangesEvent; +import pro.taskana.spi.history.api.events.task.TaskRequestReviewEvent; +import pro.taskana.spi.history.api.events.task.TaskTerminatedEvent; +import pro.taskana.spi.history.api.events.task.TaskUpdatedEvent; +import pro.taskana.spi.history.internal.HistoryEventManager; +import pro.taskana.spi.priority.internal.PriorityServiceManager; +import pro.taskana.spi.task.internal.AfterRequestChangesManager; +import pro.taskana.spi.task.internal.AfterRequestReviewManager; +import pro.taskana.spi.task.internal.BeforeRequestChangesManager; +import pro.taskana.spi.task.internal.BeforeRequestReviewManager; +import pro.taskana.spi.task.internal.CreateTaskPreprocessorManager; +import pro.taskana.spi.task.internal.ReviewRequiredManager; +import pro.taskana.task.api.CallbackState; +import pro.taskana.task.api.TaskCommentQuery; +import pro.taskana.task.api.TaskCustomField; +import pro.taskana.task.api.TaskQuery; +import pro.taskana.task.api.TaskService; +import pro.taskana.task.api.TaskState; +import pro.taskana.task.api.exceptions.AttachmentPersistenceException; +import pro.taskana.task.api.exceptions.InvalidCallbackStateException; +import pro.taskana.task.api.exceptions.InvalidOwnerException; +import pro.taskana.task.api.exceptions.InvalidTaskStateException; +import pro.taskana.task.api.exceptions.NotAuthorizedOnTaskCommentException; +import pro.taskana.task.api.exceptions.ObjectReferencePersistenceException; +import pro.taskana.task.api.exceptions.TaskAlreadyExistException; +import pro.taskana.task.api.exceptions.TaskCommentNotFoundException; +import pro.taskana.task.api.exceptions.TaskNotFoundException; +import pro.taskana.task.api.models.Attachment; +import pro.taskana.task.api.models.AttachmentSummary; +import pro.taskana.task.api.models.ObjectReference; +import pro.taskana.task.api.models.Task; +import pro.taskana.task.api.models.TaskComment; +import pro.taskana.task.api.models.TaskSummary; +import pro.taskana.task.internal.ServiceLevelHandler.BulkLog; +import pro.taskana.task.internal.models.AttachmentImpl; +import pro.taskana.task.internal.models.AttachmentSummaryImpl; +import pro.taskana.task.internal.models.MinimalTaskSummary; +import pro.taskana.task.internal.models.ObjectReferenceImpl; +import pro.taskana.task.internal.models.TaskImpl; +import pro.taskana.task.internal.models.TaskSummaryImpl; +import pro.taskana.user.api.models.User; +import pro.taskana.user.internal.UserMapper; +import pro.taskana.workbasket.api.WorkbasketPermission; +import pro.taskana.workbasket.api.WorkbasketService; +import pro.taskana.workbasket.api.exceptions.NotAuthorizedOnWorkbasketException; +import pro.taskana.workbasket.api.exceptions.WorkbasketNotFoundException; +import pro.taskana.workbasket.api.models.Workbasket; +import pro.taskana.workbasket.api.models.WorkbasketSummary; +import pro.taskana.workbasket.internal.WorkbasketQueryImpl; +import pro.taskana.workbasket.internal.models.WorkbasketSummaryImpl; + +/** This is the implementation of TaskService. */ +@SuppressWarnings("checkstyle:OverloadMethodsDeclarationOrder") +public class TaskServiceImpl implements TaskService { + + private static final Logger LOGGER = LoggerFactory.getLogger(TaskServiceImpl.class); + + private final InternalTaskanaEngine taskanaEngine; + private final WorkbasketService workbasketService; + private final ClassificationService classificationService; + private final TaskMapper taskMapper; + private final TaskTransferrer taskTransferrer; + private final TaskCommentServiceImpl taskCommentService; + private final ServiceLevelHandler serviceLevelHandler; + private final AttachmentHandler attachmentHandler; + private final AttachmentMapper attachmentMapper; + private final ObjectReferenceMapper objectReferenceMapper; + private final ObjectReferenceHandler objectReferenceHandler; + private final UserMapper userMapper; + private final HistoryEventManager historyEventManager; + private final CreateTaskPreprocessorManager createTaskPreprocessorManager; + private final PriorityServiceManager priorityServiceManager; + private final ReviewRequiredManager reviewRequiredManager; + private final BeforeRequestReviewManager beforeRequestReviewManager; + private final AfterRequestReviewManager afterRequestReviewManager; + private final BeforeRequestChangesManager beforeRequestChangesManager; + private final AfterRequestChangesManager afterRequestChangesManager; + + public TaskServiceImpl( + InternalTaskanaEngine taskanaEngine, + TaskMapper taskMapper, + TaskCommentMapper taskCommentMapper, + AttachmentMapper attachmentMapper, + ObjectReferenceMapper objectReferenceMapper, + UserMapper userMapper) { + this.taskanaEngine = taskanaEngine; + this.taskMapper = taskMapper; + this.workbasketService = taskanaEngine.getEngine().getWorkbasketService(); + this.attachmentMapper = attachmentMapper; + this.objectReferenceMapper = objectReferenceMapper; + this.userMapper = userMapper; + this.classificationService = taskanaEngine.getEngine().getClassificationService(); + this.historyEventManager = taskanaEngine.getHistoryEventManager(); + this.createTaskPreprocessorManager = taskanaEngine.getCreateTaskPreprocessorManager(); + this.priorityServiceManager = taskanaEngine.getPriorityServiceManager(); + this.reviewRequiredManager = taskanaEngine.getReviewRequiredManager(); + this.beforeRequestReviewManager = taskanaEngine.getBeforeRequestReviewManager(); + this.afterRequestReviewManager = taskanaEngine.getAfterRequestReviewManager(); + this.beforeRequestChangesManager = taskanaEngine.getBeforeRequestChangesManager(); + this.afterRequestChangesManager = taskanaEngine.getAfterRequestChangesManager(); + this.taskTransferrer = new TaskTransferrer(taskanaEngine, taskMapper, this); + this.taskCommentService = + new TaskCommentServiceImpl(taskanaEngine, taskCommentMapper, userMapper, this); + this.serviceLevelHandler = + new ServiceLevelHandler(taskanaEngine, taskMapper, attachmentMapper, this); + this.attachmentHandler = new AttachmentHandler(attachmentMapper, classificationService); + this.objectReferenceHandler = new ObjectReferenceHandler(objectReferenceMapper); + } + + @Override + public Task claim(String taskId) + throws TaskNotFoundException, + InvalidOwnerException, + NotAuthorizedOnWorkbasketException, + InvalidTaskStateException { + return claim(taskId, false); + } + + @Override + public Task forceClaim(String taskId) + throws TaskNotFoundException, + InvalidOwnerException, + NotAuthorizedOnWorkbasketException, + InvalidTaskStateException { + return claim(taskId, true); + } + + @Override + public Task cancelClaim(String taskId) + throws TaskNotFoundException, + InvalidOwnerException, + NotAuthorizedOnWorkbasketException, + InvalidTaskStateException { + return this.cancelClaim(taskId, false); + } + + @Override + public Task forceCancelClaim(String taskId) + throws TaskNotFoundException, + InvalidTaskStateException, + InvalidOwnerException + try { + return this.cancelClaim(taskId, true); + } catch (NotAuthorizedOnWorkbasketException e) { + throw new SystemException( + "this should not have happened. You've discovered a new bug! :D", e); + } + } + + @Override + public Task requestReview(String taskId) + throws InvalidTaskStateException, + TaskNotFoundException, + InvalidOwnerException, + NotAuthorizedOnWorkbasketException { + return requestReview(taskId, false); + } + + @Override + public Task forceRequestReview(String taskId) + throws InvalidTaskStateException, + TaskNotFoundException, + InvalidOwnerException, + NotAuthorizedOnWorkbasketException { + return requestReview(taskId, true); + } + + @Override + public Task requestChanges(String taskId) + throws InvalidTaskStateException, + TaskNotFoundException, + InvalidOwnerException, + NotAuthorizedOnWorkbasketException { + return requestChanges(taskId, false); + } + + @Override + public Task forceRequestChanges(String taskId) + throws InvalidTaskStateException, + TaskNotFoundException, + InvalidOwnerException, + NotAuthorizedOnWorkbasketException { + return requestChanges(taskId, true); + } + + @Override + public Task completeTask(String taskId) + throws TaskNotFoundException, + InvalidOwnerException, + NotAuthorizedOnWorkbasketException, + InvalidTaskStateException { + return completeTask(taskId, false); + } + + @Override + public Task forceCompleteTask(String taskId) + throws TaskNotFoundException, + InvalidOwnerException, + NotAuthorizedOnWorkbasketException, + InvalidTaskStateException { + return completeTask(taskId, true); + } + + @Override + public Task createTask(Task taskToCreate) + throws WorkbasketNotFoundException, + ClassificationNotFoundException, + TaskAlreadyExistException, + InvalidArgumentException, + AttachmentPersistenceException, + ObjectReferencePersistenceException, + NotAuthorizedOnWorkbasketException { + + if (createTaskPreprocessorManager.isEnabled()) { + taskToCreate = createTaskPreprocessorManager.processTaskBeforeCreation(taskToCreate); + } + + TaskImpl task = (TaskImpl) taskToCreate; + + try { + taskanaEngine.openConnection(); + + if (task.getId() != null && !task.getId().isEmpty()) { + throw new InvalidArgumentException("taskId must be empty when creating a task"); + } + + if (LOGGER.isDebugEnabled()) { + LOGGER.debug("Task {} cannot be found, so it can be created.", task.getId()); + } + Workbasket workbasket; + + if (task.getWorkbasketSummary() != null && task.getWorkbasketSummary().getId() != null) { + workbasket = workbasketService.getWorkbasket(task.getWorkbasketSummary().getId()); + } else if (task.getWorkbasketKey() != null) { + workbasket = workbasketService.getWorkbasket(task.getWorkbasketKey(), task.getDomain()); + } else { + String workbasketId = taskanaEngine.getTaskRoutingManager().determineWorkbasketId(task); + if (workbasketId != null) { + workbasket = workbasketService.getWorkbasket(workbasketId); + } else { + throw new InvalidArgumentException("Cannot create a Task outside a Workbasket"); + } + } + + if (workbasket.isMarkedForDeletion()) { + throw new WorkbasketNotFoundException(workbasket.getId()); + } + + task.setWorkbasketSummary(workbasket.asSummary()); + task.setDomain(workbasket.getDomain()); + + if (!taskanaEngine.getEngine().isUserInRole(TaskanaRole.TASK_ROUTER)) { + workbasketService.checkAuthorization( + task.getWorkbasketSummary().getId(), WorkbasketPermission.APPEND); + } + + // we do use the key and not the id to make sure that we use the classification from the right + // domain. + // otherwise we would have to check the classification and its domain for validity. + String classificationKey = task.getClassificationKey(); + if (classificationKey == null || classificationKey.length() == 0) { + throw new InvalidArgumentException("classificationKey of task must not be empty"); + } + + Classification classification = + this.classificationService.getClassification(classificationKey, workbasket.getDomain()); + task.setClassificationSummary(classification.asSummary()); + ObjectReferenceImpl.validate(task.getPrimaryObjRef(), "primary ObjectReference", "Task"); + standardSettingsOnTaskCreation(task, classification); + setCallbackStateOnTaskCreation(task); + priorityServiceManager.calculatePriorityOfTask(task).ifPresent(task::setPriority); + + try { + this.taskMapper.insert(task); + if (LOGGER.isDebugEnabled()) { + LOGGER.debug("Method createTask() created Task '{}'.", task.getId()); + } + if (historyEventManager.isEnabled()) { + + String details = + ObjectAttributeChangeDetector.determineChangesInAttributes(newTask(), task); + historyEventManager.createEvent( + new TaskCreatedEvent( + IdGenerator.generateWithPrefix(IdGenerator.ID_PREFIX_TASK_HISTORY_EVENT), + task, + taskanaEngine.getEngine().getCurrentUserContext().getUserid(), + details)); + } + } catch (PersistenceException e) { + // Error messages: + // Postgres: ERROR: duplicate key value violates unique constraint "uc_external_id" + // DB/2: ### Error updating database. Cause: + // com.ibm.db2.jcc.am.SqlIntegrityConstraintViolationException: DB2 SQL Error: SQLCODE=-803, + // SQLSTATE=23505, SQLERRMC=2;TASKANA.TASK, DRIVER=4.22.29 + // ### The error may involve pro.taskana.mappings.TaskMapper.insert-Inline + // ### The error occurred while setting parameters + // ### SQL: INSERT INTO TASK(ID, EXTERNAL_ID, CREATED, CLAIMED, COMPLETED, MODIFIED, + // PLANNED, DUE, NAME, CREATOR, DESCRIPTION, NOTE, PRIORITY, STATE, + // CLASSIFICATION_CATEGORY, CLASSIFICATION_KEY, CLASSIFICATION_ID, WORKBASKET_ID, + // WORKBASKET_KEY, DOMAIN, BUSINESS_PROCESS_ID, PARENT_BUSINESS_PROCESS_ID, OWNER, + // POR_COMPANY, POR_SYSTEM, POR_INSTANCE, POR_TYPE, POR_VALUE, IS_READ, IS_TRANSFERRED, + // CALLBACK_INFO, CUSTOM_ATTRIBUTES, CUSTOM_1, CUSTOM_2, CUSTOM_3, CUSTOM_4, CUSTOM_5, + // CUSTOM_6, CUSTOM_7, CUSTOM_8, CUSTOM_9, CUSTOM_10, CUSTOM_11, CUSTOM_12, CUSTOM_13, + // CUSTOM_14, CUSTOM_15, CUSTOM_16 ) VALUES(?,?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, + // ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, + // ?, ?) + // ### Cause: com.ibm.db2.jcc.am.SqlIntegrityConstraintViolationException: DB2 SQL + // Error: SQLCODE=-803, SQLSTATE=23505, SQLERRMC=2;TASKANA.TASK, DRIVER=4.22.29 + // H2: ### Error updating database. Cause: org.h2.jdbc.JdbcSQLException: Unique index or + // primary key violation: "UC_EXTERNAL_ID_INDEX_2 ON TASKANA.TASK(EXTERNAL_ID) ... + String msg = e.getMessage() != null ? e.getMessage().toLowerCase() : null; + if (msg != null + && (msg.contains("violation") + || msg.contains("violates") + || msg.contains("violated") + || msg.contains("verletzt")) + && msg.contains("external_id")) { + throw new TaskAlreadyExistException(task.getExternalId()); + } else { + throw e; + } + } + return task; + } finally { + taskanaEngine.returnConnection(); + } + } + + @Override + public Task getTask(String id) throws NotAuthorizedOnWorkbasketException, TaskNotFoundException { + try { + taskanaEngine.openConnection(); + + TaskImpl resultTask = taskMapper.findById(id); + if (resultTask != null) { + WorkbasketQueryImpl query = (WorkbasketQueryImpl) workbasketService.createWorkbasketQuery(); + query.setUsedToAugmentTasks(true); + String workbasketId = resultTask.getWorkbasketSummary().getId(); + List workbaskets = query.idIn(workbasketId).list(); + if (workbaskets.isEmpty()) { + throw new NotAuthorizedOnWorkbasketException( + taskanaEngine.getEngine().getCurrentUserContext().getUserid(), + workbasketId, + WorkbasketPermission.READ); + } else { + resultTask.setWorkbasketSummary(workbaskets.get(0)); + } + + List attachmentImpls = + attachmentMapper.findAttachmentsByTaskId(resultTask.getId()); + if (attachmentImpls == null) { + attachmentImpls = new ArrayList<>(); + } + List secondaryObjectReferences = + objectReferenceMapper.findObjectReferencesByTaskId(resultTask.getId()); + if (secondaryObjectReferences == null) { + secondaryObjectReferences = new ArrayList<>(); + } + Map classificationSummariesById = + findClassificationForTaskImplAndAttachments(resultTask, attachmentImpls); + addClassificationSummariesToAttachments(attachmentImpls, classificationSummariesById); + resultTask.setAttachments(new ArrayList<>(attachmentImpls)); + resultTask.setSecondaryObjectReferences(new ArrayList<>(secondaryObjectReferences)); + String classificationId = resultTask.getClassificationSummary().getId(); + ClassificationSummary classification = classificationSummariesById.get(classificationId); + if (classification == null) { + throw new SystemException( + "Could not find a Classification for task " + resultTask.getId()); + } + + resultTask.setClassificationSummary(classification); + + if (resultTask.getOwner() != null + && !resultTask.getOwner().isEmpty() + && taskanaEngine.getEngine().getConfiguration().isAddAdditionalUserInfo()) { + User owner = userMapper.findById(resultTask.getOwner()); + if (owner != null) { + resultTask.setOwnerLongName(owner.getLongName()); + } + } + return resultTask; + } else { + throw new TaskNotFoundException(id); + } + } finally { + taskanaEngine.returnConnection(); + } + } + + @Override + public Task transfer(String taskId, String destinationWorkbasketId, boolean setTransferFlag) + throws TaskNotFoundException, + WorkbasketNotFoundException, + NotAuthorizedOnWorkbasketException, + InvalidTaskStateException { + return taskTransferrer.transfer(taskId, destinationWorkbasketId, setTransferFlag); + } + + @Override + public Task transfer(String taskId, String workbasketKey, String domain, boolean setTransferFlag) + throws TaskNotFoundException, + WorkbasketNotFoundException, + NotAuthorizedOnWorkbasketException, + InvalidTaskStateException { + return taskTransferrer.transfer(taskId, workbasketKey, domain, setTransferFlag); + } + + @Override + public Task setTaskRead(String taskId, boolean isRead) + throws TaskNotFoundException, NotAuthorizedOnWorkbasketException { + TaskImpl task; + try { + taskanaEngine.openConnection(); + task = (TaskImpl) getTask(taskId); + task.setRead(isRead); + task.setModified(Instant.now()); + taskMapper.update(task); + if (LOGGER.isDebugEnabled()) { + LOGGER.debug("Method setTaskRead() set read property of Task '{}' to {} ", task, isRead); + } + return task; + } finally { + taskanaEngine.returnConnection(); + } + } + + @Override + public TaskQuery createTaskQuery() { + return new TaskQueryImpl(taskanaEngine); + } + + @Override + public TaskCommentQuery createTaskCommentQuery() { + return new TaskCommentQueryImpl(taskanaEngine); + } + + @Override + public Task newTask() { + return newTask(null); + } + + @Override + public Task newTask(String workbasketId) { + TaskImpl task = new TaskImpl(); + WorkbasketSummaryImpl wb = new WorkbasketSummaryImpl(); + wb.setId(workbasketId); + task.setWorkbasketSummary(wb); + task.setCallbackState(CallbackState.NONE); + return task; + } + + @Override + public Task newTask(String workbasketKey, String domain) { + TaskImpl task = new TaskImpl(); + WorkbasketSummaryImpl wb = new WorkbasketSummaryImpl(); + wb.setKey(workbasketKey); + wb.setDomain(domain); + task.setWorkbasketSummary(wb); + return task; + } + + @Override + public TaskComment newTaskComment(String taskId) { + return taskCommentService.newTaskComment(taskId); + } + + @Override + public Attachment newAttachment() { + return new AttachmentImpl(); + } + + @Override + public ObjectReference newObjectReference() { + return new ObjectReferenceImpl(); + } + + @Override + public ObjectReference newObjectReference( + String company, String system, String systemInstance, String type, String value) { + return new ObjectReferenceImpl(company, system, systemInstance, type, value); + } + + @Override + public Task updateTask(Task task) + throws InvalidArgumentException, + TaskNotFoundException, + ConcurrencyException, + AttachmentPersistenceException, + ObjectReferencePersistenceException, + ClassificationNotFoundException, + NotAuthorizedOnWorkbasketException, + InvalidTaskStateException { + String userId = taskanaEngine.getEngine().getCurrentUserContext().getUserid(); + TaskImpl newTaskImpl = (TaskImpl) task; + try { + taskanaEngine.openConnection(); + TaskImpl oldTaskImpl = (TaskImpl) getTask(newTaskImpl.getId()); + + checkConcurrencyAndSetModified(newTaskImpl, oldTaskImpl); + + attachmentHandler.insertAndDeleteAttachmentsOnTaskUpdate(newTaskImpl, oldTaskImpl); + objectReferenceHandler.insertAndDeleteObjectReferencesOnTaskUpdate(newTaskImpl, oldTaskImpl); + ObjectReferenceImpl.validate( + newTaskImpl.getPrimaryObjRef(), "primary ObjectReference", "Task"); + + standardUpdateActions(oldTaskImpl, newTaskImpl); + + priorityServiceManager + .calculatePriorityOfTask(newTaskImpl) + .ifPresent(newTaskImpl::setPriority); + + taskMapper.update(newTaskImpl); + + if (LOGGER.isDebugEnabled()) { + LOGGER.debug("Method updateTask() updated task '{}' for user '{}'.", task.getId(), userId); + } + + if (historyEventManager.isEnabled()) { + + String changeDetails = + ObjectAttributeChangeDetector.determineChangesInAttributes(oldTaskImpl, newTaskImpl); + + historyEventManager.createEvent( + new TaskUpdatedEvent( + IdGenerator.generateWithPrefix(IdGenerator.ID_PREFIX_TASK_HISTORY_EVENT), + task, + userId, + changeDetails)); + } + + } finally { + taskanaEngine.returnConnection(); + } + return task; + } + + @Override + public BulkOperationResults transferTasks( + String destinationWorkbasketId, List taskIds, boolean setTransferFlag) + throws InvalidArgumentException, + WorkbasketNotFoundException, + NotAuthorizedOnWorkbasketException { + return taskTransferrer.transfer(taskIds, destinationWorkbasketId, setTransferFlag); + } + + @Override + public BulkOperationResults transferTasks( + String destinationWorkbasketKey, + String destinationWorkbasketDomain, + List taskIds, + boolean setTransferFlag) + throws InvalidArgumentException, + WorkbasketNotFoundException, + NotAuthorizedOnWorkbasketException { + return taskTransferrer.transfer( + taskIds, destinationWorkbasketKey, destinationWorkbasketDomain, setTransferFlag); + } + + @Override + public void deleteTask(String taskId) + throws TaskNotFoundException, + NotAuthorizedException, + NotAuthorizedOnWorkbasketException, + InvalidTaskStateException, + InvalidCallbackStateException { + deleteTask(taskId, false); + } + + @Override + public void forceDeleteTask(String taskId) + throws TaskNotFoundException, + NotAuthorizedException, + NotAuthorizedOnWorkbasketException, + InvalidTaskStateException, + InvalidCallbackStateException { + deleteTask(taskId, true); + } + + @Override + public Optional selectAndClaim(TaskQuery taskQuery) { + ((TaskQueryImpl) taskQuery).selectAndClaimEquals(true); + return taskanaEngine.executeInDatabaseConnection( + () -> + Optional.ofNullable(taskQuery.single()).map(TaskSummary::getId).map(wrap(this::claim))); + } + + @Override + public BulkOperationResults deleteTasks(List taskIds) + throws InvalidArgumentException, NotAuthorizedException { + + taskanaEngine.getEngine().checkRoleMembership(TaskanaRole.ADMIN); + + try { + taskanaEngine.openConnection(); + if (taskIds == null) { + throw new InvalidArgumentException("List of TaskIds must not be null."); + } + taskIds = new ArrayList<>(taskIds); + + BulkOperationResults bulkLog = new BulkOperationResults<>(); + + if (taskIds.isEmpty()) { + return bulkLog; + } + + List taskSummaries = taskMapper.findExistingTasks(taskIds, null); + + Iterator taskIdIterator = taskIds.iterator(); + while (taskIdIterator.hasNext()) { + removeSingleTaskForTaskDeletionById(bulkLog, taskSummaries, taskIdIterator); + } + + if (!taskIds.isEmpty()) { + attachmentMapper.deleteMultipleByTaskIds(taskIds); + objectReferenceMapper.deleteMultipleByTaskIds(taskIds); + taskMapper.deleteMultiple(taskIds); + + if (taskanaEngine.getEngine().isHistoryEnabled() + && taskanaEngine + .getEngine() + .getConfiguration() + .isDeleteHistoryEventsOnTaskDeletionEnabled()) { + historyEventManager.deleteEvents(taskIds); + } + } + return bulkLog; + } finally { + taskanaEngine.returnConnection(); + } + } + + @Override + public BulkOperationResults completeTasks(List taskIds) + throws InvalidArgumentException { + return completeTasks(taskIds, false); + } + + @Override + public BulkOperationResults forceCompleteTasks(List taskIds) + throws InvalidArgumentException { + return completeTasks(taskIds, true); + } + + @Override + public List updateTasks( + ObjectReference selectionCriteria, Map customFieldsToUpdate) + throws InvalidArgumentException { + + ObjectReferenceImpl.validate(selectionCriteria, "ObjectReference", "updateTasks call"); + validateCustomFields(customFieldsToUpdate); + TaskCustomPropertySelector fieldSelector = new TaskCustomPropertySelector(); + TaskImpl updated = initUpdatedTask(customFieldsToUpdate, fieldSelector); + + try { + taskanaEngine.openConnection(); + + // use query in order to find only those tasks that are visible to the current user + List taskSummaries = getTasksToChange(selectionCriteria); + + List changedTasks = new ArrayList<>(); + if (!taskSummaries.isEmpty()) { + changedTasks = taskSummaries.stream().map(TaskSummary::getId).collect(Collectors.toList()); + taskMapper.updateTasks(changedTasks, updated, fieldSelector); + if (LOGGER.isDebugEnabled()) { + LOGGER.debug("updateTasks() updated the following tasks: {} ", changedTasks); + } + + } else { + if (LOGGER.isDebugEnabled()) { + LOGGER.debug("updateTasks() found no tasks for update "); + } + } + return changedTasks; + } finally { + taskanaEngine.returnConnection(); + } + } + + @Override + public List updateTasks( + List taskIds, Map customFieldsToUpdate) + throws InvalidArgumentException { + + validateCustomFields(customFieldsToUpdate); + TaskCustomPropertySelector fieldSelector = new TaskCustomPropertySelector(); + TaskImpl updatedTask = initUpdatedTask(customFieldsToUpdate, fieldSelector); + + try { + taskanaEngine.openConnection(); + + // use query in order to find only those tasks that are visible to the current user + List taskSummaries = getTasksToChange(taskIds); + + List changedTasks = new ArrayList<>(); + if (!taskSummaries.isEmpty()) { + changedTasks = taskSummaries.stream().map(TaskSummary::getId).collect(Collectors.toList()); + taskMapper.updateTasks(changedTasks, updatedTask, fieldSelector); + if (LOGGER.isDebugEnabled()) { + LOGGER.debug("updateTasks() updated the following tasks: {} ", changedTasks); + } + + } else { + if (LOGGER.isDebugEnabled()) { + LOGGER.debug("updateTasks() found no tasks for update "); + } + } + return changedTasks; + } finally { + taskanaEngine.returnConnection(); + } + } + + @Override + public TaskComment createTaskComment(TaskComment taskComment) + throws TaskNotFoundException, InvalidArgumentException, NotAuthorizedOnWorkbasketException { + return taskCommentService.createTaskComment(taskComment); + } + + @Override + public TaskComment updateTaskComment(TaskComment taskComment) + throws ConcurrencyException, + TaskCommentNotFoundException, + TaskNotFoundException, + InvalidArgumentException, + NotAuthorizedOnTaskCommentException, + NotAuthorizedOnWorkbasketException { + return taskCommentService.updateTaskComment(taskComment); + } + + @Override + public void deleteTaskComment(String taskCommentId) + throws TaskCommentNotFoundException, + TaskNotFoundException, + InvalidArgumentException, + NotAuthorizedOnTaskCommentException, + NotAuthorizedOnWorkbasketException { + taskCommentService.deleteTaskComment(taskCommentId); + } + + @Override + public TaskComment getTaskComment(String taskCommentid) + throws TaskCommentNotFoundException, + TaskNotFoundException, + InvalidArgumentException, + NotAuthorizedOnWorkbasketException { + return taskCommentService.getTaskComment(taskCommentid); + } + + @Override + public List getTaskComments(String taskId) + throws TaskNotFoundException, NotAuthorizedOnWorkbasketException { + + return taskCommentService.getTaskComments(taskId); + } + + @Override + public BulkOperationResults setCallbackStateForTasks( + List externalIds, CallbackState state) { + + try { + taskanaEngine.openConnection(); + + BulkOperationResults bulkLog = new BulkOperationResults<>(); + + if (externalIds == null || externalIds.isEmpty()) { + return bulkLog; + } + + List taskSummaries = taskMapper.findExistingTasks(null, externalIds); + + Iterator taskIdIterator = new ArrayList<>(externalIds).iterator(); + while (taskIdIterator.hasNext()) { + removeSingleTaskForCallbackStateByExternalId(bulkLog, taskSummaries, taskIdIterator, state); + } + if (!externalIds.isEmpty()) { + taskMapper.setCallbackStateMultiple(externalIds, state); + } + return bulkLog; + } finally { + taskanaEngine.returnConnection(); + } + } + + @Override + public BulkOperationResults setOwnerOfTasks( + String owner, List taskIds) { + + BulkOperationResults bulkLog = new BulkOperationResults<>(); + if (taskIds == null || taskIds.isEmpty()) { + return bulkLog; + } + + try { + taskanaEngine.openConnection(); + Pair, BulkLog> existingAndAuthorizedTasks = + getMinimalTaskSummaries(taskIds); + bulkLog.addAllErrors(existingAndAuthorizedTasks.getRight()); + Pair, BulkLog> taskIdsToUpdate = + filterOutTasksWhichAreInInvalidState(existingAndAuthorizedTasks.getLeft()); + bulkLog.addAllErrors(taskIdsToUpdate.getRight()); + + if (!taskIdsToUpdate.getLeft().isEmpty()) { + taskMapper.setOwnerOfTasks(owner, taskIdsToUpdate.getLeft(), Instant.now()); + } + + if (LOGGER.isDebugEnabled()) { + LOGGER.debug( + "Received the Request to set owner on {} tasks, actually modified tasks = {}" + + ", could not set owner on {} tasks.", + taskIds.size(), + taskIdsToUpdate.getLeft().size(), + bulkLog.getFailedIds().size()); + } + + return bulkLog; + } finally { + taskanaEngine.returnConnection(); + } + } + + @Override + public BulkOperationResults setPlannedPropertyOfTasks( + Instant planned, List argTaskIds) { + + BulkLog bulkLog = new BulkLog(); + if (argTaskIds == null || argTaskIds.isEmpty()) { + return bulkLog; + } + try { + taskanaEngine.openConnection(); + Pair, BulkLog> resultsPair = getMinimalTaskSummaries(argTaskIds); + List tasksToModify = resultsPair.getLeft(); + bulkLog.addAllErrors(resultsPair.getRight()); + BulkLog errorsFromProcessing = + serviceLevelHandler.setPlannedPropertyOfTasksImpl(planned, tasksToModify); + bulkLog.addAllErrors(errorsFromProcessing); + return bulkLog; + } finally { + taskanaEngine.returnConnection(); + } + } + + @Override + public Task cancelTask(String taskId) + throws TaskNotFoundException, NotAuthorizedOnWorkbasketException, InvalidTaskStateException { + + Task cancelledTask; + + try { + taskanaEngine.openConnection(); + cancelledTask = terminateCancelCommonActions(taskId, TaskState.CANCELLED); + + if (historyEventManager.isEnabled()) { + historyEventManager.createEvent( + new TaskCancelledEvent( + IdGenerator.generateWithPrefix(IdGenerator.ID_PREFIX_TASK_HISTORY_EVENT), + cancelledTask, + taskanaEngine.getEngine().getCurrentUserContext().getUserid())); + } + } finally { + taskanaEngine.returnConnection(); + } + + return cancelledTask; + } + + @Override + public Task terminateTask(String taskId) + throws TaskNotFoundException, + NotAuthorizedException, + NotAuthorizedOnWorkbasketException, + InvalidTaskStateException { + + taskanaEngine.getEngine().checkRoleMembership(TaskanaRole.ADMIN, TaskanaRole.TASK_ADMIN); + + Task terminatedTask; + + try { + taskanaEngine.openConnection(); + terminatedTask = terminateCancelCommonActions(taskId, TaskState.TERMINATED); + + if (historyEventManager.isEnabled()) { + historyEventManager.createEvent( + new TaskTerminatedEvent( + IdGenerator.generateWithPrefix(IdGenerator.ID_PREFIX_TASK_HISTORY_EVENT), + terminatedTask, + taskanaEngine.getEngine().getCurrentUserContext().getUserid())); + } + + } finally { + taskanaEngine.returnConnection(); + } + return terminatedTask; + } + + public List findTasksIdsAffectedByClassificationChange(String classificationId) { + // tasks directly affected + List tasksAffectedDirectly = + createTaskQuery() + .classificationIdIn(classificationId) + .stateIn(TaskState.READY, TaskState.CLAIMED) + .list(); + + // tasks indirectly affected via attachments + List> affectedPairs = + tasksAffectedDirectly.stream() + .map(t -> Pair.of(t.getId(), t.getPlanned())) + .collect(Collectors.toList()); + // tasks indirectly affected via attachments + List> taskIdsAndPlannedFromAttachments = + attachmentMapper.findTaskIdsAndPlannedAffectedByClassificationChange(classificationId); + + List taskIdsFromAttachments = + taskIdsAndPlannedFromAttachments.stream().map(Pair::getLeft).collect(Collectors.toList()); + List> filteredTaskIdsAndPlannedFromAttachments = + taskIdsFromAttachments.isEmpty() + ? new ArrayList<>() + : taskMapper.filterTaskIdsForReadyAndClaimed(taskIdsFromAttachments); + affectedPairs.addAll(filteredTaskIdsAndPlannedFromAttachments); + // sort all affected tasks according to the planned instant + List affectedTaskIds = + affectedPairs.stream() + .sorted(Comparator.comparing(Pair::getRight)) + .distinct() + .map(Pair::getLeft) + .collect(Collectors.toList()); + + if (LOGGER.isDebugEnabled()) { + LOGGER.debug( + "the following tasks are affected by the update of classification {} : {}", + classificationId, + affectedTaskIds); + } + return affectedTaskIds; + } + + public void refreshPriorityAndDueDatesOfTasksOnClassificationUpdate( + List taskIds, boolean serviceLevelChanged, boolean priorityChanged) { + Pair, BulkLog> resultsPair = getMinimalTaskSummaries(taskIds); + List tasks = resultsPair.getLeft(); + try { + taskanaEngine.openConnection(); + Set adminAccessIds = + taskanaEngine.getEngine().getConfiguration().getRoleMap().get(TaskanaRole.ADMIN); + if (adminAccessIds.contains(taskanaEngine.getEngine().getCurrentUserContext().getUserid())) { + serviceLevelHandler.refreshPriorityAndDueDatesOfTasks( + tasks, serviceLevelChanged, priorityChanged); + } else { + taskanaEngine + .getEngine() + .runAsAdmin( + () -> + serviceLevelHandler.refreshPriorityAndDueDatesOfTasks( + tasks, serviceLevelChanged, priorityChanged)); + } + } finally { + taskanaEngine.returnConnection(); + } + } + + Pair, BulkLog> getMinimalTaskSummaries(Collection argTaskIds) { + BulkLog bulkLog = new BulkLog(); + // remove duplicates + Set taskIds = new HashSet<>(argTaskIds); + // get existing tasks + List minimalTaskSummaries = taskMapper.findExistingTasks(taskIds, null); + bulkLog.addAllErrors(addExceptionsForNonExistingTasksToBulkLog(taskIds, minimalTaskSummaries)); + Pair, BulkLog> filteredPair = + filterTasksAuthorizedForAndLogErrorsForNotAuthorized(minimalTaskSummaries); + bulkLog.addAllErrors(filteredPair.getRight()); + return Pair.of(filteredPair.getLeft(), bulkLog); + } + + Pair, BulkLog> filterTasksAuthorizedForAndLogErrorsForNotAuthorized( + List existingTasks) { + BulkLog bulkLog = new BulkLog(); + // check authorization only for non-admin or task-admin users + if (taskanaEngine.getEngine().isUserInRole(TaskanaRole.ADMIN, TaskanaRole.TASK_ADMIN)) { + return Pair.of(existingTasks, bulkLog); + } else { + List accessIds = taskanaEngine.getEngine().getCurrentUserContext().getAccessIds(); + List> taskAndWorkbasketIdsNotAuthorizedFor = + taskMapper.getTaskAndWorkbasketIdsNotAuthorizedFor(existingTasks, accessIds); + String userId = taskanaEngine.getEngine().getCurrentUserContext().getUserid(); + + for (Pair taskAndWorkbasketIds : taskAndWorkbasketIdsNotAuthorizedFor) { + bulkLog.addError( + taskAndWorkbasketIds.getLeft(), + new NotAuthorizedOnWorkbasketException( + userId, taskAndWorkbasketIds.getRight(), WorkbasketPermission.READ)); + } + + Set taskIdsToRemove = + taskAndWorkbasketIdsNotAuthorizedFor.stream() + .map(Pair::getLeft) + .collect(Collectors.toSet()); + List tasksAuthorizedFor = + existingTasks.stream() + .filter(not(t -> taskIdsToRemove.contains(t.getTaskId()))) + .collect(Collectors.toList()); + return Pair.of(tasksAuthorizedFor, bulkLog); + } + } + + BulkLog addExceptionsForNonExistingTasksToBulkLog( + Collection requestTaskIds, List existingMinimalTaskSummaries) { + BulkLog bulkLog = new BulkLog(); + Set existingTaskIds = + existingMinimalTaskSummaries.stream() + .map(MinimalTaskSummary::getTaskId) + .collect(Collectors.toSet()); + requestTaskIds.stream() + .filter(not(existingTaskIds::contains)) + .forEach(taskId -> bulkLog.addError(taskId, new TaskNotFoundException(taskId))); + return bulkLog; + } + + List augmentTaskSummariesByContainedSummariesWithPartitioning( + List taskSummaries) { + // splitting Augmentation into steps of maximal 32000 tasks + // reason: DB2 has a maximum for parameters in a query + return CollectionUtil.partitionBasedOnSize(taskSummaries, 32000).stream() + .map(this::appendComplexAttributesToTaskSummariesWithoutPartitioning) + .flatMap(Collection::stream) + .collect(Collectors.toList()); + } + + private Pair, BulkLog> filterOutTasksWhichAreInInvalidState( + Collection minimalTaskSummaries) { + List filteredTasks = new ArrayList<>(minimalTaskSummaries.size()); + BulkLog bulkLog = new BulkLog(); + + for (MinimalTaskSummary taskSummary : minimalTaskSummaries) { + if (!taskSummary.getTaskState().in(TaskState.READY, TaskState.READY_FOR_REVIEW)) { + bulkLog.addError( + taskSummary.getTaskId(), + new InvalidTaskStateException( + taskSummary.getTaskId(), + taskSummary.getTaskState(), + TaskState.READY, + TaskState.READY_FOR_REVIEW)); + } else { + filteredTasks.add(taskSummary.getTaskId()); + } + } + return Pair.of(filteredTasks, bulkLog); + } + + private List appendComplexAttributesToTaskSummariesWithoutPartitioning( + List taskSummaries) { + Set taskIds = + taskSummaries.stream().map(TaskSummaryImpl::getId).collect(Collectors.toSet()); + + if (taskIds.isEmpty()) { + taskIds = null; + } + + if (LOGGER.isDebugEnabled()) { + LOGGER.debug( + "augmentTaskSummariesByContainedSummariesWithoutPartitioning() with sublist {} " + + "about to query for attachmentSummaries ", + taskSummaries); + } + + List attachmentSummaries = + attachmentMapper.findAttachmentSummariesByTaskIds(taskIds); + Map classificationSummariesById = + findClassificationsForTasksAndAttachments(taskSummaries, attachmentSummaries); + addClassificationSummariesToAttachments(attachmentSummaries, classificationSummariesById); + addClassificationSummariesToTaskSummaries(taskSummaries, classificationSummariesById); + addAttachmentSummariesToTaskSummaries(taskSummaries, attachmentSummaries); + Map workbasketSummariesById = findWorkbasketsForTasks(taskSummaries); + List objectReferences = + objectReferenceMapper.findObjectReferencesByTaskIds(taskIds); + + addWorkbasketSummariesToTaskSummaries(taskSummaries, workbasketSummariesById); + addObjectReferencesToTaskSummaries(taskSummaries, objectReferences); + + return taskSummaries; + } + + private BulkOperationResults completeTasks( + List taskIds, boolean forced) throws InvalidArgumentException { + try { + taskanaEngine.openConnection(); + if (taskIds == null) { + throw new InvalidArgumentException("TaskIds can't be used as NULL-Parameter."); + } + BulkOperationResults bulkLog = new BulkOperationResults<>(); + + Instant now = Instant.now().truncatedTo(ChronoUnit.MILLIS); + Stream filteredSummaries = + filterNotExistingTaskIds(taskIds, bulkLog) + .filter(task -> task.getState() != TaskState.COMPLETED) + .filter( + addErrorToBulkLog(TaskServiceImpl::checkIfTaskIsTerminatedOrCancelled, bulkLog)); + if (!forced) { + filteredSummaries = + filteredSummaries.filter( + addErrorToBulkLog(this::checkPreconditionsForCompleteTask, bulkLog)); + } else { + String userId = taskanaEngine.getEngine().getCurrentUserContext().getUserid(); + String userLongName; + if (taskanaEngine.getEngine().getConfiguration().isAddAdditionalUserInfo()) { + User user = userMapper.findById(userId); + if (user != null) { + userLongName = user.getLongName(); + } else { + userLongName = null; + } + } else { + userLongName = null; + } + filteredSummaries = + filteredSummaries.filter( + addErrorToBulkLog( + summary -> { + if (taskIsNotClaimed(summary)) { + checkPreconditionsForClaimTask(summary, true); + claimActionsOnTask(summary, userId, userLongName, now); + } + }, + bulkLog)); + } + + updateTasksToBeCompleted(filteredSummaries, now); + + return bulkLog; + } finally { + taskanaEngine.returnConnection(); + } + } + + private Stream filterNotExistingTaskIds( + List taskIds, BulkOperationResults bulkLog) { + + Map taskSummaryMap = + getTasksToChange(taskIds).stream() + .collect(Collectors.toMap(TaskSummary::getId, TaskSummaryImpl.class::cast)); + return taskIds.stream() + .map(id -> Pair.of(id, taskSummaryMap.get(id))) + .filter( + pair -> { + if (pair.getRight() != null) { + return true; + } + String taskId = pair.getLeft(); + bulkLog.addError(taskId, new TaskNotFoundException(taskId)); + return false; + }) + .map(Pair::getRight); + } + + private static Predicate addErrorToBulkLog( + CheckedConsumer checkedConsumer, + BulkOperationResults bulkLog) { + return summary -> { + try { + checkedConsumer.accept(summary); + return true; + } catch (TaskanaException e) { + bulkLog.addError(summary.getId(), e); + return false; + } + }; + } + + private void checkConcurrencyAndSetModified(TaskImpl newTaskImpl, TaskImpl oldTaskImpl) + throws ConcurrencyException { + // TODO: not safe to rely only on different timestamps. + // With fast execution below 1ms there will be no concurrencyException + if (oldTaskImpl.getModified() != null + && !oldTaskImpl.getModified().equals(newTaskImpl.getModified()) + || oldTaskImpl.getClaimed() != null + && !oldTaskImpl.getClaimed().equals(newTaskImpl.getClaimed()) + || oldTaskImpl.getState() != null + && !oldTaskImpl.getState().equals(newTaskImpl.getState())) { + throw new ConcurrencyException(newTaskImpl.getId()); + } + newTaskImpl.setModified(Instant.now()); + } + + private TaskImpl terminateCancelCommonActions(String taskId, TaskState targetState) + throws TaskNotFoundException, NotAuthorizedOnWorkbasketException, InvalidTaskStateException { + if (taskId == null || taskId.isEmpty()) { + throw new TaskNotFoundException(taskId); + } + TaskImpl task = (TaskImpl) getTask(taskId); + TaskState state = task.getState(); + if (state.isEndState()) { + throw new InvalidTaskStateException(taskId, state, TaskState.READY); + } + + Instant now = Instant.now(); + task.setModified(now); + task.setCompleted(now); + task.setState(targetState); + taskMapper.update(task); + if (LOGGER.isDebugEnabled()) { + LOGGER.debug( + "Task '{}' cancelled by user '{}'.", + taskId, + taskanaEngine.getEngine().getCurrentUserContext().getUserid()); + } + return task; + } + + private Task claim(String taskId, boolean forceClaim) + throws TaskNotFoundException, + InvalidOwnerException, + NotAuthorizedOnWorkbasketException, + InvalidTaskStateException { + String userId = taskanaEngine.getEngine().getCurrentUserContext().getUserid(); + String userLongName = null; + if (taskanaEngine.getEngine().getConfiguration().isAddAdditionalUserInfo()) { + User user = userMapper.findById(userId); + if (user != null) { + userLongName = user.getLongName(); + } + } + TaskImpl task; + try { + taskanaEngine.openConnection(); + task = (TaskImpl) getTask(taskId); + + TaskImpl oldTask = duplicateTaskExactly(task); + Instant now = Instant.now(); + + checkPreconditionsForClaimTask(task, forceClaim); + claimActionsOnTask(task, userId, userLongName, now); + taskMapper.update(task); + if (LOGGER.isDebugEnabled()) { + LOGGER.debug("Task '{}' claimed by user '{}'.", taskId, userId); + } + if (historyEventManager.isEnabled()) { + String changeDetails = + ObjectAttributeChangeDetector.determineChangesInAttributes(oldTask, task); + + historyEventManager.createEvent( + new TaskClaimedEvent( + IdGenerator.generateWithPrefix(IdGenerator.ID_PREFIX_TASK_HISTORY_EVENT), + task, + userId, + changeDetails)); + } + } finally { + taskanaEngine.returnConnection(); + } + return task; + } + + private Task requestReview(String taskId, boolean force) + throws TaskNotFoundException, + InvalidTaskStateException, + InvalidOwnerException, + NotAuthorizedOnWorkbasketException { + String userId = taskanaEngine.getEngine().getCurrentUserContext().getUserid(); + TaskImpl task; + try { + taskanaEngine.openConnection(); + task = (TaskImpl) getTask(taskId); + task = (TaskImpl) beforeRequestReviewManager.beforeRequestReview(task); + + TaskImpl oldTask = duplicateTaskExactly(task); + + if (force && task.getState().isEndState()) { + throw new InvalidTaskStateException( + task.getId(), task.getState(), EnumUtil.allValuesExceptFor(TaskState.END_STATES)); + } + if (!force && taskIsNotClaimed(task)) { + throw new InvalidTaskStateException( + task.getId(), task.getState(), TaskState.CLAIMED, TaskState.IN_REVIEW); + } + if (!force && !task.getOwner().equals(userId)) { + throw new InvalidOwnerException(userId, task.getId()); + } + + task.setState(TaskState.READY_FOR_REVIEW); + task.setOwner(null); + task.setModified(Instant.now()); + + taskMapper.requestReview(task); + if (LOGGER.isDebugEnabled()) { + LOGGER.debug("Requested review for Task '{}' by user '{}'.", taskId, userId); + } + if (historyEventManager.isEnabled()) { + String changeDetails = + ObjectAttributeChangeDetector.determineChangesInAttributes(oldTask, task); + + historyEventManager.createEvent( + new TaskRequestReviewEvent( + IdGenerator.generateWithPrefix(IdGenerator.ID_PREFIX_TASK_HISTORY_EVENT), + task, + userId, + changeDetails)); + } + + task = (TaskImpl) afterRequestReviewManager.afterRequestReview(task); + } finally { + taskanaEngine.returnConnection(); + } + return task; + } + + private Task requestChanges(String taskId, boolean force) + throws InvalidTaskStateException, + TaskNotFoundException, + InvalidOwnerException, + NotAuthorizedOnWorkbasketException { + String userId = taskanaEngine.getEngine().getCurrentUserContext().getUserid(); + TaskImpl task; + try { + taskanaEngine.openConnection(); + task = (TaskImpl) getTask(taskId); + task = (TaskImpl) beforeRequestChangesManager.beforeRequestChanges(task); + + TaskImpl oldTask = duplicateTaskExactly(task); + + if (force && task.getState().isEndState()) { + throw new InvalidTaskStateException( + task.getId(), task.getState(), EnumUtil.allValuesExceptFor(TaskState.END_STATES)); + } + if (!force && task.getState() != TaskState.IN_REVIEW) { + throw new InvalidTaskStateException(task.getId(), task.getState(), TaskState.IN_REVIEW); + } + if (!force && !task.getOwner().equals(userId)) { + throw new InvalidOwnerException(userId, task.getId()); + } + + task.setState(TaskState.READY); + task.setOwner(null); + task.setModified(Instant.now()); + + taskMapper.requestChanges(task); + if (LOGGER.isDebugEnabled()) { + LOGGER.debug("Requested changes for Task '{}' by user '{}'.", taskId, userId); + } + if (historyEventManager.isEnabled()) { + String changeDetails = + ObjectAttributeChangeDetector.determineChangesInAttributes(oldTask, task); + + historyEventManager.createEvent( + new TaskRequestChangesEvent( + IdGenerator.generateWithPrefix(IdGenerator.ID_PREFIX_TASK_HISTORY_EVENT), + task, + userId, + changeDetails)); + } + task = (TaskImpl) afterRequestChangesManager.afterRequestChanges(task); + } finally { + taskanaEngine.returnConnection(); + } + return task; + } + + private static void claimActionsOnTask( + TaskSummaryImpl task, String userId, String userLongName, Instant now) { + task.setOwner(userId); + task.setOwnerLongName(userLongName); + task.setModified(now); + task.setClaimed(now); + task.setRead(true); + if (Set.of(TaskState.READY_FOR_REVIEW, TaskState.IN_REVIEW).contains(task.getState())) { + task.setState(TaskState.IN_REVIEW); + } else { + task.setState(TaskState.CLAIMED); + } + } + + private static void cancelClaimActionsOnTask(TaskSummaryImpl task, Instant now) { + task.setOwner(null); + task.setModified(now); + task.setClaimed(null); + task.setRead(true); + if (task.getState() == TaskState.IN_REVIEW) { + task.setState(TaskState.READY_FOR_REVIEW); + } else { + task.setState(TaskState.READY); + } + } + + private static void completeActionsOnTask(TaskSummaryImpl task, String userId, Instant now) { + task.setCompleted(now); + task.setModified(now); + task.setState(TaskState.COMPLETED); + task.setOwner(userId); + } + + private void checkPreconditionsForClaimTask(TaskSummary task, boolean forced) + throws InvalidOwnerException, InvalidTaskStateException { + TaskState state = task.getState(); + if (state.isEndState()) { + throw new InvalidTaskStateException( + task.getId(), task.getState(), EnumUtil.allValuesExceptFor(TaskState.END_STATES)); + } + + String userId = taskanaEngine.getEngine().getCurrentUserContext().getUserid(); + if (!forced + && (state == TaskState.CLAIMED || state == TaskState.IN_REVIEW) + && !task.getOwner().equals(userId)) { + throw new InvalidOwnerException(userId, task.getId()); + } + } + + private static boolean taskIsNotClaimed(TaskSummary task) { + return task.getClaimed() == null + || (task.getState() != TaskState.CLAIMED && task.getState() != TaskState.IN_REVIEW); + } + + private static void checkIfTaskIsTerminatedOrCancelled(TaskSummary task) + throws InvalidTaskStateException { + if (task.getState().in(TaskState.CANCELLED, TaskState.TERMINATED)) { + throw new InvalidTaskStateException( + task.getId(), + task.getState(), + EnumUtil.allValuesExceptFor(TaskState.CANCELLED, TaskState.TERMINATED)); + } + } + + private void checkPreconditionsForCompleteTask(TaskSummary task) + throws InvalidOwnerException, InvalidTaskStateException { + if (taskIsNotClaimed(task)) { + throw new InvalidTaskStateException( + task.getId(), task.getState(), TaskState.CLAIMED, TaskState.IN_REVIEW); + } else if (!taskanaEngine + .getEngine() + .getCurrentUserContext() + .getAccessIds() + .contains(task.getOwner()) + && !taskanaEngine.getEngine().isUserInRole(TaskanaRole.ADMIN)) { + throw new InvalidOwnerException( + taskanaEngine.getEngine().getCurrentUserContext().getUserid(), task.getId()); + } + } + + private Task cancelClaim(String taskId, boolean forceUnclaim) + throws TaskNotFoundException, + InvalidOwnerException, + NotAuthorizedOnWorkbasketException, + InvalidTaskStateException { + String userId = taskanaEngine.getEngine().getCurrentUserContext().getUserid(); + TaskImpl task; + try { + taskanaEngine.openConnection(); + task = (TaskImpl) getTask(taskId); + + TaskImpl oldTask = duplicateTaskExactly(task); + + TaskState state = task.getState(); + if (state.isEndState()) { + throw new InvalidTaskStateException( + taskId, state, EnumUtil.allValuesExceptFor(TaskState.END_STATES)); + } + if ((state == TaskState.CLAIMED || state == TaskState.IN_REVIEW) + && !forceUnclaim + && !userId.equals(task.getOwner())) { + throw new InvalidOwnerException(userId, taskId); + } + Instant now = Instant.now(); + cancelClaimActionsOnTask(task, now); + taskMapper.update(task); + if (LOGGER.isDebugEnabled()) { + LOGGER.debug("Task '{}' unclaimed by user '{}'.", taskId, userId); + } + if (historyEventManager.isEnabled()) { + String changeDetails = + ObjectAttributeChangeDetector.determineChangesInAttributes(oldTask, task); + + historyEventManager.createEvent( + new TaskClaimCancelledEvent( + IdGenerator.generateWithPrefix(IdGenerator.ID_PREFIX_TASK_HISTORY_EVENT), + task, + userId, + changeDetails)); + } + } finally { + taskanaEngine.returnConnection(); + } + return task; + } + + private Task completeTask(String taskId, boolean isForced) + throws TaskNotFoundException, + InvalidOwnerException, + NotAuthorizedOnWorkbasketException, + InvalidTaskStateException { + String userId = taskanaEngine.getEngine().getCurrentUserContext().getUserid(); + TaskImpl task; + try { + taskanaEngine.openConnection(); + task = (TaskImpl) this.getTask(taskId); + if (reviewRequiredManager.reviewRequired(task)) { + return requestReview(taskId); + } + + if (task.getState() == TaskState.COMPLETED) { + return task; + } + + checkIfTaskIsTerminatedOrCancelled(task); + + if (!isForced) { + checkPreconditionsForCompleteTask(task); + } else if (taskIsNotClaimed(task)) { + task = (TaskImpl) this.forceClaim(taskId); + } + + Instant now = Instant.now(); + completeActionsOnTask(task, userId, now); + taskMapper.update(task); + if (LOGGER.isDebugEnabled()) { + LOGGER.debug("Task '{}' completed by user '{}'.", taskId, userId); + } + if (historyEventManager.isEnabled()) { + historyEventManager.createEvent( + new TaskCompletedEvent( + IdGenerator.generateWithPrefix(IdGenerator.ID_PREFIX_TASK_HISTORY_EVENT), + task, + userId)); + } + } finally { + taskanaEngine.returnConnection(); + } + return task; + } + + private void deleteTask(String taskId, boolean forceDelete) + throws TaskNotFoundException, + NotAuthorizedException, + NotAuthorizedOnWorkbasketException, + InvalidTaskStateException, + InvalidCallbackStateException { + taskanaEngine.getEngine().checkRoleMembership(TaskanaRole.ADMIN); + TaskImpl task; + try { + taskanaEngine.openConnection(); + task = (TaskImpl) getTask(taskId); + + if (!(task.getState().isEndState()) && !forceDelete) { + throw new InvalidTaskStateException(taskId, task.getState(), TaskState.END_STATES); + } + if ((!task.getState().in(TaskState.TERMINATED, TaskState.CANCELLED)) + && CallbackState.CALLBACK_PROCESSING_REQUIRED.equals(task.getCallbackState())) { + throw new InvalidCallbackStateException( + taskId, + task.getCallbackState(), + EnumUtil.allValuesExceptFor(CallbackState.CALLBACK_PROCESSING_REQUIRED)); + } + + attachmentMapper.deleteMultipleByTaskIds(Collections.singletonList(taskId)); + objectReferenceMapper.deleteMultipleByTaskIds(Collections.singletonList(taskId)); + taskMapper.delete(taskId); + + if (taskanaEngine.getEngine().isHistoryEnabled() + && taskanaEngine + .getEngine() + .getConfiguration() + .isDeleteHistoryEventsOnTaskDeletionEnabled()) { + historyEventManager.deleteEvents(Collections.singletonList(taskId)); + } + + if (LOGGER.isDebugEnabled()) { + LOGGER.debug("Task {} deleted.", taskId); + } + } finally { + taskanaEngine.returnConnection(); + } + } + + private void removeSingleTaskForTaskDeletionById( + BulkOperationResults bulkLog, + List taskSummaries, + Iterator taskIdIterator) { + String currentTaskId = taskIdIterator.next(); + if (currentTaskId == null || currentTaskId.equals("")) { + bulkLog.addError("", new TaskNotFoundException(null)); + taskIdIterator.remove(); + } else { + MinimalTaskSummary foundSummary = + taskSummaries.stream() + .filter(taskSummary -> currentTaskId.equals(taskSummary.getTaskId())) + .findFirst() + .orElse(null); + if (foundSummary == null) { + bulkLog.addError(currentTaskId, new TaskNotFoundException(currentTaskId)); + taskIdIterator.remove(); + } else if (!(foundSummary.getTaskState().isEndState())) { + bulkLog.addError( + currentTaskId, + new InvalidTaskStateException( + currentTaskId, foundSummary.getTaskState(), TaskState.END_STATES)); + taskIdIterator.remove(); + } else { + if ((!foundSummary.getTaskState().in(TaskState.CANCELLED, TaskState.TERMINATED)) + && CallbackState.CALLBACK_PROCESSING_REQUIRED.equals(foundSummary.getCallbackState())) { + bulkLog.addError( + currentTaskId, + new InvalidCallbackStateException( + currentTaskId, + foundSummary.getCallbackState(), + EnumUtil.allValuesExceptFor(CallbackState.CALLBACK_PROCESSING_REQUIRED))); + taskIdIterator.remove(); + } + } + } + } + + private void removeSingleTaskForCallbackStateByExternalId( + BulkOperationResults bulkLog, + List taskSummaries, + Iterator externalIdIterator, + CallbackState desiredCallbackState) { + String currentExternalId = externalIdIterator.next(); + if (currentExternalId == null || currentExternalId.equals("")) { + bulkLog.addError("", new TaskNotFoundException(null)); + externalIdIterator.remove(); + } else { + Optional foundSummary = + taskSummaries.stream() + .filter(taskSummary -> currentExternalId.equals(taskSummary.getExternalId())) + .findFirst(); + if (foundSummary.isPresent()) { + Optional invalidStateException = + desiredCallbackStateCanBeSetForFoundSummary(foundSummary.get(), desiredCallbackState); + if (invalidStateException.isPresent()) { + bulkLog.addError(currentExternalId, invalidStateException.get()); + externalIdIterator.remove(); + } + } else { + bulkLog.addError(currentExternalId, new TaskNotFoundException(currentExternalId)); + externalIdIterator.remove(); + } + } + } + + private Optional desiredCallbackStateCanBeSetForFoundSummary( + MinimalTaskSummary foundSummary, CallbackState desiredCallbackState) { + + CallbackState currentTaskCallbackState = foundSummary.getCallbackState(); + TaskState currentTaskState = foundSummary.getTaskState(); + + switch (desiredCallbackState) { + case CALLBACK_PROCESSING_COMPLETED: + if (!currentTaskState.isEndState()) { + return Optional.of( + new InvalidTaskStateException( + foundSummary.getTaskId(), foundSummary.getTaskState(), TaskState.END_STATES)); + } + break; + case CLAIMED: + if (!currentTaskState.equals(TaskState.CLAIMED)) { + return Optional.of( + new InvalidTaskStateException( + foundSummary.getTaskId(), foundSummary.getTaskState(), TaskState.CLAIMED)); + } + if (!currentTaskCallbackState.equals(CallbackState.CALLBACK_PROCESSING_REQUIRED)) { + return Optional.of( + new InvalidCallbackStateException( + foundSummary.getTaskId(), + currentTaskCallbackState, + CallbackState.CALLBACK_PROCESSING_REQUIRED)); + } + break; + case CALLBACK_PROCESSING_REQUIRED: + if (currentTaskCallbackState.equals(CallbackState.CALLBACK_PROCESSING_COMPLETED)) { + return Optional.of( + new InvalidCallbackStateException( + foundSummary.getTaskId(), + currentTaskCallbackState, + EnumUtil.allValuesExceptFor(CallbackState.CALLBACK_PROCESSING_COMPLETED))); + } + break; + default: + return Optional.of( + new InvalidCallbackStateException( + foundSummary.getTaskId(), + currentTaskCallbackState, + CallbackState.CALLBACK_PROCESSING_COMPLETED, + CallbackState.CLAIMED, + CallbackState.CALLBACK_PROCESSING_REQUIRED)); + } + return Optional.empty(); + } + + private void standardSettingsOnTaskCreation(TaskImpl task, Classification classification) + throws InvalidArgumentException, + ClassificationNotFoundException, + AttachmentPersistenceException, + ObjectReferencePersistenceException { + final Instant now = Instant.now(); + task.setId(IdGenerator.generateWithPrefix(IdGenerator.ID_PREFIX_TASK)); + if (task.getExternalId() == null) { + task.setExternalId(IdGenerator.generateWithPrefix(IdGenerator.ID_PREFIX_EXT_TASK)); + } + task.setState(TaskState.READY); + task.setCreated(now); + task.setModified(now); + task.setRead(false); + task.setTransferred(false); + + String creator = taskanaEngine.getEngine().getCurrentUserContext().getUserid(); + if (taskanaEngine.getEngine().getConfiguration().isSecurityEnabled() && creator == null) { + throw new SystemException( + "TaskanaSecurity is enabled, but the current UserId is NULL while creating a Task."); + } + task.setCreator(creator); + + // if no business process id is provided, a unique id is created. + if (task.getBusinessProcessId() == null) { + task.setBusinessProcessId( + IdGenerator.generateWithPrefix(IdGenerator.ID_PREFIX_BUSINESS_PROCESS)); + } + // null in case of manual tasks + if (task.getPlanned() == null && (classification == null || task.getDue() == null)) { + task.setPlanned(now); + } + if (task.getName() == null && classification != null) { + task.setName(classification.getName()); + } + if (task.getDescription() == null && classification != null) { + task.setDescription(classification.getDescription()); + } + if (task.getOwner() != null + && taskanaEngine.getEngine().getConfiguration().isAddAdditionalUserInfo()) { + User user = userMapper.findById(task.getOwner()); + if (user != null) { + task.setOwnerLongName(user.getLongName()); + } + } + setDefaultTaskReceivedDateFromAttachments(task); + + attachmentHandler.insertNewAttachmentsOnTaskCreation(task); + objectReferenceHandler.insertNewSecondaryObjectReferencesOnTaskCreation(task); + // This has to be called after the AttachmentHandler because the AttachmentHandler fetches + // the Classifications of the Attachments. + // This is necessary to guarantee that the following calculation is correct. + serviceLevelHandler.updatePrioPlannedDueOfTask(task, null); + } + + private void setDefaultTaskReceivedDateFromAttachments(TaskImpl task) { + if (task.getReceived() == null) { + task.getAttachments().stream() + .map(AttachmentSummary::getReceived) + .filter(Objects::nonNull) + .min(Instant::compareTo) + .ifPresent(task::setReceived); + } + } + + private void setCallbackStateOnTaskCreation(TaskImpl task) throws InvalidArgumentException { + Map callbackInfo = task.getCallbackInfo(); + if (callbackInfo != null && callbackInfo.containsKey(Task.CALLBACK_STATE)) { + String value = callbackInfo.get(Task.CALLBACK_STATE); + if (value != null && !value.isEmpty()) { + try { + CallbackState state = CallbackState.valueOf(value); + task.setCallbackState(state); + } catch (Exception e) { + LOGGER.warn( + "Attempted to determine callback state from {} and caught exception", value, e); + throw new InvalidArgumentException( + String.format("Attempted to set callback state for task %s.", task.getId()), e); + } + } + } + } + + private void updateTasksToBeCompleted(Stream taskSummaries, Instant now) { + + List taskIds = new ArrayList<>(); + List updateClaimedTaskIds = new ArrayList<>(); + List taskSummaryList = + taskSummaries + .peek( + summary -> + completeActionsOnTask( + summary, + taskanaEngine.getEngine().getCurrentUserContext().getUserid(), + now)) + .peek(summary -> taskIds.add(summary.getId())) + .peek( + summary -> { + if (summary.getClaimed().equals(now)) { + updateClaimedTaskIds.add(summary.getId()); + } + }) + .collect(Collectors.toList()); + TaskSummary claimedReference = + taskSummaryList.stream() + .filter(summary -> updateClaimedTaskIds.contains(summary.getId())) + .findFirst() + .orElse(null); + + if (!taskSummaryList.isEmpty()) { + taskMapper.updateCompleted(taskIds, taskSummaryList.get(0)); + if (!updateClaimedTaskIds.isEmpty()) { + taskMapper.updateClaimed(updateClaimedTaskIds, claimedReference); + } + if (historyEventManager.isEnabled()) { + createTasksCompletedEvents(taskSummaryList); + } + } + } + + private Map findWorkbasketsForTasks( + List taskSummaries) { + if (taskSummaries == null || taskSummaries.isEmpty()) { + return Collections.emptyMap(); + } + + Set workbasketIds = + taskSummaries.stream() + .map(TaskSummary::getWorkbasketSummary) + .map(WorkbasketSummary::getId) + .collect(Collectors.toSet()); + + return queryWorkbasketsForTasks(workbasketIds).stream() + .collect(Collectors.toMap(WorkbasketSummary::getId, Function.identity())); + } + + private Map findClassificationsForTasksAndAttachments( + List taskSummaries, + List attachmentSummaries) { + if (taskSummaries == null || taskSummaries.isEmpty()) { + return Collections.emptyMap(); + } + + Set classificationIds = + Stream.concat( + taskSummaries.stream().map(TaskSummary::getClassificationSummary), + attachmentSummaries.stream().map(AttachmentSummary::getClassificationSummary)) + .map(ClassificationSummary::getId) + .collect(Collectors.toSet()); + + return queryClassificationsForTasksAndAttachments(classificationIds).stream() + .collect(Collectors.toMap(ClassificationSummary::getId, Function.identity())); + } + + private Map findClassificationForTaskImplAndAttachments( + TaskImpl task, List attachmentImpls) { + return findClassificationsForTasksAndAttachments( + Collections.singletonList(task), attachmentImpls); + } + + private List queryClassificationsForTasksAndAttachments( + Set classificationIds) { + + if (LOGGER.isDebugEnabled()) { + LOGGER.debug( + "queryClassificationsForTasksAndAttachments() about to query classifications and exit"); + } + return this.classificationService + .createClassificationQuery() + .idIn(classificationIds.toArray(new String[0])) + .list(); + } + + private List queryWorkbasketsForTasks(Set workbasketIds) { + + if (LOGGER.isDebugEnabled()) { + LOGGER.debug("queryWorkbasketsForTasks() about to query workbaskets and exit"); + } + // perform classification query + return this.workbasketService + .createWorkbasketQuery() + .idIn(workbasketIds.toArray(new String[0])) + .list(); + } + + private void addClassificationSummariesToTaskSummaries( + List tasks, Map classificationSummaryById) { + + if (tasks == null || tasks.isEmpty()) { + return; + } + + for (TaskSummaryImpl task : tasks) { + String classificationId = task.getClassificationSummary().getId(); + ClassificationSummary classificationSummary = classificationSummaryById.get(classificationId); + if (classificationSummary == null) { + throw new SystemException( + "Did not find a Classification for task (Id=" + + task.getId() + + ",Classification=" + + task.getClassificationSummary().getId() + + ")"); + } + task.setClassificationSummary(classificationSummary); + } + } + + private void addWorkbasketSummariesToTaskSummaries( + List tasks, Map workbasketSummaryById) { + if (tasks == null || tasks.isEmpty()) { + return; + } + + for (TaskSummaryImpl task : tasks) { + String workbasketId = task.getWorkbasketSummary().getId(); + WorkbasketSummary workbasketSummary = workbasketSummaryById.get(workbasketId); + if (workbasketSummary == null) { + throw new SystemException( + "Did not find a Workbasket for task (Id=" + + task.getId() + + ",Workbasket=" + + task.getWorkbasketSummary().getId() + + ")"); + } + task.setWorkbasketSummary(workbasketSummary); + } + } + + private void addAttachmentSummariesToTaskSummaries( + List taskSummaries, List attachmentSummaries) { + + if (taskSummaries == null || taskSummaries.isEmpty()) { + return; + } + + Map taskSummariesById = + taskSummaries.stream() + .collect( + Collectors.toMap( + TaskSummary::getId, + Function.identity(), + // Currently, we still have a bug (TSK-1204), where the TaskQuery#list function + // returns the same task multiple times when that task has more than one + // attachment...Therefore, this MergeFunction is necessary. + (a, b) -> b)); + + for (AttachmentSummaryImpl attachmentSummary : attachmentSummaries) { + String taskId = attachmentSummary.getTaskId(); + TaskSummaryImpl taskSummary = taskSummariesById.get(taskId); + if (taskSummary != null) { + taskSummary.addAttachmentSummary(attachmentSummary); + } + } + } + + private void addClassificationSummariesToAttachments( + List attachments, + Map classificationSummariesById) { + + if (attachments == null || attachments.isEmpty()) { + return; + } + + for (AttachmentSummaryImpl attachment : attachments) { + String classificationId = attachment.getClassificationSummary().getId(); + ClassificationSummary classificationSummary = + classificationSummariesById.get(classificationId); + + if (classificationSummary == null) { + throw new SystemException("Could not find a Classification for attachment " + attachment); + } + attachment.setClassificationSummary(classificationSummary); + } + } + + private void addObjectReferencesToTaskSummaries( + List taskSummaries, List objectReferences) { + if (taskSummaries == null || taskSummaries.isEmpty()) { + return; + } + + Map taskSummariesById = + taskSummaries.stream() + .collect( + Collectors.toMap( + TaskSummary::getId, + Function.identity(), + // The TaskQuery#list function + // returns the same task multiple times when that task has more than one + // object reference...Therefore, this MergeFunction is necessary. + (a, b) -> b)); + + for (ObjectReferenceImpl objectReference : objectReferences) { + String taskId = objectReference.getTaskId(); + TaskSummaryImpl taskSummary = taskSummariesById.get(taskId); + if (taskSummary != null) { + taskSummary.addSecondaryObjectReference(objectReference); + } + } + } + + private TaskImpl initUpdatedTask( + Map customFieldsToUpdate, TaskCustomPropertySelector fieldSelector) { + + TaskImpl newTask = new TaskImpl(); + newTask.setModified(Instant.now()); + + for (Map.Entry entry : customFieldsToUpdate.entrySet()) { + TaskCustomField key = entry.getKey(); + fieldSelector.setCustomProperty(key, true); + newTask.setCustomField(key, entry.getValue()); + } + return newTask; + } + + private void validateCustomFields(Map customFieldsToUpdate) + throws InvalidArgumentException { + + if (customFieldsToUpdate == null || customFieldsToUpdate.isEmpty()) { + throw new InvalidArgumentException( + "The customFieldsToUpdate argument to updateTasks must not be empty."); + } + } + + private List getTasksToChange(List taskIds) { + return createTaskQuery().idIn(taskIds.toArray(new String[0])).list(); + } + + private List getTasksToChange(ObjectReference selectionCriteria) { + return createTaskQuery() + .primaryObjectReferenceCompanyIn(selectionCriteria.getCompany()) + .primaryObjectReferenceSystemIn(selectionCriteria.getSystem()) + .primaryObjectReferenceSystemInstanceIn(selectionCriteria.getSystemInstance()) + .primaryObjectReferenceTypeIn(selectionCriteria.getType()) + .primaryObjectReferenceValueIn(selectionCriteria.getValue()) + .list(); + } + + private void standardUpdateActions(TaskImpl oldTaskImpl, TaskImpl newTaskImpl) + throws InvalidArgumentException, ClassificationNotFoundException, InvalidTaskStateException { + + if (oldTaskImpl.getExternalId() == null + || !(oldTaskImpl.getExternalId().equals(newTaskImpl.getExternalId()))) { + throw new InvalidArgumentException( + "A task's external Id cannot be changed via update of the task"); + } + + String newWorkbasketKey = newTaskImpl.getWorkbasketKey(); + if (newWorkbasketKey != null && !newWorkbasketKey.equals(oldTaskImpl.getWorkbasketKey())) { + throw new InvalidArgumentException( + "A task's Workbasket cannot be changed via update of the task"); + } + + if (newTaskImpl.getClassificationSummary() == null) { + newTaskImpl.setClassificationSummary(oldTaskImpl.getClassificationSummary()); + } + + setDefaultTaskReceivedDateFromAttachments(newTaskImpl); + + updateClassificationSummary(newTaskImpl, oldTaskImpl); + + TaskImpl newTaskImpl1 = + serviceLevelHandler.updatePrioPlannedDueOfTask(newTaskImpl, oldTaskImpl); + + // if no business process id is provided, use the id of the old task. + if (newTaskImpl1.getBusinessProcessId() == null) { + newTaskImpl1.setBusinessProcessId(oldTaskImpl.getBusinessProcessId()); + } + + // owner can only be changed if task is either in state ready or ready_for_review + boolean isOwnerChanged = !Objects.equals(newTaskImpl1.getOwner(), oldTaskImpl.getOwner()); + if (isOwnerChanged && !oldTaskImpl.getState().in(TaskState.READY, TaskState.READY_FOR_REVIEW)) { + throw new InvalidTaskStateException( + oldTaskImpl.getId(), oldTaskImpl.getState(), TaskState.READY, TaskState.READY_FOR_REVIEW); + } + } + + private void updateClassificationSummary(TaskImpl newTaskImpl, TaskImpl oldTaskImpl) + throws ClassificationNotFoundException { + ClassificationSummary oldClassificationSummary = oldTaskImpl.getClassificationSummary(); + ClassificationSummary newClassificationSummary = newTaskImpl.getClassificationSummary(); + if (newClassificationSummary == null) { + newClassificationSummary = oldClassificationSummary; + } + if (!oldClassificationSummary.getKey().equals(newClassificationSummary.getKey())) { + Classification newClassification = + this.classificationService.getClassification( + newClassificationSummary.getKey(), newTaskImpl.getWorkbasketSummary().getDomain()); + newClassificationSummary = newClassification.asSummary(); + newTaskImpl.setClassificationSummary(newClassificationSummary); + } + } + + private void createTasksCompletedEvents(List taskSummaries) { + taskSummaries.forEach( + task -> + historyEventManager.createEvent( + new TaskCompletedEvent( + IdGenerator.generateWithPrefix(IdGenerator.ID_PREFIX_TASK_HISTORY_EVENT), + task, + taskanaEngine.getEngine().getCurrentUserContext().getUserid()))); + } + + private TaskImpl duplicateTaskExactly(TaskImpl task) { + TaskImpl oldTask = task.copy(); + oldTask.setId(task.getId()); + oldTask.setExternalId(task.getExternalId()); + oldTask.setAttachments(task.getAttachments()); + oldTask.setSecondaryObjectReferences(task.getSecondaryObjectReferences()); + return oldTask; + } +}