From d68886fc813c94dd71932dd9d059c727e82abb4c Mon Sep 17 00:00:00 2001 From: Sharon Akinyi <79141719+sharon2719@users.noreply.github.com> Date: Sat, 21 Dec 2024 06:22:11 +0300 Subject: [PATCH] Add Composition.entry and Composition.focus (#3662) * Add Composition.entry and Composition.focus * Update changes * Run spotlessApply * Add testFetchNonWorkflowConfigResourcesWithAllEntry test * Add tests for entry and focus * refactor and fix id fetch * add test and guard for entry item with missing elements * refactor element guards --------- Co-authored-by: Peter Lubell-Doughtie Co-authored-by: Simon Njoroge --- .../configuration/ConfigurationRegistry.kt | 127 +++++++++++------ .../ConfigurationRegistryTest.kt | 134 +++++++++++++++++- 2 files changed, 216 insertions(+), 45 deletions(-) diff --git a/android/engine/src/main/java/org/smartregister/fhircore/engine/configuration/ConfigurationRegistry.kt b/android/engine/src/main/java/org/smartregister/fhircore/engine/configuration/ConfigurationRegistry.kt index c8521b3f20..d1a749e05b 100644 --- a/android/engine/src/main/java/org/smartregister/fhircore/engine/configuration/ConfigurationRegistry.kt +++ b/android/engine/src/main/java/org/smartregister/fhircore/engine/configuration/ConfigurationRegistry.kt @@ -46,6 +46,7 @@ import org.hl7.fhir.r4.model.ImplementationGuide import org.hl7.fhir.r4.model.ListResource import org.hl7.fhir.r4.model.MetadataResource import org.hl7.fhir.r4.model.Parameters +import org.hl7.fhir.r4.model.Reference import org.hl7.fhir.r4.model.Resource import org.hl7.fhir.r4.model.ResourceType import org.hl7.fhir.r4.model.SearchParameter @@ -334,19 +335,19 @@ constructor( } } } else { - composition.retrieveCompositionSections().forEach { - if (it.hasFocus() && it.focus.hasReferenceElement() && it.focus.hasIdentifier()) { - val configIdentifier = it.focus.identifier.value - val referenceResourceType = it.focus.reference.substringBefore(TYPE_REFERENCE_DELIMITER) - if (isAppConfig(referenceResourceType) && !isIconConfig(configIdentifier)) { - val extractedId = it.focus.extractId() - try { - val configBinary = fhirEngine.get(extractedId) - configsJsonMap[configIdentifier] = configBinary.content.decodeToString() - } catch (resourceNotFoundException: ResourceNotFoundException) { - Timber.e("Missing Binary file with ID :$extractedId") - withContext(dispatcherProvider.main()) { configsLoadedCallback(false) } - } + composition.retrieveCompositionSections().forEach { sectionComponent -> + if (sectionComponent.hasFocus()) { + addBinaryToConfigsJsonMap( + sectionComponent.focus, + configsLoadedCallback, + ) + } + if (sectionComponent.hasEntry() && sectionComponent.entry.isNotEmpty()) { + sectionComponent.entry.forEach { entryReference -> + addBinaryToConfigsJsonMap( + entryReference, + configsLoadedCallback, + ) } } } @@ -354,6 +355,26 @@ constructor( configsLoadedCallback(true) } + private suspend fun addBinaryToConfigsJsonMap( + entryReference: Reference, + configsLoadedCallback: (Boolean) -> Unit, + ) { + if (entryReference.hasReferenceElement() && entryReference.hasIdentifier()) { + val configIdentifier = entryReference.identifier.value + val referenceResourceType = entryReference.reference.substringBefore(TYPE_REFERENCE_DELIMITER) + if (isAppConfig(referenceResourceType) && !isIconConfig(configIdentifier)) { + val extractedId = entryReference.extractId() + try { + val configBinary = fhirEngine.get(extractedId.toString()) + configsJsonMap[configIdentifier] = configBinary.content.decodeToString() + } catch (resourceNotFoundException: ResourceNotFoundException) { + Timber.e("Missing Binary file with ID :$extractedId") + withContext(dispatcherProvider.main()) { configsLoadedCallback(false) } + } + } + } + } + private fun isAppConfig(referenceResourceType: String) = referenceResourceType in arrayOf(ResourceType.Binary.name, ResourceType.Parameters.name) @@ -411,41 +432,30 @@ constructor( val parsedAppId = appId.substringBefore(TYPE_REFERENCE_DELIMITER).trim() val compositionResource = fetchRemoteCompositionByAppId(parsedAppId) compositionResource?.let { composition -> - composition - .retrieveCompositionSections() - .asSequence() - .filter { it.hasFocus() && it.focus.hasReferenceElement() } - .groupBy { section -> - section.focus.reference.substringBefore( - TYPE_REFERENCE_DELIMITER, - missingDelimiterValue = "", - ) + val compositionSections = composition.retrieveCompositionSections() + val sectionComponentMap = mutableMapOf>() + compositionSections.forEach { sectionComponent -> + if (sectionComponent.hasFocus() && sectionComponent.focus.hasReferenceElement()) { + val key = + sectionComponent.focus.reference.substringBefore( + delimiter = TYPE_REFERENCE_DELIMITER, + missingDelimiterValue = "", + ) + sectionComponentMap.getOrPut(key) { mutableListOf() }.apply { add(sectionComponent) } } - .filter { entry -> entry.key in FILTER_RESOURCE_LIST } - .forEach { entry: Map.Entry> -> - if (entry.key == ResourceType.List.name) { - processCompositionListResources(entry) - } else { - val chunkedResourceIdList = entry.value.chunked(MANIFEST_PROCESSOR_BATCH_SIZE) - - chunkedResourceIdList.forEach { sectionComponents -> - Timber.d( - "Fetching config resource ${entry.key}: with ids ${ - sectionComponents.joinToString( - ",", - ) - }", + if (sectionComponent.hasEntry() && sectionComponent.entry.isNotEmpty()) { + sectionComponent.entry.forEach { + val key = + it.reference.substringBefore( + delimiter = TYPE_REFERENCE_DELIMITER, + missingDelimiterValue = "", ) - fetchResources( - resourceType = entry.key, - resourceIdList = - sectionComponents.map { sectionComponent -> - sectionComponent.focus.extractId() - }, - ) - } + sectionComponentMap.getOrPut(key) { mutableListOf() }.apply { add(sectionComponent) } } } + } + + processCompositionSectionComponent(sectionComponentMap) // Save composition after fetching all the referenced section resources addOrUpdate(compositionResource) @@ -455,6 +465,35 @@ constructor( } } + private suspend fun processCompositionSectionComponent( + sectionComponentMap: Map>, + ) { + sectionComponentMap + .filter { entry -> entry.key in FILTER_RESOURCE_LIST } + .forEach { entry: Map.Entry> -> + if (entry.key == ResourceType.List.name) { + processCompositionListResources(entry) + } else { + val chunkedResourceIdList = entry.value.chunked(MANIFEST_PROCESSOR_BATCH_SIZE) + + chunkedResourceIdList.forEach { sectionComponents -> + Timber.d( + "Fetching config resource ${entry.key}: with ids ${ + sectionComponents.joinToString( + ",", + ) + }", + ) + fetchResources( + resourceType = entry.key, + resourceIdList = + sectionComponents.map { sectionComponent -> sectionComponent.focus.extractId() }, + ) + } + } + } + } + suspend fun fetchRemoteImplementationGuideByAppId( appId: String?, appVersionCode: Int?, diff --git a/android/engine/src/test/java/org/smartregister/fhircore/engine/configuration/ConfigurationRegistryTest.kt b/android/engine/src/test/java/org/smartregister/fhircore/engine/configuration/ConfigurationRegistryTest.kt index b08d672b4c..730a08815a 100644 --- a/android/engine/src/test/java/org/smartregister/fhircore/engine/configuration/ConfigurationRegistryTest.kt +++ b/android/engine/src/test/java/org/smartregister/fhircore/engine/configuration/ConfigurationRegistryTest.kt @@ -24,6 +24,7 @@ import com.google.android.fhir.FhirEngine 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.get import com.google.android.fhir.search.Search import dagger.hilt.android.testing.HiltAndroidRule import dagger.hilt.android.testing.HiltAndroidTest @@ -52,6 +53,7 @@ import org.hl7.fhir.r4.model.ResourceType import org.hl7.fhir.r4.model.StructureMap import org.junit.Assert import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse import org.junit.Assert.assertNotNull import org.junit.Assert.assertThrows import org.junit.Assert.assertTrue @@ -617,7 +619,7 @@ class ConfigurationRegistryTest : RobolectricTest() { } returns Binary().apply { content = ByteArray(0) } runTest { configRegistry.loadConfigurations(appId, context) } - Assert.assertFalse(configRegistry.configsJsonMap.isEmpty()) + assertFalse(configRegistry.configsJsonMap.isEmpty()) } @Test @@ -1080,4 +1082,134 @@ class ConfigurationRegistryTest : RobolectricTest() { assertEquals(questionnaireId, questionnaire.logicalId) } } + + @Test + fun testFetchNonWorkflowConfigResourcesWithNoFocusOrEntry() = runTest { + val appId = "app-id" + val composition = + Composition().apply { + identifier = Identifier().apply { value = appId } + section = listOf(SectionComponent()) // Neither focus nor entry + } + + configRegistry.fetchNonWorkflowConfigResources() + + // Validate no crash occurs and configsJsonMap remains empty + assertTrue(configRegistry.configsJsonMap.isEmpty()) + } + + @Test + fun testPopulateConfigurationsMapWithNeitherFocusNorEntry() = runTest { + val composition = Composition() + + configRegistry.populateConfigurationsMap(context, composition, false, "app-id") {} + + assertTrue(configRegistry.configsJsonMap.isEmpty()) + } + + @Test + fun testPopulateConfigurationsMapWithAllFocus() = runTest { + val composition = + Composition().apply { + section = + listOf( + SectionComponent().apply { + focus = + Reference().apply { + reference = "Binary/1" + identifier = Identifier().apply { value = "resource1" } + } + }, + ) + } + + coEvery { fhirEngine.get(any()) } returns Binary().apply { content = ByteArray(0) } + configRegistry.populateConfigurationsMap(context, composition, false, "app-id") {} + assertEquals(1, configRegistry.configsJsonMap.size) + assertTrue(configRegistry.configsJsonMap.containsKey("resource1")) + } + + @Test + fun testPopulateConfigurationsMapWithAllEntry() = runTest { + val composition = + Composition().apply { + section = + listOf( + SectionComponent().apply { + entry = + listOf( + Reference().apply { + reference = "Binary/1" + identifier = Identifier().apply { value = "resource1" } + }, + Reference().apply { + reference = "Binary/2" + identifier = Identifier().apply { value = "resource2" } + }, + ) + }, + ) + } + + coEvery { fhirEngine.get(any()) } returns Binary().apply { content = ByteArray(0) } + configRegistry.populateConfigurationsMap(context, composition, false, "app-id") {} + assertEquals(2, configRegistry.configsJsonMap.size) + assertTrue(configRegistry.configsJsonMap.containsKey("resource1")) + assertTrue(configRegistry.configsJsonMap.containsKey("resource2")) + } + + @Test + fun testPopulateConfigurationsMapWithEntryMissingId() = runTest { + val composition = + Composition().apply { + section = + listOf( + SectionComponent().apply { + entry = + listOf( + Reference().apply { reference = "Binary/1" }, + Reference().apply { + reference = "Binary/2" + identifier = Identifier().apply { value = "resource2" } + }, + ) + }, + ) + } + + coEvery { fhirEngine.get(any()) } returns Binary().apply { content = ByteArray(0) } + configRegistry.populateConfigurationsMap(context, composition, false, "app-id") {} + assertEquals(1, configRegistry.configsJsonMap.size) + assertTrue(configRegistry.configsJsonMap.containsKey("resource2")) + } + + @Test + fun testPopulateConfigurationsMapWithFocusAndEntry() = runTest { + val composition = + Composition().apply { + section = + listOf( + SectionComponent().apply { + focus = + Reference().apply { + reference = "Binary/1" + identifier = Identifier().apply { value = "resource1" } + } + entry = + listOf( + Reference().apply { + reference = "Binary/2" + identifier = Identifier().apply { value = "resource2" } + }, + ) + }, + ) + } + + coEvery { fhirEngine.get(any()) } returns Binary().apply { content = ByteArray(0) } + configRegistry.populateConfigurationsMap(context, composition, false, "app-id") {} + assertEquals(2, configRegistry.configsJsonMap.size) + assertTrue(configRegistry.configsJsonMap.containsKey("resource1")) + assertTrue(configRegistry.configsJsonMap.containsKey("resource2")) + } }