From ec4db1ccdd69a0424e6f985b5bd4ffc403994e87 Mon Sep 17 00:00:00 2001 From: BerndBreier <33351391+BerndBreier@users.noreply.github.com> Date: Wed, 20 Nov 2019 16:53:53 +0100 Subject: [PATCH] Add CALLBACK_STATE to task table --- .../main/java/pro/taskana/CallbackState.java | 8 + .../src/main/java/pro/taskana/Task.java | 7 + .../src/main/java/pro/taskana/TaskQuery.java | 9 + .../main/java/pro/taskana/TaskService.java | 16 +- .../TaskanaEngineConfiguration.java | 2 +- .../pro/taskana/impl/MinimalTaskSummary.java | 19 + .../main/java/pro/taskana/impl/TaskImpl.java | 27 +- .../java/pro/taskana/impl/TaskQueryImpl.java | 38 +- .../pro/taskana/impl/TaskServiceImpl.java | 443 +++++------------- .../pro/taskana/impl/TaskTransferrer.java | 365 +++++++++++++++ .../pro/taskana/mappings/QueryMapper.java | 5 + .../java/pro/taskana/mappings/TaskMapper.java | 29 +- .../main/resources/sql/taskana-schema-db2.sql | 3 +- .../resources/sql/taskana-schema-postgres.sql | 3 +- .../src/main/resources/sql/taskana-schema.sql | 3 +- ...schema_update_1.0.6_to_1.1.5._postgres.sql | 9 + .../taskana_schema_update_1.0.6_to_1.1.5.sql | 9 + .../acceptance/task/CallbackStateAccTest.java | 233 +++++++++ .../pro/taskana/impl/TaskServiceImplTest.java | 61 +-- .../pro/taskana/impl/TaskTransferrerTest.java | 133 ++++++ .../resources/sql/monitor-sample-data.sql | 102 ++-- .../src/test/resources/sql/task.sql | 148 +++--- .../main/resources/sql/sample-data/task.sql | 148 +++--- 23 files changed, 1205 insertions(+), 615 deletions(-) create mode 100644 lib/taskana-core/src/main/java/pro/taskana/CallbackState.java create mode 100644 lib/taskana-core/src/main/java/pro/taskana/impl/TaskTransferrer.java create mode 100644 lib/taskana-core/src/main/resources/sql/taskana_schema_update_1.0.6_to_1.1.5._postgres.sql create mode 100644 lib/taskana-core/src/main/resources/sql/taskana_schema_update_1.0.6_to_1.1.5.sql create mode 100644 lib/taskana-core/src/test/java/acceptance/task/CallbackStateAccTest.java create mode 100644 lib/taskana-core/src/test/java/pro/taskana/impl/TaskTransferrerTest.java diff --git a/lib/taskana-core/src/main/java/pro/taskana/CallbackState.java b/lib/taskana-core/src/main/java/pro/taskana/CallbackState.java new file mode 100644 index 000000000..b584638bd --- /dev/null +++ b/lib/taskana-core/src/main/java/pro/taskana/CallbackState.java @@ -0,0 +1,8 @@ +package pro.taskana; + +/** + * This enum contains all status of synchronization between a taskana task and a task in a remote system. + */ +public enum CallbackState { + NONE, CALLBACK_PROCESSING_REQUIRED, CALLBACK_PROCESSING_COMPLETED +} diff --git a/lib/taskana-core/src/main/java/pro/taskana/Task.java b/lib/taskana-core/src/main/java/pro/taskana/Task.java index 9ceccba66..504abf393 100644 --- a/lib/taskana-core/src/main/java/pro/taskana/Task.java +++ b/lib/taskana-core/src/main/java/pro/taskana/Task.java @@ -362,4 +362,11 @@ public interface Task { * @return classificationCategory */ String getClassificationCategory(); + + /** + * The key that is used to supply Callback_state within the CallbackInfo map. + * The Callback_state is used predominantly by the taskana adapter. It controls synchronization between taskana and the external system. + * + */ + String CALLBACK_STATE = "callbackState"; } diff --git a/lib/taskana-core/src/main/java/pro/taskana/TaskQuery.java b/lib/taskana-core/src/main/java/pro/taskana/TaskQuery.java index d77885405..be540e0c7 100644 --- a/lib/taskana-core/src/main/java/pro/taskana/TaskQuery.java +++ b/lib/taskana-core/src/main/java/pro/taskana/TaskQuery.java @@ -608,6 +608,15 @@ public interface TaskQuery extends BaseQuery { TaskQuery attachmentReceivedWithin(TimeInterval... receivedIn); + /** + * Add your callbackState to your query. + * + * @param states + * the callback states as {@link CallbackState} + * @return the query + */ + TaskQuery callbackStateIn(CallbackState... states); + /** * This method provides a query builder for quering the database. * diff --git a/lib/taskana-core/src/main/java/pro/taskana/TaskService.java b/lib/taskana-core/src/main/java/pro/taskana/TaskService.java index c3e275f12..f59e3d77b 100644 --- a/lib/taskana-core/src/main/java/pro/taskana/TaskService.java +++ b/lib/taskana-core/src/main/java/pro/taskana/TaskService.java @@ -276,7 +276,7 @@ public interface TaskService { * if an Attachment with ID will be added multiple times without using the task-methods. */ Task updateTask(Task task) throws InvalidArgumentException, TaskNotFoundException, ConcurrencyException, - ClassificationNotFoundException, NotAuthorizedException, AttachmentPersistenceException; + ClassificationNotFoundException, NotAuthorizedException, AttachmentPersistenceException; /** * Transfers a list of tasks to an other workbasket. Exceptions will be thrown if the caller got no permissions on @@ -317,7 +317,7 @@ public interface TaskService { */ BulkOperationResults transferTasks(String destinationWorkbasketKey, String destinationWorkbasketDomain, List taskIds) - throws NotAuthorizedException, InvalidArgumentException, WorkbasketNotFoundException; + throws NotAuthorizedException, InvalidArgumentException, WorkbasketNotFoundException; /** * Deletes the task with the given Id. @@ -405,4 +405,16 @@ public interface TaskService { */ List updateTasks(List taskIds, Map customFieldsToUpdate) throws InvalidArgumentException; + + /** + * Sets the callback state on a list of tasks. + * Note: this method is primarily intended to be used by the TaskanaAdapter + * + * @param externalIds the EXTERNAL_IDs of the tasks on which the callback state is set. + * @param state the callback state that is to be set on the tasks + * + * @return the result of the operations with Id and Exception for each failed task deletion. + */ + BulkOperationResults setCallbackStateForTasks(List externalIds, CallbackState state); + } diff --git a/lib/taskana-core/src/main/java/pro/taskana/configuration/TaskanaEngineConfiguration.java b/lib/taskana-core/src/main/java/pro/taskana/configuration/TaskanaEngineConfiguration.java index e3d72963b..b2dd1cbce 100644 --- a/lib/taskana-core/src/main/java/pro/taskana/configuration/TaskanaEngineConfiguration.java +++ b/lib/taskana-core/src/main/java/pro/taskana/configuration/TaskanaEngineConfiguration.java @@ -58,7 +58,7 @@ public class TaskanaEngineConfiguration { private static final String TASKANA_DOMAINS_PROPERTY = "taskana.domains"; private static final String TASKANA_CLASSIFICATION_TYPES_PROPERTY = "taskana.classification.types"; private static final String TASKANA_CLASSIFICATION_CATEGORIES_PROPERTY = "taskana.classification.categories"; - protected static final String TASKANA_SCHEMA_VERSION = "1.0.6"; // must match the VERSION value in table + protected static final String TASKANA_SCHEMA_VERSION = "1.1.5"; // must match the VERSION value in table // TASKANA_SCHEMA_VERSION private static final String DEFAULT_SCHEMA_NAME = "TASKANA"; diff --git a/lib/taskana-core/src/main/java/pro/taskana/impl/MinimalTaskSummary.java b/lib/taskana-core/src/main/java/pro/taskana/impl/MinimalTaskSummary.java index c465c7b06..9b9a1008b 100644 --- a/lib/taskana-core/src/main/java/pro/taskana/impl/MinimalTaskSummary.java +++ b/lib/taskana-core/src/main/java/pro/taskana/impl/MinimalTaskSummary.java @@ -1,5 +1,6 @@ package pro.taskana.impl; +import pro.taskana.CallbackState; import pro.taskana.TaskState; /** @@ -8,8 +9,10 @@ import pro.taskana.TaskState; public class MinimalTaskSummary { private String taskId; + private String externalId; private String workbasketId; private TaskState taskState; + private CallbackState callbackState; MinimalTaskSummary() { @@ -23,6 +26,14 @@ public class MinimalTaskSummary { this.taskId = taskId; } + public String getExternalId() { + return externalId; + } + + public void setExternalId(String externalId) { + this.externalId = externalId; + } + public String getWorkbasketId() { return workbasketId; } @@ -39,6 +50,14 @@ public class MinimalTaskSummary { this.taskState = taskState; } + public CallbackState getCallbackState() { + return callbackState; + } + + public void setCallbackState(CallbackState callbackState) { + this.callbackState = callbackState; + } + @Override public String toString() { return "MinimalTaskSummary [taskId=" + taskId + ", workbasketId=" + workbasketId + ", taskState=" + taskState diff --git a/lib/taskana-core/src/main/java/pro/taskana/impl/TaskImpl.java b/lib/taskana-core/src/main/java/pro/taskana/impl/TaskImpl.java index 6d3b753a3..482cb3e22 100644 --- a/lib/taskana-core/src/main/java/pro/taskana/impl/TaskImpl.java +++ b/lib/taskana-core/src/main/java/pro/taskana/impl/TaskImpl.java @@ -12,6 +12,7 @@ import pro.taskana.Attachment; import pro.taskana.AttachmentSummary; import pro.taskana.ClassificationSummary; import pro.taskana.ObjectReference; +import pro.taskana.CallbackState; import pro.taskana.Task; import pro.taskana.TaskState; import pro.taskana.TaskSummary; @@ -49,6 +50,7 @@ public class TaskImpl implements Task { // All objects have to be serializable private Map customAttributes = Collections.emptyMap(); private Map callbackInfo = Collections.emptyMap(); + private CallbackState callbackState; private List attachments = new ArrayList<>(); private String custom1; private String custom2; @@ -348,6 +350,14 @@ public class TaskImpl implements Task { this.callbackInfo = callbackInfo; } + public CallbackState getCallbackState() { + return callbackState; + } + + public void setCallbackState(CallbackState callbackState) { + this.callbackState = callbackState; + } + @Override public String getCustomAttribute(String number) throws InvalidArgumentException { int num = 0; @@ -728,11 +738,12 @@ public class TaskImpl implements Task { + ", workbasketSummary=" + workbasketSummary + ", businessProcessId=" + businessProcessId + ", parentBusinessProcessId=" + parentBusinessProcessId + ", owner=" + owner + ", primaryObjRef=" + primaryObjRef + ", isRead=" + isRead + ", isTransferred=" + isTransferred + ", customAttributes=" - + customAttributes + ", callbackInfo=" + callbackInfo + ", attachments=" + attachments + ", custom1=" - + custom1 + ", custom2=" + custom2 + ", custom3=" + custom3 + ", custom4=" + custom4 + ", custom5=" - + custom5 + ", custom6=" + custom6 + ", custom7=" + custom7 + ", custom8=" + custom8 + ", custom9=" - + custom9 + ", custom10=" + custom10 + ", custom11=" + custom11 + ", custom12=" + custom12 + ", custom13=" - + custom13 + ", custom14=" + custom14 + ", custom15=" + custom15 + ", custom16=" + custom16 + "]"; + + customAttributes + ", callbackInfo=" + callbackInfo + ", callbackState=" + callbackState + + ", attachments=" + attachments + ", custom1=" + custom1 + ", custom2=" + custom2 + ", custom3=" + + custom3 + ", custom4=" + custom4 + ", custom5=" + custom5 + ", custom6=" + custom6 + ", custom7=" + + custom7 + ", custom8=" + custom8 + ", custom9=" + custom9 + ", custom10=" + custom10 + ", custom11=" + + custom11 + ", custom12=" + custom12 + ", custom13=" + custom13 + ", custom14=" + custom14 + ", custom15=" + + custom15 + ", custom16=" + custom16 + "]"; } @Override @@ -741,7 +752,7 @@ public class TaskImpl implements Task { int result = 1; Object[] myFields = {externalId, attachments, businessProcessId, claimed, classificationSummary, completed, created, creator, custom1, custom10, custom11, custom12, custom13, custom14, custom15, custom16, custom2, - custom3, custom4, custom5, custom6, custom7, custom8, custom9, customAttributes, callbackInfo, + custom3, custom4, custom5, custom6, custom7, custom8, custom9, customAttributes, callbackInfo, callbackState, description, due, id, modified, name, note, owner, parentBusinessProcessId, planned, primaryObjRef, state, workbasketSummary}; @@ -769,14 +780,14 @@ public class TaskImpl implements Task { Object[] myFields = {externalId, attachments, businessProcessId, claimed, classificationSummary, completed, created, creator, custom1, custom10, custom11, custom12, custom13, custom14, custom15, custom16, custom2, - custom3, custom4, custom5, custom6, custom7, custom8, custom9, customAttributes, callbackInfo, + custom3, custom4, custom5, custom6, custom7, custom8, custom9, customAttributes, callbackInfo, callbackState, description, due, id, modified, name, note, owner, parentBusinessProcessId, planned, primaryObjRef, state, workbasketSummary}; Object[] otherFields = {other.externalId, other.attachments, other.businessProcessId, other.claimed, other.classificationSummary, other.completed, other.created, other.creator, other.custom1, other.custom10, other.custom11, other.custom12, other.custom13, other.custom14, other.custom15, other.custom16, other.custom2, other.custom3, other.custom4, other.custom5, other.custom6, other.custom7, - other.custom8, other.custom9, other.customAttributes, other.callbackInfo, other.description, other.due, other.id, other.modified, + other.custom8, other.custom9, other.customAttributes, other.callbackInfo, other.callbackState, other.description, other.due, other.id, other.modified, other.name, other.note, other.owner, other.parentBusinessProcessId, other.planned, other.primaryObjRef, other.state, other.workbasketSummary}; if (myFields.length != otherFields.length) { diff --git a/lib/taskana-core/src/main/java/pro/taskana/impl/TaskQueryImpl.java b/lib/taskana-core/src/main/java/pro/taskana/impl/TaskQueryImpl.java index 8a25501cb..60c79e996 100644 --- a/lib/taskana-core/src/main/java/pro/taskana/impl/TaskQueryImpl.java +++ b/lib/taskana-core/src/main/java/pro/taskana/impl/TaskQueryImpl.java @@ -10,6 +10,7 @@ import org.apache.ibatis.session.RowBounds; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import pro.taskana.CallbackState; import pro.taskana.KeyDomain; import pro.taskana.ObjectReferenceQuery; import pro.taskana.TaskQuery; @@ -32,6 +33,8 @@ import pro.taskana.security.CurrentUserContext; */ public class TaskQueryImpl implements TaskQuery { + private static final String ARGUMENT = "Argument '"; + private static final String GET_CUSTOM_ATTRIBUTE_NOT_A_NUMBER_BETWEEN_1_AND_16 = "' to getCustomAttribute does not represent a number between 1 and 16"; private static final String LINK_TO_MAPPER = "pro.taskana.mappings.QueryMapper.queryTaskSummaries"; private static final String LINK_TO_MAPPER_DB2 = "pro.taskana.mappings.QueryMapper.queryTaskSummariesDb2"; private static final String LINK_TO_COUNTER = "pro.taskana.mappings.QueryMapper.countQueryTasks"; @@ -83,6 +86,7 @@ public class TaskQueryImpl implements TaskQuery { private String[] parentBusinessProcessIdLike; private String[] businessProcessIdIn; private String[] businessProcessIdLike; + private CallbackState[] callbackStateIn; private String[] custom1In; private String[] custom1Like; private String[] custom2In; @@ -444,7 +448,7 @@ public class TaskQueryImpl implements TaskQuery { @Override public TaskQuery stateNotIn(TaskState... states) { // No benefit in introducing a new variable - List stateIn = new LinkedList(Arrays.asList(TaskState.values())); + List stateIn = new LinkedList<>(Arrays.asList(TaskState.values())); for (TaskState state : states) { stateIn.remove(state); } @@ -452,6 +456,12 @@ public class TaskQueryImpl implements TaskQuery { return this; } + @Override + public TaskQuery callbackStateIn(CallbackState... states) { + this.callbackStateIn = states; + return this; + } + @Override public TaskQuery customAttributeIn(String number, String... strings) throws InvalidArgumentException { int num = 0; @@ -459,7 +469,7 @@ public class TaskQueryImpl implements TaskQuery { num = Integer.parseInt(number); } catch (NumberFormatException e) { throw new InvalidArgumentException( - "Argument '" + number + "' to getCustomAttribute cannot be converted to a number between 1 and 16", + ARGUMENT + number + GET_CUSTOM_ATTRIBUTE_NOT_A_NUMBER_BETWEEN_1_AND_16, e.getCause()); } if (strings.length == 0) { @@ -518,7 +528,7 @@ public class TaskQueryImpl implements TaskQuery { break; default: throw new InvalidArgumentException( - "Argument '" + number + "' to getCustomAttribute does not represent a number between 1 and 16"); + ARGUMENT + number + GET_CUSTOM_ATTRIBUTE_NOT_A_NUMBER_BETWEEN_1_AND_16); } return this; @@ -531,7 +541,7 @@ public class TaskQueryImpl implements TaskQuery { num = Integer.parseInt(number); } catch (NumberFormatException e) { throw new InvalidArgumentException( - "Argument '" + number + "' to getCustomAttribute cannot be converted to a number between 1 and 16", + ARGUMENT + number + "' to getCustomAttribute cannot be converted to a number between 1 and 16", e.getCause()); } if (strings.length == 0) { @@ -590,7 +600,7 @@ public class TaskQueryImpl implements TaskQuery { break; default: throw new InvalidArgumentException( - "Argument '" + number + "' to getCustomAttribute does not represent a number between 1 and 16"); + ARGUMENT + number + GET_CUSTOM_ATTRIBUTE_NOT_A_NUMBER_BETWEEN_1_AND_16); } return this; @@ -698,7 +708,7 @@ public class TaskQueryImpl implements TaskQuery { addClassificationNameToSelectClauseForOrdering = true; return this.taskanaEngine.getSqlSession().getConfiguration().getDatabaseId().equals("db2") ? addOrderCriteria("CNAME", sortDirection) - : addOrderCriteria("c.NAME", sortDirection); + : addOrderCriteria("c.NAME", sortDirection); } @Override @@ -707,14 +717,14 @@ public class TaskQueryImpl implements TaskQuery { addAttachmentClassificationNameToSelectClauseForOrdering = true; return this.taskanaEngine.getSqlSession().getConfiguration().getDatabaseId().equals("db2") ? addOrderCriteria("ACNAME", sortDirection) - : addOrderCriteria("ac.NAME", sortDirection); + : addOrderCriteria("ac.NAME", sortDirection); } @Override public TaskQuery orderByClassificationKey(SortDirection sortDirection) { return this.taskanaEngine.getSqlSession().getConfiguration().getDatabaseId().equals("db2") ? addOrderCriteria("TCLASSIFICATION_KEY", sortDirection) - : addOrderCriteria("t.CLASSIFICATION_KEY", sortDirection); + : addOrderCriteria("t.CLASSIFICATION_KEY", sortDirection); } @Override @@ -803,7 +813,7 @@ public class TaskQueryImpl implements TaskQuery { addAttachmentColumnsToSelectClauseForOrdering = true; return this.taskanaEngine.getSqlSession().getConfiguration().getDatabaseId().equals("db2") ? addOrderCriteria("ACLASSIFICATION_KEY", sortDirection) - : addOrderCriteria("a.CLASSIFICATION_KEY", sortDirection); + : addOrderCriteria("a.CLASSIFICATION_KEY", sortDirection); } @Override @@ -812,7 +822,7 @@ public class TaskQueryImpl implements TaskQuery { addAttachmentColumnsToSelectClauseForOrdering = true; return this.taskanaEngine.getSqlSession().getConfiguration().getDatabaseId().equals("db2") ? addOrderCriteria("ACLASSIFICATION_ID", sortDirection) - : addOrderCriteria("a.CLASSIFICATION_ID", sortDirection); + : addOrderCriteria("a.CLASSIFICATION_ID", sortDirection); } @Override @@ -849,7 +859,7 @@ public class TaskQueryImpl implements TaskQuery { num = Integer.parseInt(number); } catch (NumberFormatException e) { throw new InvalidArgumentException( - "Argument '" + number + "' to getCustomAttribute cannot be converted to a number between 1 and 16", + ARGUMENT + number + "' to getCustomAttribute cannot be converted to a number between 1 and 16", e.getCause()); } @@ -888,7 +898,7 @@ public class TaskQueryImpl implements TaskQuery { return addOrderCriteria("CUSTOM_16", sortDirection); default: throw new InvalidArgumentException( - "Argument '" + number + "' to getCustomAttribute does not represent a number between 1 and 16"); + ARGUMENT + number + GET_CUSTOM_ATTRIBUTE_NOT_A_NUMBER_BETWEEN_1_AND_16); } } @@ -978,13 +988,13 @@ public class TaskQueryImpl implements TaskQuery { .getSqlSession() .getConfiguration().getDatabaseId().equals("db2") ? LINK_TO_MAPPER_DB2 - : LINK_TO_MAPPER; + : LINK_TO_MAPPER; } public String getLinkToCounterTaskScript() { return this.taskanaEngine.getSqlSession().getConfiguration().getDatabaseId().equals("db2") ? LINK_TO_COUNTER_DB2 - : LINK_TO_COUNTER; + : LINK_TO_COUNTER; } private void setupAccessIds() { diff --git a/lib/taskana-core/src/main/java/pro/taskana/impl/TaskServiceImpl.java b/lib/taskana-core/src/main/java/pro/taskana/impl/TaskServiceImpl.java index bca7e6a52..110a928ca 100644 --- a/lib/taskana-core/src/main/java/pro/taskana/impl/TaskServiceImpl.java +++ b/lib/taskana-core/src/main/java/pro/taskana/impl/TaskServiceImpl.java @@ -19,6 +19,7 @@ import org.slf4j.LoggerFactory; import pro.taskana.Attachment; import pro.taskana.BulkOperationResults; +import pro.taskana.CallbackState; import pro.taskana.Classification; import pro.taskana.ClassificationService; import pro.taskana.ClassificationSummary; @@ -50,7 +51,6 @@ import pro.taskana.history.events.task.ClaimCancelledEvent; import pro.taskana.history.events.task.ClaimedEvent; import pro.taskana.history.events.task.CompletedEvent; import pro.taskana.history.events.task.CreatedEvent; -import pro.taskana.history.events.task.TransferredEvent; import pro.taskana.impl.report.header.TimeIntervalColumnHeader; import pro.taskana.impl.util.IdGenerator; import pro.taskana.impl.util.LoggerUtils; @@ -67,8 +67,6 @@ public class TaskServiceImpl implements TaskService { private static final String IS_ALREADY_CLAIMED_BY = " is already claimed by "; private static final String IS_ALREADY_COMPLETED = " is already completed."; private static final String WAS_NOT_FOUND2 = " was not found."; - private static final String CANNOT_BE_TRANSFERRED = " cannot be transferred."; - private static final String COMPLETED_TASK_WITH_ID = "Completed task with id "; private static final String WAS_NOT_FOUND = " was not found"; private static final String TASK_WITH_ID = "Task with id "; private static final String WAS_MARKED_FOR_DELETION = " was marked for deletion"; @@ -89,6 +87,7 @@ public class TaskServiceImpl implements TaskService { private TaskMapper taskMapper; private AttachmentMapper attachmentMapper; private HistoryEventProducer historyEventProducer; + private TaskTransferrer taskTransferrer; TaskServiceImpl(InternalTaskanaEngine taskanaEngine, TaskMapper taskMapper, AttachmentMapper attachmentMapper) { @@ -105,6 +104,7 @@ public class TaskServiceImpl implements TaskService { this.attachmentMapper = attachmentMapper; this.classificationService = taskanaEngine.getEngine().getClassificationService(); this.historyEventProducer = taskanaEngine.getHistoryEventProducer(); + this.taskTransferrer = new TaskTransferrer(taskanaEngine, taskMapper, this); } @Override @@ -219,6 +219,7 @@ public class TaskServiceImpl implements TaskService { validateObjectReference(task.getPrimaryObjRef(), "primary ObjectReference", "Task"); PrioDurationHolder prioDurationFromAttachments = handleAttachments(task); standardSettings(task, classification, prioDurationFromAttachments); + setCallbackStateOnTaskCreation(task); try { this.taskMapper.insert(task); LOGGER.debug("Method createTask() created Task '{}'.", task.getId()); @@ -307,169 +308,6 @@ public class TaskServiceImpl implements TaskService { } } - @Override - public Task transfer(String taskId, String destinationWorkbasketId) - throws TaskNotFoundException, WorkbasketNotFoundException, NotAuthorizedException, InvalidStateException { - LOGGER.debug("entry to transfer(taskId = {}, destinationWorkbasketId = {})", taskId, destinationWorkbasketId); - TaskImpl task = null; - WorkbasketSummary oldWorkbasketSummary = null; - try { - taskanaEngine.openConnection(); - task = (TaskImpl) getTask(taskId); - - if (task.getState() == TaskState.COMPLETED) { - throw new InvalidStateException(COMPLETED_TASK_WITH_ID + task.getId() + CANNOT_BE_TRANSFERRED); - } - oldWorkbasketSummary = task.getWorkbasketSummary(); - - // transfer requires TRANSFER in source and APPEND on destination workbasket - workbasketService.checkAuthorization(destinationWorkbasketId, WorkbasketPermission.APPEND); - workbasketService.checkAuthorization(task.getWorkbasketSummary().getId(), - WorkbasketPermission.TRANSFER); - - Workbasket destinationWorkbasket = workbasketService.getWorkbasket(destinationWorkbasketId); - - // reset read flag and set transferred flag - task.setRead(false); - task.setTransferred(true); - - // transfer task from source to destination workbasket - if (!destinationWorkbasket.isMarkedForDeletion()) { - task.setWorkbasketSummary(destinationWorkbasket.asSummary()); - } else { - throw new WorkbasketNotFoundException(destinationWorkbasket.getId(), - THE_WORKBASKET + destinationWorkbasket.getId() + WAS_MARKED_FOR_DELETION); - } - - task.setModified(Instant.now()); - task.setState(TaskState.READY); - task.setOwner(null); - taskMapper.update(task); - LOGGER.debug("Method transfer() transferred Task '{}' to destination workbasket {}", taskId, - destinationWorkbasketId); - if (HistoryEventProducer.isHistoryEnabled()) { - createTaskTransferredEvent(task, oldWorkbasketSummary, destinationWorkbasket.asSummary()); - } - return task; - } finally { - taskanaEngine.returnConnection(); - LOGGER.debug("exit from transfer(). Returning result {} ", task); - } - } - - @Override - public Task transfer(String taskId, String destinationWorkbasketKey, String domain) - throws TaskNotFoundException, WorkbasketNotFoundException, NotAuthorizedException, InvalidStateException { - LOGGER.debug("entry to transfer(taskId = {}, destinationWorkbasketKey = {}, domain = {})", taskId, - destinationWorkbasketKey, domain); - TaskImpl task = null; - WorkbasketSummary oldWorkbasketSummary = null; - try { - taskanaEngine.openConnection(); - task = (TaskImpl) getTask(taskId); - - if (task.getState() == TaskState.COMPLETED) { - throw new InvalidStateException(COMPLETED_TASK_WITH_ID + task.getId() + CANNOT_BE_TRANSFERRED); - } - - // Save previous workbasket id before transfer it. - oldWorkbasketSummary = task.getWorkbasketSummary(); - - // transfer requires TRANSFER in source and APPEND on destination workbasket - workbasketService.checkAuthorization(destinationWorkbasketKey, domain, WorkbasketPermission.APPEND); - workbasketService.checkAuthorization(task.getWorkbasketSummary().getId(), - WorkbasketPermission.TRANSFER); - - Workbasket destinationWorkbasket = workbasketService.getWorkbasket(destinationWorkbasketKey, domain); - - // reset read flag and set transferred flag - task.setRead(false); - task.setTransferred(true); - - // transfer task from source to destination workbasket - if (!destinationWorkbasket.isMarkedForDeletion()) { - task.setWorkbasketSummary(destinationWorkbasket.asSummary()); - } else { - throw new WorkbasketNotFoundException(destinationWorkbasket.getId(), - THE_WORKBASKET + destinationWorkbasket.getId() + WAS_MARKED_FOR_DELETION); - } - - task.setModified(Instant.now()); - task.setState(TaskState.READY); - task.setOwner(null); - taskMapper.update(task); - LOGGER.debug("Method transfer() transferred Task '{}' to destination workbasket {}", taskId, - destinationWorkbasket.getId()); - if (HistoryEventProducer.isHistoryEnabled()) { - createTaskTransferredEvent(task, oldWorkbasketSummary, destinationWorkbasket.asSummary()); - } - return task; - } finally { - taskanaEngine.returnConnection(); - LOGGER.debug("exit from transfer(). Returning result {} ", task); - } - } - - @Override - public BulkOperationResults transferTasks(String destinationWorkbasketId, - List taskIds) throws NotAuthorizedException, InvalidArgumentException, WorkbasketNotFoundException { - try { - taskanaEngine.openConnection(); - if (LOGGER.isDebugEnabled()) { - LOGGER.debug("entry to transferTasks(targetWbId = {}, taskIds = {})", destinationWorkbasketId, - LoggerUtils.listToString(taskIds)); - } - - // Check pre-conditions with trowing Exceptions - if (destinationWorkbasketId == null || destinationWorkbasketId.isEmpty()) { - throw new InvalidArgumentException( - "DestinationWorkbasketId must not be null or empty."); - } - Workbasket destinationWorkbasket = workbasketService.getWorkbasket(destinationWorkbasketId); - - return transferTasks(taskIds, destinationWorkbasket); - } finally { - if (LOGGER.isDebugEnabled()) { - LOGGER.debug("exit from transferTasks(targetWbKey = {}, taskIds = {})", destinationWorkbasketId, - LoggerUtils.listToString(taskIds)); - } - - taskanaEngine.returnConnection(); - } - } - - @Override - public BulkOperationResults transferTasks(String destinationWorkbasketKey, - String destinationWorkbasketDomain, List taskIds) - throws NotAuthorizedException, InvalidArgumentException, WorkbasketNotFoundException { - try { - taskanaEngine.openConnection(); - if (LOGGER.isDebugEnabled()) { - LOGGER.debug("entry to transferTasks(targetWbKey = {}, domain = {}, taskIds = {})", - destinationWorkbasketKey, - destinationWorkbasketDomain, LoggerUtils.listToString(taskIds)); - } - - // Check pre-conditions with trowing Exceptions - if (destinationWorkbasketKey == null || destinationWorkbasketDomain == null) { - throw new InvalidArgumentException( - "DestinationWorkbasketKey or domain can´t be used as NULL-Parameter."); - } - Workbasket destinationWorkbasket = workbasketService.getWorkbasket(destinationWorkbasketKey, - destinationWorkbasketDomain); - - return transferTasks(taskIds, destinationWorkbasket); - } finally { - if (LOGGER.isDebugEnabled()) { - LOGGER.debug("exit from transferTasks(targetWbKey = {}, targetWbDomain = {}, destination taskIds = {})", - destinationWorkbasketKey, - destinationWorkbasketDomain, LoggerUtils.listToString(taskIds)); - } - - taskanaEngine.returnConnection(); - } - } - @Override public Task setTaskRead(String taskId, boolean isRead) throws TaskNotFoundException, NotAuthorizedException { @@ -489,6 +327,31 @@ public class TaskServiceImpl implements TaskService { } } + @Override + public Task transfer(String taskId, String destinationWorkbasketId) + throws TaskNotFoundException, WorkbasketNotFoundException, NotAuthorizedException, InvalidStateException { + return taskTransferrer.transfer(taskId, destinationWorkbasketId); + } + + @Override + public Task transfer(String taskId, String workbasketKey, String domain) + throws TaskNotFoundException, WorkbasketNotFoundException, NotAuthorizedException, InvalidStateException { + return taskTransferrer.transfer(taskId, workbasketKey, domain); + } + + @Override + public BulkOperationResults transferTasks(String destinationWorkbasketId, + List taskIds) throws NotAuthorizedException, InvalidArgumentException, WorkbasketNotFoundException { + return taskTransferrer.transferTasks(destinationWorkbasketId, taskIds); + } + + @Override + public BulkOperationResults transferTasks(String destinationWorkbasketKey, + String destinationWorkbasketDomain, List taskIds) + throws NotAuthorizedException, InvalidArgumentException, WorkbasketNotFoundException { + return taskTransferrer.transferTasks(destinationWorkbasketKey, destinationWorkbasketDomain, taskIds); + } + @Override public TaskQuery createTaskQuery() { return new TaskQueryImpl(taskanaEngine); @@ -500,6 +363,7 @@ public class TaskServiceImpl implements TaskService { WorkbasketSummaryImpl wb = new WorkbasketSummaryImpl(); wb.setId(workbasketId); task.setWorkbasketSummary(wb); + task.setCallbackState(CallbackState.NONE); return task; } @@ -550,11 +414,11 @@ public class TaskServiceImpl implements TaskService { return bulkLog; } - List taskSummaries = taskMapper.findExistingTasks(taskIds); + List taskSummaries = taskMapper.findExistingTasks(taskIds, null); Iterator taskIdIterator = taskIds.iterator(); while (taskIdIterator.hasNext()) { - removeSingleTask(bulkLog, taskSummaries, taskIdIterator); + removeSingleTaskForTaskDeletionById(bulkLog, taskSummaries, taskIdIterator); } if (!taskIds.isEmpty()) { taskMapper.deleteMultiple(taskIds); @@ -566,7 +430,37 @@ public class TaskServiceImpl implements TaskService { } } - private void removeSingleTask(BulkOperationResults bulkLog, + @Override + public BulkOperationResults setCallbackStateForTasks(List externalIds, CallbackState state) { + if (LOGGER.isDebugEnabled()) { + LOGGER.debug("entry to setCallbackStateForTasks(externalIds = {})", LoggerUtils.listToString(externalIds)); + } + try { + taskanaEngine.openConnection(); + + BulkOperationResults bulkLog = new BulkOperationResults<>(); + + if (externalIds == null || externalIds.isEmpty()) { + return bulkLog; + } + + List taskSummaries = taskMapper.findExistingTasks(null, externalIds); + + Iterator taskIdIterator = externalIds.iterator(); + while (taskIdIterator.hasNext()) { + removeSingleTaskForCallbackStateByExternalId(bulkLog, taskSummaries, taskIdIterator); + } + if (!externalIds.isEmpty()) { + taskMapper.setCallbackStateMultiple(externalIds, state); + } + return bulkLog; + } finally { + LOGGER.debug("exit from setCallbckStateForTasks()"); + taskanaEngine.returnConnection(); + } + } + + private void removeSingleTaskForTaskDeletionById(BulkOperationResults bulkLog, List taskSummaries, Iterator taskIdIterator) { LOGGER.debug("entry to removeSingleTask()"); String currentTaskId = taskIdIterator.next(); @@ -576,16 +470,20 @@ public class TaskServiceImpl implements TaskService { taskIdIterator.remove(); } else { MinimalTaskSummary foundSummary = taskSummaries.stream() - .filter(taskState -> currentTaskId.equals(taskState.getTaskId())) + .filter(taskSummary -> currentTaskId.equals(taskSummary.getTaskId())) .findFirst() .orElse(null); if (foundSummary == null) { bulkLog.addError(currentTaskId, new TaskNotFoundException(currentTaskId, TASK_WITH_ID + currentTaskId + WAS_NOT_FOUND2)); taskIdIterator.remove(); + } else if (!TaskState.COMPLETED.equals(foundSummary.getTaskState())) { + bulkLog.addError(currentTaskId, new InvalidStateException(currentTaskId)); + taskIdIterator.remove(); } else { - if (!TaskState.COMPLETED.equals(foundSummary.getTaskState())) { - bulkLog.addError(currentTaskId, new InvalidStateException(currentTaskId)); + if (CallbackState.CALLBACK_PROCESSING_REQUIRED.equals(foundSummary.getCallbackState())) { + bulkLog.addError(currentTaskId, new InvalidStateException("Task " + currentTaskId + + " cannot be deleted before callback is processed")); taskIdIterator.remove(); } } @@ -593,6 +491,33 @@ public class TaskServiceImpl implements TaskService { LOGGER.debug("exit from removeSingleTask()"); } + + private void removeSingleTaskForCallbackStateByExternalId(BulkOperationResults bulkLog, + List taskSummaries, Iterator externalIdIterator) { + LOGGER.debug("entry to removeSingleTask()"); + String currentExternalId = externalIdIterator.next(); + if (currentExternalId == null || currentExternalId.equals("")) { + bulkLog.addError("", + new InvalidArgumentException("IDs with EMPTY or NULL value are not allowed.")); + externalIdIterator.remove(); + } else { + MinimalTaskSummary foundSummary = taskSummaries.stream() + .filter(taskSummary -> currentExternalId.equals(taskSummary.getExternalId())) + .findFirst() + .orElse(null); + if (foundSummary == null) { + bulkLog.addError(currentExternalId, new TaskNotFoundException(currentExternalId, + TASK_WITH_ID + currentExternalId + WAS_NOT_FOUND2)); + externalIdIterator.remove(); + } else if (!TaskState.COMPLETED.equals(foundSummary.getTaskState())) { + bulkLog.addError(currentExternalId, new InvalidStateException(currentExternalId)); + externalIdIterator.remove(); + } + } + LOGGER.debug("exit from removeSingleTask()"); + } + @Override public List updateTasks(ObjectReference selectionCriteria, Map customFieldsToUpdate) throws InvalidArgumentException { @@ -766,43 +691,24 @@ public class TaskServiceImpl implements TaskService { LOGGER.debug("exit from processStandardSettingsForConfiguration()"); } - private BulkOperationResults transferTasks(List taskIdsToBeTransferred, - Workbasket destinationWorkbasket) - throws InvalidArgumentException, WorkbasketNotFoundException, NotAuthorizedException { - if (LOGGER.isDebugEnabled()) { - LOGGER.debug("entry to transferTasks(taskIdsToBeTransferred = {}, destinationWorkbasket = {})", - LoggerUtils.listToString(taskIdsToBeTransferred), destinationWorkbasket); + 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 {}", value, e); + throw new InvalidArgumentException("Attempted to set callback state for task " + + task.getId(), e); + } + } } - - workbasketService.checkAuthorization(destinationWorkbasket.getId(), WorkbasketPermission.APPEND); - - if (taskIdsToBeTransferred == null) { - throw new InvalidArgumentException("TaskIds must not be null."); - } - BulkOperationResults bulkLog = new BulkOperationResults<>(); - List taskIds = new ArrayList<>(taskIdsToBeTransferred); - removeNonExistingTasksFromTaskIdList(taskIds, bulkLog); - - if (taskIds.isEmpty()) { - throw new InvalidArgumentException("TaskIds must not contain only invalid arguments."); - } - - List taskSummaries; - if (taskIds.isEmpty()) { - taskSummaries = new ArrayList<>(); - } else { - taskSummaries = taskMapper.findExistingTasks(taskIds); - } - checkIfTransferConditionsAreFulfilled(taskIds, taskSummaries, bulkLog); - updateTasksToBeTransferred(taskIds, taskSummaries, destinationWorkbasket); - if (LOGGER.isDebugEnabled()) { - LOGGER.debug("exit from transferTasks(), returning {}", bulkLog); - } - - return bulkLog; } - private void removeNonExistingTasksFromTaskIdList(List taskIds, + void removeNonExistingTasksFromTaskIdList(List taskIds, BulkOperationResults bulkLog) { if (LOGGER.isDebugEnabled()) { LOGGER.debug("entry to removeNonExistingTasksFromTaskIdList(targetWbId = {}, taskIds = {})", taskIds, @@ -821,66 +727,6 @@ public class TaskServiceImpl implements TaskService { LOGGER.debug("exit from removeNonExistingTasksFromTaskIdList()"); } - private void checkIfTransferConditionsAreFulfilled(List taskIds, List taskSummaries, - BulkOperationResults bulkLog) { - if (LOGGER.isDebugEnabled()) { - LOGGER.debug( - "entry to checkIfTransferConditionsAreFulfilled(taskIds = {}, taskSummaries = {}, bulkLog = {})", - LoggerUtils.listToString(taskIds), LoggerUtils.listToString(taskSummaries), bulkLog); - } - - Set workbasketIds = new HashSet<>(); - taskSummaries.forEach(t -> workbasketIds.add(t.getWorkbasketId())); - WorkbasketQueryImpl query = (WorkbasketQueryImpl) workbasketService.createWorkbasketQuery(); - query.setUsedToAugmentTasks(true); - List sourceWorkbaskets; - if (taskSummaries.isEmpty()) { - sourceWorkbaskets = new ArrayList<>(); - } else { - sourceWorkbaskets = query - .callerHasPermission(WorkbasketPermission.TRANSFER) - .idIn(workbasketIds.toArray(new String[0])) - .list(); - } - checkIfTasksMatchTransferCriteria(taskIds, taskSummaries, sourceWorkbaskets, bulkLog); - LOGGER.debug("exit from checkIfTransferConditionsAreFulfilled()"); - } - - private void checkIfTasksMatchTransferCriteria(List taskIds, List taskSummaries, - List sourceWorkbaskets, BulkOperationResults bulkLog) { - if (LOGGER.isDebugEnabled()) { - LOGGER.debug( - "entry to checkIfTasksMatchTransferCriteria(taskIds = {}, taskSummaries = {}, sourceWorkbaskets = {}, bulkLog = {})", - LoggerUtils.listToString(taskIds), LoggerUtils.listToString(taskSummaries), - LoggerUtils.listToString(sourceWorkbaskets), bulkLog); - } - - Iterator taskIdIterator = taskIds.iterator(); - while (taskIdIterator.hasNext()) { - String currentTaskId = taskIdIterator.next(); - MinimalTaskSummary taskSummary = taskSummaries.stream() - .filter(t -> currentTaskId.equals(t.getTaskId())) - .findFirst() - .orElse(null); - if (taskSummary == null) { - bulkLog.addError(currentTaskId, - new TaskNotFoundException(currentTaskId, TASK_WITH_ID + currentTaskId + WAS_NOT_FOUND2)); - taskIdIterator.remove(); - } else if (taskSummary.getTaskState() == TaskState.COMPLETED) { - bulkLog.addError(currentTaskId, - new InvalidStateException(COMPLETED_TASK_WITH_ID + currentTaskId + CANNOT_BE_TRANSFERRED)); - taskIdIterator.remove(); - } else if (sourceWorkbaskets.stream() - .noneMatch(wb -> taskSummary.getWorkbasketId().equals(wb.getId()))) { - bulkLog.addError(currentTaskId, - new NotAuthorizedException( - "The workbasket of this task got not TRANSFER permissions. TaskId=" + currentTaskId)); - taskIdIterator.remove(); - } - } - LOGGER.debug("exit from checkIfTasksMatchTransferCriteria()"); - } - private void checkIfTasksMatchCompleteCriteria(List taskIds, List taskSummaries, BulkOperationResults bulkLog) { if (LOGGER.isDebugEnabled()) { @@ -917,35 +763,6 @@ public class TaskServiceImpl implements TaskService { LOGGER.debug("exit from checkIfTasksMatchCompleteCriteria()"); } - private void updateTasksToBeTransferred(List taskIds, - List taskSummaries, Workbasket destinationWorkbasket) { - if (LOGGER.isDebugEnabled()) { - LOGGER.debug("entry to updateTasksToBeTransferred(taskIds = {}, taskSummaries = {})", - LoggerUtils.listToString(taskIds), LoggerUtils.listToString(taskSummaries), destinationWorkbasket); - } - - taskSummaries = taskSummaries.stream() - .filter(ts -> taskIds.contains(ts.getTaskId())) - .collect( - Collectors.toList()); - if (!taskSummaries.isEmpty()) { - Instant now = Instant.now(); - TaskSummaryImpl updateObject = new TaskSummaryImpl(); - updateObject.setRead(false); - updateObject.setTransferred(true); - updateObject.setWorkbasketSummary(destinationWorkbasket.asSummary()); - updateObject.setDomain(destinationWorkbasket.getDomain()); - updateObject.setModified(now); - updateObject.setState(TaskState.READY); - updateObject.setOwner(null); - taskMapper.updateTransfered(taskIds, updateObject); - if (HistoryEventProducer.isHistoryEnabled()) { - createTasksTransferredEvents(taskSummaries, updateObject); - } - } - LOGGER.debug("exit from updateTasksToBeTransferred()"); - } - private void updateTasksToBeCompleted(List taskIds, List taskSummaries) { if (LOGGER.isDebugEnabled()) { @@ -1325,7 +1142,7 @@ public class TaskServiceImpl implements TaskService { private void standardUpdateActions(TaskImpl oldTaskImpl, TaskImpl newTaskImpl, PrioDurationHolder prioDurationFromAttachments) - throws InvalidArgumentException, ConcurrencyException, ClassificationNotFoundException { + throws InvalidArgumentException, ConcurrencyException, ClassificationNotFoundException { validateObjectReference(newTaskImpl.getPrimaryObjRef(), "primary ObjectReference", "Task"); if (oldTaskImpl.getModified() != null && !oldTaskImpl.getModified().equals(newTaskImpl.getModified()) || oldTaskImpl.getClaimed() != null && !oldTaskImpl.getClaimed().equals(newTaskImpl.getClaimed()) @@ -1358,7 +1175,7 @@ public class TaskServiceImpl implements TaskService { private void updateClassificationRelatedProperties(TaskImpl oldTaskImpl, TaskImpl newTaskImpl, PrioDurationHolder prioDurationFromAttachments) - throws ClassificationNotFoundException { + throws ClassificationNotFoundException { LOGGER.debug("entry to updateClassificationRelatedProperties()"); // insert Classification specifications if Classification is given. ClassificationSummary oldClassificationSummary = oldTaskImpl.getClassificationSummary(); @@ -1546,7 +1363,7 @@ public class TaskServiceImpl implements TaskService { throw new AttachmentPersistenceException( "Cannot insert the Attachement " + attachmentImpl.getId() + " for Task " + newTaskImpl.getId() + " because it already exists.", - e.getCause()); + e.getCause()); } LOGGER.debug("exit from handleNewAttachmentOnTaskUpdate(), returning {}", prioDuration); return prioDuration; @@ -1841,7 +1658,7 @@ public class TaskServiceImpl implements TaskService { String id = att.getClassificationSummary().getId(); bulkLog.addError(att.getClassificationSummary().getId(), new ClassificationNotFoundException(id, "When processing task updates due to change of classification, the classification with id " + id - + WAS_NOT_FOUND2)); + + WAS_NOT_FOUND2)); } else { att.setClassificationSummary(classificationSummary); result.add(att); @@ -1864,6 +1681,10 @@ public class TaskServiceImpl implements TaskService { if (!TaskState.COMPLETED.equals(task.getState()) && !forceDelete) { throw new InvalidStateException("Cannot delete Task " + taskId + " because it is not completed."); } + if (CallbackState.CALLBACK_PROCESSING_REQUIRED.equals(task.getCallbackState())) { + throw new InvalidStateException("Task " + taskId + " cannot be deleted because its callback is not yet processed"); + } + taskMapper.delete(taskId); LOGGER.debug("Task {} deleted.", taskId); } finally { @@ -1872,30 +1693,9 @@ public class TaskServiceImpl implements TaskService { } } - private void createTaskTransferredEvent(Task task, WorkbasketSummary oldWorkbasketSummary, - WorkbasketSummary newWorkbasketSummary) { - historyEventProducer.createEvent(new TransferredEvent(task, oldWorkbasketSummary, newWorkbasketSummary)); - } - - private void createTasksTransferredEvents(List taskSummaries, TaskSummaryImpl updateObject) { - taskSummaries.stream().forEach(task -> { - TaskImpl newTask = (TaskImpl) newTask(task.getWorkbasketId()); - newTask.setWorkbasketSummary(updateObject.getWorkbasketSummary()); - newTask.setRead(updateObject.isRead()); - newTask.setTransferred(updateObject.isTransferred()); - newTask.setWorkbasketSummary(updateObject.getWorkbasketSummary()); - newTask.setDomain(updateObject.getDomain()); - newTask.setModified(updateObject.getModified()); - newTask.setState(updateObject.getState()); - newTask.setOwner(updateObject.getOwner()); - createTaskTransferredEvent(newTask, newTask.getWorkbasketSummary(), - updateObject.getWorkbasketSummary()); - }); - } - private void createTasksCompletedEvents(List taskSummaries) { taskSummaries.stream().forEach(task -> historyEventProducer.createEvent(new CompletedEvent(task)) - ); + ); } List augmentTaskSummariesByContainedSummaries(List taskSummaries) { @@ -1965,4 +1765,5 @@ public class TaskServiceImpl implements TaskService { return "PrioDurationHolder [duration=" + duration + ", prio=" + prio + "]"; } } + } diff --git a/lib/taskana-core/src/main/java/pro/taskana/impl/TaskTransferrer.java b/lib/taskana-core/src/main/java/pro/taskana/impl/TaskTransferrer.java new file mode 100644 index 000000000..c237ae721 --- /dev/null +++ b/lib/taskana-core/src/main/java/pro/taskana/impl/TaskTransferrer.java @@ -0,0 +1,365 @@ +package pro.taskana.impl; + +import java.time.Instant; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import pro.taskana.BulkOperationResults; +import pro.taskana.Task; +import pro.taskana.TaskState; +import pro.taskana.Workbasket; +import pro.taskana.WorkbasketPermission; +import pro.taskana.WorkbasketService; +import pro.taskana.WorkbasketSummary; +import pro.taskana.exceptions.InvalidArgumentException; +import pro.taskana.exceptions.InvalidStateException; +import pro.taskana.exceptions.NotAuthorizedException; +import pro.taskana.exceptions.TaskNotFoundException; +import pro.taskana.exceptions.TaskanaException; +import pro.taskana.exceptions.WorkbasketNotFoundException; +import pro.taskana.history.HistoryEventProducer; +import pro.taskana.history.events.task.TransferredEvent; +import pro.taskana.impl.util.LoggerUtils; +import pro.taskana.mappings.TaskMapper; + +/** + * This class is responsible for the transfer of tasks. + */ +public class TaskTransferrer { + private static final String WAS_NOT_FOUND2 = " was not found."; + private static final String CANNOT_BE_TRANSFERRED = " cannot be transferred."; + private static final String COMPLETED_TASK_WITH_ID = "Completed task with id "; + private static final String TASK_WITH_ID = "Task with id "; + private static final String WAS_MARKED_FOR_DELETION = " was marked for deletion"; + private static final String THE_WORKBASKET = "The workbasket "; + + private InternalTaskanaEngine taskanaEngine; + private WorkbasketService workbasketService; + private TaskServiceImpl taskService; + private TaskMapper taskMapper; + private HistoryEventProducer historyEventProducer; + + private static final Logger LOGGER = LoggerFactory.getLogger(TaskTransferrer.class); + + TaskTransferrer(InternalTaskanaEngine taskanaEngine, TaskMapper taskMapper, TaskServiceImpl taskService) { + super(); + this.taskanaEngine = taskanaEngine; + this.taskService = taskService; + this.taskMapper = taskMapper; + this.workbasketService = taskanaEngine.getEngine().getWorkbasketService(); + this.historyEventProducer = taskanaEngine.getHistoryEventProducer(); + } + + Task transfer(String taskId, String destinationWorkbasketKey, String domain) + throws TaskNotFoundException, WorkbasketNotFoundException, NotAuthorizedException, InvalidStateException { + LOGGER.debug("entry to transfer(taskId = {}, destinationWorkbasketKey = {}, domain = {})", taskId, + destinationWorkbasketKey, domain); + TaskImpl task = null; + WorkbasketSummary oldWorkbasketSummary = null; + try { + taskanaEngine.openConnection(); + task = (TaskImpl) taskService.getTask(taskId); + + if (task.getState() == TaskState.COMPLETED) { + throw new InvalidStateException(COMPLETED_TASK_WITH_ID + task.getId() + CANNOT_BE_TRANSFERRED); + } + + // Save previous workbasket id before transfer it. + oldWorkbasketSummary = task.getWorkbasketSummary(); + + // transfer requires TRANSFER in source and APPEND on destination workbasket + workbasketService.checkAuthorization(destinationWorkbasketKey, domain, WorkbasketPermission.APPEND); + workbasketService.checkAuthorization(task.getWorkbasketSummary().getId(), + WorkbasketPermission.TRANSFER); + + Workbasket destinationWorkbasket = workbasketService.getWorkbasket(destinationWorkbasketKey, domain); + + // reset read flag and set transferred flag + task.setRead(false); + task.setTransferred(true); + + // transfer task from source to destination workbasket + if (!destinationWorkbasket.isMarkedForDeletion()) { + task.setWorkbasketSummary(destinationWorkbasket.asSummary()); + } else { + throw new WorkbasketNotFoundException(destinationWorkbasket.getId(), + THE_WORKBASKET + destinationWorkbasket.getId() + WAS_MARKED_FOR_DELETION); + } + + task.setModified(Instant.now()); + task.setState(TaskState.READY); + task.setOwner(null); + taskMapper.update(task); + LOGGER.debug("Method transfer() transferred Task '{}' to destination workbasket {}", taskId, + destinationWorkbasket.getId()); + if (HistoryEventProducer.isHistoryEnabled()) { + createTaskTransferredEvent(task, oldWorkbasketSummary, destinationWorkbasket.asSummary()); + } + return task; + } finally { + taskanaEngine.returnConnection(); + LOGGER.debug("exit from transfer(). Returning result {} ", task); + } + } + + Task transfer(String taskId, String destinationWorkbasketId) + throws TaskNotFoundException, WorkbasketNotFoundException, NotAuthorizedException, InvalidStateException { + LOGGER.debug("entry to transfer(taskId = {}, destinationWorkbasketId = {})", taskId, destinationWorkbasketId); + TaskImpl task = null; + WorkbasketSummary oldWorkbasketSummary = null; + try { + taskanaEngine.openConnection(); + task = (TaskImpl) taskService.getTask(taskId); + + if (task.getState() == TaskState.COMPLETED) { + throw new InvalidStateException(COMPLETED_TASK_WITH_ID + task.getId() + CANNOT_BE_TRANSFERRED); + } + oldWorkbasketSummary = task.getWorkbasketSummary(); + + // transfer requires TRANSFER in source and APPEND on destination workbasket + workbasketService.checkAuthorization(destinationWorkbasketId, WorkbasketPermission.APPEND); + workbasketService.checkAuthorization(task.getWorkbasketSummary().getId(), + WorkbasketPermission.TRANSFER); + + Workbasket destinationWorkbasket = workbasketService.getWorkbasket(destinationWorkbasketId); + + // reset read flag and set transferred flag + task.setRead(false); + task.setTransferred(true); + + // transfer task from source to destination workbasket + if (!destinationWorkbasket.isMarkedForDeletion()) { + task.setWorkbasketSummary(destinationWorkbasket.asSummary()); + } else { + throw new WorkbasketNotFoundException(destinationWorkbasket.getId(), + THE_WORKBASKET + destinationWorkbasket.getId() + WAS_MARKED_FOR_DELETION); + } + + task.setModified(Instant.now()); + task.setState(TaskState.READY); + task.setOwner(null); + taskMapper.update(task); + LOGGER.debug("Method transfer() transferred Task '{}' to destination workbasket {}", taskId, + destinationWorkbasketId); + if (HistoryEventProducer.isHistoryEnabled()) { + createTaskTransferredEvent(task, oldWorkbasketSummary, destinationWorkbasket.asSummary()); + } + return task; + } finally { + taskanaEngine.returnConnection(); + LOGGER.debug("exit from transfer(). Returning result {} ", task); + } + } + + BulkOperationResults transferTasks(String destinationWorkbasketKey, + String destinationWorkbasketDomain, List taskIds) + throws NotAuthorizedException, InvalidArgumentException, WorkbasketNotFoundException { + try { + taskanaEngine.openConnection(); + if (LOGGER.isDebugEnabled()) { + LOGGER.debug("entry to transferTasks(targetWbKey = {}, domain = {}, taskIds = {})", + destinationWorkbasketKey, + destinationWorkbasketDomain, LoggerUtils.listToString(taskIds)); + } + + // Check pre-conditions with trowing Exceptions + if (destinationWorkbasketKey == null || destinationWorkbasketDomain == null) { + throw new InvalidArgumentException( + "DestinationWorkbasketKey or domain can´t be used as NULL-Parameter."); + } + Workbasket destinationWorkbasket = workbasketService.getWorkbasket(destinationWorkbasketKey, + destinationWorkbasketDomain); + + return transferTasks(taskIds, destinationWorkbasket); + } finally { + if (LOGGER.isDebugEnabled()) { + LOGGER.debug("exit from transferTasks(targetWbKey = {}, targetWbDomain = {}, destination taskIds = {})", + destinationWorkbasketKey, + destinationWorkbasketDomain, LoggerUtils.listToString(taskIds)); + } + + taskanaEngine.returnConnection(); + } + } + + BulkOperationResults transferTasks(String destinationWorkbasketId, + List taskIds) throws NotAuthorizedException, InvalidArgumentException, WorkbasketNotFoundException { + try { + taskanaEngine.openConnection(); + if (LOGGER.isDebugEnabled()) { + LOGGER.debug("entry to transferTasks(targetWbId = {}, taskIds = {})", destinationWorkbasketId, + LoggerUtils.listToString(taskIds)); + } + + // Check pre-conditions with trowing Exceptions + if (destinationWorkbasketId == null || destinationWorkbasketId.isEmpty()) { + throw new InvalidArgumentException( + "DestinationWorkbasketId must not be null or empty."); + } + Workbasket destinationWorkbasket = workbasketService.getWorkbasket(destinationWorkbasketId); + + return transferTasks(taskIds, destinationWorkbasket); + } finally { + if (LOGGER.isDebugEnabled()) { + LOGGER.debug("exit from transferTasks(targetWbKey = {}, taskIds = {})", destinationWorkbasketId, + LoggerUtils.listToString(taskIds)); + } + + taskanaEngine.returnConnection(); + } + } + + private BulkOperationResults transferTasks(List taskIdsToBeTransferred, + Workbasket destinationWorkbasket) + throws InvalidArgumentException, WorkbasketNotFoundException, NotAuthorizedException { + if (LOGGER.isDebugEnabled()) { + LOGGER.debug("entry to transferTasks(taskIdsToBeTransferred = {}, destinationWorkbasket = {})", + LoggerUtils.listToString(taskIdsToBeTransferred), destinationWorkbasket); + } + + workbasketService.checkAuthorization(destinationWorkbasket.getId(), WorkbasketPermission.APPEND); + + if (taskIdsToBeTransferred == null) { + throw new InvalidArgumentException("TaskIds must not be null."); + } + BulkOperationResults bulkLog = new BulkOperationResults<>(); + List taskIds = new ArrayList<>(taskIdsToBeTransferred); + taskService.removeNonExistingTasksFromTaskIdList(taskIds, bulkLog); + + if (taskIds.isEmpty()) { + throw new InvalidArgumentException("TaskIds must not contain only invalid arguments."); + } + + List taskSummaries; + if (taskIds.isEmpty()) { + taskSummaries = new ArrayList<>(); + } else { + taskSummaries = taskMapper.findExistingTasks(taskIds, null); + } + checkIfTransferConditionsAreFulfilled(taskIds, taskSummaries, bulkLog); + updateTasksToBeTransferred(taskIds, taskSummaries, destinationWorkbasket); + if (LOGGER.isDebugEnabled()) { + LOGGER.debug("exit from transferTasks(), returning {}", bulkLog); + } + + return bulkLog; + } + + private void checkIfTransferConditionsAreFulfilled(List taskIds, List taskSummaries, + BulkOperationResults bulkLog) { + if (LOGGER.isDebugEnabled()) { + LOGGER.debug( + "entry to checkIfTransferConditionsAreFulfilled(taskIds = {}, taskSummaries = {}, bulkLog = {})", + LoggerUtils.listToString(taskIds), LoggerUtils.listToString(taskSummaries), bulkLog); + } + + Set workbasketIds = new HashSet<>(); + taskSummaries.forEach(t -> workbasketIds.add(t.getWorkbasketId())); + WorkbasketQueryImpl query = (WorkbasketQueryImpl) workbasketService.createWorkbasketQuery(); + query.setUsedToAugmentTasks(true); + List sourceWorkbaskets; + if (taskSummaries.isEmpty()) { + sourceWorkbaskets = new ArrayList<>(); + } else { + sourceWorkbaskets = query + .callerHasPermission(WorkbasketPermission.TRANSFER) + .idIn(workbasketIds.toArray(new String[0])) + .list(); + } + checkIfTasksMatchTransferCriteria(taskIds, taskSummaries, sourceWorkbaskets, bulkLog); + LOGGER.debug("exit from checkIfTransferConditionsAreFulfilled()"); + } + + private void checkIfTasksMatchTransferCriteria(List taskIds, List taskSummaries, + List sourceWorkbaskets, BulkOperationResults bulkLog) { + if (LOGGER.isDebugEnabled()) { + LOGGER.debug( + "entry to checkIfTasksMatchTransferCriteria(taskIds = {}, taskSummaries = {}, sourceWorkbaskets = {}, bulkLog = {})", + LoggerUtils.listToString(taskIds), LoggerUtils.listToString(taskSummaries), + LoggerUtils.listToString(sourceWorkbaskets), bulkLog); + } + + Iterator taskIdIterator = taskIds.iterator(); + while (taskIdIterator.hasNext()) { + String currentTaskId = taskIdIterator.next(); + MinimalTaskSummary taskSummary = taskSummaries.stream() + .filter(t -> currentTaskId.equals(t.getTaskId())) + .findFirst() + .orElse(null); + if (taskSummary == null) { + bulkLog.addError(currentTaskId, + new TaskNotFoundException(currentTaskId, TASK_WITH_ID + currentTaskId + WAS_NOT_FOUND2)); + taskIdIterator.remove(); + } else if (taskSummary.getTaskState() == TaskState.COMPLETED) { + bulkLog.addError(currentTaskId, + new InvalidStateException(COMPLETED_TASK_WITH_ID + currentTaskId + CANNOT_BE_TRANSFERRED)); + taskIdIterator.remove(); + } else if (sourceWorkbaskets.stream() + .noneMatch(wb -> taskSummary.getWorkbasketId().equals(wb.getId()))) { + bulkLog.addError(currentTaskId, + new NotAuthorizedException( + "The workbasket of this task got not TRANSFER permissions. TaskId=" + currentTaskId)); + taskIdIterator.remove(); + } + } + LOGGER.debug("exit from checkIfTasksMatchTransferCriteria()"); + } + + private void createTaskTransferredEvent(Task task, WorkbasketSummary oldWorkbasketSummary, + WorkbasketSummary newWorkbasketSummary) { + historyEventProducer.createEvent(new TransferredEvent(task, oldWorkbasketSummary, newWorkbasketSummary)); + } + + private void updateTasksToBeTransferred(List taskIds, + List taskSummaries, Workbasket destinationWorkbasket) { + if (LOGGER.isDebugEnabled()) { + LOGGER.debug("entry to updateTasksToBeTransferred(taskIds = {}, taskSummaries = {})", + LoggerUtils.listToString(taskIds), LoggerUtils.listToString(taskSummaries), destinationWorkbasket); + } + + taskSummaries = taskSummaries.stream() + .filter(ts -> taskIds.contains(ts.getTaskId())) + .collect( + Collectors.toList()); + if (!taskSummaries.isEmpty()) { + Instant now = Instant.now(); + TaskSummaryImpl updateObject = new TaskSummaryImpl(); + updateObject.setRead(false); + updateObject.setTransferred(true); + updateObject.setWorkbasketSummary(destinationWorkbasket.asSummary()); + updateObject.setDomain(destinationWorkbasket.getDomain()); + updateObject.setModified(now); + updateObject.setState(TaskState.READY); + updateObject.setOwner(null); + taskMapper.updateTransfered(taskIds, updateObject); + if (HistoryEventProducer.isHistoryEnabled()) { + createTasksTransferredEvents(taskSummaries, updateObject); + } + } + LOGGER.debug("exit from updateTasksToBeTransferred()"); + } + + private void createTasksTransferredEvents(List taskSummaries, TaskSummaryImpl updateObject) { + taskSummaries.stream().forEach(task -> { + TaskImpl newTask = (TaskImpl) taskService.newTask(task.getWorkbasketId()); + newTask.setWorkbasketSummary(updateObject.getWorkbasketSummary()); + newTask.setRead(updateObject.isRead()); + newTask.setTransferred(updateObject.isTransferred()); + newTask.setWorkbasketSummary(updateObject.getWorkbasketSummary()); + newTask.setDomain(updateObject.getDomain()); + newTask.setModified(updateObject.getModified()); + newTask.setState(updateObject.getState()); + newTask.setOwner(updateObject.getOwner()); + createTaskTransferredEvent(newTask, newTask.getWorkbasketSummary(), + updateObject.getWorkbasketSummary()); + }); + } + +} diff --git a/lib/taskana-core/src/main/java/pro/taskana/mappings/QueryMapper.java b/lib/taskana-core/src/main/java/pro/taskana/mappings/QueryMapper.java index 16b9d0c17..e577deda6 100644 --- a/lib/taskana-core/src/main/java/pro/taskana/mappings/QueryMapper.java +++ b/lib/taskana-core/src/main/java/pro/taskana/mappings/QueryMapper.java @@ -75,6 +75,7 @@ public interface QueryMapper { + "AND (UPPER(t.NOTE) LIKE #{item}) " + "AND t.PRIORITY IN(#{item}) " + "AND t.STATE IN(#{item}) " + + "AND t.CALLBACK_STATE IN(#{item}) " + "AND t.WORKBASKET_ID IN(#{item}) " + "AND ((t.WORKBASKET_KEY = #{item.key} AND t.DOMAIN = #{item.domain})) " + "AND t.CLASSIFICATION_KEY IN(#{item}) " @@ -253,6 +254,7 @@ public interface QueryMapper { + "AND (UPPER(NOTE) LIKE #{item}) " + "AND PRIORITY IN(#{item}) " + "AND STATE IN(#{item}) " + + "AND t.CALLBACK_STATE IN(#{item}) " + "AND WORKBASKET_ID IN(#{item}) " + "AND ((WORKBASKET_KEY = #{item.key} AND DOMAIN = #{item.domain})) " + "AND t.CLASSIFICATION_KEY IN(#{item}) " @@ -688,6 +690,7 @@ public interface QueryMapper { + "AND (UPPER(t.NOTE) LIKE #{item}) " + "AND t.PRIORITY IN(#{item}) " + "AND t.STATE IN(#{item}) " + + "AND t.CALLBACK_STATE IN(#{item}) " + "AND t.WORKBASKET_ID IN(#{item}) " + "AND ((t.WORKBASKET_KEY = #{item.key} AND t.DOMAIN = #{item.domain})) " + "AND t.CLASSIFICATION_KEY IN(#{item}) " @@ -792,6 +795,7 @@ public interface QueryMapper { + "AND (UPPER(t.NOTE) LIKE #{item}) " + "AND t.PRIORITY IN(#{item}) " + "AND t.STATE IN(#{item}) " + + "AND t.CALLBACK_STATE IN(#{item}) " + "AND t.WORKBASKET_ID IN(#{item}) " + "AND ((t.WORKBASKET_KEY = #{item.key} AND t.DOMAIN = #{item.domain})) " + "AND t.CLASSIFICATION_KEY IN(#{item}) " @@ -1058,6 +1062,7 @@ public interface QueryMapper { + "AND (UPPER(t.NOTE) LIKE #{item}) " + "AND t.PRIORITY IN(#{item}) " + "AND t.STATE IN(#{item}) " + + "AND t.CALLBACK_STATE IN(#{item}) " + "AND t.WORKBASKET_ID IN(#{item}) " + "AND ((t.WORKBASKET_KEY = #{item.key} AND t.DOMAIN = #{item.domain})) " + "AND t.CLASSIFICATION_KEY IN(#{item}) " diff --git a/lib/taskana-core/src/main/java/pro/taskana/mappings/TaskMapper.java b/lib/taskana-core/src/main/java/pro/taskana/mappings/TaskMapper.java index 297ba803a..a967815ea 100644 --- a/lib/taskana-core/src/main/java/pro/taskana/mappings/TaskMapper.java +++ b/lib/taskana-core/src/main/java/pro/taskana/mappings/TaskMapper.java @@ -12,6 +12,7 @@ import org.apache.ibatis.annotations.Results; import org.apache.ibatis.annotations.Select; import org.apache.ibatis.annotations.Update; +import pro.taskana.CallbackState; import pro.taskana.impl.MinimalTaskSummary; import pro.taskana.impl.TaskImpl; import pro.taskana.impl.TaskSummaryImpl; @@ -22,7 +23,7 @@ import pro.taskana.impl.persistence.MapTypeHandler; */ public interface TaskMapper { - @Select("") void deleteMultiple(@Param("ids") List ids); + @Update("") + void setCallbackStateMultiple(@Param("externalIds") List externalIds, @Param("state") CallbackState state); + @Select("") @Results(value = { @Result(property = "taskId", column = "ID"), + @Result(property = "externalId", column = "EXTERNAL_ID"), @Result(property = "workbasketId", column = "WORKBASKET_ID"), - @Result(property = "taskState", column = "STATE")}) - List findExistingTasks(@Param("taskIds") List taskIds); + @Result(property = "taskState", column = "STATE"), + @Result(property = "callbackState", column = "CALLBACK_STATE")}) + List findExistingTasks(@Param("taskIds") List taskIds, + @Param("externalIds") List externalIds); @Update("