diff --git a/buildSrc/build.gradle.kts b/buildSrc/build.gradle.kts
index 4fe27f3e24..d423ebb078 100644
--- a/buildSrc/build.gradle.kts
+++ b/buildSrc/build.gradle.kts
@@ -11,7 +11,7 @@ repositories {
dependencies {
implementation("com.diffplug.spotless:spotless-plugin-gradle:6.6.0")
- implementation("com.android.tools.build:gradle:8.0.2")
+ implementation("com.android.tools.build:gradle:8.1.1")
implementation("app.cash.licensee:licensee-gradle-plugin:1.3.0")
implementation("com.osacky.flank.gradle:fladle:0.17.4")
diff --git a/catalog/build.gradle.kts b/catalog/build.gradle.kts
index 19cfc139ad..e72b1dc08a 100644
--- a/catalog/build.gradle.kts
+++ b/catalog/build.gradle.kts
@@ -30,8 +30,6 @@ android {
// Flag to enable support for the new language APIs
// See https://developer.android.com/studio/write/java8-support
isCoreLibraryDesugaringEnabled = true
- sourceCompatibility = javaVersion
- targetCompatibility = javaVersion
}
packaging {
diff --git a/catalog/src/main/assets/behavior_skip_logic_with_expression.json b/catalog/src/main/assets/behavior_skip_logic_with_expression.json
new file mode 100644
index 0000000000..8151396ba9
--- /dev/null
+++ b/catalog/src/main/assets/behavior_skip_logic_with_expression.json
@@ -0,0 +1,74 @@
+{
+ "resourceType": "Questionnaire",
+ "extension": [
+ {
+ "url": "http://hl7.org/fhir/StructureDefinition/variable",
+ "valueExpression": {
+ "name": "has-fever",
+ "language": "text/fhirpath",
+ "expression": "%resource.descendants().where(linkId='1').answer.value"
+ }
+ }
+ ],
+ "item": [
+ {
+ "linkId": "1",
+ "type": "boolean",
+ "text": "Does patient has fever?",
+ "item": [
+ {
+ "extension": [
+ {
+ "url": "http://hl7.org/fhir/StructureDefinition/questionnaire-displayCategory",
+ "valueCodeableConcept": {
+ "coding": [
+ {
+ "system": "http://hl7.org/fhir/questionnaire-display-category",
+ "code": "instructions"
+ }
+ ]
+ }
+ }
+ ],
+ "linkId": "1.1",
+ "text": "Define the questionnaire variable 'has-fever' based on the answer to the question 'Does the patient have a fever?",
+ "type": "display"
+ }
+ ]
+ },
+ {
+ "linkId": "2",
+ "text": "Since when?",
+ "type": "date",
+ "extension": [
+ {
+ "url": "http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-enableWhenExpression",
+ "valueExpression": {
+ "language": "text/fhirpath",
+ "expression": "%has-fever"
+ }
+ }
+ ],
+ "item": [
+ {
+ "extension": [
+ {
+ "url": "http://hl7.org/fhir/StructureDefinition/questionnaire-displayCategory",
+ "valueCodeableConcept": {
+ "coding": [
+ {
+ "system": "http://hl7.org/fhir/questionnaire-display-category",
+ "code": "instructions"
+ }
+ ]
+ }
+ }
+ ],
+ "linkId": "2.1",
+ "text": "Enabled if variable 'has-fever' evaluates to true",
+ "type": "display"
+ }
+ ]
+ }
+ ]
+}
\ No newline at end of file
diff --git a/catalog/src/main/assets/component_initial_value.json b/catalog/src/main/assets/component_initial_value.json
new file mode 100644
index 0000000000..1a294ef2f5
--- /dev/null
+++ b/catalog/src/main/assets/component_initial_value.json
@@ -0,0 +1,124 @@
+{
+ "resourceType": "Questionnaire",
+ "item": [
+ {
+ "linkId": "1.0",
+ "type": "choice",
+ "text": "Initially selected single choice",
+ "answerOption": [
+ {
+ "valueCoding": {
+ "code": "Y",
+ "display": "Yes",
+ "system": "custom"
+ }
+ },
+ {
+ "valueCoding": {
+ "code": "N",
+ "display": "No",
+ "system": "custom"
+ },
+ "initialSelected": true
+ },
+ {
+ "valueCoding": {
+ "code": "unknown",
+ "display": "Unknown",
+ "system": "custom"
+ }
+ }
+ ]
+ },
+ {
+ "linkId": "2.0",
+ "type": "choice",
+ "text": "Initially selected multiple choice",
+ "repeats": true,
+ "answerOption": [
+ {
+ "valueCoding": {
+ "code": "1st",
+ "display": "First",
+ "system": "custom"
+ }
+ },
+ {
+ "valueCoding": {
+ "code": "2nd",
+ "display": "Second",
+ "system": "custom"
+ },
+ "initialSelected": true
+ },
+ {
+ "valueCoding": {
+ "code": "3rd",
+ "display": "Third",
+ "system": "custom"
+ },
+ "initialSelected": true
+ }
+ ]
+ },
+ {
+ "linkId": "3.0",
+ "type": "string",
+ "text": "Initially provided text value",
+ "initial": [
+ {
+ "valueString": "Here is a sample text"
+ }
+ ]
+ },
+ {
+ "linkId": "4.0",
+ "type": "boolean",
+ "text": "Initially provided boolean value",
+ "initial": [
+ {
+ "valueBoolean": false
+ }
+ ]
+ },
+ {
+ "linkId": "5.0",
+ "type": "date",
+ "text": "Initially provided date value",
+ "initial": [
+ {
+ "valueDate": "2022-01-22"
+ }
+ ]
+ },
+ {
+ "linkId": "6.0",
+ "type": "quantity",
+ "text": "Initially provided quantity value",
+ "initial": [
+ {
+ "valueQuantity": {
+ "value": 30,
+ "unit": "$",
+ "system": "http://measureunit.org",
+ "code": "USD"
+ }
+ }
+ ]
+ },
+ {
+ "linkId": "7.0",
+ "type": "quantity",
+ "text": "Initially provided quantity unit only",
+ "initial": [
+ {
+ "valueQuantity": {
+ "unit": "$",
+ "system": "http://measureunit.org",
+ "code": "USD"
+ }
+ }
+ ]
+ }
+ ]
+}
\ No newline at end of file
diff --git a/catalog/src/main/java/com/google/android/fhir/catalog/BehaviorListViewModel.kt b/catalog/src/main/java/com/google/android/fhir/catalog/BehaviorListViewModel.kt
index 0f5ffe3c16..86df9c6314 100644
--- a/catalog/src/main/java/com/google/android/fhir/catalog/BehaviorListViewModel.kt
+++ b/catalog/src/main/java/com/google/android/fhir/catalog/BehaviorListViewModel.kt
@@ -1,5 +1,5 @@
/*
- * Copyright 2022 Google LLC
+ * Copyright 2022-2023 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -48,6 +48,11 @@ class BehaviorListViewModel(application: Application) : AndroidViewModel(applica
R.string.behavior_name_skip_logic,
"behavior_skip_logic.json"
),
+ SKIP_LOGIC_WITH_EXPRESSION(
+ R.drawable.ic_skiplogic_behavior,
+ R.string.behavior_name_skip_logic_with_expression,
+ "behavior_skip_logic_with_expression.json"
+ ),
DYNAMIC_QUESTION_TEXT(
R.drawable.ic_dynamic_text_behavior,
R.string.behavior_name_dynamic_question_text,
diff --git a/catalog/src/main/java/com/google/android/fhir/catalog/ComponentListViewModel.kt b/catalog/src/main/java/com/google/android/fhir/catalog/ComponentListViewModel.kt
index 539aa86e87..7ad3971221 100644
--- a/catalog/src/main/java/com/google/android/fhir/catalog/ComponentListViewModel.kt
+++ b/catalog/src/main/java/com/google/android/fhir/catalog/ComponentListViewModel.kt
@@ -140,6 +140,11 @@ class ComponentListViewModel(application: Application, private val state: SavedS
R.drawable.ic_item_answer_media,
R.string.component_name_item_answer_media,
""
+ ),
+ INITIAL_VALUE(
+ R.drawable.ic_initial_value_component,
+ R.string.component_name_initial_value,
+ "component_initial_value.json"
)
}
@@ -164,5 +169,6 @@ class ComponentListViewModel(application: Application, private val state: SavedS
ViewItem.ComponentItem(Component.HELP),
ViewItem.ComponentItem(Component.ITEM_MEDIA),
ViewItem.ComponentItem(Component.ITEM_ANSWER_MEDIA),
+ ViewItem.ComponentItem(Component.INITIAL_VALUE),
)
}
diff --git a/catalog/src/main/res/drawable/ic_initial_value_component.xml b/catalog/src/main/res/drawable/ic_initial_value_component.xml
new file mode 100644
index 0000000000..312a17cf61
--- /dev/null
+++ b/catalog/src/main/res/drawable/ic_initial_value_component.xml
@@ -0,0 +1,23 @@
+
+
+
+
+
+
+
diff --git a/catalog/src/main/res/values/strings.xml b/catalog/src/main/res/values/strings.xml
index bb5b1aa767..1429f57926 100644
--- a/catalog/src/main/res/values/strings.xml
+++ b/catalog/src/main/res/values/strings.xml
@@ -41,6 +41,9 @@
Review
Read only
Skip logic
+ Skip logic with expression
If Yes is selected, a follow-up question is displayed. If No is selected, no follow-up questions are displayed.
@@ -48,6 +51,7 @@
Dynamic question text
+ Initial Value
Input age to automatically calculate birthdate until birthdate is updated manually.
diff --git a/common/build.gradle.kts b/common/build.gradle.kts
index 0df63e6f0d..229dd2a8df 100644
--- a/common/build.gradle.kts
+++ b/common/build.gradle.kts
@@ -25,10 +25,6 @@ android {
defaultConfig { minSdk = Sdk.minSdk }
configureJacocoTestOptions()
kotlin { jvmToolchain(11) }
- compileOptions {
- sourceCompatibility = javaVersion
- targetCompatibility = javaVersion
- }
}
configurations { all { exclude(module = "xpp3") } }
diff --git a/contrib/barcode/build.gradle.kts b/contrib/barcode/build.gradle.kts
index 861d85af1a..a6366d6c00 100644
--- a/contrib/barcode/build.gradle.kts
+++ b/contrib/barcode/build.gradle.kts
@@ -52,10 +52,6 @@ android {
testOptions { animationsDisabled = true }
kotlin { jvmToolchain(11) }
- compileOptions {
- sourceCompatibility = javaVersion
- targetCompatibility = javaVersion
- }
}
configurations { all { exclude(module = "xpp3") } }
diff --git a/datacapture/build.gradle.kts b/datacapture/build.gradle.kts
index aea69ae388..96010180e0 100644
--- a/datacapture/build.gradle.kts
+++ b/datacapture/build.gradle.kts
@@ -44,8 +44,6 @@ android {
// Flag to enable support for the new language APIs
// See https://developer.android.com/studio/write/java8-support
isCoreLibraryDesugaringEnabled = true
- sourceCompatibility = javaVersion
- targetCompatibility = javaVersion
}
packaging {
diff --git a/datacapture/src/androidTest/java/com/google/android/fhir/datacapture/test/views/QuestionnaireItemDialogMultiSelectViewHolderFactoryEspressoTest.kt b/datacapture/src/androidTest/java/com/google/android/fhir/datacapture/test/views/QuestionnaireItemDialogMultiSelectViewHolderFactoryEspressoTest.kt
index cb6b22c3c5..4d1bf1cb48 100644
--- a/datacapture/src/androidTest/java/com/google/android/fhir/datacapture/test/views/QuestionnaireItemDialogMultiSelectViewHolderFactoryEspressoTest.kt
+++ b/datacapture/src/androidTest/java/com/google/android/fhir/datacapture/test/views/QuestionnaireItemDialogMultiSelectViewHolderFactoryEspressoTest.kt
@@ -360,7 +360,7 @@ class QuestionnaireItemDialogMultiSelectViewHolderFactoryEspressoTest {
}
@Test
- fun `shouldHideErrorTextviewInHeader`() {
+ fun shouldHideErrorTextviewInHeader() {
val questionnaireItem = answerOptions(true, "Coding 1")
questionnaireItem.addExtension(openChoiceType)
val questionnaireViewItem =
diff --git a/datacapture/src/main/java/com/google/android/fhir/datacapture/QuestionnaireViewModel.kt b/datacapture/src/main/java/com/google/android/fhir/datacapture/QuestionnaireViewModel.kt
index aa0361e303..357ae7300c 100644
--- a/datacapture/src/main/java/com/google/android/fhir/datacapture/QuestionnaireViewModel.kt
+++ b/datacapture/src/main/java/com/google/android/fhir/datacapture/QuestionnaireViewModel.kt
@@ -46,9 +46,7 @@ import com.google.android.fhir.datacapture.extensions.shouldHaveNestedItemsUnder
import com.google.android.fhir.datacapture.extensions.unpackRepeatedGroups
import com.google.android.fhir.datacapture.extensions.validateLaunchContextExtensions
import com.google.android.fhir.datacapture.extensions.zipByLinkId
-import com.google.android.fhir.datacapture.fhirpath.ExpressionEvaluator.detectExpressionCyclicDependency
-import com.google.android.fhir.datacapture.fhirpath.ExpressionEvaluator.evaluateCalculatedExpressions
-import com.google.android.fhir.datacapture.fhirpath.ExpressionEvaluator.evaluateExpression
+import com.google.android.fhir.datacapture.fhirpath.ExpressionEvaluator
import com.google.android.fhir.datacapture.validation.Invalid
import com.google.android.fhir.datacapture.validation.NotValidated
import com.google.android.fhir.datacapture.validation.QuestionnaireResponseItemValidator
@@ -335,12 +333,30 @@ internal class QuestionnaireViewModel(application: Application, state: SavedStat
modificationCount.update { it + 1 }
}
+ private val expressionEvaluator: ExpressionEvaluator =
+ ExpressionEvaluator(
+ questionnaire,
+ questionnaireResponse,
+ questionnaireItemParentMap,
+ questionnaireLaunchContextMap
+ )
+
+ private val enablementEvaluator: EnablementEvaluator =
+ EnablementEvaluator(
+ questionnaire,
+ questionnaireResponse,
+ questionnaireItemParentMap,
+ questionnaireLaunchContextMap
+ )
+
private val answerOptionsEvaluator: EnabledAnswerOptionsEvaluator =
EnabledAnswerOptionsEvaluator(
questionnaire,
- questionnaireLaunchContextMap,
+ questionnaireResponse,
xFhirQueryResolver,
- externalValueSetResolver
+ externalValueSetResolver,
+ questionnaireItemParentMap,
+ questionnaireLaunchContextMap
)
/**
@@ -404,7 +420,9 @@ internal class QuestionnaireViewModel(application: Application, state: SavedStat
QuestionnaireResponseValidator.validateQuestionnaireResponse(
questionnaire,
questionnaireResponse,
- getApplication()
+ getApplication(),
+ questionnaireItemParentMap,
+ questionnaireLaunchContextMap,
)
.also { result ->
if (result.values.flatten().filterIsInstance().isNotEmpty()) {
@@ -480,13 +498,14 @@ internal class QuestionnaireViewModel(application: Application, state: SavedStat
.withIndex()
.onEach {
if (it.index == 0) {
- detectExpressionCyclicDependency(questionnaire.item)
+ expressionEvaluator.detectExpressionCyclicDependency(questionnaire.item)
questionnaire.item.flattened().forEach { qItem ->
updateDependentQuestionnaireResponseItems(
qItem,
questionnaireResponse.allItems.find { qrItem -> qrItem.linkId == qItem.linkId }
)
}
+ modificationCount.update { count -> count + 1 }
}
}
.map { it.value }
@@ -497,15 +516,13 @@ internal class QuestionnaireViewModel(application: Application, state: SavedStat
)
private fun updateDependentQuestionnaireResponseItems(
- updatedQuestionnaireItem: QuestionnaireItemComponent,
+ questionnaireItem: QuestionnaireItemComponent,
updatedQuestionnaireResponseItem: QuestionnaireResponseItemComponent?,
) {
- evaluateCalculatedExpressions(
- updatedQuestionnaireItem,
+ expressionEvaluator
+ .evaluateCalculatedExpressions(
+ questionnaireItem,
updatedQuestionnaireResponseItem,
- questionnaire,
- questionnaireResponse,
- questionnaireItemParentMap
)
.forEach { (questionnaireItem, calculatedAnswers) ->
// update all response item with updated values
@@ -538,13 +555,10 @@ internal class QuestionnaireViewModel(application: Application, state: SavedStat
if (!cqfExpression.isFhirPath) {
throw UnsupportedOperationException("${cqfExpression.language} not supported yet")
}
- return evaluateExpression(
- questionnaire,
- questionnaireResponse,
+ return expressionEvaluator.evaluateExpression(
questionnaireItem,
questionnaireResponseItem,
cqfExpression,
- questionnaireItemParentMap
)
}
@@ -653,8 +667,10 @@ internal class QuestionnaireViewModel(application: Application, state: SavedStat
// Hidden questions should not get QuestionnaireItemViewItem instances
if (questionnaireItem.isHidden) return emptyList()
val enabled =
- EnablementEvaluator(questionnaireResponse)
- .evaluate(questionnaireItem, questionnaireResponseItem)
+ enablementEvaluator.evaluate(
+ questionnaireItem,
+ questionnaireResponseItem,
+ )
// Disabled questions should not get QuestionnaireItemViewItem instances
if (!enabled) {
cacheDisabledQuestionnaireItemAnswers(questionnaireResponseItem)
@@ -688,8 +704,6 @@ internal class QuestionnaireViewModel(application: Application, state: SavedStat
answerOptionsEvaluator.evaluate(
questionnaireItem,
questionnaireResponseItem,
- questionnaireResponse,
- questionnaireItemParentMap
)
if (disabledQuestionnaireResponseAnswers.isNotEmpty()) {
removeDisabledAnswers(
@@ -713,7 +727,10 @@ internal class QuestionnaireViewModel(application: Application, state: SavedStat
enabledDisplayItems =
questionnaireItem.item.filter {
it.isDisplayItem &&
- EnablementEvaluator(questionnaireResponse).evaluate(it, questionnaireResponseItem)
+ enablementEvaluator.evaluate(
+ it,
+ questionnaireResponseItem,
+ )
},
questionViewTextConfiguration =
QuestionTextConfiguration(
@@ -790,7 +807,6 @@ internal class QuestionnaireViewModel(application: Application, state: SavedStat
questionnaireItemList: List,
questionnaireResponseItemList: List,
): List {
- val enablementEvaluator = EnablementEvaluator(questionnaireResponse)
val responseItemKeys = questionnaireResponseItemList.map { it.linkId }
return questionnaireItemList
.asSequence()
@@ -828,11 +844,10 @@ internal class QuestionnaireViewModel(application: Application, state: SavedStat
->
QuestionnairePage(
index,
- EnablementEvaluator(questionnaireResponse)
- .evaluate(
- questionnaireItem,
- questionnaireResponseItem,
- ),
+ enablementEvaluator.evaluate(
+ questionnaireItem,
+ questionnaireResponseItem,
+ ),
questionnaireItem.isHidden
)
}
diff --git a/datacapture/src/main/java/com/google/android/fhir/datacapture/enablement/EnablementEvaluator.kt b/datacapture/src/main/java/com/google/android/fhir/datacapture/enablement/EnablementEvaluator.kt
index ab293b0200..0cbc02e96e 100644
--- a/datacapture/src/main/java/com/google/android/fhir/datacapture/enablement/EnablementEvaluator.kt
+++ b/datacapture/src/main/java/com/google/android/fhir/datacapture/enablement/EnablementEvaluator.kt
@@ -1,5 +1,5 @@
/*
- * Copyright 2022 Google LLC
+ * Copyright 2022-2023 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -19,10 +19,12 @@ package com.google.android.fhir.datacapture.enablement
import com.google.android.fhir.compareTo
import com.google.android.fhir.datacapture.extensions.allItems
import com.google.android.fhir.datacapture.extensions.enableWhenExpression
+import com.google.android.fhir.datacapture.fhirpath.ExpressionEvaluator
import com.google.android.fhir.datacapture.fhirpath.evaluateToBoolean
import com.google.android.fhir.equals
import org.hl7.fhir.r4.model.Questionnaire
import org.hl7.fhir.r4.model.QuestionnaireResponse
+import org.hl7.fhir.r4.model.Resource
/**
* Evaluator for the enablement status of a [Questionnaire.QuestionnaireItemComponent].
@@ -50,16 +52,39 @@ import org.hl7.fhir.r4.model.QuestionnaireResponse
* is shown or hidden. However, it is also possible that only user interaction is enabled or
* disabled (e.g. grayed out) with the [Questionnaire.QuestionnaireItemComponent] always shown.
*
- * The evaluator does not track the changes in the `questionnaire` and `questionnaireResponse`.
- * Therefore, a new evaluator should be created if they were modified.
+ * The evaluator works in the context of a Questionnaire and the corresponding
+ * QuestionnaireResponse. It is the caller's responsibility to make sure to call the evaluator with
+ * QuestionnaireItems and QuestionnaireResponseItems that belong to the Questionnaire and the
+ * QuestionnaireResponse.
*
* For more information see
* [Questionnaire.item.enableWhen](https://www.hl7.org/fhir/questionnaire-definitions.html#Questionnaire.item.enableWhen)
* and
* [Questionnaire.item.enableBehavior](https://www.hl7.org/fhir/questionnaire-definitions.html#Questionnaire.item.enableBehavior)
* .
+ *
+ * @param questionnaire the [Questionnaire] where the expression belong to
+ * @param questionnaireResponse the [QuestionnaireResponse] related to the [Questionnaire]
+ * @param questionnaireItemParentMap the [Map] of items parent
+ * @param questionnaireLaunchContextMap the [Map] of launchContext names to their resource values
*/
-internal class EnablementEvaluator(val questionnaireResponse: QuestionnaireResponse) {
+internal class EnablementEvaluator(
+ private val questionnaire: Questionnaire,
+ private val questionnaireResponse: QuestionnaireResponse,
+ private val questionnaireItemParentMap:
+ Map =
+ emptyMap(),
+ private val questionnaireLaunchContextMap: Map? = emptyMap(),
+) {
+
+ private val expressionEvaluator =
+ ExpressionEvaluator(
+ questionnaire,
+ questionnaireResponse,
+ questionnaireItemParentMap,
+ questionnaireLaunchContextMap
+ )
+
/**
* The pre-order traversal trace of the items in the [QuestionnaireResponse]. This essentially
* represents the order in which all items are displayed in the UI.
@@ -95,6 +120,7 @@ internal class EnablementEvaluator(val questionnaireResponse: QuestionnaireRespo
/**
* Returns whether [questionnaireItem] should be enabled.
*
+ * @param questionnaireItem the corresponding questionnaire item.
* @param questionnaireResponseItem the corresponding questionnaire response item.
*/
fun evaluate(
@@ -110,10 +136,16 @@ internal class EnablementEvaluator(val questionnaireResponse: QuestionnaireRespo
// Evaluate `enableWhenExpression`.
if (enableWhenExpression != null && enableWhenExpression.hasExpression()) {
+ val contextMap =
+ expressionEvaluator.extractDependentVariables(
+ questionnaireItem.enableWhenExpression!!,
+ questionnaireItem,
+ )
return evaluateToBoolean(
questionnaireResponse,
questionnaireResponseItem,
- enableWhenExpression.expression
+ enableWhenExpression.expression,
+ contextMap,
)
}
diff --git a/datacapture/src/main/java/com/google/android/fhir/datacapture/expressions/EnabledAnswerOptionsEvaluator.kt b/datacapture/src/main/java/com/google/android/fhir/datacapture/expressions/EnabledAnswerOptionsEvaluator.kt
index f0bb0a0188..fa11cca5ab 100644
--- a/datacapture/src/main/java/com/google/android/fhir/datacapture/expressions/EnabledAnswerOptionsEvaluator.kt
+++ b/datacapture/src/main/java/com/google/android/fhir/datacapture/expressions/EnabledAnswerOptionsEvaluator.kt
@@ -29,17 +29,48 @@ import org.hl7.fhir.r4.model.Coding
import org.hl7.fhir.r4.model.Questionnaire
import org.hl7.fhir.r4.model.Questionnaire.QuestionnaireItemComponent
import org.hl7.fhir.r4.model.QuestionnaireResponse
+import org.hl7.fhir.r4.model.QuestionnaireResponse.QuestionnaireResponseItemComponent
import org.hl7.fhir.r4.model.Resource
import org.hl7.fhir.r4.model.ResourceType
import org.hl7.fhir.r4.model.ValueSet
+/**
+ * Evaluates and manages answer options within a [Questionnaire] and its corresponding
+ * [QuestionnaireResponse]. It handles enablement, disablement, and presentation of options based on
+ * expressions and criteria.
+ *
+ * The evaluator works in the context of a [Questionnaire] and the corresponding
+ * [QuestionnaireResponse]. It is the caller's responsibility to make sure to call the evaluator
+ * with [QuestionnaireItemComponent] and [QuestionnaireResponseItemComponent] that belong to the
+ * [Questionnaire] and the [QuestionnaireResponse].
+ *
+ * @param questionnaire the [Questionnaire] where the expression belong to
+ * @param questionnaireResponse the [QuestionnaireResponse] related to the [Questionnaire]
+ * @param xFhirQueryResolver the [XFhirQueryResolver] to resolve resources based on the X-FHIR-Query
+ * @param externalValueSetResolver the [ExternalAnswerValueSetResolver] to resolve value sets
+ * externally/outside of the [Questionnaire]
+ * @param questionnaireItemParentMap the [Map] of items parent
+ * @param questionnaireLaunchContextMap the [Map] of launchContext names to their resource values
+ */
internal class EnabledAnswerOptionsEvaluator(
private val questionnaire: Questionnaire,
- private val questionnaireLaunchContextMap: Map?,
+ private val questionnaireResponse: QuestionnaireResponse,
private val xFhirQueryResolver: XFhirQueryResolver?,
- private val externalValueSetResolver: ExternalAnswerValueSetResolver?
+ private val externalValueSetResolver: ExternalAnswerValueSetResolver?,
+ private val questionnaireItemParentMap:
+ Map =
+ emptyMap(),
+ private val questionnaireLaunchContextMap: Map? = emptyMap(),
) {
+ private val expressionEvaluator =
+ ExpressionEvaluator(
+ questionnaire,
+ questionnaireResponse,
+ questionnaireItemParentMap,
+ questionnaireLaunchContextMap
+ )
+
private val answerValueSetMap =
mutableMapOf>()
@@ -60,16 +91,13 @@ internal class EnabledAnswerOptionsEvaluator(
*/
internal suspend fun evaluate(
questionnaireItem: QuestionnaireItemComponent,
- questionnaireResponseItem: QuestionnaireResponse.QuestionnaireResponseItemComponent,
- questionnaireResponse: QuestionnaireResponse,
- questionnaireItemParentMap: Map
+ questionnaireResponseItem: QuestionnaireResponseItemComponent,
): Pair<
List,
List
> {
- val resolvedAnswerOptions =
- answerOptions(questionnaireItem, questionnaireResponse, questionnaireItemParentMap)
+ val resolvedAnswerOptions = answerOptions(questionnaireItem)
if (questionnaireItem.answerOptionsToggleExpressions.isEmpty())
return Pair(resolvedAnswerOptions, emptyList())
@@ -78,9 +106,7 @@ internal class EnabledAnswerOptionsEvaluator(
evaluateAnswerOptionsToggleExpressions(
questionnaireItem,
questionnaireResponseItem,
- questionnaireResponse,
resolvedAnswerOptions,
- questionnaireItemParentMap
)
val disabledAnswers =
questionnaireResponseItem.answer
@@ -107,19 +133,12 @@ internal class EnabledAnswerOptionsEvaluator(
*/
private suspend fun answerOptions(
questionnaireItem: QuestionnaireItemComponent,
- questionnaireResponse: QuestionnaireResponse,
- questionnaireItemParentMap: Map
): List =
when {
questionnaireItem.answerOption.isNotEmpty() -> questionnaireItem.answerOption
!questionnaireItem.answerValueSet.isNullOrEmpty() ->
resolveAnswerValueSet(questionnaireItem.answerValueSet)
- questionnaireItem.answerExpression != null ->
- resolveAnswerExpression(
- questionnaireItem,
- questionnaireResponse,
- questionnaireItemParentMap
- )
+ questionnaireItem.answerExpression != null -> resolveAnswerExpression(questionnaireItem)
else -> emptyList()
}
@@ -166,8 +185,6 @@ internal class EnabledAnswerOptionsEvaluator(
// https://build.fhir.org/ig/HL7/sdc/expressions.html#x-fhir-query-enhancements
private suspend fun resolveAnswerExpression(
item: QuestionnaireItemComponent,
- questionnaireResponse: QuestionnaireResponse,
- questionnaireItemParentMap: Map
): List {
// Check cache first for database queries
val answerExpression = item.answerExpression ?: return emptyList()
@@ -176,13 +193,9 @@ internal class EnabledAnswerOptionsEvaluator(
answerExpression.isXFhirQuery -> {
xFhirQueryResolver?.let { xFhirQueryResolver ->
val xFhirExpressionString =
- ExpressionEvaluator.createXFhirQueryFromExpression(
- questionnaire,
- questionnaireResponse,
+ expressionEvaluator.createXFhirQueryFromExpression(
item,
- questionnaireItemParentMap,
answerExpression,
- questionnaireLaunchContextMap
)
if (answerExpressionMap.containsKey(xFhirExpressionString)) {
answerExpressionMap[xFhirExpressionString]
@@ -212,9 +225,7 @@ internal class EnabledAnswerOptionsEvaluator(
private fun evaluateAnswerOptionsToggleExpressions(
item: QuestionnaireItemComponent,
questionnaireResponseItem: QuestionnaireResponse.QuestionnaireResponseItemComponent,
- questionnaireResponse: QuestionnaireResponse,
answerOptions: List,
- questionnaireItemParentMap: Map
): List {
val results =
item.answerOptionsToggleExpressions
@@ -223,13 +234,10 @@ internal class EnabledAnswerOptionsEvaluator(
val evaluationResult =
if (expression.isFhirPath)
fhirPathEngine.convertToBoolean(
- ExpressionEvaluator.evaluateExpression(
- questionnaire,
- questionnaireResponse,
+ expressionEvaluator.evaluateExpression(
item,
questionnaireResponseItem,
expression,
- questionnaireItemParentMap
)
)
else
diff --git a/datacapture/src/main/java/com/google/android/fhir/datacapture/extensions/MoreQuestionnaireItemAnswerOptionComponents.kt b/datacapture/src/main/java/com/google/android/fhir/datacapture/extensions/MoreQuestionnaireItemAnswerOptionComponents.kt
index 8ea452481c..4bcc87fd20 100644
--- a/datacapture/src/main/java/com/google/android/fhir/datacapture/extensions/MoreQuestionnaireItemAnswerOptionComponents.kt
+++ b/datacapture/src/main/java/com/google/android/fhir/datacapture/extensions/MoreQuestionnaireItemAnswerOptionComponents.kt
@@ -1,5 +1,5 @@
/*
- * Copyright 2022 Google LLC
+ * Copyright 2022-2023 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -24,6 +24,8 @@ import com.google.android.fhir.datacapture.R
import org.hl7.fhir.r4.model.Attachment
import org.hl7.fhir.r4.model.BooleanType
import org.hl7.fhir.r4.model.Questionnaire
+import org.hl7.fhir.r4.model.Questionnaire.QuestionnaireItemAnswerOptionComponent
+import org.hl7.fhir.r4.model.Type
internal const val EXTENSION_OPTION_EXCLUSIVE_URL =
"http://hl7.org/fhir/StructureDefinition/questionnaire-optionExclusive"
@@ -42,6 +44,10 @@ internal val Questionnaire.QuestionnaireItemAnswerOptionComponent.optionExclusiv
return false
}
+/** Get the answer options values with `initialSelected` set to true */
+internal val List.initialSelected: List
+ get() = this.filter { it.initialSelected }.map { it.value }
+
fun Questionnaire.QuestionnaireItemAnswerOptionComponent.itemAnswerOptionImage(
context: Context
): Drawable? {
diff --git a/datacapture/src/main/java/com/google/android/fhir/datacapture/extensions/MoreQuestionnaireItemComponents.kt b/datacapture/src/main/java/com/google/android/fhir/datacapture/extensions/MoreQuestionnaireItemComponents.kt
index e698578dfd..7388e6df61 100644
--- a/datacapture/src/main/java/com/google/android/fhir/datacapture/extensions/MoreQuestionnaireItemComponents.kt
+++ b/datacapture/src/main/java/com/google/android/fhir/datacapture/extensions/MoreQuestionnaireItemComponents.kt
@@ -143,6 +143,9 @@ internal const val EXTENSION_SLIDER_STEP_VALUE_URL =
internal const val EXTENSION_VARIABLE_URL = "http://hl7.org/fhir/StructureDefinition/variable"
+internal const val ITEM_INITIAL_EXPRESSION_URL: String =
+ "http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-initialExpression"
+
// ********************************************************************************************** //
// //
// Rendering extensions: item control, choice orientation, etc. //
@@ -169,6 +172,17 @@ enum class ItemControlTypes(
PHONE_NUMBER("phone-number", QuestionnaireViewHolderType.PHONE_NUMBER),
}
+/**
+ * The initial-expression extension on [QuestionnaireItemComponent] to allow dynamic selection of
+ * default or initially selected answers
+ */
+val Questionnaire.QuestionnaireItemComponent.initialExpression: Expression?
+ get() {
+ return this.extension
+ .firstOrNull { it.url == ITEM_INITIAL_EXPRESSION_URL }
+ ?.let { it.value as Expression }
+ }
+
/**
* The [ItemControlTypes] of the questionnaire item if it is specified by the item control
* extension, or `null`.
@@ -834,11 +848,20 @@ fun Questionnaire.QuestionnaireItemComponent.createQuestionnaireResponseItem():
*/
private fun Questionnaire.QuestionnaireItemComponent.createQuestionnaireResponseItemAnswers():
MutableList? {
+ // TODO https://github.com/google/android-fhir/issues/2161
+ // The rule can be by-passed if initial value was set by an initial-expression.
+ // The [ResourceMapper] at L260 wrongfully sets the initial property of questionnaire after
+ // evaluation of initial-expression.
+ require(answerOption.isEmpty() || initial.isEmpty() || initialExpression != null) {
+ "Questionnaire item $linkId has both initial value(s) and has answerOption. See rule que-11 at https://www.hl7.org/fhir/questionnaire-definitions.html#Questionnaire.item.initial."
+ }
+
// https://build.fhir.org/ig/HL7/sdc/behavior.html#initial
// quantity given as initial without value is for unit reference purpose only. Answer conversion
// not needed
- if (initial.isEmpty() ||
- (initialFirstRep.hasValueQuantity() && initialFirstRep.valueQuantity.value == null)
+ if (answerOption.initialSelected.isEmpty() &&
+ (initial.isEmpty() ||
+ (initialFirstRep.hasValueQuantity() && initialFirstRep.valueQuantity.value == null))
) {
return null
}
@@ -851,16 +874,16 @@ private fun Questionnaire.QuestionnaireItemComponent.createQuestionnaireResponse
)
}
- if (initial.size > 1 && !repeats) {
+ if ((answerOption.initialSelected.size > 1 || initial.size > 1) && !repeats) {
throw IllegalArgumentException(
"Questionnaire item $linkId can only have multiple initial values for repeating items. See rule que-13 at https://www.hl7.org/fhir/questionnaire-definitions.html#Questionnaire.item.initial."
)
}
return initial
- .map {
- QuestionnaireResponse.QuestionnaireResponseItemAnswerComponent().apply { value = it.value }
- }
+ .map { it.value }
+ .plus(answerOption.initialSelected)
+ .map { QuestionnaireResponse.QuestionnaireResponseItemAnswerComponent().apply { value = it } }
.toMutableList()
}
diff --git a/datacapture/src/main/java/com/google/android/fhir/datacapture/fhirpath/ExpressionEvaluator.kt b/datacapture/src/main/java/com/google/android/fhir/datacapture/fhirpath/ExpressionEvaluator.kt
index ed0c1c84a8..9736bf6c30 100644
--- a/datacapture/src/main/java/com/google/android/fhir/datacapture/fhirpath/ExpressionEvaluator.kt
+++ b/datacapture/src/main/java/com/google/android/fhir/datacapture/fhirpath/ExpressionEvaluator.kt
@@ -35,12 +35,29 @@ import timber.log.Timber
/**
* Evaluates an expression and returns its result.
*
+ * The evaluator works in the context of a [Questionnaire] and the corresponding
+ * [QuestionnaireResponse]. It is the caller's responsibility to make sure to call the evaluator
+ * with [QuestionnaireItemComponent] and [QuestionnaireResponseItemComponent] that belong to the
+ * [Questionnaire] and the [QuestionnaireResponse].
+ *
* Expressions can be defined at questionnaire level and questionnaire item level. This
* [ExpressionEvaluator] supports evaluation of
* [variable expression](http://hl7.org/fhir/R4/extension-variable.html) defined at either
* questionnaire level or questionnaire item level.
+ *
+ * @param questionnaire the [Questionnaire] where the expression belong to
+ * @param questionnaireResponse the [QuestionnaireResponse] related to the [Questionnaire]
+ * @param questionnaireItemParentMap the [Map] of items parent
+ * @param questionnaireLaunchContextMap the [Map] of launchContext names to their resource values
*/
-object ExpressionEvaluator {
+internal class ExpressionEvaluator(
+ private val questionnaire: Questionnaire,
+ private val questionnaireResponse: QuestionnaireResponse,
+ private val questionnaireItemParentMap:
+ Map =
+ emptyMap(),
+ private val questionnaireLaunchContextMap: Map? = emptyMap(),
+) {
private val reservedVariables =
listOf("sct", "loinc", "ucum", "resource", "rootResource", "context", "map-codes")
@@ -64,9 +81,7 @@ object ExpressionEvaluator {
private val xFhirQueryEnhancementRegex = Regex("\\{\\{(.*?)\\}\\}")
/** Detects if any item into list is referencing a dependent item in its calculated expression */
- internal fun detectExpressionCyclicDependency(
- items: List
- ) {
+ internal fun detectExpressionCyclicDependency(items: List) {
items
.flattened()
.filter { it.calculatedExpression != null }
@@ -92,30 +107,17 @@ object ExpressionEvaluator {
* %resource = [QuestionnaireResponse] %context = [QuestionnaireResponseItemComponent]
*/
fun evaluateExpression(
- questionnaire: Questionnaire,
- questionnaireResponse: QuestionnaireResponse,
questionnaireItem: QuestionnaireItemComponent,
questionnaireResponseItem: QuestionnaireResponseItemComponent?,
expression: Expression,
- questionnaireItemParentMap: Map
): List {
- val appContext =
- mutableMapOf().apply {
- extractDependentVariables(
- expression,
- questionnaire,
- questionnaireResponse,
- questionnaireItemParentMap,
- questionnaireItem,
- this
- )
- }
+ val appContext = extractDependentVariables(expression, questionnaireItem)
return fhirPathEngine.evaluate(
- appContext,
- questionnaireResponse,
- null,
- questionnaireResponseItem,
- expression.expression
+ /* appContext= */ appContext,
+ /* focusResource= */ questionnaireResponse,
+ /* rootResource= */ null,
+ /* base= */ questionnaireResponseItem,
+ /* path= */ expression.expression,
)
}
@@ -124,11 +126,8 @@ object ExpressionEvaluator {
* calculated expression extension, which is dependent on value of updated response
*/
fun evaluateCalculatedExpressions(
- updatedQuestionnaireItem: QuestionnaireItemComponent,
+ questionnaireItem: QuestionnaireItemComponent,
updatedQuestionnaireResponseItemComponent: QuestionnaireResponseItemComponent?,
- questionnaire: Questionnaire,
- questionnaireResponse: QuestionnaireResponse,
- questionnaireItemParentMap: Map
): List {
return questionnaire.item
.flattened()
@@ -136,21 +135,18 @@ object ExpressionEvaluator {
// Condition 1. item is calculable
// Condition 2. item answer depends on the updated item answer OR has a variable dependency
item.calculatedExpression != null &&
- (updatedQuestionnaireItem.isReferencedBy(item) ||
+ (questionnaireItem.isReferencedBy(item) ||
findDependentVariables(item.calculatedExpression!!).isNotEmpty())
}
- .map { questionnaireItem ->
+ .map { item ->
val updatedAnswer =
evaluateExpression(
- questionnaire,
- questionnaireResponse,
- questionnaireItem,
+ item,
updatedQuestionnaireResponseItemComponent,
- questionnaireItem.calculatedExpression!!,
- questionnaireItemParentMap
+ item.calculatedExpression!!,
)
.map { it.castToType(it) }
- questionnaireItem to updatedAnswer
+ item to updatedAnswer
}
}
@@ -167,9 +163,6 @@ object ExpressionEvaluator {
* expression being evaluated.
*
* @param expression the [Expression] Variable expression
- * @param questionnaire the [Questionnaire] respective questionnaire
- * @param questionnaireResponse the [QuestionnaireResponse] respective questionnaire response
- * @param questionnaireItemParentMap the [Map] of child to parent
* @param questionnaireItem the [Questionnaire.QuestionnaireItemComponent] where this expression
* is defined,
@@ -179,12 +172,8 @@ object ExpressionEvaluator {
*/
internal fun evaluateQuestionnaireItemVariableExpression(
expression: Expression,
- questionnaire: Questionnaire,
- questionnaireResponse: QuestionnaireResponse,
- questionnaireItemParentMap:
- Map,
- questionnaireItem: Questionnaire.QuestionnaireItemComponent,
- variablesMap: MutableMap = mutableMapOf()
+ questionnaireItem: QuestionnaireItemComponent,
+ variablesMap: MutableMap = mutableMapOf(),
): Base? {
require(
questionnaireItem.variableExpressions.any {
@@ -193,14 +182,14 @@ object ExpressionEvaluator {
) { "The expression should come from the same questionnaire item" }
extractDependentVariables(
expression,
- questionnaire,
- questionnaireResponse,
- questionnaireItemParentMap,
questionnaireItem,
- variablesMap
+ variablesMap,
)
- return evaluateVariable(expression, questionnaireResponse, variablesMap)
+ return evaluateVariable(
+ expression,
+ variablesMap,
+ )
}
/**
@@ -208,35 +197,27 @@ object ExpressionEvaluator {
* values respecting the scope and hierarchy level
*
* @param expression the [Expression] expression to find variables applicable
- * @param questionnaire the [Questionnaire] respective questionnaire
- * @param questionnaireResponse the [QuestionnaireResponse] respective questionnaire response
- * @param questionnaireItemParentMap the [Map] of child to parent
* @param questionnaireItem the [Questionnaire.QuestionnaireItemComponent] where this expression
* @param variablesMap the [Map] of variables, the default value is empty map is
* defined
*/
- private fun extractDependentVariables(
+ internal fun extractDependentVariables(
expression: Expression,
- questionnaire: Questionnaire,
- questionnaireResponse: QuestionnaireResponse,
- questionnaireItemParentMap:
- Map,
- questionnaireItem: Questionnaire.QuestionnaireItemComponent,
- variablesMap: MutableMap = mutableMapOf()
- ) =
+ questionnaireItem: QuestionnaireItemComponent,
+ variablesMap: MutableMap = mutableMapOf(),
+ ): MutableMap {
+ questionnaireLaunchContextMap?.let { variablesMap.putAll(it) }
findDependentVariables(expression).forEach { variableName ->
if (variablesMap[variableName] == null) {
findAndEvaluateVariable(
variableName,
questionnaireItem,
- questionnaire,
- questionnaireResponse,
- questionnaireItemParentMap,
- variablesMap
+ variablesMap,
)
}
}
+ return variablesMap
+ }
/**
* Evaluates variable expression defined at questionnaire level and returns the evaluated result.
@@ -249,17 +230,13 @@ object ExpressionEvaluator {
* the evaluated values to the expression being evaluated.
*
* @param expression the [Expression] Variable expression
- * @param questionnaire the [Questionnaire] respective questionnaire
- * @param questionnaireResponse the [QuestionnaireResponse] respective questionnaire response
* @param variablesMap the [Map] of variables, the default value is empty map
*
* @return [Base] the result of expression
*/
internal fun evaluateQuestionnaireVariableExpression(
expression: Expression,
- questionnaire: Questionnaire,
- questionnaireResponse: QuestionnaireResponse,
- variablesMap: MutableMap = mutableMapOf()
+ variablesMap: MutableMap = mutableMapOf(),
): Base? {
findDependentVariables(expression).forEach { variableName ->
questionnaire.findVariableExpression(variableName)?.let { expression ->
@@ -267,15 +244,16 @@ object ExpressionEvaluator {
variablesMap[expression.name] =
evaluateQuestionnaireVariableExpression(
expression,
- questionnaire,
- questionnaireResponse,
- variablesMap
+ variablesMap,
)
}
}
}
- return evaluateVariable(expression, questionnaireResponse, variablesMap)
+ return evaluateVariable(
+ expression,
+ variablesMap,
+ )
}
/**
@@ -283,31 +261,24 @@ object ExpressionEvaluator {
* fhir-paths in the expression.
*/
internal fun createXFhirQueryFromExpression(
- questionnaire: Questionnaire,
- questionnaireResponse: QuestionnaireResponse,
questionnaireItem: QuestionnaireItemComponent,
- questionnaireItemParentMap: Map,
expression: Expression,
- launchContextMap: Map?
): String {
// get all dependent variables and their evaluated values
val variablesEvaluatedPairs =
- mutableMapOf()
- .apply {
- extractDependentVariables(
- expression,
- questionnaire,
- questionnaireResponse,
- questionnaireItemParentMap,
- questionnaireItem,
- this
- )
- }
+ extractDependentVariables(
+ expression,
+ questionnaireItem,
+ )
.filterKeys { expression.expression.contains("{{%$it}}") }
.map { Pair("{{%${it.key}}}", it.value!!.primitiveValue()) }
val fhirPathsEvaluatedPairs =
- launchContextMap?.let { evaluateXFhirEnhancement(expression, it) } ?: emptySequence()
+ questionnaireLaunchContextMap
+ .takeIf { !it.isNullOrEmpty() }
+ ?.let { evaluateXFhirEnhancement(expression, it) }
+ ?: emptySequence()
+
return (fhirPathsEvaluatedPairs + variablesEvaluatedPairs).fold(expression.expression) {
acc: String,
pair: Pair ->
@@ -340,11 +311,11 @@ object ExpressionEvaluator {
?: expressionNode.name?.lowercase()
val evaluatedResult =
fhirPathEngine.evaluateToString(
- launchContextMap,
- null,
- null,
- launchContextMap[resourceType],
- expressionNode
+ /* appInfo= */ launchContextMap,
+ /* focusResource= */ null,
+ /* rootResource= */ null,
+ /* base= */ launchContextMap[resourceType],
+ /* node= */ expressionNode,
)
// If the result of evaluating the FHIRPath expressions is an invalid query, it returns
@@ -376,50 +347,34 @@ object ExpressionEvaluator {
* @param variableName the [String] to match the variable in the ancestors
* @param questionnaireItem the [Questionnaire.QuestionnaireItemComponent] from where we have to
* track hierarchy up in the ancestors
- * @param questionnaire the [Questionnaire] respective questionnaire
- * @param questionnaireResponse the [QuestionnaireResponse] respective questionnaire response
- * @param questionnaireItemParentMap the [Map] of child to parent
* @param variablesMap the [Map] of variables
*/
private fun findAndEvaluateVariable(
variableName: String,
- questionnaireItem: Questionnaire.QuestionnaireItemComponent,
- questionnaire: Questionnaire,
- questionnaireResponse: QuestionnaireResponse,
- questionnaireItemParentMap:
- Map,
- variablesMap: MutableMap
+ questionnaireItem: QuestionnaireItemComponent,
+ variablesMap: MutableMap = mutableMapOf(),
) {
// First, check the questionnaire item itself
val evaluatedValue =
questionnaireItem.findVariableExpression(variableName)?.let { expression ->
evaluateQuestionnaireItemVariableExpression(
expression,
- questionnaire,
- questionnaireResponse,
- questionnaireItemParentMap,
questionnaireItem,
- variablesMap
+ variablesMap,
)
} // Secondly, check the ancestors of the questionnaire item
- ?: findVariableInAncestors(variableName, questionnaireItemParentMap, questionnaireItem)
- ?.let { (questionnaireItem, expression) ->
- evaluateQuestionnaireItemVariableExpression(
- expression,
- questionnaire,
- questionnaireResponse,
- questionnaireItemParentMap,
- questionnaireItem,
- variablesMap
- )
- } // Finally, check the variables defined on the questionnaire itself
+ ?: findVariableInAncestors(variableName, questionnaireItem)?.let {
+ (questionnaireItem, expression) ->
+ evaluateQuestionnaireItemVariableExpression(
+ expression,
+ questionnaireItem,
+ variablesMap,
+ )
+ } // Finally, check the variables defined on the questionnaire itself
?: questionnaire.findVariableExpression(variableName)?.let { expression ->
evaluateQuestionnaireVariableExpression(
expression,
- questionnaire,
- questionnaireResponse,
- variablesMap
+ variablesMap,
)
}
@@ -433,16 +388,12 @@ object ExpressionEvaluator {
* @param variableName the [String] to match the variable in the ancestors
* @param questionnaireItem the [Questionnaire.QuestionnaireItemComponent] whose ancestors we
* visit
- * @param questionnaireItemParentMap the [Map] of child to parent
* @return [Pair] containing [Questionnaire.QuestionnaireItemComponent] and an [Expression]
*/
private fun findVariableInAncestors(
variableName: String,
- questionnaireItemParentMap:
- Map,
- questionnaireItem: Questionnaire.QuestionnaireItemComponent
- ): Pair? {
+ questionnaireItem: QuestionnaireItemComponent
+ ): Pair? {
var parent = questionnaireItemParentMap[questionnaireItem]
while (parent != null) {
val expression = parent.findVariableExpression(variableName)
@@ -457,15 +408,13 @@ object ExpressionEvaluator {
* Evaluates the value of variable expression and returns its evaluated value
*
* @param expression the [Expression] the expression to evaluate
- * @param questionnaireResponse the [QuestionnaireResponse] respective questionnaire response
* @param dependentVariables the [Map] of variable names to their values
*
* @return [Base] the result of an expression
*/
private fun evaluateVariable(
expression: Expression,
- questionnaireResponse: QuestionnaireResponse,
- dependentVariables: Map = mapOf()
+ dependentVariables: Map = emptyMap(),
) =
try {
require(expression.name?.isNotBlank() == true) {
@@ -477,7 +426,13 @@ object ExpressionEvaluator {
}
fhirPathEngine
- .evaluate(dependentVariables, questionnaireResponse, null, null, expression.expression)
+ .evaluate(
+ /* appContext= */ dependentVariables,
+ /* focusResource= */ questionnaireResponse,
+ /* rootResource= */ null,
+ /* base= */ null,
+ /* path= */ expression.expression,
+ )
.firstOrNull()
} catch (exception: FHIRException) {
Timber.w("Could not evaluate expression with FHIRPathEngine", exception)
@@ -486,4 +441,4 @@ object ExpressionEvaluator {
}
/** Pair of a [Questionnaire.QuestionnaireItemComponent] with its evaluated answers */
-internal typealias ItemToAnswersPair = Pair>
+internal typealias ItemToAnswersPair = Pair>
diff --git a/datacapture/src/main/java/com/google/android/fhir/datacapture/fhirpath/FhirPathUtil.kt b/datacapture/src/main/java/com/google/android/fhir/datacapture/fhirpath/FhirPathUtil.kt
index d09342e94c..9504262cdf 100644
--- a/datacapture/src/main/java/com/google/android/fhir/datacapture/fhirpath/FhirPathUtil.kt
+++ b/datacapture/src/main/java/com/google/android/fhir/datacapture/fhirpath/FhirPathUtil.kt
@@ -1,5 +1,5 @@
/*
- * Copyright 2022 Google LLC
+ * Copyright 2022-2023 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -19,6 +19,7 @@ package com.google.android.fhir.datacapture.fhirpath
import ca.uhn.fhir.context.FhirContext
import ca.uhn.fhir.context.FhirVersionEnum
import org.hl7.fhir.r4.hapi.ctx.HapiWorkerContext
+import org.hl7.fhir.r4.model.Base
import org.hl7.fhir.r4.model.QuestionnaireResponse
import org.hl7.fhir.r4.model.QuestionnaireResponse.QuestionnaireResponseItemComponent
import org.hl7.fhir.r4.model.Resource
@@ -47,11 +48,15 @@ internal fun evaluateToDisplay(expressions: List, data: Resource) =
internal fun evaluateToBoolean(
questionnaireResponse: QuestionnaireResponse,
questionnaireResponseItemComponent: QuestionnaireResponseItemComponent,
- expression: String
-) =
- fhirPathEngine.evaluateToBoolean(
- questionnaireResponse,
- null,
- questionnaireResponseItemComponent,
- expression
+ expression: String,
+ contextMap: Map = mapOf(),
+): Boolean {
+ val expressionNode = fhirPathEngine.parse(expression)
+ return fhirPathEngine.evaluateToBoolean(
+ /* appInfo= */ contextMap,
+ /* focusResource= */ questionnaireResponse,
+ /* rootResource= */ null,
+ /* base= */ questionnaireResponseItemComponent,
+ /* node= */ expressionNode,
)
+}
diff --git a/datacapture/src/main/java/com/google/android/fhir/datacapture/mapping/ResourceMapper.kt b/datacapture/src/main/java/com/google/android/fhir/datacapture/mapping/ResourceMapper.kt
index 8fee63df06..5a38ae34b7 100644
--- a/datacapture/src/main/java/com/google/android/fhir/datacapture/mapping/ResourceMapper.kt
+++ b/datacapture/src/main/java/com/google/android/fhir/datacapture/mapping/ResourceMapper.kt
@@ -18,6 +18,7 @@ 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.initialExpression
import com.google.android.fhir.datacapture.extensions.logicalId
import com.google.android.fhir.datacapture.extensions.targetStructureMap
import com.google.android.fhir.datacapture.extensions.toCodeType
@@ -280,13 +281,6 @@ object ResourceMapper {
?: resources.firstOrNull()
}
- private val Questionnaire.QuestionnaireItemComponent.initialExpression: Expression?
- get() {
- return this.extension
- .firstOrNull { it.url == ITEM_INITIAL_EXPRESSION_URL }
- ?.let { it.value as Expression }
- }
-
/**
* Updates corresponding fields in [extractionContext] with answers in
* [questionnaireResponseItemList]. The fields are defined in the definitions in
@@ -721,9 +715,6 @@ private fun wrapAnswerInFieldType(answer: Base, fieldType: Field): Base {
return answer
}
-internal const val ITEM_INITIAL_EXPRESSION_URL: String =
- "http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-initialExpression"
-
private val Field.isList: Boolean
get() = isParameterized && type == List::class.java
diff --git a/datacapture/src/main/java/com/google/android/fhir/datacapture/validation/QuestionnaireResponseValidator.kt b/datacapture/src/main/java/com/google/android/fhir/datacapture/validation/QuestionnaireResponseValidator.kt
index 06d2b275d9..0073fc9c64 100644
--- a/datacapture/src/main/java/com/google/android/fhir/datacapture/validation/QuestionnaireResponseValidator.kt
+++ b/datacapture/src/main/java/com/google/android/fhir/datacapture/validation/QuestionnaireResponseValidator.kt
@@ -1,5 +1,5 @@
/*
- * Copyright 2022 Google LLC
+ * Copyright 2022-2023 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -21,6 +21,7 @@ import com.google.android.fhir.datacapture.enablement.EnablementEvaluator
import com.google.android.fhir.datacapture.extensions.packRepeatedGroups
import org.hl7.fhir.r4.model.Questionnaire
import org.hl7.fhir.r4.model.QuestionnaireResponse
+import org.hl7.fhir.r4.model.Resource
import org.hl7.fhir.r4.model.Type
object QuestionnaireResponseValidator {
@@ -56,7 +57,11 @@ object QuestionnaireResponseValidator {
fun validateQuestionnaireResponse(
questionnaire: Questionnaire,
questionnaireResponse: QuestionnaireResponse,
- context: Context
+ context: Context,
+ questionnaireItemParentMap:
+ Map =
+ mapOf(),
+ launchContextMap: Map? = mapOf(),
): Map> {
require(
questionnaireResponse.questionnaire == null ||
@@ -71,7 +76,12 @@ object QuestionnaireResponseValidator {
questionnaire.item,
questionnaireResponse.item,
context,
- EnablementEvaluator(questionnaireResponse),
+ EnablementEvaluator(
+ questionnaire,
+ questionnaireResponse,
+ questionnaireItemParentMap,
+ launchContextMap
+ ),
linkIdToValidationResultMap,
)
@@ -98,7 +108,11 @@ object QuestionnaireResponseValidator {
questionnaireItem = questionnaireItemListIterator.next()
} while (questionnaireItem!!.linkId != questionnaireResponseItem.linkId)
- val enabled = enablementEvaluator.evaluate(questionnaireItem, questionnaireResponseItem)
+ val enabled =
+ enablementEvaluator.evaluate(
+ questionnaireItem,
+ questionnaireResponseItem,
+ )
if (enabled) {
validateQuestionnaireResponseItem(
@@ -118,7 +132,7 @@ object QuestionnaireResponseValidator {
questionnaireResponseItem: QuestionnaireResponse.QuestionnaireResponseItemComponent,
context: Context,
enablementEvaluator: EnablementEvaluator,
- linkIdToValidationResultMap: MutableMap>
+ linkIdToValidationResultMap: MutableMap>,
): Map> {
when (checkNotNull(questionnaireItem.type) { "Questionnaire item must have type" }) {
diff --git a/datacapture/src/main/res/values/dimens.xml b/datacapture/src/main/res/values/dimens.xml
index b32fe068d1..62747fef03 100644
--- a/datacapture/src/main/res/values/dimens.xml
+++ b/datacapture/src/main/res/values/dimens.xml
@@ -32,6 +32,7 @@
4dp
24dp
24dp
+ 2dp
16dp
4dp
4dp
diff --git a/datacapture/src/main/res/values/styles.xml b/datacapture/src/main/res/values/styles.xml
index 7e83568071..df21394e69 100644
--- a/datacapture/src/main/res/values/styles.xml
+++ b/datacapture/src/main/res/values/styles.xml
@@ -63,7 +63,7 @@
name="questionnaireHelpTextStyle"
>@style/TextAppearance.Material3.BodyMedium
-
- @style/Widget.MaterialComponents.Button.TextButton.Icon
+ @style/Questionnaire.HelpIconStyle
-
@style/Questionnaire.MediaImageStyle
@@ -372,6 +372,13 @@
>@dimen/option_item_after_text_padding
+
+