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

Update p2p version to 0.6.11-preview1-SNAPSHOT #106

Draft
wants to merge 1 commit into
base: mwcore-dev
Choose a base branch
from
Draft
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 android/engine/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,7 @@ dependencies {
implementation("androidx.datastore:datastore-preferences:1.1.1")

// P2P dependency
api("org.smartregister:p2p-lib:0.3.0-SNAPSHOT")
api("org.smartregister:p2p-lib:0.6.11-preview1-SNAPSHOT")

// Configure Jetpack Compose
api(platform("androidx.compose:compose-bom:2024.05.00"))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,19 @@
"en",
"sw"
],
"deviceToDeviceSync": {
"resourcesToSync": [
"Group",
"Patient",
"CarePlan",
"Task",
"Encounter",
"Observation",
"Condition",
"Questionnaire",
"QuestionnaireResponse"
]
},
"applicationName": "Sample App",
"appLogoIconResourceFile": "ic_launcher",
"count": "100",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,4 @@ sealed class AppFeature(val name: String) {
object PatientManagement : AppFeature(name = "PatientManagement")

object HouseholdManagement : AppFeature(name = "HouseholdManagement")

object DeviceToDeviceSync : AppFeature(name = "DeviceToDeviceSync")
}
Original file line number Diff line number Diff line change
Expand Up @@ -66,3 +66,5 @@ data class Resource(
data class Parameter(
@SerializedName("resource") var resource: Resource,
)

@Serializable data class DeviceToDeviceSyncConfig(val resourcesToSync: List<String>? = null)
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ package org.smartregister.fhircore.engine.configuration.app
import kotlinx.serialization.Serializable
import org.smartregister.fhircore.engine.configuration.Configuration
import org.smartregister.fhircore.engine.configuration.ConfigurationRegistry
import org.smartregister.fhircore.engine.configuration.DeviceToDeviceSyncConfig
import org.smartregister.fhircore.engine.util.SystemConstants

@Serializable
Expand All @@ -38,6 +39,7 @@ data class ApplicationConfiguration(
var registrationForm: String = "patient-demographic-registration",
var supportEmail: String = "[email protected]",
var supportPhoneNumber: String = "",
val deviceToDeviceSync: DeviceToDeviceSyncConfig? = null,
) : Configuration

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,44 +17,61 @@
package org.smartregister.fhircore.engine.p2p.dao

import ca.uhn.fhir.context.FhirContext
import ca.uhn.fhir.context.FhirVersionEnum
import ca.uhn.fhir.parser.IParser
import ca.uhn.fhir.rest.gclient.DateClientParam
import ca.uhn.fhir.rest.gclient.StringClientParam
import ca.uhn.fhir.rest.param.ParamPrefixEnum
import com.google.android.fhir.FhirEngine
import com.google.android.fhir.datacapture.extensions.logicalId
import com.google.android.fhir.db.ResourceNotFoundException
import com.google.android.fhir.get
import com.google.android.fhir.SearchResult
import com.google.android.fhir.search.Order
import com.google.android.fhir.search.Search
import com.google.android.fhir.search.search
import com.google.android.fhir.sync.SyncDataParams
import java.util.Date
import java.util.TreeSet
import kotlinx.coroutines.withContext
import org.hl7.fhir.r4.model.DateTimeType
import org.hl7.fhir.r4.model.Encounter
import org.hl7.fhir.r4.model.Group
import org.hl7.fhir.r4.model.Observation
import org.hl7.fhir.r4.model.Patient
import org.hl7.fhir.r4.model.Questionnaire
import org.hl7.fhir.r4.model.QuestionnaireResponse
import org.hl7.fhir.r4.model.Resource
import org.hl7.fhir.r4.model.ResourceType
import org.smartregister.fhircore.engine.configuration.ConfigurationRegistry
import org.smartregister.fhircore.engine.util.DispatcherProvider
import org.smartregister.fhircore.engine.util.extension.generateMissingId
import org.smartregister.fhircore.engine.util.extension.updateFrom
import org.smartregister.fhircore.engine.util.extension.updateLastUpdated
import org.smartregister.fhircore.engine.util.extension.isValidResourceType
import org.smartregister.fhircore.engine.util.extension.resourceClassType
import org.smartregister.p2p.model.RecordCount
import org.smartregister.p2p.sync.DataType

open class BaseP2PTransferDao
constructor(open val fhirEngine: FhirEngine, open val dispatcherProvider: DispatcherProvider) {
constructor(
open val fhirEngine: FhirEngine,
open val dispatcherProvider: DispatcherProvider,
open val configurationRegistry: ConfigurationRegistry,
) {

protected val jsonParser: IParser = FhirContext.forCached(FhirVersionEnum.R4).newJsonParser()
protected val jsonParser: IParser = FhirContext.forR4Cached().newJsonParser()

open fun getDataTypes(): TreeSet<DataType> =
open fun getDataTypes(): TreeSet<DataType> {
val appRegistry = configurationRegistry.getAppConfigs()
val deviceToDeviceSyncConfigs = appRegistry.deviceToDeviceSync

return if (
deviceToDeviceSyncConfigs?.resourcesToSync != null &&
deviceToDeviceSyncConfigs.resourcesToSync.isNotEmpty()
) {
getDynamicDataTypes(deviceToDeviceSyncConfigs.resourcesToSync)
} else {
getDefaultDataTypes()
}
}

open fun getDefaultDataTypes(): TreeSet<DataType> =
TreeSet<DataType>(
listOf(
ResourceType.Group,
ResourceType.Patient,
ResourceType.CarePlan,
ResourceType.Task,
ResourceType.Condition,
ResourceType.List,
ResourceType.RelatedPerson,
ResourceType.Questionnaire,
ResourceType.QuestionnaireResponse,
ResourceType.Observation,
Expand All @@ -65,73 +82,64 @@ constructor(open val fhirEngine: FhirEngine, open val dispatcherProvider: Dispat
},
)

suspend fun <R : Resource> addOrUpdate(resource: R) {
return withContext(dispatcherProvider.io()) {
resource.updateLastUpdated()
try {
fhirEngine.get(resource.resourceType, resource.logicalId).run {
fhirEngine.update(updateFrom(resource))
}
} catch (resourceNotFoundException: ResourceNotFoundException) {
resource.generateMissingId()
fhirEngine.create(resource)
}
}
}
open fun getDynamicDataTypes(resourceList: List<String>): TreeSet<DataType> =
TreeSet<DataType>(
resourceList
.filter { isValidResourceType(it) }
.mapIndexed { index, resource -> DataType(name = resource, DataType.Filetype.JSON, index) },
)

suspend fun loadResources(
lastRecordUpdatedAt: Long,
batchSize: Int,
offset: Int,
classType: Class<out Resource>,
): List<Resource> {
): List<SearchResult<Resource>> {
return withContext(dispatcherProvider.io()) {
// TODO FIX search order by _lastUpdated; SearchQuery no longer allowed in search API

/* val searchQuery =
SearchQuery(
"""
SELECT a.serializedResource, b.index_to
FROM ResourceEntity a
LEFT JOIN DateTimeIndexEntity b
ON a.resourceType = b.resourceType AND a.resourceId = b.resourceId AND b.index_name = '_lastUpdated'
WHERE a.resourceType = '${classType.newInstance().resourceType}'
AND a.resourceId IN (
SELECT resourceId FROM DateTimeIndexEntity
WHERE resourceType = '${classType.newInstance().resourceType}' AND index_name = '_lastUpdated' AND index_to > ?
)
ORDER BY b.index_from ASC
LIMIT ?
""".trimIndent(),
listOf(lastRecordUpdatedAt, batchSize)
)

fhirEngine.search(searchQuery)*/

val search =
Search(type = classType.newInstance().resourceType).apply {
filter(
DateClientParam("_lastUpdated"),
DateClientParam(SyncDataParams.LAST_UPDATED_KEY),
{
value = of(DateTimeType(Date(lastRecordUpdatedAt)))
prefix = ParamPrefixEnum.GREATERTHAN
prefix = ParamPrefixEnum.GREATERTHAN_OR_EQUALS
},
)

// sort(StringClientParam("_lastUpdated"), Order.ASCENDING)
sort(StringClientParam(SyncDataParams.LAST_UPDATED_KEY), Order.ASCENDING)
count = batchSize
from = offset
}
fhirEngine.search<Resource>(search).map { it.resource }
fhirEngine.search(search)
}
}

suspend fun countTotalRecordsForSync(highestRecordIdMap: HashMap<String, Long>): RecordCount {
var totalRecordCount: Long = 0
val resourceCountMap: HashMap<String, Long> = HashMap()

getDataTypes().forEach {
it.name.resourceClassType().let { classType ->
val lastRecordId = highestRecordIdMap[it.name] ?: 0L
val searchCount = getSearchObjectForCount(lastRecordId, classType)
val resourceCount = fhirEngine.count(searchCount)
totalRecordCount += resourceCount
resourceCountMap[it.name] = resourceCount
}
}

return RecordCount(totalRecordCount, resourceCountMap)
}

fun resourceClassType(type: DataType) =
when (ResourceType.valueOf(type.name)) {
ResourceType.Group -> Group::class.java
ResourceType.Encounter -> Encounter::class.java
ResourceType.Observation -> Observation::class.java
ResourceType.Patient -> Patient::class.java
ResourceType.Questionnaire -> Questionnaire::class.java
ResourceType.QuestionnaireResponse -> QuestionnaireResponse::class.java
else -> null // TODO support other resource types
fun getSearchObjectForCount(lastRecordUpdatedAt: Long, classType: Class<out Resource>): Search {
return Search(type = classType.newInstance().resourceType).apply {
filter(
DateClientParam(SyncDataParams.LAST_UPDATED_KEY),
{
value = of(DateTimeType(Date(lastRecordUpdatedAt)))
prefix = ParamPrefixEnum.GREATERTHAN_OR_EQUALS
},
)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,39 +16,42 @@

package org.smartregister.fhircore.engine.p2p.dao

import androidx.annotation.NonNull
import com.google.android.fhir.FhirEngine
import com.google.android.fhir.datacapture.extensions.logicalId
import java.util.TreeSet
import javax.inject.Inject
import kotlinx.coroutines.runBlocking
import org.json.JSONArray
import org.smartregister.fhircore.engine.configuration.ConfigurationRegistry
import org.smartregister.fhircore.engine.data.local.DefaultRepository
import org.smartregister.fhircore.engine.util.DispatcherProvider
import org.smartregister.fhircore.engine.util.extension.resourceClassType
import org.smartregister.p2p.dao.ReceiverTransferDao
import org.smartregister.p2p.sync.DataType
import timber.log.Timber

open class P2PReceiverTransferDao
@Inject
constructor(fhirEngine: FhirEngine, dispatcherProvider: DispatcherProvider) :
BaseP2PTransferDao(fhirEngine, dispatcherProvider), ReceiverTransferDao {
constructor(
fhirEngine: FhirEngine,
dispatcherProvider: DispatcherProvider,
configurationRegistry: ConfigurationRegistry,
val defaultRepository: DefaultRepository,
) : BaseP2PTransferDao(fhirEngine, dispatcherProvider, configurationRegistry), ReceiverTransferDao {

override fun getP2PDataTypes(): TreeSet<DataType> = getDataTypes()

override fun receiveJson(@NonNull type: DataType, @NonNull jsonArray: JSONArray): Long {
override fun receiveJson(type: DataType, jsonArray: JSONArray): Long {
var maxLastUpdated = 0L
Timber.e("saving resources from base dai")
Timber.i("saving resources from base dai ${type.name} -> ${jsonArray.length()}")
(0 until jsonArray.length()).forEach {
runBlocking {
val resource =
jsonParser.parseResource(resourceClassType(type), jsonArray.get(it).toString())
addOrUpdate(resource = resource)
jsonParser.parseResource(type.name.resourceClassType(), jsonArray.get(it).toString())
val recordLastUpdated = resource.meta.lastUpdated.time
defaultRepository.addOrUpdate(resource = resource)
maxLastUpdated =
(if (resource.meta.lastUpdated.time > maxLastUpdated) {
resource.meta.lastUpdated.time
} else {
maxLastUpdated
})
(if (recordLastUpdated > maxLastUpdated) recordLastUpdated else maxLastUpdated)
Timber.e("Received ${resource.resourceType} with id = ${resource.logicalId}")
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,49 +22,70 @@ import java.util.TreeSet
import javax.inject.Inject
import kotlinx.coroutines.runBlocking
import org.json.JSONArray
import org.smartregister.fhircore.engine.configuration.ConfigurationRegistry
import org.smartregister.fhircore.engine.util.DefaultDispatcherProvider
import org.smartregister.fhircore.engine.util.extension.resourceClassType
import org.smartregister.p2p.dao.SenderTransferDao
import org.smartregister.p2p.model.RecordCount
import org.smartregister.p2p.search.data.JsonData
import org.smartregister.p2p.sync.DataType
import timber.log.Timber

class P2PSenderTransferDao
@Inject
constructor(fhirEngine: FhirEngine, dispatcherProvider: DefaultDispatcherProvider) :
BaseP2PTransferDao(fhirEngine, dispatcherProvider), SenderTransferDao {
constructor(
fhirEngine: FhirEngine,
dispatcherProvider: DefaultDispatcherProvider,
configurationRegistry: ConfigurationRegistry,
) : BaseP2PTransferDao(fhirEngine, dispatcherProvider, configurationRegistry), SenderTransferDao {

override fun getP2PDataTypes(): TreeSet<DataType> = getDataTypes()

override fun getJsonData(dataType: DataType, lastUpdated: Long, batchSize: Int): JsonData? {
override fun getTotalRecordCount(highestRecordIdMap: HashMap<String, Long>): RecordCount {
return runBlocking { countTotalRecordsForSync(highestRecordIdMap) }
}

override fun getJsonData(
dataType: DataType,
lastUpdated: Long,
batchSize: Int,
offset: Int,
): JsonData? {
// TODO: complete retrieval of data implementation
Timber.e("Last updated at value is $lastUpdated")
var highestRecordId = lastUpdated

val records =
runBlocking {
resourceClassType(dataType)?.let { classType ->
loadResources(lastRecordUpdatedAt = highestRecordId, batchSize = batchSize, classType)
}
} ?: listOf()
val records = runBlocking {
dataType.name.resourceClassType().let { classType ->
loadResources(
lastRecordUpdatedAt = highestRecordId,
batchSize = batchSize,
offset = offset,
classType,
)
}
}

Timber.e("Fetching resources from base dao of type $dataType.name")
Timber.i("Fetching resources from base dao of type $dataType.name")
highestRecordId =
(if (records.isNotEmpty()) {
records.last().meta?.lastUpdated?.time ?: highestRecordId
records.last().resource.meta?.lastUpdated?.time ?: highestRecordId
} else {
lastUpdated
})

val jsonArray = JSONArray()
records.forEach {
jsonArray.put(jsonParser.encodeResourceToString(it))
jsonArray.put(jsonParser.encodeResourceToString(it.resource))
highestRecordId =
if (it.meta?.lastUpdated?.time!! > highestRecordId) {
it.meta?.lastUpdated?.time!!
if (it.resource.meta?.lastUpdated?.time!! > highestRecordId) {
it.resource.meta?.lastUpdated?.time!!
} else {
highestRecordId
}
Timber.e("Sending ${it.resourceType} with id ====== ${it.logicalId}")
Timber.i(
"Sending ${it.resource.resourceType} with id ====== ${it.resource.logicalId} and lastUpdated = ${it.resource.meta?.lastUpdated?.time!!}",
)
}

Timber.e("New highest Last updated at value is $highestRecordId")
Expand Down
Loading
Loading