Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/847 support multiple sort criterias #875

Merged
merged 5 commits into from
Oct 10, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/reference-guide/components/view-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ interface PageableSortableQuery {
The `page` parameter denotes the page number to deliver (starting with `0`). The `size` parameter denotes the number of elements on a page. By default, the `page` is set to `0`
and the size is set to `Int.MAX`.

An optional `sort` parameter allows to sort the results by a field attribute. The format of the `sort` string is `<+|->fieldName`, `+fieldName` means sort by `fieldName` ascending,
An optional `sort` list allows to sort the results by multiple field attributes. The format of the `sort` string is `<+|->fieldName`, `+fieldName` means sort by `fieldName` ascending,
`-fieldName` means sort by `fieldName` descending. The field must be a direct member of the result (`Task` for queries on `Task` and `TaskWithDataEntries` or `DataEntry`) and must be one of the following type:

* java.lang.Integer
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -200,7 +200,7 @@ class JpaPolyflowViewDataEntryService(
}

private fun reportMissingFeature(query: PageableSortableQuery) {
if (query.sort != null) {
if (query.sort.isEmpty()) {
logger.warn { "Sorting is currently not supported, but the sort was requested: ${query.sort}, see https://github.com/holunda-io/camunda-bpm-taskpool/issues/701" }
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -537,7 +537,7 @@ class JpaPolyflowViewTaskService(
}

private fun reportMissingFeature(query: PageableSortableQuery) {
if (query.sort != null) {
if (query.sort.isEmpty()) {
logger.warn { "Sorting is currently not supported, but the sort was requested: ${query.sort}, see https://github.com/holunda-io/camunda-bpm-taskpool/issues/701" }
}
if (query.page != 1 || query.size != Int.MAX_VALUE) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import io.holunda.polyflow.view.query.PageableSortableQuery
import org.springframework.data.domain.PageRequest
import org.springframework.data.domain.Sort
import org.springframework.data.domain.Sort.Direction
import org.springframework.data.domain.Sort.Order
import org.springframework.data.jpa.domain.Specification
import org.springframework.data.jpa.domain.Specification.where
import java.time.Instant
Expand Down Expand Up @@ -92,32 +93,54 @@ fun pageRequest(page: Int, size: Int, sort: String?): PageRequest {
}
}

/**
* Constructs page request.
* @param page page number.
* @param size page size
* @param sort optional sort, where each element in format +filedName or -fieldName
*/
fun pageRequest(page: Int, size: Int, sort: List<String> = listOf()): PageRequest {
val sortCriteria = sort.map { s ->
val direction = if (s.substring(0, 1) == "+") {
Direction.ASC
} else {
Direction.DESC
}
Order.by(s.substring(1)).with(direction)
}.toList()


return PageRequest.of(page, size, Sort.by(sortCriteria))
}

/**
* Map sort string from the view API to implementation sort of the entities.
*/
fun PageableSortableQuery.mapTaskSort(): String {
return if (this.sort == null) {
fun PageableSortableQuery.mapTaskSort(): List<String> {
return if (this.sort.isEmpty()) {
// no sort is specified, we don't want unsorted results.
"-${TaskEntity::createdDate.name}"
listOf("-${TaskEntity::createdDate.name}")
} else {
val direction = sort!!.substring(0, 1)
val field = sort!!.substring(1)
return when (field) {
Task::name.name -> TaskEntity::name.name
Task::description.name -> TaskEntity::description.name
Task::assignee.name -> TaskEntity::assignee.name
Task::createTime.name -> TaskEntity::createdDate.name
Task::dueDate.name -> TaskEntity::dueDate.name
Task::followUpDate.name -> TaskEntity::followUpDate.name
Task::owner.name -> TaskEntity::owner.name
Task::priority.name -> TaskEntity::priority.name
Task::formKey.name -> TaskEntity::formKey.name
Task::businessKey.name -> TaskEntity::businessKey.name
Task::id.name -> TaskEntity::taskId.name
Task::taskDefinitionKey.name -> TaskEntity::taskDefinitionKey.name
else -> throw IllegalArgumentException("'$field' is not supported for sorting in JPA View")
}.let { "$direction$it" }
}
sort.map {
val direction = it.substring(0,1)
val field = it.substring(1)
when (field) {
Task::name.name -> TaskEntity::name.name
Task::description.name -> TaskEntity::description.name
Task::assignee.name -> TaskEntity::assignee.name
Task::createTime.name -> TaskEntity::createdDate.name
Task::dueDate.name -> TaskEntity::dueDate.name
Task::followUpDate.name -> TaskEntity::followUpDate.name
Task::owner.name -> TaskEntity::owner.name
Task::priority.name -> TaskEntity::priority.name
Task::formKey.name -> TaskEntity::formKey.name
Task::businessKey.name -> TaskEntity::businessKey.name
Task::id.name -> TaskEntity::taskId.name
Task::taskDefinitionKey.name -> TaskEntity::taskDefinitionKey.name
else -> throw IllegalArgumentException("'$field' is not supported for sorting in JPA View")
}.let { "$direction$it" }
}
}
}


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ internal class JpaPolyflowViewServiceDataEntryITest {
private val id = UUID.randomUUID().toString()
private val id2 = UUID.randomUUID().toString()
private val id3 = UUID.randomUUID().toString()
private val id4 = UUID.randomUUID().toString()
private val now = Instant.now()

@BeforeEach
Expand Down Expand Up @@ -185,6 +186,29 @@ internal class JpaPolyflowViewServiceDataEntryITest {
),
metaData = MetaData.emptyInstance()
)

jpaPolyflowViewService.on(
event = DataEntryCreatedEvent(
entryType = "io.polyflow.test",
entryId = id4,
type = "Test sort",
applicationName = "test-application",
name = "Test Entry 4",
state = ProcessingType.IN_PROGRESS.of("In review"),
payload = serialize(payload = mapOf("key-int" to 2, "key" to "other-value"), mapper = objectMapper),
authorizations = listOf(
addUser("hulk"),
addGroup("avenger")
),
createModification = Modification(
time = OffsetDateTime.ofInstant(now, ZoneOffset.UTC),
username = "piggy",
log = "Created",
logNotes = "Created the entry"
)
),
metaData = MetaData.emptyInstance()
)
}

@AfterEach
Expand Down Expand Up @@ -280,6 +304,24 @@ internal class JpaPolyflowViewServiceDataEntryITest {
assertThat(result.payload).isNull()
}

@Test
fun `should sort entries with multiple criteria`() {

val result = jpaPolyflowViewService.query(
DataEntriesQuery(sort = listOf("+type", "-name"))
)
assertThat(result.payload.elements.map { it.entryId }).containsExactly(id2, id, id4)
}

@Test
fun `sort should be backwards compatible`() {

val result = jpaPolyflowViewService.query(
DataEntriesQuery(sort = "+type")
)
assertThat(result.payload.elements.map { it.entryId }).containsExactly(id, id2, id4)
}


private fun <T : Any> query_updates_have_been_emitted(query: T, id: String, revision: Long) {
captureEmittedQueryUpdates()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -269,12 +269,12 @@ internal class JpaPolyflowViewServiceTaskITest {
fun `should find the task by user with data entries and sort results correctly`() {
val strawhats = jpaPolyflowViewService.query(TasksWithDataEntriesForUserQuery(
user = User("other", setOf("strawhats")),
sort = "+name",
sort = listOf("+name"),
assignedToMeOnly = false
))
val strawhatsInverse = jpaPolyflowViewService.query(TasksWithDataEntriesForUserQuery(
user = User("other", setOf("strawhats")),
sort = "-name",
sort = listOf("-name"),
assignedToMeOnly = false
))

Expand All @@ -284,29 +284,42 @@ internal class JpaPolyflowViewServiceTaskITest {
assertThat(strawhatsInverse.elements.map { it.task.id }).containsExactly(id4, id3)
}

@Test
fun `should find the task with data entries and sort by multiple correctly`() {
val query = jpaPolyflowViewService.query(AllTasksWithDataEntriesQuery(
sort = listOf("+priority", "-name")
))

val inverseQuery = jpaPolyflowViewService.query(AllTasksWithDataEntriesQuery(
sort = listOf("-priority", "+name")
))
assertThat(query.elements.map { it.task.id }).containsExactly(id4, id3, id)
assertThat(inverseQuery.elements.map { it.task.id }).containsExactly(id, id3, id4)
}

@Test
fun `should not execute query because of wrong sort`() {
val user = User("other", setOf("strawhats"))
assertThat(assertThrows<IllegalArgumentException> {
jpaPolyflowViewService.query(TasksWithDataEntriesForUserQuery(
user = user,
sort = "+createdTime", // entity property
sort = listOf("+createdTime"), // entity property
assignedToMeOnly = false
))
}.message).startsWith("Sort parameter must be one of ").endsWith(" but it was createdTime.")

assertThat(assertThrows<IllegalArgumentException> {
jpaPolyflowViewService.query(TasksWithDataEntriesForUserQuery(
user = user,
sort = "+candidateUsers", // unsupported by JPA view
sort = listOf("+candidateUsers"), // unsupported by JPA view
assignedToMeOnly = false
))
}.message).isEqualTo("'candidateUsers' is not supported for sorting in JPA View")

assertThat(assertThrows<IllegalArgumentException> {
jpaPolyflowViewService.query(TasksWithDataEntriesForUserQuery(
user = user,
sort = "*name", // wrong order
sort = listOf("*name"), // wrong order
assignedToMeOnly = false
))
}.message).isEqualTo("Sort must start either with '+' or '-' but it was starting with '*'")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -506,3 +506,13 @@ internal fun sort(sort: String?): Sort =
} else {
Sort.unsorted()
}

internal fun sort(sort: List<String>): Sort {
return if (sort.isNotEmpty()) {
val combinedSort = sort.map { sort(it) }.filter { it.isSorted }
if (combinedSort.isEmpty()) Sort.unsorted()
else combinedSort.reduce { combined, element -> combined.and(element) }
} else {
Sort.unsorted()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ class PolyflowMongoServiceSortTest {

@Test
fun `should be unsorted for null, empty or wrong sort`() {
assertThat(sort(listOf())).isEqualTo(Sort.unsorted())
assertThat(sort(null)).isEqualTo(Sort.unsorted())
assertThat(sort("")).isEqualTo(Sort.unsorted())
assertThat(sort("foo")).isEqualTo(Sort.unsorted())
Expand All @@ -21,4 +22,12 @@ class PolyflowMongoServiceSortTest {
assertThat(sort("-b")).isEqualTo(Sort.by(Sort.Direction.DESC, "b"))
}

@Test
fun `should combine sort`() {
assertThat(sort(listOf("+foo", "-bar"))).isEqualTo(Sort.by(Sort.Direction.ASC, "foo").and(Sort.by(Sort.Direction.DESC, "bar")))
assertThat(sort(listOf("+foo", ""))).isEqualTo(Sort.by(Sort.Direction.ASC, "foo"))
assertThat(sort(listOf("", "foo"))).isEqualTo(Sort.unsorted())
}


}
Original file line number Diff line number Diff line change
Expand Up @@ -278,6 +278,18 @@ class SimpleTaskPoolServiceTest : ScenarioTest<SimpleTaskPoolGivenStage<*>, Simp
.tasks_are_returned(listOf(then().tasks[5]))
}

@Test
fun `retrieves all task sort by multiple values`() {
given()
.tasks_exist(5)

`when`()
.all_tasks_are_queried(filters = listOf(), sort = listOf("+task.dueDate", "-task.businessKey"))

then()
.all_task_are_returned_and_sorted_by(reversed = true) { it.task.businessKey }
}

private infix fun String.withTaskCount(taskCount: Int) = ApplicationWithTaskCount(this, taskCount)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ class SimpleTaskPoolGivenStage<SELF : SimpleTaskPoolGivenStage<SELF>> : Abstract
private fun procRef(applicationName: String = "app") = ProcessReference("instance1", "exec1", "def1", "def-key", "proce1", applicationName)

private fun task(i: Int, applicationName: String = "app") = TestTaskData(
id ="id$i",
id = "id$i",
sourceReference = procRef(applicationName),
taskDefinitionKey = "task-key-$i",
businessKey = "BUS-$i",
Expand Down Expand Up @@ -103,7 +103,7 @@ class SimpleTaskPoolWhenStage<SELF : SimpleTaskPoolWhenStage<SELF>> : AbstractSi
private var returnedTasksForApplication = TaskQueryResult(listOf())

private fun query(page: Int, size: Int) = TasksWithDataEntriesForUserQuery(User("kermit", setOf()), true, page, size)
private fun filterQuery(sort: String, filters: List<String>) = TasksForUserQuery(assignedToMeOnly = false, user = User("kermit", setOf()), filters = filters, sort = sort)
private fun filterQuery(sort: List<String>, filters: List<String>) = TasksForUserQuery(assignedToMeOnly = false, user = User("kermit", setOf()), filters = filters, sort = sort)

@As("Page $ is queried with a page size of $")
fun page_is_queried(page: Int, size: Int) = step {
Expand All @@ -122,12 +122,12 @@ class SimpleTaskPoolWhenStage<SELF : SimpleTaskPoolWhenStage<SELF>> : AbstractSi

@As("Tasks are queried with filter $")
fun tasks_are_queried(filters: List<String>) = step {
queriedTasks.addAll(simpleTaskPoolService.query(filterQuery("+createdDate", filters)).elements.map { TaskWithDataEntries(it) })
queriedTasks.addAll(simpleTaskPoolService.query(filterQuery(listOf("+createdDate"), filters)).elements.map { TaskWithDataEntries(it) })
}

@As("All tasks are queried with filter $")
fun all_tasks_are_queried(filters: List<String>) = step {
queriedTasks.addAll(simpleTaskPoolService.query(AllTasksQuery(sort = "+name", filters = filters)).elements.map { TaskWithDataEntries(it) })
fun all_tasks_are_queried(filters: List<String>, sort: List<String> = listOf("+name")) = step {
queriedTasks.addAll(simpleTaskPoolService.query(AllTasksQuery(sort = sort, filters = filters)).elements.map { TaskWithDataEntries(it) })
}

}
Expand All @@ -154,14 +154,24 @@ class SimpleTaskPoolThenStage<SELF : SimpleTaskPoolThenStage<SELF>> : AbstractSi

@As("expected tasks are returned")
fun tasks_are_returned(@Hidden expected: List<TaskWithDataEntries>) = step {
assertThat(queriedTasks).containsExactlyInAnyOrderElementsOf( expected )
assertThat(queriedTasks).containsExactlyInAnyOrderElementsOf(expected)
}

@As("all tasks are returned once")
fun all_tasks_are_returned() = step {
assertThat(queriedTasks).isEqualTo(tasks)
}

@As("all tasks are returned and sorted once")
fun <R : Comparable<R>> all_task_are_returned_and_sorted_by(reversed: Boolean = false , selector: (TaskWithDataEntries) -> R?) {
if (reversed) {
assertThat(queriedTasks).isEqualTo(tasks.sortedByDescending(selector))
} else {
assertThat(queriedTasks).isEqualTo(tasks.sortedBy(selector))
}
}


@As("tasks $ are returned for application")
fun tasks_are_returned_for_application(@Hidden vararg expectedTasks: Task) = step {
assertThat(returnedTasksForApplication.elements).containsExactlyInAnyOrder(*expectedTasks)
Expand Down Expand Up @@ -208,7 +218,7 @@ class SimpleTaskPoolThenStage<SELF : SimpleTaskPoolThenStage<SELF>> : AbstractSi
assertThat(returnedTaskCounts).containsOnly(*entries)
}

fun task_does_not_exist(taskId: String) = step {
fun task_does_not_exist(taskId: String) = step {
val result = simpleTaskPoolService.query(TaskForIdQuery(taskId))
assertThat(result).isNotPresent
}
Expand Down
Loading