Skip to content

Commit

Permalink
Added a versatile approach to eval expressions with AND and OR
Browse files Browse the repository at this point in the history
  • Loading branch information
grzesiek2010 committed Dec 13, 2024
1 parent a5eba56 commit 5a2d685
Show file tree
Hide file tree
Showing 6 changed files with 164 additions and 33 deletions.
6 changes: 6 additions & 0 deletions db/src/main/java/org/odk/collect/db/sqlite/Query.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package org.odk.collect.db.sqlite

data class Query(
val selection: String,
val selectionArgs: Array<String>
)
1 change: 1 addition & 0 deletions entities/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ dependencies {
implementation(project(":material"))
implementation(project(":async"))
implementation(project(":lists"))
implementation(project(":db"))

implementation(libs.kotlinStdlib)
implementation(libs.javarosa) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,10 @@ import org.javarosa.core.model.condition.EvaluationContext
import org.javarosa.core.model.condition.FilterStrategy
import org.javarosa.core.model.instance.DataInstance
import org.javarosa.core.model.instance.TreeReference
import org.javarosa.xpath.expr.XPathBoolExpr
import org.javarosa.xpath.expr.XPathEqExpr
import org.javarosa.xpath.expr.XPathExpression
import org.odk.collect.db.sqlite.Query
import org.odk.collect.entities.javarosa.intance.LocalEntitiesInstanceAdapter
import org.odk.collect.entities.javarosa.intance.LocalEntitiesInstanceProvider
import org.odk.collect.entities.storage.EntitiesRepository
Expand Down Expand Up @@ -36,30 +38,86 @@ class LocalEntitiesFilterStrategy(entitiesRepository: EntitiesRepository) :
return next.get()
}

val query = xPathExpressionToQuery(predicate, sourceInstance, evaluationContext)

return if (query != null) {
queryToTreeReferences(query, sourceInstance, next)
} else {
next.get()
}
}

private fun xPathExpressionToQuery(
predicate: XPathExpression,
sourceInstance: DataInstance<*>,
evaluationContext: EvaluationContext,
): Query? {
return when (predicate) {
is XPathBoolExpr -> xPathBoolExprToQuery(predicate, sourceInstance, evaluationContext)
is XPathEqExpr -> xPathEqExprToQuery(predicate, sourceInstance, evaluationContext)
else -> null
}
}

private fun xPathBoolExprToQuery(
predicate: XPathBoolExpr,
sourceInstance: DataInstance<*>,
evaluationContext: EvaluationContext,
): Query? {
val queryA = xPathExpressionToQuery(predicate.a, sourceInstance, evaluationContext)
val queryB = xPathExpressionToQuery(predicate.b, sourceInstance, evaluationContext)

return if (queryA != null && queryB != null) {
val selection = if (predicate.op == XPathBoolExpr.AND) {
"${queryA.selection} AND ${queryB.selection}"
} else {
"${queryA.selection} OR ${queryB.selection}"
}

return Query(selection, arrayOf(*queryA.selectionArgs, *queryB.selectionArgs))
} else {
null
}
}

private fun xPathEqExprToQuery(
predicate: XPathEqExpr,
sourceInstance: DataInstance<*>,
evaluationContext: EvaluationContext,
): Query? {
val candidate = CompareToNodeExpression.parse(predicate)
return when (val original = candidate?.original) {
is XPathEqExpr -> {
val child = candidate.nodeSide.steps[0].name.name
val value = candidate.evalContextSide(sourceInstance, evaluationContext) as String

val selection = if (original.isEqual) {
"$child = ?"
} else {
"$child != ?"
}
val selectionArgs = arrayOf(value)
return if (candidate != null) {
val child = candidate.nodeSide.steps[0].name.name
val value = candidate.evalContextSide(sourceInstance, evaluationContext) as String

val results = instanceAdapter.query(sourceInstance.instanceId, selection, selectionArgs)
sourceInstance.replacePartialElements(results)
results.map {
it.parent = sourceInstance.root
it.ref
}
val selection = if (predicate.isEqual) {
"$child = ?"
} else {
"$child != ?"
}
val selectionArgs = arrayOf(value)

else -> {
next.get()
Query(selection, selectionArgs)
} else {
null
}
}

private fun queryToTreeReferences(
query: Query?,
sourceInstance: DataInstance<*>,
next: Supplier<MutableList<TreeReference>>
): List<TreeReference> {
return if (query != null) {
val results = instanceAdapter.query(sourceInstance.instanceId, query.selection, query.selectionArgs)
sourceInstance.replacePartialElements(results)
results.map {
it.parent = sourceInstance.root
it.ref
}
} else {
next.get()
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -51,23 +51,51 @@ class InMemEntitiesRepository : EntitiesRepository {
selection: String,
selectionArgs: Array<String>
): List<Entity.Saved> {
val conditions = selection.split("AND", "OR").map { it.trim() }
val operators = Regex("(AND|OR)").findAll(selection).map { it.value }.toList()

return getEntities(list).filter { entity ->
val (fieldName, operator, _) = selection.split(" ").map { it }
val value = selectionArgs.first()

val fieldValue = when (fieldName) {
"name" -> entity.id
"label" -> entity.label
"__version" -> entity.version
else -> entity.properties.find { it.first == fieldName }?.second
}.toString()

when (operator) {
"=" -> fieldValue == value
"!=" -> fieldValue != value
else -> false
val results = conditions.mapIndexed { index, condition ->
val (fieldName, operator, _) = condition.split(" ").map { it }
val value = selectionArgs.getOrNull(index) ?: ""

evaluateCondition(entity, fieldName, operator, value)
}
combineResults(results, operators)
}
}

private fun evaluateCondition(
entity: Entity.Saved,
fieldName: String,
operator: String,
value: String
): Boolean {
val fieldValue = when (fieldName) {
"name" -> entity.id
"label" -> entity.label
"__version" -> entity.version
else -> entity.properties.find { it.first == fieldName }?.second
}.toString()

return when (operator) {
"=" -> fieldValue == value
"!=" -> fieldValue != value
else -> false
}
}

private fun combineResults(results: List<Boolean>, operators: List<String>): Boolean {
var combinedResult = results.firstOrNull() ?: false

for (i in 1 until results.size) {
when (operators.getOrNull(i - 1)) {
"AND" -> combinedResult = combinedResult && results[i]
"OR" -> combinedResult = combinedResult || results[i]
}
}

return combinedResult
}

override fun getById(list: String, id: String): Entity.Saved? {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -480,6 +480,44 @@ class LocalEntitiesFilterStrategyTest {
val choices = scenario.choicesOf("/data/question").map { it.value }
assertThat(choices, containsInAnyOrder("thing1"))
}

@Test
fun `x`() {
entitiesRepository.save("things", Entity.New("thing1", "Thing1", properties = listOf("foo" to "1", "bar" to "1")))
entitiesRepository.save("things", Entity.New("thing2", "Thing2", properties = listOf("foo" to "1", "bar" to "2")))
entitiesRepository.save("things", Entity.New("thing3", "Thing3", properties = listOf("foo" to "2", "bar" to "3")))

val scenario = Scenario.init(
"Secondary instance form",
html(
head(
title("Secondary instance form"),
model(
mainInstance(
t(
"data id=\"create-entity-form\"",
t("question"),
)
),
t("instance id=\"things\" src=\"jr://file-csv/things.csv\""),
bind("/data/question").type("string")
)
),
body(
select1Dynamic(
"/data/question",
"instance('things')/root/item[foo='1' and bar='2']",
"name",
"label"
)
)
),
controllerSupplier
)

val choices = scenario.choicesOf("/data/question").map { it.value }
assertThat(choices, containsInAnyOrder("thing2"))
}
}

private class FallthroughFilterStrategy : FilterStrategy {
Expand Down
2 changes: 1 addition & 1 deletion gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ danlewAndroidJoda = { group = "net.danlew", name = "android.joda", version = "2.
rarepebbleColorpicker = { group = "com.github.martin-stone", name = "hsv-alpha-color-picker-android", version = "3.1.0" }
commonsIo = { group = "commons-io", name = "commons-io", version = "2.5" } # Commons 2.6+ introduce java.nio usage that we can't access until our minSdkVersion >= 26 (https://developer.android.com/reference/java/io/File#toPath())
opencsv = { group = "com.opencsv", name = "opencsv", version = "5.9" }
javarosa = { group = "org.getodk", name = "javarosa", version = "5.0.0" } # Online
javarosa = { group = "org.getodk", name = "javarosa", version = "5.1.0-SNAPSHOT-ab0e8f4" } # Online
# javarosa = { group = "org.getodk", name = "javarosa", version = "local" } # Local
karumiDexter = { group = "com.karumi", name = "dexter", version = "6.2.3" }
zxingAndroidEmbedded = { group = "com.journeyapps", name = "zxing-android-embedded", version = "4.3.0" }
Expand Down

0 comments on commit 5a2d685

Please sign in to comment.