Skip to content

Commit

Permalink
Merge branch 'develop' into feature/852-spring-boot-3
Browse files Browse the repository at this point in the history
  • Loading branch information
zambrovski committed Oct 10, 2023
2 parents 2bbdfa8 + 84aff37 commit e27037e
Show file tree
Hide file tree
Showing 30 changed files with 422 additions and 119 deletions.
8 changes: 4 additions & 4 deletions bom/parent/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -18,17 +18,17 @@
<url>https://github.com/holunda-io/camunda-bpm-taskpool/</url>

<properties>
<springboot.version>3.1.3</springboot.version>
<camunda-commons-typed-values.version>7.20.0-alpha4</camunda-commons-typed-values.version>
<springboot.version>3.1.4</springboot.version>
<camunda-commons-typed-values.version>7.20.0</camunda-commons-typed-values.version>

<axon-bom.version>4.8.2</axon-bom.version>
<axon-kotlin.version>4.8.0</axon-kotlin.version>
<axon-gateway-extension.version>2.0.0</axon-gateway-extension.version>

<awaitility.version>4.2.0</awaitility.version>
<mockito-kotlin.version>5.0.0</mockito-kotlin.version>
<mockito-kotlin.version>5.1.0</mockito-kotlin.version>
<jgiven.version>1.3.0</jgiven.version>
<jgiven-kotlin.version>1.2.5.0</jgiven-kotlin.version>
<jgiven-kotlin.version>1.3.0.0</jgiven-kotlin.version>

<pattern.class.itest>**/*ITest.*</pattern.class.itest>
<pattern.package.itest>**/itest/**/*.*</pattern.package.itest>
Expand Down
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
4 changes: 2 additions & 2 deletions integration/camunda-bpm/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,9 @@
<packaging>pom</packaging>

<properties>
<camunda-bpm.version>7.20.0-alpha4</camunda-bpm.version>
<camunda-bpm.version>7.20.0</camunda-bpm.version>
<camunda-bpm-assert.version>${camunda-bpm.version}</camunda-bpm-assert.version>
<camunda-platform-7-mockito.version>6.19.0</camunda-platform-7-mockito.version>
<camunda-platform-7-mockito.version>7.20.0</camunda-platform-7-mockito.version>
</properties>

<modules>
Expand Down
2 changes: 1 addition & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>versions-maven-plugin</artifactId>
<version>2.16.0</version>
<version>2.16.1</version>
<configuration>
<generateBackupPoms>false</generateBackupPoms>
</configuration>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -191,7 +191,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 @@ -484,7 +484,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 @@ -82,32 +83,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 Expand Up @@ -142,7 +165,7 @@ internal fun List<Criterion>.toDataEntryPayloadSpecification(): Specification<Da
val relevant = this.filterIsInstance<Criterion.PayloadEntryCriterion>()
// compose criteria with same name with OR and criteria with different names with AND
val relevantByName = relevant.groupBy { it.name }
val orComposedByName = relevantByName.map { (_, criteria) -> composeOr(criteria.map { it.toDataEntrySpecification() }) }
val orComposedByName = relevantByName.map { (_, criteria) -> criteria.toOrDataEntrySpecification() }

return composeAnd(orComposedByName)
}
Expand All @@ -154,7 +177,7 @@ internal fun List<Criterion>.toTaskPayloadSpecification(): Specification<TaskEnt
val relevant = this.filterIsInstance<Criterion.PayloadEntryCriterion>()
// compose criteria with same name with OR and criteria with different names with AND
val relevantByName = relevant.groupBy { it.name }
val orComposedByName = relevantByName.map { (_, criteria) -> composeOr(criteria.map { it.toTaskSpecification() }) }
val orComposedByName = relevantByName.map { (_, criteria) -> criteria.toOrTaskSpecification() }

return composeAnd(orComposedByName)
}
Expand Down Expand Up @@ -208,13 +231,15 @@ internal fun Criterion.TaskCriterion.toTaskSpecification(): Specification<TaskEn
}

/**
* Creates JPA Specification for query of payload attributes based on JSON paths.
* Creates JPA Specification for query of payload attributes based on JSON paths. All criteria must have the same path
* and will be composed by the logical OR operator.
*/
internal fun Criterion.PayloadEntryCriterion.toTaskSpecification(): Specification<TaskEntity> {
return when (this.operator) {
EQUALS -> hasTaskPayloadAttribute(this.name, this.value)
else -> throw IllegalArgumentException("JPA View currently supports only equals as operator for filtering of payload attributes.")
}
internal fun List<Criterion.PayloadEntryCriterion>.toOrTaskSpecification(): Specification<TaskEntity> {
require(this.isNotEmpty()) { "List of criteria must not be empty." }
require(this.all { it.operator == EQUALS }) { "JPA View currently supports only equals as operator for filtering of payload attributes." }
require(this.distinctBy { it.name }.size == 1) { "All criteria must have the same path." }

return hasTaskPayloadAttribute(this.first().name, this.map { it.value })
}

/**
Expand All @@ -232,13 +257,15 @@ internal fun Criterion.DataEntryCriterion.toDataEntrySpecification(): Specificat
}

/**
* Creates JPA Specification for query of payload attributes based on JSON paths.
* Creates JPA Specification for query of payload attributes based on JSON paths. All criteria must have the same path
* and will be composed by the logical OR operator.
*/
internal fun Criterion.PayloadEntryCriterion.toDataEntrySpecification(): Specification<DataEntryEntity> {
return when (this.operator) {
EQUALS -> hasDataEntryPayloadAttribute(this.name, this.value)
else -> throw IllegalArgumentException("JPA View currently supports only equals as operator for filtering of payload attributes.")
}
internal fun List<Criterion.PayloadEntryCriterion>.toOrDataEntrySpecification(): Specification<DataEntryEntity> {
require(this.isNotEmpty()) { "List of criteria must not be empty." }
require(this.all { it.operator == EQUALS }) { "JPA View currently supports only equals as operator for filtering of payload attributes." }
require(this.distinctBy { it.name }.size == 1) { "All criteria must have the same path." }

return hasDataEntryPayloadAttribute(this.first().name, this.map { it.value })
}


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,21 +84,26 @@ interface DataEntryRepository : CrudRepository<DataEntryEntity, DataEntryId>, Jp
}

/**
* Specification for checking the payload attribute.
* Specification for checking the payload attribute. If multiple values are given, one of them must match.
* payload.name = ? AND (payload.value = ? OR payload.value = ? OR ...)
*/
fun hasDataEntryPayloadAttribute(name: String, value: String): Specification<DataEntryEntity> =
fun hasDataEntryPayloadAttribute(name: String, values: List<String>): Specification<DataEntryEntity> =
Specification { dataEntry, query, builder ->
query.distinct(true)
val join = dataEntry.join<DataEntryEntity, Set<PayloadAttribute>>(DataEntryEntity::payloadAttributes.name)
val pathEquals = builder.equal(
join.get<String>(PayloadAttribute::path.name),
name
)
val valueEquals = builder.equal(
join.get<String>(PayloadAttribute::value.name),
value
)
builder.and(pathEquals, valueEquals)

val valueAnyOf = values.map {
builder.equal(
join.get<String>(PayloadAttribute::value.name),
it
)
}.let { builder.or(*it.toTypedArray()) }

builder.and(pathEquals, valueAnyOf)
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -235,21 +235,26 @@ interface TaskRepository : CrudRepository<TaskEntity, String>, JpaSpecificationE
.or(likeProcessName(pattern))

/**
* Specification for checking the payload attribute of a task.
* Specification for checking the payload attribute of a task. If multiple values are given, one of them must match.
* payload.name = ? AND (payload.value = ? OR payload.value = ? OR ...)
*/
fun hasTaskPayloadAttribute(name: String, value: String): Specification<TaskEntity> =
fun hasTaskPayloadAttribute(name: String, values: List<String>): Specification<TaskEntity> =
Specification { task, query, builder ->
query.distinct(true)
val join = task.join<TaskEntity, Set<PayloadAttribute>>(TaskEntity::payloadAttributes.name)
val pathEquals = builder.equal(
join.get<String>(PayloadAttribute::path.name),
name
)
val valueEquals = builder.equal(
join.get<String>(PayloadAttribute::value.name),
value
)
builder.and(pathEquals, valueEquals)

val valueAnyOf = values.map {
builder.equal(
join.get<String>(PayloadAttribute::value.name),
it
)
}.let { builder.or(*it.toTypedArray()) }

builder.and(pathEquals, valueAnyOf)
}
}
}
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 @@ -265,7 +289,7 @@ internal class JpaPolyflowViewServiceDataEntryITest {
fun `should find the entry by filter`() {
assertResultIsTestEntry1And2(
jpaPolyflowViewService.query(
DataEntriesQuery(filters = listOf("key=value"))
DataEntriesQuery(filters = listOf("key=value", "key=value2", "key=value3"))
)
)
}
Expand All @@ -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
Loading

0 comments on commit e27037e

Please sign in to comment.