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 + +