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

Download custom resources via sync configurations #3344

Open
wants to merge 14 commits into
base: main
Choose a base branch
from
Open
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

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ constructor(

override fun getDownloadWorkManager(): DownloadWorkManager =
OpenSrpDownloadManager(
syncParams = syncListenerManager.loadSyncParams(),
resourceSearchParams = syncListenerManager.loadResourceSearchParams(),
context = appTimeStampContext,
)

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
/*
* Copyright 2021-2024 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.sync

import android.content.Context
import androidx.hilt.work.HiltWorker
import androidx.work.CoroutineWorker
import androidx.work.WorkerParameters
import com.google.android.fhir.sync.concatParams
import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
import java.net.UnknownHostException
import kotlinx.coroutines.withContext
import org.smartregister.fhircore.engine.configuration.ConfigurationRegistry
import org.smartregister.fhircore.engine.data.remote.fhir.resource.FhirResourceDataSource
import org.smartregister.fhircore.engine.util.DispatcherProvider
import retrofit2.HttpException
import retrofit2.Response
import timber.log.Timber

@HiltWorker
class CustomSyncWorker
@AssistedInject
constructor(
@Assisted appContext: Context,
@Assisted workerParams: WorkerParameters,
val configurationRegistry: ConfigurationRegistry,
val dispatcherProvider: DispatcherProvider,
val fhirResourceDataSource: FhirResourceDataSource,
) : CoroutineWorker(appContext, workerParams) {
override suspend fun doWork(): Result {
return withContext(dispatcherProvider.io()) {
try {
with(configurationRegistry) {
val (resourceSearchParams, _) = loadResourceSearchParams()
Timber.i("Custom resource sync parameters $resourceSearchParams")
resourceSearchParams
.asSequence()
.filter { it.value.isNotEmpty() }
.map { "${it.key}?${it.value.concatParams()}" }
.forEach { url ->
fetchResources(
gatewayModeHeaderValue = ConfigurationRegistry.FHIR_GATEWAY_MODE_HEADER_VALUE,
url = url,

Check warning on line 58 in android/engine/src/main/java/org/smartregister/fhircore/engine/sync/CustomSyncWorker.kt

View check run for this annotation

Codecov / codecov/patch

android/engine/src/main/java/org/smartregister/fhircore/engine/sync/CustomSyncWorker.kt#L56-L58

Added lines #L56 - L58 were not covered by tests
)
}
}
Result.success()
} catch (httpException: HttpException) {
Timber.e(httpException)
val response: Response<*>? = httpException.response()

Check warning on line 65 in android/engine/src/main/java/org/smartregister/fhircore/engine/sync/CustomSyncWorker.kt

View check run for this annotation

Codecov / codecov/patch

android/engine/src/main/java/org/smartregister/fhircore/engine/sync/CustomSyncWorker.kt#L63-L65

Added lines #L63 - L65 were not covered by tests
if (response != null && (400..503).contains(response.code())) {
Timber.e("HTTP exception ${response.code()} -> ${response.errorBody()}")

Check warning on line 67 in android/engine/src/main/java/org/smartregister/fhircore/engine/sync/CustomSyncWorker.kt

View check run for this annotation

Codecov / codecov/patch

android/engine/src/main/java/org/smartregister/fhircore/engine/sync/CustomSyncWorker.kt#L67

Added line #L67 was not covered by tests
}
Result.failure()
} catch (unknownHostException: UnknownHostException) {
Timber.e(unknownHostException)
Result.failure()

Check warning on line 72 in android/engine/src/main/java/org/smartregister/fhircore/engine/sync/CustomSyncWorker.kt

View check run for this annotation

Codecov / codecov/patch

android/engine/src/main/java/org/smartregister/fhircore/engine/sync/CustomSyncWorker.kt#L69-L72

Added lines #L69 - L72 were not covered by tests
} catch (exception: Exception) {
Timber.e(exception)
Result.failure()
}
}
}

companion object {
const val WORK_ID = "CustomResourceSyncWorker"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,12 @@ import org.hl7.fhir.r4.model.ResourceType
import org.smartregister.fhircore.engine.util.extension.updateLastUpdated

class OpenSrpDownloadManager(
syncParams: ResourceSearchParams,
resourceSearchParams: ResourceSearchParams,
val context: ResourceParamsBasedDownloadWorkManager.TimestampContext,
) : DownloadWorkManager {

private val downloadWorkManager = ResourceParamsBasedDownloadWorkManager(syncParams, context)
private val downloadWorkManager =
ResourceParamsBasedDownloadWorkManager(resourceSearchParams, context)

override suspend fun getNextRequest(): DownloadRequest? = downloadWorkManager.getNextRequest()

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@
import android.content.Context
import androidx.work.Constraints
import androidx.work.NetworkType
import androidx.work.OneTimeWorkRequestBuilder
import androidx.work.WorkManager
import com.google.android.fhir.FhirEngine
import com.google.android.fhir.sync.CurrentSyncJobStatus
import com.google.android.fhir.sync.PeriodicSyncConfiguration
Expand Down Expand Up @@ -57,16 +59,25 @@
val fhirEngine: FhirEngine,
val syncListenerManager: SyncListenerManager,
val dispatcherProvider: DispatcherProvider,
val workManager: WorkManager,
@ApplicationContext val context: Context,
) {

/**
* Run one time sync. The [SyncJobStatus] will be broadcast to all the registered [OnSyncListener]
* 's
*/
suspend fun runOneTimeSync() = coroutineScope {
suspend fun runOneTimeSync(): Unit = coroutineScope {
Timber.i("Running one time sync...")
Sync.oneTimeSync<AppSyncWorker>(context).handleOneTimeSyncJobStatus(this)

workManager.enqueue(
OneTimeWorkRequestBuilder<CustomSyncWorker>()
.setConstraints(
Constraints.Builder().setRequiredNetworkType(NetworkType.CONNECTED).build(),

Check warning on line 77 in android/engine/src/main/java/org/smartregister/fhircore/engine/sync/SyncBroadcaster.kt

View check run for this annotation

Codecov / codecov/patch

android/engine/src/main/java/org/smartregister/fhircore/engine/sync/SyncBroadcaster.kt#L74-L77

Added lines #L74 - L77 were not covered by tests
)
.build(),

Check warning on line 79 in android/engine/src/main/java/org/smartregister/fhircore/engine/sync/SyncBroadcaster.kt

View check run for this annotation

Codecov / codecov/patch

android/engine/src/main/java/org/smartregister/fhircore/engine/sync/SyncBroadcaster.kt#L79

Added line #L79 was not covered by tests
)
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,27 +17,19 @@
package org.smartregister.fhircore.engine.sync

import android.content.Context
import androidx.compose.ui.state.ToggleableState
import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleOwner
import com.google.android.fhir.sync.SyncJobStatus
import com.google.android.fhir.sync.download.ResourceSearchParams
import dagger.hilt.android.qualifiers.ApplicationContext
import java.lang.ref.WeakReference
import javax.inject.Inject
import javax.inject.Singleton
import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.runBlocking
import org.hl7.fhir.r4.model.Parameters
import org.hl7.fhir.r4.model.ResourceType
import org.hl7.fhir.r4.model.SearchParameter
import org.smartregister.fhircore.engine.configuration.ConfigType
import org.smartregister.fhircore.engine.configuration.ConfigurationRegistry
import org.smartregister.fhircore.engine.configuration.app.ApplicationConfiguration
import org.smartregister.fhircore.engine.configuration.app.ConfigService
import org.smartregister.fhircore.engine.datastore.syncLocationIdsProtoStore
import org.smartregister.fhircore.engine.util.DefaultDispatcherProvider
import org.smartregister.fhircore.engine.util.SharedPreferenceKey
import org.smartregister.fhircore.engine.util.SharedPreferencesHelper
import timber.log.Timber

Expand All @@ -55,14 +47,6 @@ constructor(
@ApplicationContext val context: Context,
val dispatcherProvider: DefaultDispatcherProvider,
) {
private val appConfig by lazy {
configurationRegistry.retrieveConfiguration<ApplicationConfiguration>(
ConfigType.Application,
)
}
private val syncConfig by lazy {
configurationRegistry.retrieveResourceConfiguration<Parameters>(ConfigType.Sync)
}

private val _onSyncListeners = mutableListOf<WeakReference<OnSyncListener>>()
val onSyncListeners: List<OnSyncListener>
Expand Down Expand Up @@ -98,96 +82,16 @@ constructor(
}
}

/** Retrieve registry sync params */
fun loadSyncParams(): Map<ResourceType, Map<String, String>> {
val pairs = mutableListOf<Pair<ResourceType, MutableMap<String, String>>>()

val organizationResourceTag =
configService.defineResourceTags().find { it.type == ResourceType.Organization.name }

val mandatoryTags = configService.provideResourceTags(sharedPreferencesHelper)

val relatedResourceTypes: List<String>? =
sharedPreferencesHelper.read(SharedPreferenceKey.REMOTE_SYNC_RESOURCES.name)

// TODO Does not support nested parameters i.e. parameters.parameters...
// TODO: expressionValue supports for Organization and Publisher literals for now
syncConfig.parameter
.map { it.resource as SearchParameter }
.forEach { sp ->
val paramName = sp.name // e.g. organization
val paramLiteral = "#$paramName" // e.g. #organization in expression for replacement
val paramExpression = sp.expression
val expressionValue =
when (paramName) {
// TODO: Does not support multi organization yet,
// https://github.com/opensrp/fhircore/issues/1550
ConfigurationRegistry.ORGANIZATION ->
mandatoryTags
.firstOrNull {
it.display.contentEquals(organizationResourceTag?.tag?.display, ignoreCase = true)
}
?.code
ConfigurationRegistry.ID -> paramExpression
ConfigurationRegistry.COUNT -> appConfig.remoteSyncPageSize.toString()
else -> null
}?.let {
// replace the evaluated value into expression for complex expressions
// e.g. #organization -> 123
// e.g. patient.organization eq #organization -> patient.organization eq 123
paramExpression?.replace(paramLiteral, it)
}

// for each entity in base create and add param map
// [Patient=[ name=Abc, organization=111 ], Encounter=[ type=MyType, location=MyHospital
// ],..]
if (relatedResourceTypes.isNullOrEmpty()) {
sp.base.mapNotNull { it.code }
} else {
relatedResourceTypes
}
.forEach { clinicalResource ->
val resourceType = ResourceType.fromCode(clinicalResource)
val pair = pairs.find { it.first == resourceType }
if (pair == null) {
pairs.add(
Pair(
resourceType,
expressionValue?.let { mutableMapOf(sp.code to expressionValue) }
?: mutableMapOf(),
),
)
} else {
expressionValue?.let {
// add another parameter if there is a matching resource type
// e.g. [(Patient, {organization=105})] to [(Patient, {organization=105,
// _count=100})]
val updatedPair = pair.second.apply { put(sp.code, expressionValue) }
val index = pairs.indexOfFirst { it.first == resourceType }
pairs.set(index, Pair(resourceType, updatedPair))
}
}
}
}

// Set sync locations Location query params
runBlocking {
context.syncLocationIdsProtoStore.data
.firstOrNull()
?.filter { it.toggleableState == ToggleableState.On }
?.map { it.locationId }
.takeIf { !it.isNullOrEmpty() }
?.let { locationIds ->
pairs.forEach { it.second[SYNC_LOCATION_IDS] = locationIds.joinToString(",") }
}
}

Timber.i("SYNC CONFIG $pairs")

return mapOf(*pairs.toTypedArray())
}

companion object {
private const val SYNC_LOCATION_IDS = "_syncLocations"
/**
* This function is used to retrieve search parameters for the various [ResourceType]'s synced by
* the application. The function returns a pair of maps, one contains the the custom Resource
* types and the other returns the supported FHIR [ResourceType]s. The [OpenSrpDownloadManager]
* does not support downloading of custom resource, a separate worker is implemented instead to
* download the custom resources.
*/
fun loadResourceSearchParams(): ResourceSearchParams {
val (_, resourceSearchParams) = configurationRegistry.loadResourceSearchParams()
Timber.i("FHIR resource sync parameters $resourceSearchParams")
return resourceSearchParams
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@ enum class SharedPreferenceKey {
PRACTITIONER_LOCATION_HIERARCHIES,
PRACTITIONER_LOCATION,
PRACTITIONER_LOCATION_ID,
REMOTE_SYNC_RESOURCES,
LOGIN_CREDENTIAL_KEY,
LOGIN_PIN_KEY,
LOGIN_PIN_SALT,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@ import com.google.android.fhir.SearchResult
import com.google.android.fhir.datacapture.extensions.logicalId
import com.google.android.fhir.db.ResourceNotFoundException
import com.google.android.fhir.search.Search
import com.google.common.reflect.TypeToken
import dagger.hilt.android.testing.HiltAndroidRule
import dagger.hilt.android.testing.HiltAndroidTest
import dagger.hilt.android.testing.HiltTestApplication
Expand Down Expand Up @@ -1019,30 +1018,6 @@ class ConfigurationRegistryTest : RobolectricTest() {
assertEquals(ResourceType.Composition, requestPathArgumentSlot.last().resourceType)
}

@Test
fun testSaveSyncSharedPreferencesShouldVerifyDataSave() {
val resourceType =
listOf(ResourceType.Task, ResourceType.Patient, ResourceType.Task, ResourceType.Patient)

configRegistry.saveSyncSharedPreferences(resourceType)

val savedSyncResourcesResult =
configRegistry.sharedPreferencesHelper.read(
SharedPreferenceKey.REMOTE_SYNC_RESOURCES.name,
null,
)!!
val listResourceTypeToken = object : TypeToken<List<ResourceType>>() {}.type
val savedSyncResourceTypes: List<ResourceType> =
configRegistry.sharedPreferencesHelper.gson.fromJson(
savedSyncResourcesResult,
listResourceTypeToken,
)

assertEquals(2, savedSyncResourceTypes.size)
assertEquals(ResourceType.Task, savedSyncResourceTypes.first())
assertEquals(ResourceType.Patient, savedSyncResourceTypes.last())
}

@Test
fun writeToFileWithMetadataResourceWithNameShouldCreateFileWithResourceName() {
val parser = fhirContext.newJsonParser()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,13 +41,13 @@ class AppSyncWorkerTest : RobolectricTest() {

every { taskExecutor.serialTaskExecutor } returns mockk()
every { workerParams.taskExecutor } returns taskExecutor
every { syncListenerManager.loadSyncParams() } returns syncParams
every { syncListenerManager.loadResourceSearchParams() } returns syncParams

val appSyncWorker =
AppSyncWorker(mockk(), workerParams, syncListenerManager, fhirEngine, timeContext)

appSyncWorker.getDownloadWorkManager()
verify { syncListenerManager.loadSyncParams() }
verify { syncListenerManager.loadResourceSearchParams() }

Assert.assertEquals(AcceptLocalConflictResolver, appSyncWorker.getConflictResolver())
Assert.assertEquals(fhirEngine, appSyncWorker.getFhirEngine())
Expand Down
Loading
Loading