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

Migrate FHIR SDK #3600

Merged
merged 15 commits into from
Nov 13, 2024
Merged
2 changes: 0 additions & 2 deletions android/engine/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,6 @@ dependencies {
api(libs.fhir.common.utils) { exclude(group = "org.slf4j", module = "jcl-over-slf4j") }
api(libs.runtime.livedata)
api(libs.foundation)
api(libs.fhir.common.utils)
api(libs.kotlinx.serialization.json)
api(libs.work.runtime.ktx)
api(libs.prettytime)
Expand Down Expand Up @@ -210,7 +209,6 @@ dependencies {
}
api(libs.fhir.engine) {
isTransitive = true
exclude(group = "com.google.android.fhir", module = "common")
exclude(group = "com.github.ben-manes.caffeine")
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,15 +19,13 @@
import android.content.Context
import android.database.SQLException
import ca.uhn.fhir.context.ConfigurationException
import ca.uhn.fhir.context.FhirContext
import ca.uhn.fhir.parser.DataFormatException
import com.google.android.fhir.FhirEngine
import com.google.android.fhir.db.ResourceNotFoundException
import com.google.android.fhir.get
import com.google.android.fhir.knowledge.KnowledgeManager
import com.google.android.fhir.sync.download.ResourceSearchParams
import dagger.hilt.android.qualifiers.ApplicationContext
import java.io.File
import java.io.FileNotFoundException
import java.io.InputStreamReader
import java.net.UnknownHostException
Expand All @@ -36,6 +34,8 @@
import java.util.ResourceBundle
import javax.inject.Inject
import javax.inject.Singleton
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext
import kotlinx.serialization.json.Json
import okhttp3.RequestBody.Companion.toRequestBody
Expand All @@ -60,6 +60,7 @@
import org.smartregister.fhircore.engine.di.NetworkModule
import org.smartregister.fhircore.engine.domain.model.MultiSelectViewAction
import org.smartregister.fhircore.engine.util.DispatcherProvider
import org.smartregister.fhircore.engine.util.KnowledgeManagerUtil
import org.smartregister.fhircore.engine.util.SharedPreferenceKey
import org.smartregister.fhircore.engine.util.SharedPreferencesHelper
import org.smartregister.fhircore.engine.util.extension.camelCase
Expand All @@ -71,7 +72,6 @@
import org.smartregister.fhircore.engine.util.extension.fileExtension
import org.smartregister.fhircore.engine.util.extension.generateMissingId
import org.smartregister.fhircore.engine.util.extension.interpolate
import org.smartregister.fhircore.engine.util.extension.referenceValue
import org.smartregister.fhircore.engine.util.extension.retrieveCompositionSections
import org.smartregister.fhircore.engine.util.extension.retrieveRelatedEntitySyncLocationState
import org.smartregister.fhircore.engine.util.extension.searchCompositionByIdentifier
Expand Down Expand Up @@ -100,8 +100,7 @@
val localizationHelper: LocalizationHelper by lazy { LocalizationHelper(this) }
private val supportedFileExtensions = listOf("json", "properties")
private var _isNonProxy = BuildConfig.IS_NON_PROXY_APK
private val fhirContext = FhirContext.forR4Cached()
private val authConfiguration = configService.provideAuthConfiguration()
private val mutex = Mutex()

/**
* Retrieve configuration for the provided [ConfigType]. The JSON retrieved from [configsJsonMap]
Expand Down Expand Up @@ -409,6 +408,7 @@
*/
@Throws(UnknownHostException::class, HttpException::class)
suspend fun fetchNonWorkflowConfigResources() {
Timber.d("Triggered fetching application configurations remotely")
configCacheMap.clear()
sharedPreferencesHelper.read(SharedPreferenceKey.APP_ID.name, null)?.let { appId ->
val parsedAppId = appId.substringBefore(TYPE_REFERENCE_DELIMITER).trim()
Expand All @@ -433,7 +433,11 @@

chunkedResourceIdList.forEach { sectionComponents ->
Timber.d(
"Fetching config resource ${entry.key}: with ids ${sectionComponents.joinToString(",")}",
"Fetching config resource ${entry.key}: with ids ${
sectionComponents.joinToString(
",",

Check warning on line 438 in android/engine/src/main/java/org/smartregister/fhircore/engine/configuration/ConfigurationRegistry.kt

View check run for this annotation

Codecov / codecov/patch

android/engine/src/main/java/org/smartregister/fhircore/engine/configuration/ConfigurationRegistry.kt#L436-L438

Added lines #L436 - L438 were not covered by tests
)
}",
)
fetchResources(
resourceType = entry.key,
Expand All @@ -449,7 +453,7 @@
// Save composition after fetching all the referenced section resources
addOrUpdate(compositionResource)

Timber.d("Done fetching application configurations remotely")
Timber.d("Done saving composition resource")
}
}
}
Expand Down Expand Up @@ -573,7 +577,7 @@
if (bundleEntryComponent.resource != null) {
addOrUpdate(bundleEntryComponent.resource)
Timber.d(
"Fetched and processed resources ${bundleEntryComponent.resource.resourceType}/${bundleEntryComponent.resource.id}",
"Fetched and processed resources ${bundleEntryComponent.resource.resourceType}/${bundleEntryComponent.resource.idPart}",
)
}
}
Expand All @@ -596,46 +600,29 @@
}

/**
* Knowledge manager [MetadataResource]s install Here we install all resources types of
* Knowledge manager [MetadataResource]s install. Here we install all resources types of
* [MetadataResource] as per FHIR Spec.This supports future use cases as well
*/
try {
if (resource is MetadataResource && resource.name != null) {
knowledgeManager.install(
writeToFile(resource.overwriteCanonicalURL()),
)
if (resource is MetadataResource) {
mutex.withLock {
knowledgeManager.install(
KnowledgeManagerUtil.writeToFile(
context = context,
configService = configService,
metadataResource = resource,

Check warning on line 613 in android/engine/src/main/java/org/smartregister/fhircore/engine/configuration/ConfigurationRegistry.kt

View check run for this annotation

Codecov / codecov/patch

android/engine/src/main/java/org/smartregister/fhircore/engine/configuration/ConfigurationRegistry.kt#L609-L613

Added lines #L609 - L613 were not covered by tests
subFilePath =
"${KnowledgeManagerUtil.KNOWLEDGE_MANAGER_ASSETS_SUBFOLDER}/${resource.resourceType}/${resource.idElement.idPart}.json",

Check warning on line 615 in android/engine/src/main/java/org/smartregister/fhircore/engine/configuration/ConfigurationRegistry.kt

View check run for this annotation

Codecov / codecov/patch

android/engine/src/main/java/org/smartregister/fhircore/engine/configuration/ConfigurationRegistry.kt#L615

Added line #L615 was not covered by tests
),
)
}
}
} catch (exception: Exception) {
Timber.e(exception)
}
}
}

private fun MetadataResource.overwriteCanonicalURL() =
this.apply {
url =
url
?: """${authConfiguration.fhirServerBaseUrl.trimEnd { it == '/' }}/${this.referenceValue()}"""
}

fun writeToFile(resource: Resource): File {
val fileName =
if (resource is MetadataResource && resource.name != null) {
resource.name
} else {
resource.idElement.idPart
}

return File(
context.filesDir,
"$KNOWLEDGE_MANAGER_ASSETS_SUBFOLDER/${resource.resourceType}/$fileName.json",
)
.apply {
this.parentFile?.mkdirs()
writeText(fhirContext.newJsonParser().encodeResourceToString(resource))
}
}

/**
* Using this [FhirEngine] and [DispatcherProvider], for all passed resources, make sure they all
* have IDs or generate if they don't, then pass them to create.
Expand Down Expand Up @@ -818,7 +805,6 @@
const val PAGINATION_NEXT = "next"
const val RESOURCES_PATH = "resources/"
const val SYNC_LOCATION_IDS = "_syncLocations"
const val KNOWLEDGE_MANAGER_ASSETS_SUBFOLDER = "km"

/**
* The list of resources whose types can be synced down as part of the Composition configs.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,42 +30,71 @@ import dagger.hilt.components.SingletonComponent
import java.io.File
import java.io.FileInputStream
import javax.inject.Singleton
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.hl7.fhir.r4.context.SimpleWorkerContext
import org.hl7.fhir.r4.model.Parameters
import org.hl7.fhir.r4.model.Resource
import org.hl7.fhir.r4.utils.FHIRPathEngine
import org.smartregister.fhircore.engine.configuration.ConfigurationRegistry
import org.smartregister.fhircore.engine.util.KnowledgeManagerUtil
import org.smartregister.fhircore.engine.util.helper.TransformSupportServices
import timber.log.Timber

@InstallIn(SingletonComponent::class)
@Module
class CoreModule {

@OptIn(DelicateCoroutinesApi::class)
@Singleton
@Provides
fun provideWorkerContextProvider(@ApplicationContext context: Context): SimpleWorkerContext =
SimpleWorkerContext().apply {
setExpansionProfile(Parameters())
isCanRunWithoutTerminology = true
context.filesDir
.resolve(ConfigurationRegistry.KNOWLEDGE_MANAGER_ASSETS_SUBFOLDER)
.list()
?.forEach { resourceFolder ->
GlobalScope.launch {
withContext(Dispatchers.IO) {
setExpansionProfile(Parameters())
isCanRunWithoutTerminology = true
context.filesDir
.resolve("${ConfigurationRegistry.KNOWLEDGE_MANAGER_ASSETS_SUBFOLDER}/$resourceFolder")
.resolve(KnowledgeManagerUtil.KNOWLEDGE_MANAGER_ASSETS_SUBFOLDER)
.list()
?.forEach { file ->
cacheResource(
FhirContext.forR4Cached()
.newJsonParser()
.parseResource(
FileInputStream(
File(context.filesDir.resolve("km/$resourceFolder/$file").toString()),
),
) as Resource,
)
?.forEach { resourceFolder ->
context.filesDir
.resolve(
"${KnowledgeManagerUtil.KNOWLEDGE_MANAGER_ASSETS_SUBFOLDER}/$resourceFolder",
)
.list()
?.forEach { file ->
try {
cacheResource(
FhirContext.forR4Cached()
.newJsonParser()
.parseResource(
FileInputStream(
File(
context.filesDir
.resolve(
"${KnowledgeManagerUtil.KNOWLEDGE_MANAGER_ASSETS_SUBFOLDER}/$resourceFolder/$file",
)
.toString(),
),
),
) as Resource,
)
} catch (e: Exception) {
Timber.e(
"EXCEPTION PROCESSING ${context.filesDir
.resolve(
"${KnowledgeManagerUtil.KNOWLEDGE_MANAGER_ASSETS_SUBFOLDER}/$resourceFolder/$file",
)}",
)
Timber.e(e)
}
}
}
}
}
}

@Singleton
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@
import com.google.android.fhir.sync.ConflictResolver
import com.google.android.fhir.sync.DownloadWorkManager
import com.google.android.fhir.sync.FhirSyncWorker
import com.google.android.fhir.sync.upload.HttpCreateMethod
import com.google.android.fhir.sync.upload.HttpUpdateMethod
import com.google.android.fhir.sync.upload.UploadStrategy
import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
Expand All @@ -50,5 +52,11 @@

override fun getFhirEngine(): FhirEngine = openSrpFhirEngine

override fun getUploadStrategy(): UploadStrategy = UploadStrategy.AllChangesSquashedBundlePut
override fun getUploadStrategy(): UploadStrategy =
UploadStrategy.forBundleRequest(
methodForCreate = HttpCreateMethod.PUT,
methodForUpdate = HttpUpdateMethod.PATCH,
squash = true,
bundleSize = 500,
)

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

View check run for this annotation

Codecov / codecov/patch

android/engine/src/main/java/org/smartregister/fhircore/engine/sync/AppSyncWorker.kt#L56-L61

Added lines #L56 - L61 were not covered by tests
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
/*
* 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.util

import android.content.Context
import ca.uhn.fhir.context.FhirContext
import java.io.File
import org.hl7.fhir.r4.model.MetadataResource
import org.smartregister.fhircore.engine.configuration.app.AuthConfiguration
import org.smartregister.fhircore.engine.configuration.app.ConfigService
import org.smartregister.fhircore.engine.util.extension.referenceValue

object KnowledgeManagerUtil {
const val KNOWLEDGE_MANAGER_ASSETS_SUBFOLDER = "km"
private val fhirContext = FhirContext.forR4Cached()

/**
* Util method that creates a physical file and writes the Metadata FHIR resource content to it.
* Note the filepath provided is appended to the apps private directory as returned by
* Context.filesDir
*
* @param subFilePath the path of the file but within the apps private directory
* {Context.filesDir}
* @param metadataResource the actual FHIR Resource of type MetadataResource
* @param configService the configuration service
* @param context the application context
* @return File the file object after creating and writing
*/
fun writeToFile(
subFilePath: String,
metadataResource: MetadataResource,
configService: ConfigService,
context: Context,
): File =
context
.createFileInPrivateDirectory(subFilePath)
.also { it.parentFile?.mkdirs() }
.apply {
writeText(
fhirContext
.newJsonParser()
.encodeResourceToString(
metadataResource.overwriteCanonicalURL(configService.provideAuthConfiguration()),
),
)
}

private fun Context.createFileInPrivateDirectory(filePath: String) = File(this.filesDir, filePath)

private fun MetadataResource.overwriteCanonicalURL(authConfiguration: AuthConfiguration) =
this.apply {
url =
url
?: """${authConfiguration.fhirServerBaseUrl.trimEnd { it == '/' }}/${this.referenceValue()}"""
}
}
Loading
Loading