Skip to content

Commit

Permalink
Update resources via rules engine
Browse files Browse the repository at this point in the history
* Allow resource update via RulesEngineService

for secondary resources in migrations

* Update android/engine/src/main/java/org/smartregister/fhircore/engine/rulesengine/RulesFactory.kt

Co-authored-by: Elly Kitoto <[email protected]>

* Update android/engine/src/main/java/org/smartregister/fhircore/engine/rulesengine/RulesFactory.kt

Co-authored-by: Elly Kitoto <[email protected]>

* Run spotlessApply

---------

Co-authored-by: Elly Kitoto <[email protected]>
  • Loading branch information
qiarie and ellykits authored Jul 19, 2024
1 parent 1b01013 commit c56258f
Show file tree
Hide file tree
Showing 6 changed files with 409 additions and 0 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ data class MigrationConfig(
val purgeAffectedResources: Boolean = false,
val createLocalChangeEntitiesAfterPurge: Boolean = true,
val resourceFilterExpression: ResourceFilterExpression? = null,
val secondaryResources: List<FhirResourceConfig>? = null,
) : java.io.Serializable

@Serializable
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,22 @@
package org.smartregister.fhircore.engine.rulesengine

import android.content.Context
import ca.uhn.fhir.context.FhirContext
import com.google.android.fhir.datacapture.extensions.logicalId
import com.google.android.fhir.search.Order
import com.jayway.jsonpath.Configuration
import com.jayway.jsonpath.JsonPath
import com.jayway.jsonpath.Option
import com.jayway.jsonpath.PathNotFoundException
import dagger.hilt.android.qualifiers.ApplicationContext
import java.math.BigDecimal
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
import javax.inject.Inject
import kotlin.system.measureTimeMillis
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import org.hl7.fhir.r4.model.Base
import org.hl7.fhir.r4.model.Enumerations.DataType
import org.hl7.fhir.r4.model.Resource
Expand All @@ -37,6 +44,7 @@ import org.joda.time.DateTime
import org.ocpsoft.prettytime.PrettyTime
import org.smartregister.fhircore.engine.BuildConfig
import org.smartregister.fhircore.engine.configuration.ConfigurationRegistry
import org.smartregister.fhircore.engine.data.local.DefaultRepository
import org.smartregister.fhircore.engine.domain.model.RelatedResourceCount
import org.smartregister.fhircore.engine.domain.model.RepositoryResourceData
import org.smartregister.fhircore.engine.domain.model.RuleConfig
Expand All @@ -48,6 +56,7 @@ import org.smartregister.fhircore.engine.util.SharedPreferenceKey
import org.smartregister.fhircore.engine.util.extension.SDF_DD_MMM_YYYY
import org.smartregister.fhircore.engine.util.extension.SDF_E_MMM_DD_YYYY
import org.smartregister.fhircore.engine.util.extension.daysPassed
import org.smartregister.fhircore.engine.util.extension.encodeResourceToString
import org.smartregister.fhircore.engine.util.extension.extractAge
import org.smartregister.fhircore.engine.util.extension.extractBirthDate
import org.smartregister.fhircore.engine.util.extension.extractGender
Expand All @@ -69,6 +78,8 @@ constructor(
val fhirPathDataExtractor: FhirPathDataExtractor,
val dispatcherProvider: DispatcherProvider,
val locationService: LocationService,
val fhirContext: FhirContext,
val defaultRepository: DefaultRepository,
) : RulesListener() {
val rulesEngineService = RulesEngineService()
private var facts: Facts = Facts()
Expand Down Expand Up @@ -136,6 +147,11 @@ constructor(
/** Provide access to utility functions accessible to the users defining rules in JSON format. */
inner class RulesEngineService {

val parser = fhirContext.newJsonParser()

private var conf: Configuration =
Configuration.defaultConfiguration().apply { addOptions(Option.DEFAULT_PATH_LEAF_TO_NULL) }

/**
* This function creates a property key from the string [value] and uses the key to retrieve the
* correct translation from the string.properties file.
Expand Down Expand Up @@ -462,6 +478,48 @@ constructor(
}
.getOrNull()

fun filterResourcesByJsonPath(
resources: List<Resource>?,
jsonPathExpression: String,
dataType: String,
value: Any,
vararg compareToResult: Any,
): List<Resource>? {
if (resources.isNullOrEmpty() || jsonPathExpression.isBlank()) return null

val expression =
if (jsonPathExpression.startsWith("\$")) {
jsonPathExpression
} else {
jsonPathExpression.replace(
jsonPathExpression.substring(0, jsonPathExpression.indexOf(".")),
"\$",
)
}

return runCatching {
resources.filter {
val document = JsonPath.using(conf).parse(it.encodeResourceToString())
val result: Any = document.read(expression)

when (DataType.valueOf(dataType.uppercase())) {
DataType.BOOLEAN -> (result as Boolean).compareTo(value as Boolean) in compareToResult
DataType.DATE -> (result as Date).compareTo(value as Date) in compareToResult
DataType.DATETIME ->
(result as DateTime).compareTo(value as DateTime) in compareToResult
DataType.DECIMAL ->
(result as BigDecimal).compareTo(value as BigDecimal) in compareToResult
DataType.INTEGER -> (result as Int).compareTo(value as Int) in compareToResult
DataType.STRING -> (result as String).compareTo(value as String) in compareToResult
else -> {
false
}
}
}
}
.getOrNull()
}

/**
* This function combines all string indexes to a list separated by the separator and regex
* defined by the content author
Expand Down Expand Up @@ -578,6 +636,59 @@ constructor(
}
}
}

@JvmOverloads
fun updateResource(
resource: Resource?,
path: String?,
value: Any?,
purgeAffectedResources: Boolean = false,
createLocalChangeEntitiesAfterPurge: Boolean = true,
) {
if (resource == null || path.isNullOrEmpty()) return

val jsonParse = JsonPath.using(conf).parse(resource.encodeResourceToString())

val updatedResourceDocument =
try {
jsonParse.apply {
// Expression stars with '$' (JSONPath) or ResourceType like in FHIRPath
if (path.startsWith("\$") && value != null) {
set(path, value)
}
if (
path.startsWith(
resource.resourceType.name,
ignoreCase = true,
) && value != null
) {
set(
path.replace(resource.resourceType.name, "\$"),
value,
)
}

if (resource.id.startsWith("#")) {
val idPath = "\$.id"
set(idPath, resource.id.replace("#", ""))
}
}
} catch (e: PathNotFoundException) {
Timber.e(e, "Path $path not found")
jsonParse
}

val updatedResource =
parser.parseResource(resource::class.java, updatedResourceDocument.jsonString())
CoroutineScope(dispatcherProvider.io()).launch {
if (purgeAffectedResources) {
defaultRepository.purge(updatedResource as Resource, forcePurge = true)
}
if (createLocalChangeEntitiesAfterPurge) {
defaultRepository.addOrUpdate(resource = updatedResource as Resource)
} else defaultRepository.createRemote(resource = arrayOf(updatedResource as Resource))
}
}
}

companion object {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,10 @@ package org.smartregister.fhircore.engine.rulesengine
import androidx.compose.runtime.mutableStateMapOf
import androidx.compose.runtime.snapshots.SnapshotStateList
import androidx.test.core.app.ApplicationProvider
import ca.uhn.fhir.context.FhirContext
import dagger.hilt.android.testing.HiltAndroidRule
import dagger.hilt.android.testing.HiltAndroidTest
import io.mockk.mockk
import io.mockk.spyk
import java.util.LinkedList
import javax.inject.Inject
Expand All @@ -40,6 +42,7 @@ import org.smartregister.fhircore.engine.configuration.ConfigurationRegistry
import org.smartregister.fhircore.engine.configuration.register.RegisterCardConfig
import org.smartregister.fhircore.engine.configuration.view.ListProperties
import org.smartregister.fhircore.engine.configuration.view.ListResource
import org.smartregister.fhircore.engine.data.local.DefaultRepository
import org.smartregister.fhircore.engine.domain.model.RepositoryResourceData
import org.smartregister.fhircore.engine.domain.model.ResourceData
import org.smartregister.fhircore.engine.domain.model.RuleConfig
Expand Down Expand Up @@ -69,10 +72,14 @@ class ResourceDataRulesExecutorTest : RobolectricTest() {
private lateinit var rulesFactory: RulesFactory
private lateinit var resourceDataRulesExecutor: ResourceDataRulesExecutor

@Inject lateinit var fhirContext: FhirContext
private lateinit var defaultRepository: DefaultRepository

@Before
@kotlinx.coroutines.ExperimentalCoroutinesApi
fun setUp() {
hiltAndroidRule.inject()
defaultRepository = mockk(relaxed = true)
rulesFactory =
spyk(
RulesFactory(
Expand All @@ -81,6 +88,8 @@ class ResourceDataRulesExecutorTest : RobolectricTest() {
fhirPathDataExtractor = fhirPathDataExtractor,
dispatcherProvider = dispatcherProvider,
locationService = locationService,
fhirContext = fhirContext,
defaultRepository = defaultRepository,
),
)
resourceDataRulesExecutor = ResourceDataRulesExecutor(rulesFactory)
Expand Down
Loading

0 comments on commit c56258f

Please sign in to comment.