diff --git a/lib/taskana-core-test/src/test/java/acceptance/TaskanaConfigurationTest.java b/lib/taskana-core-test/src/test/java/acceptance/TaskanaConfigurationTest.java index ee7956d106..050e2a0f45 100644 --- a/lib/taskana-core-test/src/test/java/acceptance/TaskanaConfigurationTest.java +++ b/lib/taskana-core-test/src/test/java/acceptance/TaskanaConfigurationTest.java @@ -289,6 +289,8 @@ void should_PopulateEveryTaskanaConfiguration_When_EveryBuilderFunctionIsCalled( boolean expectedAddAdditionalUserInfo = true; Set expectedMinimalPermissionsToAssignDomains = Set.of(WorkbasketPermission.CUSTOM_2); + // database configuration + boolean expectedUseSpecificDb2Taskquery = false; // when TaskanaConfiguration configuration = @@ -346,6 +348,7 @@ void should_PopulateEveryTaskanaConfiguration_When_EveryBuilderFunctionIsCalled( // user configuration .addAdditionalUserInfo(expectedAddAdditionalUserInfo) .minimalPermissionsToAssignDomains(expectedMinimalPermissionsToAssignDomains) + .useSpecificDb2Taskquery(expectedUseSpecificDb2Taskquery) .build(); // then @@ -478,6 +481,8 @@ void should_PopulateEveryConfigurationProperty_When_UsingCopyConstructor() { // user configuration .addAdditionalUserInfo(true) .minimalPermissionsToAssignDomains(Set.of(WorkbasketPermission.CUSTOM_2)) + //database configuration + .useSpecificDb2Taskquery(false) .build(); TaskanaConfiguration copyConfiguration = new Builder(configuration).build(); diff --git a/lib/taskana-core-test/src/test/java/acceptance/task/get/GetTaskAccTest.java b/lib/taskana-core-test/src/test/java/acceptance/task/get/GetTaskAccTest.java index ddb7640372..674c94c1ad 100644 --- a/lib/taskana-core-test/src/test/java/acceptance/task/get/GetTaskAccTest.java +++ b/lib/taskana-core-test/src/test/java/acceptance/task/get/GetTaskAccTest.java @@ -179,7 +179,8 @@ void should_ReturnTask_When_RequestingTaskByTaskId() throws Exception { assertThat(readTask.getCustomField(TaskCustomField.CUSTOM_16)).isEqualTo("custom16"); assertThatCode(() -> readTask.getCustomAttributeMap().put("X", "Y")).doesNotThrowAnyException(); assertThatCode(() -> readTask.getCallbackInfo().put("X", "Y")).doesNotThrowAnyException(); - assertThat(readTask).hasNoNullFieldsOrPropertiesExcept("ownerLongName", "completed"); + assertThat(readTask) + .hasNoNullFieldsOrPropertiesExcept("ownerLongName", "completed", "groupByCount"); } @WithAccessId(user = "user-1-1") diff --git a/lib/taskana-core-test/src/test/java/acceptance/task/query/TaskQueryImplAccTest.java b/lib/taskana-core-test/src/test/java/acceptance/task/query/TaskQueryImplAccTest.java index fb5c8e990c..f2514cfe78 100644 --- a/lib/taskana-core-test/src/test/java/acceptance/task/query/TaskQueryImplAccTest.java +++ b/lib/taskana-core-test/src/test/java/acceptance/task/query/TaskQueryImplAccTest.java @@ -249,7 +249,7 @@ void should_ReturnAllTasksWithAllAttributesSet_When_NotApplyingAnyFilter() throw List list = taskService.createTaskQuery().workbasketIdIn(wb.getId()).list(); assertThat(list).containsExactlyInAnyOrder(taskSummary1, taskSummary2); - assertThat(taskSummary1).hasNoNullFieldsOrPropertiesExcept("ownerLongName"); + assertThat(taskSummary1).hasNoNullFieldsOrPropertiesExcept("ownerLongName", "groupByCount"); } @WithAccessId(user = "user-1-1") @@ -697,6 +697,9 @@ void setup() throws Exception { taskInWorkbasket(wb) .completed(Instant.parse("2020-02-01T00:00:00Z")) .buildAndStoreAsSummary(taskService); + taskInWorkbasket(wb) + .completed(null) + .buildAndStoreAsSummary(taskService); } @WithAccessId(user = "user-1-1") @@ -871,6 +874,7 @@ void setup() throws Exception { taskSummary1 = taskInWorkbasket(wb).note("Note1").buildAndStoreAsSummary(taskService); taskSummary2 = taskInWorkbasket(wb).note("Note2").buildAndStoreAsSummary(taskService); taskSummary3 = taskInWorkbasket(wb).note("Lorem ipsum").buildAndStoreAsSummary(taskService); + taskInWorkbasket(wb).note(null).buildAndStoreAsSummary(taskService); } @WithAccessId(user = "user-1-1") @@ -1772,8 +1776,16 @@ void setup() throws Exception { wb = createWorkbasketWithPermission(); por1 = defaultTestObjectReference().company("15").build(); ObjectReference por2 = defaultTestObjectReference().build(); - taskSummary1 = taskInWorkbasket(wb).primaryObjRef(por1).buildAndStoreAsSummary(taskService); - taskSummary2 = taskInWorkbasket(wb).primaryObjRef(por2).buildAndStoreAsSummary(taskService); + taskSummary1 = + taskInWorkbasket(wb) + .primaryObjRef(por1) + .due(Instant.parse("2022-11-15T09:42:00.000Z")) + .buildAndStoreAsSummary(taskService); + taskSummary2 = + taskInWorkbasket(wb) + .primaryObjRef(por2) + .due(Instant.parse("2022-11-15T09:45:00.000Z")) + .buildAndStoreAsSummary(taskService); } @WithAccessId(user = "user-1-1") @@ -3066,13 +3078,17 @@ void setup() throws Exception { .type("SecondType") .build(); taskSummary2 = - taskInWorkbasket(wb).objectReferences(sor2).buildAndStoreAsSummary(taskService); + taskInWorkbasket(wb) + .objectReferences(sor2) + .due(Instant.parse("2022-11-15T09:42:00.000Z")) + .buildAndStoreAsSummary(taskService); ObjectReference sor2copy = sor2.copy(); ObjectReference sor1copy = sor1.copy(); taskSummary3 = taskInWorkbasket(wb) .objectReferences(sor2copy, sor1copy) + .due(Instant.parse("2022-11-15T09:45:00.000Z")) .buildAndStoreAsSummary(taskService); ObjectReference sor3 = diff --git a/lib/taskana-core-test/src/test/java/acceptance/task/query/TaskQueryImplGroupByAccTest.java b/lib/taskana-core-test/src/test/java/acceptance/task/query/TaskQueryImplGroupByAccTest.java new file mode 100644 index 0000000000..6c3c842ecc --- /dev/null +++ b/lib/taskana-core-test/src/test/java/acceptance/task/query/TaskQueryImplGroupByAccTest.java @@ -0,0 +1,334 @@ +package acceptance.task.query; + +import static org.assertj.core.api.Assertions.assertThat; +import static pro.taskana.testapi.DefaultTestEntities.defaultTestClassification; +import static pro.taskana.testapi.DefaultTestEntities.defaultTestObjectReference; +import static pro.taskana.testapi.DefaultTestEntities.defaultTestWorkbasket; + +import java.time.Instant; +import java.util.List; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.DisabledIfEnvironmentVariable; +import pro.taskana.TaskanaConfiguration; +import pro.taskana.classification.api.ClassificationService; +import pro.taskana.classification.api.models.ClassificationSummary; +import pro.taskana.common.api.BaseQuery.SortDirection; +import pro.taskana.common.api.security.CurrentUserContext; +import pro.taskana.task.api.TaskService; +import pro.taskana.task.api.models.ObjectReference; +import pro.taskana.task.api.models.TaskSummary; +import pro.taskana.testapi.TaskanaConfigurationModifier; +import pro.taskana.testapi.TaskanaInject; +import pro.taskana.testapi.TaskanaIntegrationTest; +import pro.taskana.testapi.builder.ObjectReferenceBuilder; +import pro.taskana.testapi.builder.TaskAttachmentBuilder; +import pro.taskana.testapi.builder.TaskBuilder; +import pro.taskana.testapi.builder.UserBuilder; +import pro.taskana.testapi.builder.WorkbasketAccessItemBuilder; +import pro.taskana.testapi.security.WithAccessId; +import pro.taskana.user.api.UserService; +import pro.taskana.workbasket.api.WorkbasketPermission; +import pro.taskana.workbasket.api.WorkbasketService; +import pro.taskana.workbasket.api.models.WorkbasketSummary; + +@TaskanaIntegrationTest +@DisabledIfEnvironmentVariable(named = "DB", matches = "ORACLE") +class TaskQueryImplGroupByAccTest implements TaskanaConfigurationModifier { + @TaskanaInject TaskService taskService; + @TaskanaInject WorkbasketService workbasketService; + @TaskanaInject CurrentUserContext currentUserContext; + @TaskanaInject ClassificationService classificationService; + @TaskanaInject UserService userService; + + ClassificationSummary defaultClassificationSummary; + WorkbasketSummary defaultWorkbasket; + TaskSummary taskSummary1; + TaskSummary taskSummary2; + TaskSummary taskSummary3; + + @Override + public TaskanaConfiguration.Builder modify(TaskanaConfiguration.Builder builder) { + return builder.addAdditionalUserInfo(true).useSpecificDb2Taskquery(false); + } + + @WithAccessId(user = "user-1-1") + @BeforeAll + void setup() throws Exception { + UserBuilder.newUser() + .id("user-1-1") + .longName("Mustermann, Max - (user-1-1)") + .firstName("Max") + .lastName("Mustermann") + .buildAndStore(userService, "businessadmin"); + defaultClassificationSummary = + defaultTestClassification().buildAndStoreAsSummary(classificationService, "businessadmin"); + defaultWorkbasket = createWorkbasketWithPermission(); + ObjectReference sor2 = + ObjectReferenceBuilder.newObjectReference() + .company("FirstCompany") + .value("FirstValue") + .type("SecondType") + .build(); + ObjectReference por2 = defaultTestObjectReference().build(); + taskSummary2 = + taskInWorkbasket(defaultWorkbasket) + .owner("user-1-1") + .primaryObjRef(por2) + .objectReferences(sor2) + .due(Instant.parse("2022-11-10T09:45:00.000Z")) + .name("Name2") + .attachments( + TaskAttachmentBuilder.newAttachment() + .channel("A") + .classificationSummary(defaultClassificationSummary) + .objectReference(por2) + .build()) + .buildAndStore(taskService) + .asSummary(); + ObjectReference por1 = defaultTestObjectReference().company("15").build(); + ObjectReference sor1 = + ObjectReferenceBuilder.newObjectReference() + .company("FirstCompany") + .value("FirstValue") + .type("FirstType") + .build(); + taskSummary1 = + taskInWorkbasket(defaultWorkbasket) + .owner("user-1-1") + .primaryObjRef(por1) + .objectReferences(sor1) + .due(Instant.parse("2022-11-09T09:42:00.000Z")) + .name("Name3") + .attachments( + TaskAttachmentBuilder.newAttachment() + .channel("B") + .classificationSummary(defaultClassificationSummary) + .objectReference(por1) + .build()) + .buildAndStoreAsSummary(taskService); + ObjectReference sor2copy = sor2.copy(); + ObjectReference sor1copy = sor1.copy(); + taskSummary3 = + taskInWorkbasket(defaultWorkbasket) + .owner("user-1-1") + .objectReferences(sor2copy, sor1copy) + .due(Instant.parse("2022-11-15T09:45:00.000Z")) + .name("Name1") + .attachments( + TaskAttachmentBuilder.newAttachment() + .channel("C") + .classificationSummary(defaultClassificationSummary) + .objectReference(por1) + .build()) + .buildAndStoreAsSummary(taskService); + taskInWorkbasket(createWorkbasketWithPermission()).buildAndStore(taskService); + } + + private TaskBuilder taskInWorkbasket(WorkbasketSummary wb) { + return TaskBuilder.newTask() + .classificationSummary(defaultClassificationSummary) + .primaryObjRef(defaultTestObjectReference().build()) + .workbasketSummary(wb); + } + + private WorkbasketSummary createWorkbasketWithPermission() throws Exception { + WorkbasketSummary workbasketSummary = + defaultTestWorkbasket().buildAndStoreAsSummary(workbasketService, "businessadmin"); + persistPermission(workbasketSummary); + return workbasketSummary; + } + + private void persistPermission(WorkbasketSummary workbasketSummary) throws Exception { + WorkbasketAccessItemBuilder.newWorkbasketAccessItem() + .workbasketId(workbasketSummary.getId()) + .accessId(currentUserContext.getUserid()) + .permission(WorkbasketPermission.OPEN) + .permission(WorkbasketPermission.READ) + .permission(WorkbasketPermission.APPEND) + .buildAndStore(workbasketService, "businessadmin"); + } + + @WithAccessId(user = "user-1-1") + @Test + void should_GroupByPor_When_OrderingByName() { + List list = + taskService + .createTaskQuery() + .workbasketIdIn(defaultWorkbasket.getId()) + .groupByPor() + .orderByName(SortDirection.ASCENDING) + .list(); + assertThat(list) + .usingRecursiveFieldByFieldElementComparatorIgnoringFields("groupByCount") + .containsExactly(taskSummary3) + .extracting("groupByCount") + .containsExactly(3); + } + + @WithAccessId(user = "user-1-1") + @Test + void should_GroupByPor_When_JoiningWithAllTablesAndPaging() { + List list = + taskService + .createTaskQuery() + .workbasketIdIn(defaultWorkbasket.getId()) + .groupByPor() + .ownerLongNameNotIn("Unexisting") + .attachmentChannelNotLike("Unexisting") + .sorTypeLike("%Type%") + .orderByOwnerLongName(SortDirection.ASCENDING) + .orderByAttachmentChannel(SortDirection.ASCENDING) + .orderByClassificationName(SortDirection.ASCENDING) + .listPage(1, 1); + assertThat(list) + .usingRecursiveFieldByFieldElementComparatorIgnoringFields("groupByCount") + .containsExactly(taskSummary2) + .extracting("groupByCount") + .containsExactly(3); + } + + @WithAccessId(user = "user-1-1") + @Test + void should_GroupBySor_When_JoiningWithAllTablesAndPaging() { + List list = + taskService + .createTaskQuery() + .workbasketIdIn(defaultWorkbasket.getId()) + .groupBySor("SecondType") + .ownerLongNameNotIn("Unexisting") + .attachmentChannelNotLike("Unexisting") + .sorTypeLike("%Type%") + .orderByOwnerLongName(SortDirection.ASCENDING) + .orderByAttachmentChannel(SortDirection.ASCENDING) + .orderByClassificationName(SortDirection.ASCENDING) + .listPage(1, 1); + assertThat(list) + .usingRecursiveFieldByFieldElementComparatorIgnoringFields("groupByCount") + .containsExactly(taskSummary2) + .extracting("groupByCount") + .containsExactly(2); + } + + @WithAccessId(user = "user-1-1") + @Test + void should_GroupByPorWithOrderingByDue_When_OrderingByPorValue() { + List list = + taskService + .createTaskQuery() + .workbasketIdIn(defaultWorkbasket.getId()) + .groupByPor() + .orderByPrimaryObjectReferenceValue(SortDirection.ASCENDING) + .list(); + assertThat(list).hasSize(1); + assertThat(list) + .usingRecursiveFieldByFieldElementComparatorIgnoringFields("groupByCount") + .containsExactly(taskSummary1) + .extracting("groupByCount") + .containsExactly(3); + } + + @WithAccessId(user = "user-1-1") + @Test + void should_GroupByPorWithOrderingByDue_When_NotOrdering() { + List list = + taskService.createTaskQuery().workbasketIdIn(defaultWorkbasket.getId()).groupByPor().list(); + assertThat(list).hasSize(1); + assertThat(list) + .usingRecursiveFieldByFieldElementComparatorIgnoringFields("groupByCount") + .containsExactly(taskSummary1) + .extracting("groupByCount") + .containsExactly(3); + } + + @WithAccessId(user = "user-1-1") + @Test + void should_UseSingleCorrectly_When_GroupingByPor() { + TaskSummary result = + taskService + .createTaskQuery() + .workbasketIdIn(defaultWorkbasket.getId()) + .groupByPor() + .single(); + assertThat(result) + .usingRecursiveComparison() + .ignoringFields("groupByCount") + .isEqualTo(taskSummary1); + assertThat(result.getGroupByCount()).isEqualTo(3); + } + + @WithAccessId(user = "user-1-1") + @Test + void should_UseSingleCorrectly_When_GroupingBySor() { + TaskSummary result = + taskService + .createTaskQuery() + .workbasketIdIn(defaultWorkbasket.getId()) + .groupBySor("SecondType") + .single(); + assertThat(result) + .usingRecursiveComparison() + .ignoringFields("groupByCount") + .isEqualTo(taskSummary2); + assertThat(result.getGroupByCount()).isEqualTo(2); + } + + @WithAccessId(user = "user-1-1") + @Test + void should_Count_When_GroupingByPor() { + Long numberOfTasks = + taskService + .createTaskQuery() + .workbasketIdIn(defaultWorkbasket.getId()) + .groupByPor() + .count(); + assertThat(numberOfTasks).isEqualTo(1); + } + + @WithAccessId(user = "user-1-1") + @Test + void should_GroupBySor_When_OrderingByName() { + List list = + taskService + .createTaskQuery() + .workbasketIdIn(defaultWorkbasket.getId()) + .groupBySor("SecondType") + .orderByName(SortDirection.ASCENDING) + .list(); + assertThat(list).hasSize(1); + assertThat(list) + .usingRecursiveFieldByFieldElementComparatorIgnoringFields("groupByCount") + .containsExactly(taskSummary3) + .extracting("groupByCount") + .containsExactly(2); + } + + @WithAccessId(user = "user-1-1") + @Test + void should_GroupBySorWithOrderingByDue_When_NotOrdering() { + List list = + taskService + .createTaskQuery() + .workbasketIdIn(defaultWorkbasket.getId()) + .groupBySor("SecondType") + .list(); + assertThat(list).hasSize(1); + assertThat(list) + .usingRecursiveFieldByFieldElementComparatorIgnoringFields("groupByCount") + .containsExactly(taskSummary2) + .extracting("groupByCount") + .containsExactly(2); + } + + @WithAccessId(user = "user-1-1") + @Test + void should_Count_When_GroupingBySor() { + Long numberOfTasks = + taskService + .createTaskQuery() + .workbasketIdIn(defaultWorkbasket.getId()) + .groupBySor("SecondType") + .count(); + assertThat(numberOfTasks).isEqualTo(1); + } +} diff --git a/lib/taskana-core-test/src/test/resources/fullTaskana.properties b/lib/taskana-core-test/src/test/resources/fullTaskana.properties index 4bcd662c43..a3b672bd25 100644 --- a/lib/taskana-core-test/src/test/resources/fullTaskana.properties +++ b/lib/taskana-core-test/src/test/resources/fullTaskana.properties @@ -53,6 +53,8 @@ taskana.jobs.customJobs=A | B | C # user configuration taskana.user.addAdditionalUserInfo=true taskana.user.minimalPermissionsToAssignDomains=READ | OPEN +# database configuration +taskana.feature.useSpecificDb2Taskquery=false # custom configuration my_custom_property1=my_custom_value1 my_custom_property2=my_custom_value2 diff --git a/lib/taskana-core/src/main/java/pro/taskana/TaskanaConfiguration.java b/lib/taskana-core/src/main/java/pro/taskana/TaskanaConfiguration.java index 520c54c060..a4ce66fce6 100644 --- a/lib/taskana-core/src/main/java/pro/taskana/TaskanaConfiguration.java +++ b/lib/taskana-core/src/main/java/pro/taskana/TaskanaConfiguration.java @@ -123,6 +123,10 @@ public class TaskanaConfiguration { private final Set minimalPermissionsToAssignDomains; // endregion + // region database configuration + private final boolean useSpecificDb2Taskquery; + // endregion + // region custom configuration private final Map properties; // endregion @@ -194,6 +198,8 @@ private TaskanaConfiguration(Builder builder) { this.addAdditionalUserInfo = builder.addAdditionalUserInfo; this.minimalPermissionsToAssignDomains = Collections.unmodifiableSet(builder.minimalPermissionsToAssignDomains); + // database configuration + this.useSpecificDb2Taskquery = builder.useSpecificDb2Taskquery; // custom configuration this.properties = Map.copyOf(builder.properties); } @@ -388,6 +394,10 @@ public Set getMinimalPermissionsToAssignDomains() { return minimalPermissionsToAssignDomains; } + public boolean isUseSpecificDb2Taskquery() { + return useSpecificDb2Taskquery; + } + /** * return all properties loaded from taskana properties file. Per Design the normal Properties are * not immutable, so we return here an ImmutableMap, because we don't want direct changes in the @@ -447,6 +457,7 @@ public int hashCode() { customJobs, addAdditionalUserInfo, minimalPermissionsToAssignDomains, + useSpecificDb2Taskquery, properties); } @@ -481,6 +492,7 @@ public boolean equals(Object obj) { && simpleHistoryCleanupJobAllCompletedSameParentBusiness == other.simpleHistoryCleanupJobAllCompletedSameParentBusiness && taskUpdatePriorityJobEnabled == other.taskUpdatePriorityJobEnabled + && useSpecificDb2Taskquery == other.useSpecificDb2Taskquery && taskUpdatePriorityJobBatchSize == other.taskUpdatePriorityJobBatchSize && userInfoRefreshJobEnabled == other.userInfoRefreshJobEnabled && addAdditionalUserInfo == other.addAdditionalUserInfo @@ -596,6 +608,8 @@ public String toString() { + addAdditionalUserInfo + ", minimalPermissionsToAssignDomains=" + minimalPermissionsToAssignDomains + + ", useSpecificDb2Taskquery=" + + useSpecificDb2Taskquery + ", properties=" + properties + "]"; @@ -743,6 +757,11 @@ public static class Builder { private Set minimalPermissionsToAssignDomains = new HashSet<>(); // endregion + // region database configuration + @TaskanaProperty("taskana.feature.useSpecificDb2Taskquery") + private boolean useSpecificDb2Taskquery = true; + // endregion + // region custom configuration private Map properties = Collections.emptyMap(); // endregion @@ -845,6 +864,8 @@ public Builder( // user configuration this.addAdditionalUserInfo = conf.addAdditionalUserInfo; this.minimalPermissionsToAssignDomains = conf.minimalPermissionsToAssignDomains; + // database configuration + this.useSpecificDb2Taskquery = conf.useSpecificDb2Taskquery; // custom configuration this.properties = conf.properties; } @@ -1128,6 +1149,11 @@ public Builder minimalPermissionsToAssignDomains( } // endregion + // region database configuration + public Builder useSpecificDb2Taskquery(boolean useSpecificDb2Taskquery) { + this.useSpecificDb2Taskquery = useSpecificDb2Taskquery; + return this; + } public TaskanaConfiguration build() { adjustConfiguration(); diff --git a/lib/taskana-core/src/main/java/pro/taskana/task/api/TaskQuery.java b/lib/taskana-core/src/main/java/pro/taskana/task/api/TaskQuery.java index 371daeef2e..fb72c306b0 100644 --- a/lib/taskana-core/src/main/java/pro/taskana/task/api/TaskQuery.java +++ b/lib/taskana-core/src/main/java/pro/taskana/task/api/TaskQuery.java @@ -1217,6 +1217,29 @@ public interface TaskQuery extends BaseQuery { */ TaskQuery orderByPrimaryObjectReferenceValue(SortDirection sortDirection); + // endregion + // region groupBy + + /** + * Group the {@linkplain Task Tasks} that will be returned by this query according to the value of + * their {@linkplain Task#getPrimaryObjRef() primary ObjectReference}. Only one Task will be + * returned for all Tasks with the same value. + * + * @return the query + */ + TaskQuery groupByPor(); + + /** + * Group the {@linkplain Task Tasks} that will be returned by this query according to the value of + * their {@linkplain Task#getSecondaryObjectReferences() secondary ObjectReference} with the + * specified type. Only one Task will be returned for all Tasks with the same value. + * + * @param type the type of the relevant {@linkplain Task#getSecondaryObjectReferences() secondary + * ObjectReference} + * @return the query + */ + TaskQuery groupBySor(String type); + // endregion // region read diff --git a/lib/taskana-core/src/main/java/pro/taskana/task/api/models/TaskSummary.java b/lib/taskana-core/src/main/java/pro/taskana/task/api/models/TaskSummary.java index c1786ccc50..5ee606b9c7 100644 --- a/lib/taskana-core/src/main/java/pro/taskana/task/api/models/TaskSummary.java +++ b/lib/taskana-core/src/main/java/pro/taskana/task/api/models/TaskSummary.java @@ -80,6 +80,16 @@ public interface TaskSummary { */ Instant getReceived(); + /** + * Returns the number of {@linkplain Task Tasks} that are grouped together with this {@linkplain + * Task} by a {@linkplain pro.taskana.task.api.TaskQuery}. It's only not NULL when using + * {@linkplain pro.taskana.task.api.TaskQuery#groupByPor()} or {@linkplain + * pro.taskana.task.api.TaskQuery#groupBySor(String)}. + * + * @return the number of {@linkplain Task Tasks} grouped toghether with this {@linkplain Task} + */ + Integer getGroupByCount(); + /** * Returns the time when the {@linkplain Task} is due. * diff --git a/lib/taskana-core/src/main/java/pro/taskana/task/internal/TaskQueryImpl.java b/lib/taskana-core/src/main/java/pro/taskana/task/internal/TaskQueryImpl.java index 78435614a3..3fbab64c6f 100644 --- a/lib/taskana-core/src/main/java/pro/taskana/task/internal/TaskQueryImpl.java +++ b/lib/taskana-core/src/main/java/pro/taskana/task/internal/TaskQueryImpl.java @@ -53,7 +53,8 @@ public class TaskQueryImpl implements TaskQuery { private static final Logger LOGGER = LoggerFactory.getLogger(TaskQueryImpl.class); private final InternalTaskanaEngine taskanaEngine; private final TaskServiceImpl taskService; - private final List orderBy; + private final List orderByOuter; + private final List orderByInner; private TaskQueryColumnName columnName; private String[] accessIdIn; @@ -70,7 +71,8 @@ public class TaskQueryImpl implements TaskQuery { private boolean addAttachmentClassificationNameToSelectClauseForOrdering = false; private boolean addWorkbasketNameToSelectClauseForOrdering = false; private boolean joinWithUserInfo; - + private boolean groupByPor; + private String groupBySor; private String[] taskId; private String[] taskIdNotIn; private String[] externalIdIn; @@ -338,7 +340,8 @@ public class TaskQueryImpl implements TaskQuery { TaskQueryImpl(InternalTaskanaEngine taskanaEngine) { this.taskanaEngine = taskanaEngine; this.taskService = (TaskServiceImpl) taskanaEngine.getEngine().getTaskService(); - this.orderBy = new ArrayList<>(); + this.orderByOuter = new ArrayList<>(); + this.orderByInner = new ArrayList<>(); this.filterByAccessIdIn = true; this.withoutAttachment = false; this.joinWithUserInfo = taskanaEngine.getEngine().getConfiguration().isAddAdditionalUserInfo(); @@ -767,7 +770,8 @@ public TaskQuery classificationNameNotLike(String... classificationNames) { public TaskQuery orderByClassificationName(SortDirection sortDirection) { joinWithClassifications = true; addClassificationNameToSelectClauseForOrdering = true; - return DB.DB2 == getDB() + return (DB.DB2 == getDB() + && taskanaEngine.getEngine().getConfiguration().isUseSpecificDb2Taskquery()) ? addOrderCriteria("CNAME", sortDirection) : addOrderCriteria("c.NAME", sortDirection); } @@ -1066,6 +1070,30 @@ public TaskQuery orderByPrimaryObjectReferenceValue(SortDirection sortDirection) return addOrderCriteria("POR_VALUE", sortDirection); } + @Override + public TaskQuery groupByPor() { + if (taskanaEngine.getEngine().getConfiguration().isUseSpecificDb2Taskquery() + && getDB().equals(DB.DB2)) { + throw new SystemException( + "taskana.feature.useSpecificDb2Taskquery needs to be set to false " + + "in order to group by por."); + } + groupByPor = true; + return this; + } + + @Override + public TaskQuery groupBySor(String type) { + if (taskanaEngine.getEngine().getConfiguration().isUseSpecificDb2Taskquery() + && getDB().equals(DB.DB2)) { + throw new SystemException( + "taskana.feature.useSpecificDb2Taskquery needs to be set to false " + + "in order to group by sor."); + } + groupBySor = type; + return sorTypeIn(type); + } + @Override public TaskQuery readEquals(Boolean isRead) { this.isRead = isRead; @@ -1096,7 +1124,8 @@ public TaskQuery attachmentClassificationIdNotIn(String... attachmentClassificat public TaskQuery orderByAttachmentClassificationId(SortDirection sortDirection) { joinWithAttachments = true; addAttachmentColumnsToSelectClauseForOrdering = true; - return DB.DB2 == getDB() + return (DB.DB2 == getDB() + && taskanaEngine.getEngine().getConfiguration().isUseSpecificDb2Taskquery()) ? addOrderCriteria("ACLASSIFICATION_ID", sortDirection) : addOrderCriteria("a.CLASSIFICATION_ID", sortDirection); } @@ -1133,7 +1162,8 @@ public TaskQuery attachmentClassificationKeyNotLike(String... attachmentClassifi public TaskQuery orderByAttachmentClassificationKey(SortDirection sortDirection) { joinWithAttachments = true; addAttachmentColumnsToSelectClauseForOrdering = true; - return DB.DB2 == getDB() + return (DB.DB2 == getDB() + && taskanaEngine.getEngine().getConfiguration().isUseSpecificDb2Taskquery()) ? addOrderCriteria("ACLASSIFICATION_KEY", sortDirection) : addOrderCriteria("a.CLASSIFICATION_KEY", sortDirection); } @@ -1170,7 +1200,8 @@ public TaskQuery attachmentClassificationNameNotLike(String... attachmentClassif public TaskQuery orderByAttachmentClassificationName(SortDirection sortDirection) { joinWithAttachments = true; addAttachmentClassificationNameToSelectClauseForOrdering = true; - return DB.DB2 == getDB() + return (DB.DB2 == getDB() + && taskanaEngine.getEngine().getConfiguration().isUseSpecificDb2Taskquery()) ? addOrderCriteria("ACNAME", sortDirection) : addOrderCriteria("ac.NAME", sortDirection); } @@ -1207,7 +1238,10 @@ public TaskQuery attachmentChannelNotLike(String... attachmentChannels) { public TaskQuery orderByAttachmentChannel(SortDirection sortDirection) { joinWithAttachments = true; addAttachmentColumnsToSelectClauseForOrdering = true; - return addOrderCriteria("CHANNEL", sortDirection); + return (DB.DB2 == getDB() + && taskanaEngine.getEngine().getConfiguration().isUseSpecificDb2Taskquery()) + ? addOrderCriteria("CHANNEL", sortDirection) + : addOrderCriteria("a.CHANNEL", sortDirection); } @Override @@ -1242,7 +1276,10 @@ public TaskQuery attachmentReferenceValueNotLike(String... referenceValues) { public TaskQuery orderByAttachmentReference(SortDirection sortDirection) { joinWithAttachments = true; addAttachmentColumnsToSelectClauseForOrdering = true; - return addOrderCriteria("REF_VALUE", sortDirection); + return (DB.DB2 == getDB() + && taskanaEngine.getEngine().getConfiguration().isUseSpecificDb2Taskquery()) + ? addOrderCriteria("REF_VALUE", sortDirection) + : addOrderCriteria("a.REF_VALUE", sortDirection); } @Override @@ -1265,7 +1302,8 @@ public TaskQuery attachmentNotReceivedWithin(TimeInterval... receivedNotIn) { public TaskQuery orderByAttachmentReceived(SortDirection sortDirection) { joinWithAttachments = true; addAttachmentColumnsToSelectClauseForOrdering = true; - return DB.DB2 == getDB() + return (DB.DB2 == getDB() + && taskanaEngine.getEngine().getConfiguration().isUseSpecificDb2Taskquery()) ? addOrderCriteria("ARECEIVED", sortDirection) : addOrderCriteria("a.RECEIVED", sortDirection); } @@ -1926,7 +1964,8 @@ public TaskQuery orderByWorkbasketKey(SortDirection sortDirection) { public TaskQuery orderByWorkbasketName(SortDirection sortDirection) { joinWithWorkbaskets = true; addWorkbasketNameToSelectClauseForOrdering = true; - return DB.DB2 == getDB() + return (DB.DB2 == getDB() + && taskanaEngine.getEngine().getConfiguration().isUseSpecificDb2Taskquery()) ? addOrderCriteria("WNAME", sortDirection) : addOrderCriteria("w.NAME", sortDirection); } @@ -1934,7 +1973,8 @@ public TaskQuery orderByWorkbasketName(SortDirection sortDirection) { @Override public TaskQuery orderByOwnerLongName(SortDirection sortDirection) { joinWithUserInfo = true; - return DB.DB2 == getDB() + return (DB.DB2 == getDB() + && taskanaEngine.getEngine().getConfiguration().isUseSpecificDb2Taskquery()) ? addOrderCriteria("ULONG_NAME", sortDirection) : addOrderCriteria("u.LONG_NAME", sortDirection); } @@ -1987,7 +2027,8 @@ public List listValues(TaskQueryColumnName columnName, SortDirection sor try { taskanaEngine.openConnection(); this.columnName = columnName; - this.orderBy.clear(); + this.orderByOuter.clear(); + this.orderByInner.clear(); this.addOrderCriteria(columnName.toString(), sortDirection); checkForIllegalParamCombinations(); checkOpenAndReadPermissionForSpecifiedWorkbaskets(); @@ -2069,7 +2110,9 @@ public TaskQuery selectAndClaimEquals(boolean selectAndClaim) { // optimized query for db2 can't be used for now in case of selectAndClaim because of temporary // tables and the "for update" clause clashing in db2 private String getLinkToMapperScript() { - if (DB.DB2 == getDB() && !selectAndClaim) { + if (DB.DB2 == getDB() + && !selectAndClaim + && taskanaEngine.getEngine().getConfiguration().isUseSpecificDb2Taskquery()) { return LINK_TO_MAPPER_DB2; } else if (selectAndClaim && DB.ORACLE == getDB()) { return LINK_TO_MAPPER_ORACLE; @@ -2079,7 +2122,10 @@ private String getLinkToMapperScript() { } private String getLinkToCounterTaskScript() { - return DB.DB2 == getDB() ? LINK_TO_COUNTER_DB2 : LINK_TO_COUNTER; + return DB.DB2 == getDB() + && taskanaEngine.getEngine().getConfiguration().isUseSpecificDb2Taskquery() + ? LINK_TO_COUNTER_DB2 + : LINK_TO_COUNTER; } private void validateAllTimeIntervals(TimeInterval[] intervals) { @@ -2239,7 +2285,16 @@ private TaskQuery addOrderCriteria(String columnName, SortDirection sortDirectio if (sortDirection == null) { sortDirection = SortDirection.ASCENDING; } - orderBy.add(columnName + " " + sortDirection); + orderByInner.add(columnName + " " + sortDirection); + if (columnName.startsWith("a") || columnName.startsWith("w") || columnName.startsWith("c")) { + orderByOuter.add(columnName.replace(".", "").toUpperCase() + " " + sortDirection); + } else { + if (columnName.startsWith("u")) { + orderByOuter.add(columnName.replace(".", "").substring(1) + " " + sortDirection); + } else { + orderByOuter.add(columnName + " " + sortDirection); + } + } return this; } @@ -2249,8 +2304,8 @@ public String toString() { + taskanaEngine + ", taskService=" + taskService - + ", orderBy=" - + orderBy + + ", orderByOuter=" + + orderByOuter + ", columnName=" + columnName + ", accessIdIn=" @@ -2279,6 +2334,10 @@ public String toString() { + addAttachmentClassificationNameToSelectClauseForOrdering + ", addWorkbasketNameToSelectClauseForOrdering=" + addWorkbasketNameToSelectClauseForOrdering + + ", groupByPor=" + + groupByPor + + ", groupBySor=" + + groupBySor + ", taskId=" + Arrays.toString(taskId) + ", taskIdNotIn=" diff --git a/lib/taskana-core/src/main/java/pro/taskana/task/internal/TaskQueryMapper.java b/lib/taskana-core/src/main/java/pro/taskana/task/internal/TaskQueryMapper.java index 84a8e59a9d..ef5b760df6 100644 --- a/lib/taskana-core/src/main/java/pro/taskana/task/internal/TaskQueryMapper.java +++ b/lib/taskana-core/src/main/java/pro/taskana/task/internal/TaskQueryMapper.java @@ -43,6 +43,7 @@ public interface TaskQueryMapper { @Result(property = "primaryObjRefImpl.value", column = "POR_VALUE") @Result(property = "isRead", column = "IS_READ") @Result(property = "isTransferred", column = "IS_TRANSFERRED") + @Result(property = "groupByCount", column = "R_COUNT") @Result(property = "custom1", column = "CUSTOM_1") @Result(property = "custom2", column = "CUSTOM_2") @Result(property = "custom3", column = "CUSTOM_3") diff --git a/lib/taskana-core/src/main/java/pro/taskana/task/internal/TaskQuerySqlProvider.java b/lib/taskana-core/src/main/java/pro/taskana/task/internal/TaskQuerySqlProvider.java index bebeaea3fd..f282b027ec 100644 --- a/lib/taskana-core/src/main/java/pro/taskana/task/internal/TaskQuerySqlProvider.java +++ b/lib/taskana-core/src/main/java/pro/taskana/task/internal/TaskQuerySqlProvider.java @@ -24,15 +24,22 @@ private TaskQuerySqlProvider() {} @SuppressWarnings("unused") public static String queryTaskSummaries() { return OPENING_SCRIPT_TAG + + openOuterClauseForGroupByPorOrSor() + "SELECT DISTINCT " + commonSelectFields() + + ", o.VALUE as SOR_VALUE " + "" - + ", a.CLASSIFICATION_ID, a.CLASSIFICATION_KEY, a.CHANNEL, a.REF_VALUE, a.RECEIVED" - + "" - + ", c.NAME " - + ", ac.NAME " - + ", w.NAME " - + ", u.LONG_NAME " + + ", a.CLASSIFICATION_ID as ACLASSIFICATION_ID, " + + "a.CLASSIFICATION_KEY as ACLASSIFICATION_KEY, a.CHANNEL as ACHANNEL, " + + "a.REF_VALUE as AREF_VALUE, a.RECEIVED as ARECEIVED" + + "" + + ", c.NAME as CNAME " + + ", " + + "ac.NAME as ACNAME " + + ", w.NAME as WNAME " + + ", u.LONG_NAME" + + groupByPorIfActive() + + groupBySorIfActive() + "FROM TASK t " + "" + "LEFT JOIN ATTACHMENT a ON t.ID = a.TASK_ID " @@ -57,8 +64,10 @@ public static String queryTaskSummaries() { + commonTaskWhereStatement() + " AND t.STATE = 'READY' " + CLOSING_WHERE_TAG - + "" - + "ORDER BY ${item}" + + closeOuterClauseForGroupByPor() + + closeOuterClauseForGroupBySor() + + "" + + "ORDER BY ${item}" + " " + " " + "FETCH FIRST ROW ONLY FOR UPDATE" @@ -128,8 +137,8 @@ public static String queryTaskSummariesDb2() { + db2selectFields() + "FROM Y " + "WHERE FLAG = 1 " - + "" - + "ORDER BY ${item}" + + "" + + "ORDER BY ${item}" + " " + "" + "FETCH FIRST ROW ONLY FOR UPDATE WITH RS USE AND KEEP UPDATE LOCKS" @@ -184,8 +193,8 @@ public static String queryTaskSummariesOracle() { + commonTaskWhereStatement() + " AND t.STATE = 'READY' " + CLOSING_WHERE_TAG - + "" - + "ORDER BY ${item}" + + "" + + "ORDER BY ${item}" + " " + "fetch first 1 rows only " + ") FOR UPDATE" @@ -196,6 +205,14 @@ public static String queryTaskSummariesOracle() { public static String countQueryTasks() { return OPENING_SCRIPT_TAG + "SELECT COUNT( DISTINCT t.ID) " + + " " + + "FROM (SELECT t.ID, t.POR_VALUE " + + " " + + " " + + ", o.VALUE as SOR_VALUE " + + " " + + groupByPorIfActive() + + groupBySorIfActive() + "FROM TASK t " + "" + "LEFT JOIN ATTACHMENT a ON t.ID = a.TASK_ID " @@ -216,6 +233,8 @@ public static String countQueryTasks() { + checkForAuthorization() + commonTaskWhereStatement() + CLOSING_WHERE_TAG + + closeOuterClauseForGroupByPor() + + closeOuterClauseForGroupBySor() + CLOSING_SCRIPT_TAG; } @@ -286,8 +305,8 @@ public static String queryTaskColumnValues() { + checkForAuthorization() + commonTaskWhereStatement() + CLOSING_WHERE_TAG - + "" - + "ORDER BY " + + "" + + "ORDER BY " + "" + "" + "t.CLASSIFICATION_KEY ASC" @@ -379,6 +398,108 @@ private static String checkForAuthorization() { + ""; } + private static String groupByPorIfActive() { + return " " + + ", ROW_NUMBER() OVER (PARTITION BY POR_VALUE " + + "" + + "ORDER BY ${item}" + + " " + + "" + + "ORDER BY DUE ASC" + + " " + + ")" + + "AS rn" + + " "; + } + + private static String groupBySorIfActive() { + return " " + + ", ROW_NUMBER() OVER (PARTITION BY o.VALUE " + + "" + + "ORDER BY ${item}" + + " " + + "" + + "ORDER BY DUE ASC" + + " " + + ")" + + "AS rn" + + " "; + } + + private static String openOuterClauseForGroupByPorOrSor() { + return " " + + "SELECT * FROM (" + + " "; + } + + private static String closeOuterClauseForGroupByPor() { + return " " + + ") t LEFT JOIN" + + " (SELECT POR_VALUE as PVALUE, COUNT(POR_VALUE) AS R_COUNT " + + "FROM (SELECT DISTINCT t.id , POR_VALUE " + + "FROM TASK t" + + "" + + "LEFT JOIN ATTACHMENT a ON t.ID = a.TASK_ID " + + "" + + "" + + "LEFT JOIN OBJECT_REFERENCE o ON t.ID = o.TASK_ID " + + "" + + "" + + "LEFT JOIN CLASSIFICATION c ON t.CLASSIFICATION_ID = c.ID " + + "" + + "" + + "LEFT JOIN CLASSIFICATION ac ON a.CLASSIFICATION_ID = ac.ID " + + "" + + "" + + "LEFT JOIN WORKBASKET w ON t.WORKBASKET_ID = w.ID " + + "" + + "" + + "LEFT JOIN USER_INFO u ON t.owner = u.USER_ID " + + "" + + OPENING_WHERE_TAG + + checkForAuthorization() + + commonTaskWhereStatement() + + " AND t.STATE = 'READY' " + + CLOSING_WHERE_TAG + + ") as y " + + "GROUP BY POR_VALUE) AS tt ON t.POR_VALUE=tt.PVALUE " + + "WHERE rn = 1" + + " "; + } + + private static String closeOuterClauseForGroupBySor() { + return " " + + ") t LEFT JOIN" + + " (SELECT o.VALUE, COUNT(o.VALUE) AS R_COUNT " + + "FROM TASK t " + + "LEFT JOIN OBJECT_REFERENCE o on t.ID=o.TASK_ID " + + "" + + "LEFT JOIN ATTACHMENT a ON t.ID = a.TASK_ID " + + "" + + "" + + "LEFT JOIN CLASSIFICATION c ON t.CLASSIFICATION_ID = c.ID " + + "" + + "" + + "LEFT JOIN CLASSIFICATION ac ON a.CLASSIFICATION_ID = ac.ID " + + "" + + "" + + "LEFT JOIN WORKBASKET w ON t.WORKBASKET_ID = w.ID " + + "" + + "" + + "LEFT JOIN USER_INFO u ON t.owner = u.USER_ID " + + "" + + OPENING_WHERE_TAG + + checkForAuthorization() + + commonTaskWhereStatement() + + "AND o.TYPE=#{groupBySor} " + + CLOSING_WHERE_TAG + + "GROUP BY o.VALUE) AS tt ON t.SOR_VALUE=tt.VALUE " + + "WHERE rn = 1" + + " "; + } + private static String commonTaskObjectReferenceWhereStatement() { return "" + "AND ( " diff --git a/lib/taskana-core/src/main/java/pro/taskana/task/internal/models/TaskImpl.java b/lib/taskana-core/src/main/java/pro/taskana/task/internal/models/TaskImpl.java index ae697d1213..53951b7412 100644 --- a/lib/taskana-core/src/main/java/pro/taskana/task/internal/models/TaskImpl.java +++ b/lib/taskana-core/src/main/java/pro/taskana/task/internal/models/TaskImpl.java @@ -33,6 +33,7 @@ private TaskImpl(TaskImpl copyFrom) { callbackInfo = new HashMap<>(copyFrom.callbackInfo); callbackState = copyFrom.callbackState; attachments = copyFrom.attachments.stream().map(Attachment::copy).collect(Collectors.toList()); + groupByCount = copyFrom.groupByCount; } public Map getCustomAttributes() { @@ -280,6 +281,7 @@ public TaskSummary asSummary() { taskSummary.setNote(note); taskSummary.setDescription(description); taskSummary.setOwner(owner); + taskSummary.setOwnerLongName(ownerLongName); taskSummary.setParentBusinessProcessId(parentBusinessProcessId); taskSummary.setPlanned(planned); taskSummary.setReceived(received); diff --git a/lib/taskana-core/src/main/java/pro/taskana/task/internal/models/TaskSummaryImpl.java b/lib/taskana-core/src/main/java/pro/taskana/task/internal/models/TaskSummaryImpl.java index a0ccc1c057..a06ae94361 100644 --- a/lib/taskana-core/src/main/java/pro/taskana/task/internal/models/TaskSummaryImpl.java +++ b/lib/taskana-core/src/main/java/pro/taskana/task/internal/models/TaskSummaryImpl.java @@ -38,6 +38,7 @@ public class TaskSummaryImpl implements TaskSummary { protected int manualPriority = DEFAULT_MANUAL_PRIORITY; protected TaskState state; protected ClassificationSummary classificationSummary; + protected Integer groupByCount; protected WorkbasketSummary workbasketSummary; protected String businessProcessId; protected String parentBusinessProcessId; @@ -105,6 +106,7 @@ protected TaskSummaryImpl(TaskSummaryImpl copyFrom) { copyFrom.secondaryObjectReferences.stream() .map(ObjectReference::copy) .collect(Collectors.toList()); + groupByCount = copyFrom.groupByCount; custom1 = copyFrom.custom1; custom2 = copyFrom.custom2; custom3 = copyFrom.custom3; @@ -212,6 +214,11 @@ public void setReceived(Instant received) { this.received = received != null ? received.truncatedTo(ChronoUnit.MILLIS) : null; } + @Override + public Integer getGroupByCount() { + return this.groupByCount; + } + @Override public Instant getDue() { return due != null ? due.truncatedTo(ChronoUnit.MILLIS) : null; @@ -486,6 +493,10 @@ public void setWorkbasketSummaryImpl(WorkbasketSummaryImpl workbasketSummary) { setWorkbasketSummary(workbasketSummary); } + public void setGroupByCount(Integer n) { + groupByCount = n; + } + public void addAttachmentSummary(AttachmentSummary attachmentSummary) { if (this.attachmentSummaries == null) { this.attachmentSummaries = new ArrayList<>(); @@ -797,6 +808,7 @@ public int hashCode() { primaryObjRef, isRead, isTransferred, + groupByCount, attachmentSummaries, secondaryObjectReferences, custom1, @@ -864,6 +876,7 @@ public boolean equals(Object obj) { && Objects.equals(primaryObjRef, other.primaryObjRef) && Objects.equals(attachmentSummaries, other.attachmentSummaries) && Objects.equals(secondaryObjectReferences, other.secondaryObjectReferences) + && Objects.equals(groupByCount, other.groupByCount) && Objects.equals(custom1, other.custom1) && Objects.equals(custom2, other.custom2) && Objects.equals(custom3, other.custom3) @@ -942,6 +955,8 @@ public String toString() { + isRead + ", isTransferred=" + isTransferred + + ", groupByCount=" + + groupByCount + ", attachmentSummaries=" + attachmentSummaries + ", objectReferences=" diff --git a/lib/taskana-test-api/src/main/java/pro/taskana/testapi/builder/TaskBuilder.java b/lib/taskana-test-api/src/main/java/pro/taskana/testapi/builder/TaskBuilder.java index b01f8d2444..fade0eb038 100644 --- a/lib/taskana-test-api/src/main/java/pro/taskana/testapi/builder/TaskBuilder.java +++ b/lib/taskana-test-api/src/main/java/pro/taskana/testapi/builder/TaskBuilder.java @@ -132,6 +132,11 @@ public TaskBuilder owner(String owner) { return this; } + public TaskBuilder ownerLongName(String ownerLongName) { + testTask.setOwnerLongName(ownerLongName); + return this; + } + public TaskBuilder primaryObjRef(ObjectReference primaryObjRef) { testTask.setPrimaryObjRef(primaryObjRef); return this; @@ -161,6 +166,11 @@ public TaskBuilder transferred(Boolean transferred) { return this; } + public TaskBuilder groupByCount(Integer count) { + testTask.setGroupByCount(count); + return this; + } + public TaskBuilder attachments(Attachment... attachments) { testTask.setAttachments(Arrays.asList(attachments)); return this; diff --git a/lib/taskana-test-api/src/test/java/pro/taskana/testapi/builder/TaskBuilderTest.java b/lib/taskana-test-api/src/test/java/pro/taskana/testapi/builder/TaskBuilderTest.java index 1b1296be4b..5934a04069 100644 --- a/lib/taskana-test-api/src/test/java/pro/taskana/testapi/builder/TaskBuilderTest.java +++ b/lib/taskana-test-api/src/test/java/pro/taskana/testapi/builder/TaskBuilderTest.java @@ -212,7 +212,7 @@ void should_PopulateTask_When_UsingEveryBuilderFunction() throws Exception { expectedTask.setCallbackState(CallbackState.CALLBACK_PROCESSING_COMPLETED); assertThat(task) - .hasNoNullFieldsOrPropertiesExcept("ownerLongName") + .hasNoNullFieldsOrPropertiesExcept("ownerLongName", "groupByCount") .usingRecursiveComparison() .ignoringFields("id") .isEqualTo(expectedTask); diff --git a/rest/taskana-rest-spring/src/main/java/pro/taskana/task/rest/TaskController.java b/rest/taskana-rest-spring/src/main/java/pro/taskana/task/rest/TaskController.java index a6904fdc1a..33908e5eb1 100644 --- a/rest/taskana-rest-spring/src/main/java/pro/taskana/task/rest/TaskController.java +++ b/rest/taskana-rest-spring/src/main/java/pro/taskana/task/rest/TaskController.java @@ -123,7 +123,8 @@ public ResponseEntity createTask( * @param request the HTTP request * @param filterParameter the filter parameters * @param filterCustomFields the filter parameters regarding TaskCustomFields - * @param filterCustomIntFields the filter parameters regarding TaskCustomIntFields + * @param filterCustomIntFields the filter parameters regarding TaskCustomIntFields * @param + * @param groupByParameter the group by parameters * @param sortParameter the sort parameters * @param pagingParameter the paging parameters * @return the Tasks with the given filter, sort and paging options. @@ -135,6 +136,7 @@ public ResponseEntity getTasks( TaskQueryFilterParameter filterParameter, TaskQueryFilterCustomFields filterCustomFields, TaskQueryFilterCustomIntFields filterCustomIntFields, + TaskQueryGroupByParameter groupByParameter, TaskQuerySortParameter sortParameter, QueryPagingParameter pagingParameter) { QueryParamsValidator.validateParams( @@ -142,6 +144,7 @@ public ResponseEntity getTasks( TaskQueryFilterParameter.class, TaskQueryFilterCustomFields.class, TaskQueryFilterCustomIntFields.class, + TaskQueryGroupByParameter.class, QuerySortParameter.class, QueryPagingParameter.class); TaskQuery query = taskService.createTaskQuery(); @@ -149,6 +152,7 @@ public ResponseEntity getTasks( filterParameter.apply(query); filterCustomFields.apply(query); filterCustomIntFields.apply(query); + groupByParameter.apply(query); sortParameter.apply(query); List taskSummaries = pagingParameter.apply(query); diff --git a/rest/taskana-rest-spring/src/main/java/pro/taskana/task/rest/TaskQueryGroupByParameter.java b/rest/taskana-rest-spring/src/main/java/pro/taskana/task/rest/TaskQueryGroupByParameter.java new file mode 100644 index 0000000000..40c21f0217 --- /dev/null +++ b/rest/taskana-rest-spring/src/main/java/pro/taskana/task/rest/TaskQueryGroupByParameter.java @@ -0,0 +1,61 @@ +package pro.taskana.task.rest; + +import com.fasterxml.jackson.annotation.JsonProperty; +import java.beans.ConstructorProperties; +import java.util.Optional; +import java.util.function.Consumer; +import pro.taskana.common.api.exceptions.InvalidArgumentException; +import pro.taskana.common.rest.QueryParameter; +import pro.taskana.task.api.TaskQuery; + +public class TaskQueryGroupByParameter implements QueryParameter { + public enum TaskQueryGroupBy { + POR_VALUE(TaskQuery::groupByPor); + private final Consumer consumer; + + TaskQueryGroupBy(Consumer consumer) { + this.consumer = consumer; + } + + public void applyGroupByForQuery(TaskQuery query) { + consumer.accept(query); + } + } + + // region groupBy + @JsonProperty("group-by") + private final TaskQueryGroupBy groupByPor; + + @JsonProperty("group-by-sor") + private final String groupBySor; + // endregion + + // region constructor + + @ConstructorProperties({"group-by", "group-by-sor"}) + public TaskQueryGroupByParameter(TaskQueryGroupBy groupBy, String groupBySor) + throws InvalidArgumentException { + this.groupByPor = groupBy; + this.groupBySor = groupBySor; + validateGroupByParameters(); + } + + // endregion + + @Override + public Void apply(TaskQuery query) { + + Optional.ofNullable(groupBySor).ifPresent(query::groupBySor); + Optional.ofNullable(groupByPor) + .ifPresent(taskQueryGroupBy -> taskQueryGroupBy.applyGroupByForQuery(query)); + + return null; + } + + private void validateGroupByParameters() throws InvalidArgumentException { + if (groupByPor != null && groupBySor != null) { + throw new InvalidArgumentException( + "Only one of the following can be provided: Either group-by or group-by-sor"); + } + } +} diff --git a/rest/taskana-rest-spring/src/main/java/pro/taskana/task/rest/assembler/TaskRepresentationModelAssembler.java b/rest/taskana-rest-spring/src/main/java/pro/taskana/task/rest/assembler/TaskRepresentationModelAssembler.java index b2c322b6ee..c684ca780a 100644 --- a/rest/taskana-rest-spring/src/main/java/pro/taskana/task/rest/assembler/TaskRepresentationModelAssembler.java +++ b/rest/taskana-rest-spring/src/main/java/pro/taskana/task/rest/assembler/TaskRepresentationModelAssembler.java @@ -82,6 +82,7 @@ public TaskRepresentationModel toModel(Task task) { .collect(Collectors.toList())); repModel.setRead(task.isRead()); repModel.setTransferred(task.isTransferred()); + repModel.setGroupByCount(task.getGroupByCount()); repModel.setAttachments( task.getAttachments().stream() .map(attachmentAssembler::toModel) @@ -159,6 +160,7 @@ public Task toEntityModel(TaskRepresentationModel repModel) throws InvalidArgume task.setPrimaryObjRef(objectReferenceAssembler.toEntity(repModel.getPrimaryObjRef())); task.setRead(repModel.isRead()); task.setTransferred(repModel.isTransferred()); + task.setGroupByCount(repModel.getGroupByCount()); task.setCustomField(TaskCustomField.CUSTOM_1, repModel.getCustom1()); task.setCustomField(TaskCustomField.CUSTOM_2, repModel.getCustom2()); task.setCustomField(TaskCustomField.CUSTOM_3, repModel.getCustom3()); diff --git a/rest/taskana-rest-spring/src/main/java/pro/taskana/task/rest/assembler/TaskSummaryRepresentationModelAssembler.java b/rest/taskana-rest-spring/src/main/java/pro/taskana/task/rest/assembler/TaskSummaryRepresentationModelAssembler.java index f6711bd6d4..7dbd6f4a40 100644 --- a/rest/taskana-rest-spring/src/main/java/pro/taskana/task/rest/assembler/TaskSummaryRepresentationModelAssembler.java +++ b/rest/taskana-rest-spring/src/main/java/pro/taskana/task/rest/assembler/TaskSummaryRepresentationModelAssembler.java @@ -82,6 +82,7 @@ public TaskSummaryRepresentationModel toModel(@NonNull TaskSummary taskSummary) .collect(Collectors.toList())); repModel.setRead(taskSummary.isRead()); repModel.setTransferred(taskSummary.isTransferred()); + repModel.setGroupByCount(taskSummary.getGroupByCount()); repModel.setAttachmentSummaries( taskSummary.getAttachmentSummaries().stream() .map(attachmentAssembler::toModel) @@ -148,6 +149,7 @@ public TaskSummary toEntityModel(TaskSummaryRepresentationModel repModel) { .collect(Collectors.toList())); taskSummary.setRead(repModel.isRead()); taskSummary.setTransferred(repModel.isTransferred()); + taskSummary.setGroupByCount(repModel.getGroupByCount()); taskSummary.setAttachmentSummaries( repModel.getAttachmentSummaries().stream() .map(attachmentAssembler::toEntityModel) diff --git a/rest/taskana-rest-spring/src/main/java/pro/taskana/task/rest/models/TaskSummaryRepresentationModel.java b/rest/taskana-rest-spring/src/main/java/pro/taskana/task/rest/models/TaskSummaryRepresentationModel.java index 76cc5f338c..9de67262c1 100644 --- a/rest/taskana-rest-spring/src/main/java/pro/taskana/task/rest/models/TaskSummaryRepresentationModel.java +++ b/rest/taskana-rest-spring/src/main/java/pro/taskana/task/rest/models/TaskSummaryRepresentationModel.java @@ -79,6 +79,8 @@ public class TaskSummaryRepresentationModel protected boolean isRead; /** Indicator if the task has been transferred. */ protected boolean isTransferred; + /** Number of Tasks that are grouped together with this Task during a groupBy. */ + protected Integer groupByCount; /** A custom property with name "1". */ protected String custom1; /** A custom property with name "2". */ @@ -351,6 +353,14 @@ public void setAttachmentSummaries( this.attachmentSummaries = attachmentSummaries; } + public Integer getGroupByCount() { + return groupByCount; + } + + public void setGroupByCount(Integer groupByCount) { + this.groupByCount = groupByCount; + } + public String getCustom1() { return custom1; } diff --git a/rest/taskana-rest-spring/src/test/java/pro/taskana/task/rest/TaskControllerIntTest.java b/rest/taskana-rest-spring/src/test/java/pro/taskana/task/rest/TaskControllerIntTest.java index c38e037a53..5147319217 100644 --- a/rest/taskana-rest-spring/src/test/java/pro/taskana/task/rest/TaskControllerIntTest.java +++ b/rest/taskana-rest-spring/src/test/java/pro/taskana/task/rest/TaskControllerIntTest.java @@ -7,6 +7,7 @@ import java.io.BufferedWriter; import java.io.OutputStreamWriter; +import java.lang.reflect.Field; import java.net.HttpURLConnection; import java.net.URL; import java.net.URLEncoder; @@ -34,6 +35,7 @@ import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.client.HttpStatusCodeException; +import pro.taskana.TaskanaConfiguration; import pro.taskana.classification.rest.models.ClassificationSummaryRepresentationModel; import pro.taskana.common.rest.RestEndpoints; import pro.taskana.rest.test.RestHelper; @@ -54,6 +56,7 @@ @TaskanaSpringBootTest class TaskControllerIntTest { + @Autowired TaskanaConfiguration taskanaConfiguration; private static final ParameterizedTypeReference TASK_SUMMARY_PAGE_MODEL_TYPE = new ParameterizedTypeReference<>() {}; @@ -1204,6 +1207,57 @@ void should_SortByOwnerLongName() { assertThat((response.getBody()).getLink(IanaLinkRelations.SELF)).isNotNull(); } + @Test + void should_GroupByPor() throws Exception { + Field useSpecificDb2Taskquery = + taskanaConfiguration.getClass().getDeclaredField("useSpecificDb2Taskquery"); + useSpecificDb2Taskquery.setAccessible(true); + useSpecificDb2Taskquery.setBoolean(taskanaConfiguration, true); + + String url = restHelper.toUrl(RestEndpoints.URL_TASKS) + "?group-by=POR_VALUE"; + HttpEntity auth = new HttpEntity<>(RestHelper.generateHeadersForUser("admin")); + + ResponseEntity response = + TEMPLATE.exchange(url, HttpMethod.GET, auth, TASK_SUMMARY_PAGE_MODEL_TYPE); + + assertThat(response.getBody()).isNotNull(); + assertThat((response.getBody()).getLink(IanaLinkRelations.SELF)).isNotNull(); + assertThat(response.getBody().getContent()).hasSize(14); + assertThat( + response.getBody().getContent().stream() + .filter(task -> task.getPrimaryObjRef().getValue().equals("MyValue1")) + .map(TaskSummaryRepresentationModel::getGroupByCount) + .toArray()) + .containsExactly(6); + + useSpecificDb2Taskquery.setBoolean(taskanaConfiguration, false); + } + + @Test + void should_GroupBySor() throws Exception { + Field useSpecificDb2Taskquery = + taskanaConfiguration.getClass().getDeclaredField("useSpecificDb2Taskquery"); + useSpecificDb2Taskquery.setAccessible(true); + useSpecificDb2Taskquery.setBoolean(taskanaConfiguration, true); + + String url = restHelper.toUrl(RestEndpoints.URL_TASKS) + "?group-by-sor=Type2"; + HttpEntity auth = new HttpEntity<>(RestHelper.generateHeadersForUser("admin")); + + ResponseEntity response = + TEMPLATE.exchange(url, HttpMethod.GET, auth, TASK_SUMMARY_PAGE_MODEL_TYPE); + + assertThat(response.getBody()).isNotNull(); + assertThat((response.getBody()).getLink(IanaLinkRelations.SELF)).isNotNull(); + assertThat(response.getBody().getContent()).hasSize(1); + assertThat( + response.getBody().getContent().stream() + .map(TaskSummaryRepresentationModel::getGroupByCount) + .toArray()) + .containsExactly(2); + + useSpecificDb2Taskquery.setBoolean(taskanaConfiguration, false); + } + @Test void testGetLastPageSortedByDueWithHiddenTasksRemovedFromResult() { resetDb(); diff --git a/rest/taskana-rest-spring/src/test/java/pro/taskana/task/rest/assembler/TaskRepresentationModelAssemblerTest.java b/rest/taskana-rest-spring/src/test/java/pro/taskana/task/rest/assembler/TaskRepresentationModelAssemblerTest.java index 21138d0723..fd2b616db0 100644 --- a/rest/taskana-rest-spring/src/test/java/pro/taskana/task/rest/assembler/TaskRepresentationModelAssemblerTest.java +++ b/rest/taskana-rest-spring/src/test/java/pro/taskana/task/rest/assembler/TaskRepresentationModelAssemblerTest.java @@ -96,6 +96,7 @@ void should_ReturnEntity_When_ConvertingRepresentationModelToEntity() throws Exc repModel.setCustomAttributes(List.of(TaskRepresentationModel.CustomAttribute.of("abc", "def"))); repModel.setCallbackInfo(List.of(TaskRepresentationModel.CustomAttribute.of("ghi", "jkl"))); repModel.setAttachments(List.of(attachment)); + repModel.setGroupByCount(0); repModel.setCustom1("custom1"); repModel.setCustom2("custom2"); repModel.setCustom3("custom3"); @@ -195,6 +196,7 @@ void should_ReturnRepresentationModel_When_ConvertingEntityToRepresentationModel task.setPrimaryObjRef(primaryObjRef); task.setRead(true); task.setTransferred(true); + task.setGroupByCount(0); task.setCustomAttributeMap(Map.of("abc", "def")); task.setCallbackInfo(Map.of("ghi", "jkl")); task.setAttachments(List.of(attachment)); @@ -268,6 +270,7 @@ void should_Equal_When_ComparingEntityWithConvertedEntity() throws InvalidArgume task.setPrimaryObjRef(primaryObjRef); task.setRead(true); task.setTransferred(true); + task.setGroupByCount(0); task.setCustomAttributeMap(Map.of("abc", "def")); task.setCallbackInfo(Map.of("ghi", "jkl")); task.setAttachments(List.of(attachment)); diff --git a/rest/taskana-rest-spring/src/test/java/pro/taskana/task/rest/assembler/TaskSummaryRepresentationModelAssemblerTest.java b/rest/taskana-rest-spring/src/test/java/pro/taskana/task/rest/assembler/TaskSummaryRepresentationModelAssemblerTest.java index 2d001ad5ae..bb4dacc754 100644 --- a/rest/taskana-rest-spring/src/test/java/pro/taskana/task/rest/assembler/TaskSummaryRepresentationModelAssemblerTest.java +++ b/rest/taskana-rest-spring/src/test/java/pro/taskana/task/rest/assembler/TaskSummaryRepresentationModelAssemblerTest.java @@ -104,6 +104,7 @@ void should_ReturnRepresentationModel_When_ConvertingEntityToRepresentationModel task.setPrimaryObjRef(primaryObjRef); task.setRead(true); task.setTransferred(true); + task.setGroupByCount(0); task.setCustom1("custom1"); task.setCustom2("custom2"); task.setCustom3("custom3"); @@ -178,6 +179,7 @@ void should_ReturnEntity_When_ConvertingRepresentationModelToEntity() throws Exc repModel.setOwnerLongName("ownerLongName"); repModel.setRead(true); repModel.setTransferred(true); + repModel.setGroupByCount(0); repModel.setCustom1("custom1"); repModel.setCustom2("custom2"); repModel.setCustom3("custom3"); @@ -276,6 +278,7 @@ void should_Equal_When_ComparingEntityWithConvertedEntity() { task.setPrimaryObjRef(primaryObjRef); task.setRead(true); task.setTransferred(true); + task.setGroupByCount(0); task.setCustom1("custom1"); task.setCustom2("custom2"); task.setCustom3("custom3"); @@ -340,6 +343,7 @@ static void testEquality(TaskSummary taskSummary, TaskSummaryRepresentationModel taskSummary.getPrimaryObjRef(), repModel.getPrimaryObjRef()); assertThat(taskSummary.isRead()).isEqualTo(repModel.isRead()); assertThat(taskSummary.isTransferred()).isEqualTo(repModel.isTransferred()); + assertThat(taskSummary.getGroupByCount()).isEqualTo(repModel.getGroupByCount()); assertThat(taskSummary.getCustomField(CUSTOM_1)).isEqualTo(repModel.getCustom1()); assertThat(taskSummary.getCustomField(CUSTOM_2)).isEqualTo(repModel.getCustom2()); assertThat(taskSummary.getCustomField(CUSTOM_3)).isEqualTo(repModel.getCustom3());