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