diff --git a/common/taskana-common/src/main/java/pro/taskana/common/internal/util/SqlProviderUtil.java b/common/taskana-common/src/main/java/pro/taskana/common/internal/util/SqlProviderUtil.java
index 0ed7a1979..0e933fbeb 100644
--- a/common/taskana-common/src/main/java/pro/taskana/common/internal/util/SqlProviderUtil.java
+++ b/common/taskana-common/src/main/java/pro/taskana/common/internal/util/SqlProviderUtil.java
@@ -24,7 +24,7 @@ public class SqlProviderUtil {
.append("")
.append("0=1")
.append("");
- if (column.matches("t.CUSTOM_\\d+")) {
+ if (column.matches("t.CUSTOM_\\d+") || column.matches("t.OWNER")) {
sb.append(" OR " + column + " IS NULL ");
}
return sb.append(") ");
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 706df1847..9d7608b61 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
@@ -1814,6 +1814,7 @@ class TaskQueryImplAccTest {
TaskSummary taskSummary1;
TaskSummary taskSummary2;
TaskSummary taskSummary3;
+ TaskSummary taskSummary4;
@WithAccessId(user = "user-1-1")
@BeforeAll
@@ -1822,6 +1823,7 @@ class TaskQueryImplAccTest {
taskSummary1 = taskInWorkbasket(wb).owner("user-2-1").buildAndStoreAsSummary(taskService);
taskSummary2 = taskInWorkbasket(wb).owner("user-1-2").buildAndStoreAsSummary(taskService);
taskSummary3 = taskInWorkbasket(wb).owner("user-1-3").buildAndStoreAsSummary(taskService);
+ taskSummary4 = taskInWorkbasket(wb).owner(null).buildAndStoreAsSummary(taskService);
}
@WithAccessId(user = "user-1-1")
@@ -1859,6 +1861,29 @@ class TaskQueryImplAccTest {
assertThat(list).containsExactly(taskSummary1);
}
+
+ @WithAccessId(user = "user-1-1")
+ @Test
+ void should_ReturnTaskWithOwnerNull_When_QueryingForOwnerIn() {
+ String[] nullArray = {null};
+ List list =
+ taskService.createTaskQuery().workbasketIdIn(wb.getId()).ownerIn(nullArray).list();
+
+ assertThat(list).containsExactly(taskSummary4);
+ }
+
+ @WithAccessId(user = "user-1-1")
+ @Test
+ void should_ReturnTaskWithOwnerNullAndUser11_When_QueryingForOwnerIn() {
+ List list =
+ taskService
+ .createTaskQuery()
+ .workbasketIdIn(wb.getId())
+ .ownerIn("user-1-2", null)
+ .list();
+
+ assertThat(list).containsExactlyInAnyOrder(taskSummary2, taskSummary4);
+ }
}
@Nested
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 4b13b6389..1f2704368 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
@@ -140,6 +140,7 @@ public class TaskQueryImpl implements TaskQuery {
private String[] parentBusinessProcessIdLike;
private String[] parentBusinessProcessIdNotLike;
private String[] ownerIn;
+ private boolean ownerInContainsNull;
private String[] ownerNotIn;
private String[] ownerLike;
private String[] ownerNotLike;
@@ -865,7 +866,15 @@ public class TaskQueryImpl implements TaskQuery {
@Override
public TaskQuery ownerIn(String... owners) {
- this.ownerIn = owners;
+ List conditionList = new ArrayList<>(Arrays.asList(owners));
+ boolean containsNull = conditionList.contains(null);
+ if (containsNull) {
+ conditionList.remove(null);
+ ownerInContainsNull = true;
+ this.ownerIn = conditionList.toArray(new String[owners.length - 1]);
+ } else {
+ this.ownerIn = owners;
+ }
return this;
}
diff --git a/rest/taskana-rest-spring/src/main/java/pro/taskana/common/rest/util/QueryParamsValidator.java b/rest/taskana-rest-spring/src/main/java/pro/taskana/common/rest/util/QueryParamsValidator.java
index 8fb29a0b5..f42b208e9 100644
--- a/rest/taskana-rest-spring/src/main/java/pro/taskana/common/rest/util/QueryParamsValidator.java
+++ b/rest/taskana-rest-spring/src/main/java/pro/taskana/common/rest/util/QueryParamsValidator.java
@@ -4,9 +4,12 @@ import com.fasterxml.jackson.annotation.JsonProperty;
import java.util.HashSet;
import java.util.Optional;
import java.util.Set;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import javax.servlet.http.HttpServletRequest;
+import pro.taskana.common.api.exceptions.InvalidArgumentException;
public class QueryParamsValidator {
@@ -32,5 +35,21 @@ public class QueryParamsValidator {
if (!providedParams.isEmpty()) {
throw new IllegalArgumentException("Unknown request parameters found: " + providedParams);
}
+ checkExactParam(request, "owner-is-null");
+ }
+
+ private static void checkExactParam(HttpServletRequest request, String queryParameter) {
+ String queryString = request.getQueryString();
+ boolean containParam = queryString != null && queryString.contains(queryParameter);
+ if (containParam) {
+ Pattern pattern = Pattern.compile("\\b" + queryParameter + "(&|$)");
+ Matcher matcher = pattern.matcher(queryString);
+
+ boolean hasExactParam = matcher.find();
+ if (!hasExactParam) {
+ throw new InvalidArgumentException(
+ "It is prohibited to use the param " + queryParameter + " with values.");
+ }
+ }
}
}
diff --git a/rest/taskana-rest-spring/src/main/java/pro/taskana/task/rest/TaskQueryFilterParameter.java b/rest/taskana-rest-spring/src/main/java/pro/taskana/task/rest/TaskQueryFilterParameter.java
index 843800bff..e69edbecb 100644
--- a/rest/taskana-rest-spring/src/main/java/pro/taskana/task/rest/TaskQueryFilterParameter.java
+++ b/rest/taskana-rest-spring/src/main/java/pro/taskana/task/rest/TaskQueryFilterParameter.java
@@ -3,7 +3,9 @@ package pro.taskana.task.rest;
import com.fasterxml.jackson.annotation.JsonProperty;
import java.beans.ConstructorProperties;
import java.time.Instant;
+import java.util.ArrayList;
import java.util.Arrays;
+import java.util.List;
import java.util.Optional;
import pro.taskana.common.api.IntInterval;
import pro.taskana.common.api.KeyDomain;
@@ -740,6 +742,13 @@ public class TaskQueryFilterParameter implements QueryParameter
*/
@JsonProperty("owner-not-like")
private final String[] ownerNotLike;
+
+ /**
+ * Filter by tasks that have no owner. The parameter should exactly be "owner-is-null" without
+ * being followed by "=..."
+ */
+ @JsonProperty("owner-is-null")
+ private final String ownerNull;
// endregion
// region primaryObjectReference
/**
@@ -1259,6 +1268,7 @@ public class TaskQueryFilterParameter implements QueryParameter
"owner-not",
"owner-like",
"owner-not-like",
+ "owner-is-null",
"por",
"por-company",
"por-company-not",
@@ -1415,6 +1425,7 @@ public class TaskQueryFilterParameter implements QueryParameter
String[] ownerNotIn,
String[] ownerLike,
String[] ownerNotLike,
+ String ownerNull,
ObjectReference[] primaryObjectReferenceIn,
String[] porCompanyIn,
String[] porCompanyNotIn,
@@ -1570,6 +1581,7 @@ public class TaskQueryFilterParameter implements QueryParameter
this.ownerNotIn = ownerNotIn;
this.ownerLike = ownerLike;
this.ownerNotLike = ownerNotLike;
+ this.ownerNull = ownerNull;
this.primaryObjectReferenceIn = primaryObjectReferenceIn;
this.porCompanyIn = porCompanyIn;
this.porCompanyNotIn = porCompanyNotIn;
@@ -1845,7 +1857,8 @@ public class TaskQueryFilterParameter implements QueryParameter
.map(this::wrapElementsInLikeStatement)
.ifPresent(query::parentBusinessProcessIdNotLike);
- Optional.ofNullable(ownerIn).ifPresent(query::ownerIn);
+ String[] ownerInIncludingNull = addNullToOwnerIn();
+ Optional.ofNullable(ownerInIncludingNull).ifPresent(query::ownerIn);
Optional.ofNullable(ownerNotIn).ifPresent(query::ownerNotIn);
Optional.ofNullable(ownerLike)
.map(this::wrapElementsInLikeStatement)
@@ -2184,4 +2197,16 @@ public class TaskQueryFilterParameter implements QueryParameter
"provided value of the property 'without-attachment' must be 'true'");
}
}
+
+ private String[] addNullToOwnerIn() {
+ if (this.ownerNull == null) {
+ return this.ownerIn;
+ }
+ if (this.ownerIn == null) {
+ return new String[]{null};
+ }
+ List ownerInAsList = new ArrayList(Arrays.asList(this.ownerIn));
+ ownerInAsList.add(null);
+ return ownerInAsList.toArray(new String[ownerInAsList.size()]);
+ }
}
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 514731921..1093cc9dd 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
@@ -26,6 +26,8 @@ import org.junit.jupiter.api.TestFactory;
import org.junit.jupiter.api.TestInstance;
import org.junit.jupiter.api.TestInstance.Lifecycle;
import org.junit.jupiter.api.function.ThrowingConsumer;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.CsvSource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.ParameterizedTypeReference;
@@ -37,6 +39,7 @@ 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.internal.util.Pair;
import pro.taskana.common.rest.RestEndpoints;
import pro.taskana.rest.test.RestHelper;
import pro.taskana.rest.test.TaskanaSpringBootTest;
@@ -1165,6 +1168,46 @@ class TaskControllerIntTest {
+ "&sort-by=POR_VALUE&order=DESCENDING");
}
+ @ParameterizedTest
+ @CsvSource({
+ "owner=user-1-1, 10",
+ "owner-is-null, 65",
+ "owner-is-null&owner=user-1-1, 75",
+ "state=READY&owner-is-null&owner=user-1-1, 56",
+ })
+ void should_ReturnTasksWithVariousOwnerParameters_When_GettingTasks(
+ String queryParams, int expectedSize) {
+ String url = restHelper.toUrl(RestEndpoints.URL_TASKS) + "?" + queryParams;
+ HttpEntity