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.
+ *
+ *
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 extends TaskSummary> 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 extends TaskSummary> taskSummaries,
+ List extends AttachmentSummaryImpl> 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 extends AttachmentSummaryImpl> 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 extends TaskSummary> 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;
+ }
+}