Skip to content

Commit

Permalink
Add Composition.entry and Composition.focus (#3662)
Browse files Browse the repository at this point in the history
* 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 <[email protected]>
Co-authored-by: Simon Njoroge <[email protected]>
  • Loading branch information
3 people authored Dec 21, 2024
1 parent 3c3dea1 commit d68886f
Show file tree
Hide file tree
Showing 2 changed files with 216 additions and 45 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -334,26 +335,46 @@ 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<Binary>(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,
)
}
}
}
}
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<Binary>(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)

Expand Down Expand Up @@ -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<String, MutableList<Composition.SectionComponent>>()
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<String, List<Composition.SectionComponent>> ->
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)
Expand All @@ -455,6 +465,35 @@ constructor(
}
}

private suspend fun processCompositionSectionComponent(
sectionComponentMap: Map<String, List<Composition.SectionComponent>>,
) {
sectionComponentMap
.filter { entry -> entry.key in FILTER_RESOURCE_LIST }
.forEach { entry: Map.Entry<String, List<Composition.SectionComponent>> ->
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?,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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<Binary>(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<Binary>(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<Binary>(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<Binary>(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"))
}
}

0 comments on commit d68886f

Please sign in to comment.