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

LaunchContext for initialExpression #2025

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
9fa64b6
Use launchContext for initialExpression
FikriMilano Jul 31, 2023
8b765fd
Test ResourceMapper
FikriMilano Jul 31, 2023
59c7d6a
Fix test
FikriMilano Jul 31, 2023
a4c8c8f
Merge branch 'master' of github.com:google/android-fhir into 2024-ini…
FikriMilano Jul 31, 2023
45a6ae0
Fix launchContexts for demo app when editing patient
FikriMilano Jul 31, 2023
9686da8
spotlessApply
FikriMilano Jul 31, 2023
d2d53a2
WIP
FikriMilano Aug 2, 2023
14fa29c
Merge branch 'master' of github.com:google/android-fhir into 2024-ini…
FikriMilano Aug 23, 2023
9c4210d
Revert "WIP"
FikriMilano Aug 23, 2023
33b3d03
spotlessApply
FikriMilano Aug 23, 2023
45c6dbf
Fix test
FikriMilano Aug 24, 2023
6a47a51
Refactor validateLaunchContextExtension
FikriMilano Sep 12, 2023
3f0b4f6
Remove QuestionnaireLaunchContextSet enum class
FikriMilano Sep 12, 2023
ddc1dfc
Rename vars and functions
FikriMilano Sep 12, 2023
65cbee1
Add code comment for MoreResourceTypes.kt
FikriMilano Sep 12, 2023
8f6a9c4
Unit testing
FikriMilano Sep 12, 2023
597fe7e
Merge branch 'master' of github.com:google/android-fhir into 2024-ini…
FikriMilano Sep 12, 2023
6e90a9d
Merge branch 'master' of github.com:google/android-fhir into 2024-ini…
FikriMilano Sep 13, 2023
3bff3b2
Fix post-merge-conflict
FikriMilano Sep 13, 2023
1206cfa
spotlessApply
FikriMilano Sep 13, 2023
28c86be
Address review
FikriMilano Oct 9, 2023
a6436e2
Merge branch 'master' of github.com:google/android-fhir into 2024-ini…
FikriMilano Oct 9, 2023
287512c
spotlessApply
FikriMilano Oct 9, 2023
d5d4069
Merge branch 'master' of github.com:google/android-fhir into 2024-ini…
FikriMilano Oct 9, 2023
8fec91e
Revert un-intended changes
FikriMilano Oct 9, 2023
a21e6f7
Fix failing checks
FikriMilano Oct 9, 2023
8130aef
Remove check of must contain 2 sub-extensions
FikriMilano Oct 13, 2023
f6f6b3c
Update Kdoc
FikriMilano Oct 13, 2023
b412e16
Merge branch 'master' of github.com:google/android-fhir into 2024-ini…
FikriMilano Oct 13, 2023
ad51a70
Revert
FikriMilano Oct 13, 2023
41a957d
validate launch context when using populate public API
FikriMilano Oct 13, 2023
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
Original file line number Diff line number Diff line change
Expand Up @@ -168,11 +168,11 @@ class DemoQuestionnaireFragment : Fragment() {
R.id.container,
QuestionnaireFragment.builder()
.setQuestionnaire(questionnaireJsonString)
.setQuestionnaireLaunchContexts(
.setQuestionnaireLaunchContextMap(
FhirContext.forR4Cached()
.newJsonParser()
.encodeResourceToString(Patient().apply { id = "P1" })
.let { listOf(it) },
.let { mapOf("patient" to it) },
)
.build(),
QUESTIONNAIRE_FRAGMENT_TAG,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -352,10 +352,10 @@ class QuestionnaireFragment : Fragment() {
* user, etc. is "in context" at the time the questionnaire response is being completed:
* https://build.fhir.org/ig/HL7/sdc/StructureDefinition-sdc-questionnaire-launchContext.html
*
* @param launchContexts list of serialized resources
* @param launchContextMap map of launchContext name and serialized resources
*/
fun setQuestionnaireLaunchContexts(launchContexts: List<String>) = apply {
args.add(EXTRA_QUESTIONNAIRE_LAUNCH_CONTEXT_JSON_STRINGS to launchContexts)
fun setQuestionnaireLaunchContextMap(launchContextMap: Map<String, String>) = apply {
args.add(EXTRA_QUESTIONNAIRE_LAUNCH_CONTEXT_MAP to launchContextMap)
}

/**
Expand Down Expand Up @@ -454,9 +454,10 @@ class QuestionnaireFragment : Fragment() {
*/
internal const val EXTRA_QUESTIONNAIRE_RESPONSE_JSON_STRING = "questionnaire-response"

/** A list of JSON encoded strings extra for each questionnaire context. */
internal const val EXTRA_QUESTIONNAIRE_LAUNCH_CONTEXT_JSON_STRINGS =
"questionnaire-launch-contexts"
/**
* A map of launchContext name and JSON encoded strings extra for each questionnaire context.
*/
internal const val EXTRA_QUESTIONNAIRE_LAUNCH_CONTEXT_MAP = "questionnaire-launch-contexts"

/**
* A [URI][android.net.Uri] extra for streaming a JSON encoded questionnaire response.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import com.google.android.fhir.datacapture.extensions.allItems
import com.google.android.fhir.datacapture.extensions.cqfExpression
import com.google.android.fhir.datacapture.extensions.createQuestionnaireResponseItem
import com.google.android.fhir.datacapture.extensions.entryMode
import com.google.android.fhir.datacapture.extensions.filterByCodeInNameExtension
import com.google.android.fhir.datacapture.extensions.flattened
import com.google.android.fhir.datacapture.extensions.hasDifferentAnswerSet
import com.google.android.fhir.datacapture.extensions.isDisplayItem
Expand Down Expand Up @@ -166,15 +167,16 @@ internal class QuestionnaireViewModel(application: Application, state: SavedStat

init {
questionnaireLaunchContextMap =
if (state.contains(QuestionnaireFragment.EXTRA_QUESTIONNAIRE_LAUNCH_CONTEXT_JSON_STRINGS)) {
if (state.contains(QuestionnaireFragment.EXTRA_QUESTIONNAIRE_LAUNCH_CONTEXT_MAP)) {

val launchContextJsonStrings: List<String> =
state[QuestionnaireFragment.EXTRA_QUESTIONNAIRE_LAUNCH_CONTEXT_JSON_STRINGS]!!
val launchContextMapString: Map<String, String> =
state[QuestionnaireFragment.EXTRA_QUESTIONNAIRE_LAUNCH_CONTEXT_MAP]!!

val launchContexts = launchContextJsonStrings.map { parser.parseResource(it) as Resource }
val launchContextMapResource =
launchContextMapString.mapValues { parser.parseResource(it.value) as Resource }
questionnaire.questionnaireLaunchContexts?.let { launchContextExtensions ->
validateLaunchContextExtensions(launchContextExtensions)
launchContexts.associateBy { it.resourceType.name.lowercase() }
filterByCodeInNameExtension(launchContextMapResource, launchContextExtensions)
}
} else {
null
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,15 @@

package com.google.android.fhir.datacapture.extensions

import org.hl7.fhir.exceptions.FHIRException
import org.hl7.fhir.r4.model.CanonicalType
import org.hl7.fhir.r4.model.CodeType
import org.hl7.fhir.r4.model.Coding
import org.hl7.fhir.r4.model.Expression
import org.hl7.fhir.r4.model.Extension
import org.hl7.fhir.r4.model.Questionnaire
import org.hl7.fhir.r4.model.Resource
import org.hl7.fhir.r4.model.ResourceType

/**
* The StructureMap url in the
Expand Down Expand Up @@ -64,73 +67,54 @@ internal fun Questionnaire.findVariableExpression(variableName: String): Express
*/
internal fun validateLaunchContextExtensions(launchContextExtensions: List<Extension>) =
launchContextExtensions.forEach { launchExtension ->
validateLaunchContextExtension(
Extension().apply {
addExtension(launchExtension.extension.firstOrNull { it.url == "name" })
addExtension(launchExtension.extension.firstOrNull { it.url == "type" })
},
)
validateLaunchContextExtension(launchExtension)
}

/**
* Checks that the extension:name extension exists and its value contains a valid code from
* [QuestionnaireLaunchContextSet]
* Verifies the existence of extension:name and extension:type with valid name system and type
* values.
*/
private fun validateLaunchContextExtension(launchExtension: Extension) {
check(launchExtension.extension.size == 2) {
"The extension:name or extension:type extension is missing in $EXTENSION_SDC_QUESTIONNAIRE_LAUNCH_CONTEXT"
}
val nameCoding =
launchExtension.getExtensionByUrl("name")?.value as? Coding
?: error(
"The extension:name is missing or is not of type Coding in $EXTENSION_SDC_QUESTIONNAIRE_LAUNCH_CONTEXT",
)

val isValidExtension =
QuestionnaireLaunchContextSet.values().any {
launchExtension.equalsDeep(
Extension().apply {
addExtension(
Extension().apply {
url = "name"
setValue(
Coding().apply {
code = it.code
display = it.display
system = it.system
},
)
},
)
addExtension(
Extension().apply {
url = "type"
setValue(CodeType().setValue(it.resourceType))
},
)
},
val typeCodeType =
launchExtension.getExtensionByUrl("type")?.value as? CodeType
?: error(
"The extension:type is missing or is not of type CodeType in $EXTENSION_SDC_QUESTIONNAIRE_LAUNCH_CONTEXT",
)

val isValidResourceType =
try {
ResourceType.fromCode(typeCodeType.value) != null
} catch (exception: FHIRException) {
false
}
if (!isValidExtension) {

if (nameCoding.system != EXTENSION_LAUNCH_CONTEXT || !isValidResourceType) {
error(
"The extension:name extension and/or extension:type extension do not follow the format " +
"specified in $EXTENSION_SDC_QUESTIONNAIRE_LAUNCH_CONTEXT",
"The extension:name and/or extension:type do not follow the format specified in $EXTENSION_SDC_QUESTIONNAIRE_LAUNCH_CONTEXT",
)
}
}

/**
* The set of supported launch contexts, as per: http://hl7.org/fhir/uv/sdc/ValueSet/launchContext
* Filters the provided launch contexts by matching the keys with the `code` values found in the
* "name" extensions.
*/
private enum class QuestionnaireLaunchContextSet(
val code: String,
val display: String,
val system: String,
val resourceType: String,
) {
PATIENT("patient", "Patient", EXTENSION_LAUNCH_CONTEXT, "Patient"),
ENCOUNTER("encounter", "Encounter", EXTENSION_LAUNCH_CONTEXT, "Encounter"),
LOCATION("location", "Location", EXTENSION_LAUNCH_CONTEXT, "Location"),
USER_AS_PATIENT("user", "User", EXTENSION_LAUNCH_CONTEXT, "Patient"),
USER_AS_PRACTITIONER("user", "User", EXTENSION_LAUNCH_CONTEXT, "Practitioner"),
USER_AS_PRACTITIONER_ROLE("user", "User", EXTENSION_LAUNCH_CONTEXT, "PractitionerRole"),
USER_AS_RELATED_PERSON("user", "User", EXTENSION_LAUNCH_CONTEXT, "RelatedPerson"),
STUDY("study", "ResearchStudy", EXTENSION_LAUNCH_CONTEXT, "ResearchStudy"),
internal fun filterByCodeInNameExtension(
launchContexts: Map<String, Resource>,
launchContextExtensions: List<Extension>,
): Map<String, Resource> {
val nameCodes =
launchContextExtensions
.mapNotNull { extension -> (extension.getExtensionByUrl("name").value as? Coding)?.code }
.toSet()

return launchContexts.filterKeys { nameCodes.contains(it) }
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,16 @@ package com.google.android.fhir.datacapture.mapping

import com.google.android.fhir.datacapture.DataCapture
import com.google.android.fhir.datacapture.extensions.createQuestionnaireResponseItem
import com.google.android.fhir.datacapture.extensions.filterByCodeInNameExtension
import com.google.android.fhir.datacapture.extensions.initialExpression
import com.google.android.fhir.datacapture.extensions.logicalId
import com.google.android.fhir.datacapture.extensions.questionnaireLaunchContexts
import com.google.android.fhir.datacapture.extensions.targetStructureMap
import com.google.android.fhir.datacapture.extensions.toCodeType
import com.google.android.fhir.datacapture.extensions.toCoding
import com.google.android.fhir.datacapture.extensions.toIdType
import com.google.android.fhir.datacapture.extensions.toUriType
import com.google.android.fhir.datacapture.extensions.validateLaunchContextExtensions
import com.google.android.fhir.datacapture.fhirpath.fhirPathEngine
import java.lang.reflect.Field
import java.lang.reflect.Method
Expand Down Expand Up @@ -214,41 +217,42 @@ object ResourceMapper {
* Performs
* [Expression-based population](http://build.fhir.org/ig/HL7/sdc/populate.html#expression-based-population)
* and returns a [QuestionnaireResponse] for the [questionnaire] that is populated from the
* [resources].
* [launchContexts].
*/
suspend fun populate(
questionnaire: Questionnaire,
vararg resources: Resource,
launchContexts: Map<String, Resource>,
FikriMilano marked this conversation as resolved.
Show resolved Hide resolved
): QuestionnaireResponse {
populateInitialValues(questionnaire.item, *resources)
validateLaunchContextExtensions(questionnaire.questionnaireLaunchContexts ?: listOf())
val filteredLaunchContexts =
filterByCodeInNameExtension(
launchContexts,
questionnaire.questionnaireLaunchContexts ?: listOf(),
)
populateInitialValues(questionnaire.item, filteredLaunchContexts)
return QuestionnaireResponse().apply {
item = questionnaire.item.map { it.createQuestionnaireResponseItem() }
}
}

private suspend fun populateInitialValues(
questionnaireItems: List<Questionnaire.QuestionnaireItemComponent>,
vararg resources: Resource,
launchContexts: Map<String, Resource>,
) {
questionnaireItems.forEach { populateInitialValue(it, *resources) }
questionnaireItems.forEach { populateInitialValue(it, launchContexts) }
}

private suspend fun populateInitialValue(
questionnaireItem: Questionnaire.QuestionnaireItemComponent,
vararg resources: Resource,
launchContexts: Map<String, Resource>,
) {
check(questionnaireItem.initial.isEmpty() || questionnaireItem.initialExpression == null) {
"QuestionnaireItem item is not allowed to have both initial.value and initial expression. See rule at http://build.fhir.org/ig/HL7/sdc/expressions.html#initialExpression."
}

questionnaireItem.initialExpression
?.let {
fhirPathEngine
.evaluate(
selectPopulationContext(resources.asList(), it),
it.expression.removePrefix("%"),
)
.singleOrNull()
fhirPathEngine.evaluate(launchContexts, null, null, null, it.expression).firstOrNull()
FikriMilano marked this conversation as resolved.
Show resolved Hide resolved
}
?.let {
// Set initial value for the questionnaire item. Questionnaire items should not have both
Expand All @@ -258,24 +262,7 @@ object ResourceMapper {
mutableListOf(Questionnaire.QuestionnaireItemInitialComponent().setValue(value))
}

populateInitialValues(questionnaireItem.item, *resources)
}

/**
* Returns the population context for the questionnaire/group.
*
* The resource of the same type as the expected type of the initial expression will be selected
* first. Otherwise, the first resource in the list will be selected.
*
* TODO: rewrite this using the launch context and population context.
*/
private fun selectPopulationContext(
resources: List<Resource>,
initialExpression: Expression,
): Resource? {
val resourceType = initialExpression.expression.substringBefore(".").removePrefix("%")
return resources.singleOrNull { it.resourceType.name.lowercase() == resourceType.lowercase() }
?: resources.firstOrNull()
populateInitialValues(questionnaireItem.item, launchContexts)
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ import ca.uhn.fhir.context.FhirContext
import ca.uhn.fhir.parser.IParser
import com.google.android.fhir.datacapture.QuestionnaireFragment.Companion.EXTRA_ENABLE_REVIEW_PAGE
import com.google.android.fhir.datacapture.QuestionnaireFragment.Companion.EXTRA_QUESTIONNAIRE_JSON_STRING
import com.google.android.fhir.datacapture.QuestionnaireFragment.Companion.EXTRA_QUESTIONNAIRE_LAUNCH_CONTEXT_JSON_STRINGS
import com.google.android.fhir.datacapture.QuestionnaireFragment.Companion.EXTRA_QUESTIONNAIRE_LAUNCH_CONTEXT_MAP
import com.google.android.fhir.datacapture.QuestionnaireFragment.Companion.EXTRA_QUESTIONNAIRE_RESPONSE_JSON_STRING
import com.google.android.fhir.datacapture.QuestionnaireFragment.Companion.EXTRA_READ_ONLY
import com.google.android.fhir.datacapture.QuestionnaireFragment.Companion.EXTRA_SHOW_CANCEL_BUTTON
Expand Down Expand Up @@ -4410,8 +4410,8 @@ class QuestionnaireViewModelTest {
}
state.set(EXTRA_QUESTIONNAIRE_JSON_STRING, printer.encodeResourceToString(questionnaire))
state.set(
EXTRA_QUESTIONNAIRE_LAUNCH_CONTEXT_JSON_STRINGS,
listOf(printer.encodeResourceToString(patient)),
EXTRA_QUESTIONNAIRE_LAUNCH_CONTEXT_MAP,
mapOf("patient" to printer.encodeResourceToString(patient)),
)

val viewModel = QuestionnaireViewModel(context, state)
Expand Down
Loading
Loading