Skip to content

Commit

Permalink
Update support for latest P2P library
Browse files Browse the repository at this point in the history
  • Loading branch information
LZRS committed Oct 11, 2023
1 parent 5658dc1 commit 3bc90d2
Show file tree
Hide file tree
Showing 9 changed files with 172 additions and 98 deletions.
6 changes: 3 additions & 3 deletions android/deps.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,12 @@
// This file is referenced by the project-level build.gradle file.
// Entries in each section of this file should be sorted alphabetically.
def sdk_versions = [:]
sdk_versions.compile_sdk = 33
sdk_versions.compile_sdk = 34
sdk_versions.min_sdk = 26
sdk_versions.target_sdk = 33
sdk_versions.target_sdk = 34
ext.sdk_versions = sdk_versions

def build_tool_version = '30.0.3'
def build_tool_version = '34.0.0'
ext.build_tool_version = build_tool_version

def versions = [:]
Expand Down
2 changes: 1 addition & 1 deletion android/engine/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -179,7 +179,7 @@ dependencies {
implementation "androidx.datastore:datastore-preferences:1.0.0"

// P2P dependency
implementation('org.smartregister:p2p-lib:0.3.0-SNAPSHOT')
api('org.smartregister:p2p-lib:0.6.8-SNAPSHOT')

//Configure Jetpack Compose
def composeVersion = versions.compose
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ data class ApplicationConfiguration(
var theme: String = "",
var languages: List<String> = listOf("en"),
var syncInterval: Long = 30,
val deviceToDeviceSync: DeviceToDeviceSyncConfig? = null,
var scheduleDefaultPlanWorker: Boolean = true,
var applicationName: String = "",
var appLogoIconResourceFile: String = "ic_default_logo",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
/*
* Copyright 2021 Ona Systems, Inc
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.smartregister.fhircore.engine.configuration.app

import kotlinx.serialization.Serializable

@Serializable data class DeviceToDeviceSyncConfig(val resourcesToSync: List<String>? = null)
Original file line number Diff line number Diff line change
Expand Up @@ -20,118 +20,126 @@ 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.db.ResourceNotFoundException
import com.google.android.fhir.get
import com.google.android.fhir.logicalId
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.configuration.app.AppConfigClassification
import org.smartregister.fhircore.engine.configuration.app.ApplicationConfiguration
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()

open fun getDataTypes(): TreeSet<DataType> =
open fun getDataTypes(): TreeSet<DataType> {
val appRegistry =
configurationRegistry.retrieveConfiguration<ApplicationConfiguration>(
AppConfigClassification.APPLICATION
)
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.Questionnaire,
ResourceType.QuestionnaireResponse,
ResourceType.Observation,
ResourceType.Encounter
ResourceType.Encounter,
)
.mapIndexed { index, resourceType ->
DataType(name = resourceType.name, DataType.Filetype.JSON, index)
}
},
)

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,
classType: Class<out Resource>
): List<Resource> {
offset: Int,
classType: Class<out 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,36 +16,42 @@

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

import androidx.annotation.NonNull
import com.google.android.fhir.FhirEngine
import com.google.android.fhir.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,44 +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)
}
val records = runBlocking {
dataType.name.resourceClassType().let { classType ->
loadResources(
lastRecordUpdatedAt = highestRecordId,
batchSize = batchSize,
offset = offset,
classType,
)
}
?: listOf()
}

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
else lastUpdated)
(if (records.isNotEmpty()) {
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!!
else highestRecordId
Timber.e("Sending ${it.resourceType} with id ====== ${it.logicalId}")
if (it.resource.meta?.lastUpdated?.time!! > highestRecordId) {
it.resource.meta?.lastUpdated?.time!!
} else {
highestRecordId
}
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

0 comments on commit 3bc90d2

Please sign in to comment.