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

[MWCore] Support P2P #2810

Closed
wants to merge 10 commits into from
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 @@ -37,6 +37,11 @@
"HOUSEHOLD_VISITS",
"REMOVE_HOUSEHOLD_MEMBER"
]
},
{
"feature": "DeviceToDeviceSync",
"settings": {},
"active": true
}
]
}
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 @@ -29,6 +29,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.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()

Check warning on line 66 in android/engine/src/main/java/org/smartregister/fhircore/engine/p2p/dao/BaseP2PTransferDao.kt

View check run for this annotation

Codecov / codecov/patch

android/engine/src/main/java/org/smartregister/fhircore/engine/p2p/dao/BaseP2PTransferDao.kt#L66

Added line #L66 was not covered by tests
}
}

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),

Check warning on line 102 in android/engine/src/main/java/org/smartregister/fhircore/engine/p2p/dao/BaseP2PTransferDao.kt

View check run for this annotation

Codecov / codecov/patch

android/engine/src/main/java/org/smartregister/fhircore/engine/p2p/dao/BaseP2PTransferDao.kt#L102

Added line #L102 was not covered by tests
{
value = of(DateTimeType(Date(lastRecordUpdatedAt)))
prefix = ParamPrefixEnum.GREATERTHAN
}
prefix = ParamPrefixEnum.GREATERTHAN_OR_EQUALS
},

Check warning on line 106 in android/engine/src/main/java/org/smartregister/fhircore/engine/p2p/dao/BaseP2PTransferDao.kt

View check run for this annotation

Codecov / codecov/patch

android/engine/src/main/java/org/smartregister/fhircore/engine/p2p/dao/BaseP2PTransferDao.kt#L105-L106

Added lines #L105 - L106 were not covered by tests
)

// sort(StringClientParam("_lastUpdated"), Order.ASCENDING)
sort(StringClientParam(SyncDataParams.LAST_UPDATED_KEY), Order.ASCENDING)

Check warning on line 109 in android/engine/src/main/java/org/smartregister/fhircore/engine/p2p/dao/BaseP2PTransferDao.kt

View check run for this annotation

Codecov / codecov/patch

android/engine/src/main/java/org/smartregister/fhircore/engine/p2p/dao/BaseP2PTransferDao.kt#L109

Added line #L109 was not covered by tests
count = batchSize
from = offset

Check warning on line 111 in android/engine/src/main/java/org/smartregister/fhircore/engine/p2p/dao/BaseP2PTransferDao.kt

View check run for this annotation

Codecov / codecov/patch

android/engine/src/main/java/org/smartregister/fhircore/engine/p2p/dao/BaseP2PTransferDao.kt#L111

Added line #L111 was not covered by tests
}
fhirEngine.search<Resource>(search).map { it.resource }
fhirEngine.search(search)

Check warning on line 113 in android/engine/src/main/java/org/smartregister/fhircore/engine/p2p/dao/BaseP2PTransferDao.kt

View check run for this annotation

Codecov / codecov/patch

android/engine/src/main/java/org/smartregister/fhircore/engine/p2p/dao/BaseP2PTransferDao.kt#L113

Added line #L113 was not covered by tests
}
}

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
Loading
Loading