diff --git a/datacapture/build.gradle.kts b/datacapture/build.gradle.kts index 36b3e07d21..6200639a17 100644 --- a/datacapture/build.gradle.kts +++ b/datacapture/build.gradle.kts @@ -73,8 +73,6 @@ dependencies { coreLibraryDesugaring(Dependencies.desugarJdkLibs) - debugImplementation(project(":engine")) - implementation(Dependencies.androidFhirCommon) implementation(Dependencies.Androidx.appCompat) implementation(Dependencies.Androidx.constraintLayout) @@ -92,8 +90,6 @@ dependencies { implementation(Dependencies.lifecycleExtensions) implementation(Dependencies.timber) - releaseImplementation(Dependencies.androidFhirEngine) - testImplementation(Dependencies.AndroidxTest.core) testImplementation(Dependencies.AndroidxTest.fragmentTesting) testImplementation(Dependencies.Kotlin.kotlinTestJunit) diff --git a/datacapture/src/main/java/com/google/android/fhir/datacapture/DataCaptureConfig.kt b/datacapture/src/main/java/com/google/android/fhir/datacapture/DataCaptureConfig.kt index 4b7bf17781..14e590b697 100644 --- a/datacapture/src/main/java/com/google/android/fhir/datacapture/DataCaptureConfig.kt +++ b/datacapture/src/main/java/com/google/android/fhir/datacapture/DataCaptureConfig.kt @@ -20,6 +20,7 @@ import android.app.Application import com.google.android.fhir.datacapture.DataCaptureConfig.Provider import org.hl7.fhir.r4.context.SimpleWorkerContext import org.hl7.fhir.r4.model.Coding +import org.hl7.fhir.r4.model.Resource import org.hl7.fhir.r4.model.StructureMap import org.hl7.fhir.utilities.npm.NpmPackage @@ -45,7 +46,13 @@ data class DataCaptureConfig( * should try to include the smallest [NpmPackage] possible that contains only the resources * needed by [StructureMap]s used by the client app. */ - var npmPackage: NpmPackage? = null + var npmPackage: NpmPackage? = null, + + /** + * A [XFhirQueryResolver] may be set by the client to resolve x-fhir-query for the library. See + * https://build.fhir.org/ig/HL7/sdc/expressions.html#fhirquery for more details. + */ + var xFhirQueryResolver: XFhirQueryResolver? = null, ) { internal val simpleWorkerContext: SimpleWorkerContext by lazy { @@ -75,3 +82,14 @@ data class DataCaptureConfig( interface ExternalAnswerValueSetResolver { suspend fun resolve(uri: String): List } + +/** + * Resolves resources based on the provided xFhir query. This allows the library to resolve + * x-fhir-query answer expressions. + * + * NOTE: The result of the resolution may be cached to improve performance. In other words, the + * resolver may be called only once after which the Resources may be used multiple times in the UI. + */ +fun interface XFhirQueryResolver { + suspend fun resolve(xFhirQuery: String): List +} diff --git a/datacapture/src/main/java/com/google/android/fhir/datacapture/MoreQuestionnaireItemComponents.kt b/datacapture/src/main/java/com/google/android/fhir/datacapture/MoreQuestionnaireItemComponents.kt index a4740c0bc1..fa4711cfaa 100644 --- a/datacapture/src/main/java/com/google/android/fhir/datacapture/MoreQuestionnaireItemComponents.kt +++ b/datacapture/src/main/java/com/google/android/fhir/datacapture/MoreQuestionnaireItemComponents.kt @@ -21,7 +21,6 @@ import androidx.core.text.HtmlCompat import com.google.android.fhir.datacapture.common.datatype.asStringValue import com.google.android.fhir.datacapture.utilities.evaluateToDisplay import com.google.android.fhir.getLocalizedText -import com.google.android.fhir.logicalId import org.hl7.fhir.r4.model.Base import org.hl7.fhir.r4.model.BooleanType import org.hl7.fhir.r4.model.CodeType @@ -545,3 +544,8 @@ fun List.flattened(): */ fun Questionnaire.QuestionnaireItemComponent.getNestedQuestionnaireResponseItems() = item.map { it.createQuestionnaireResponseItem() } + +val Resource.logicalId: String + get() { + return this.idElement?.idPart.orEmpty() + } 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 cf1bb87a18..d010bf8eb2 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 @@ -25,7 +25,6 @@ import androidx.lifecycle.viewModelScope import ca.uhn.fhir.context.FhirContext import ca.uhn.fhir.context.FhirVersionEnum import ca.uhn.fhir.parser.IParser -import com.google.android.fhir.FhirEngineProvider import com.google.android.fhir.datacapture.enablement.EnablementEvaluator import com.google.android.fhir.datacapture.fhirpath.ExpressionEvaluator.detectExpressionCyclicDependency import com.google.android.fhir.datacapture.fhirpath.ExpressionEvaluator.evaluateCalculatedExpressions @@ -38,7 +37,6 @@ import com.google.android.fhir.datacapture.validation.QuestionnaireResponseValid import com.google.android.fhir.datacapture.validation.Valid import com.google.android.fhir.datacapture.validation.ValidationResult import com.google.android.fhir.datacapture.views.QuestionnaireItemViewItem -import com.google.android.fhir.search.search import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow @@ -57,7 +55,9 @@ internal class QuestionnaireViewModel(application: Application, state: SavedStat AndroidViewModel(application) { private val parser: IParser by lazy { FhirContext.forCached(FhirVersionEnum.R4).newJsonParser() } - private val fhirEngine by lazy { FhirEngineProvider.getInstance(application) } + private val xFhirQueryResolver: XFhirQueryResolver? by lazy { + DataCapture.getConfiguration(application).xFhirQueryResolver + } /** The current questionnaire as questions are being answered. */ internal val questionnaire: Questionnaire @@ -448,13 +448,18 @@ internal class QuestionnaireViewModel(application: Application, state: SavedStat expression: Expression, ): List { val data = - if (expression.isXFhirQuery) fhirEngine.search(expression.expression) - else if (expression.isFhirPath) + if (expression.isXFhirQuery) { + checkNotNull(xFhirQueryResolver) { + "XFhirQueryResolver cannot be null. Please provide the XFhirQueryResolver via DataCaptureConfig." + } + xFhirQueryResolver!!.resolve(expression.expression) + } else if (expression.isFhirPath) { fhirPathEngine.evaluate(questionnaireResponse, expression.expression) - else + } else { throw UnsupportedOperationException( "${expression.language} not supported for answer-expression yet" ) + } return item.extractAnswerOptions(data) } diff --git a/datacapture/src/test/java/com/google/android/fhir/datacapture/QuestionnaireViewModelTest.kt b/datacapture/src/test/java/com/google/android/fhir/datacapture/QuestionnaireViewModelTest.kt index e9375b96b6..1b935240ea 100644 --- a/datacapture/src/test/java/com/google/android/fhir/datacapture/QuestionnaireViewModelTest.kt +++ b/datacapture/src/test/java/com/google/android/fhir/datacapture/QuestionnaireViewModelTest.kt @@ -35,7 +35,6 @@ import com.google.android.fhir.datacapture.testing.DataCaptureTestApplication import com.google.android.fhir.datacapture.validation.Invalid import com.google.android.fhir.datacapture.validation.NotValidated import com.google.android.fhir.datacapture.views.QuestionnaireItemViewItem -import com.google.android.fhir.logicalId import com.google.android.fhir.testing.FhirEngineProviderTestRule import com.google.common.truth.Truth.assertThat import java.util.Calendar @@ -62,7 +61,7 @@ import org.hl7.fhir.r4.model.QuestionnaireResponse import org.hl7.fhir.r4.model.StringType import org.hl7.fhir.r4.model.ValueSet import org.hl7.fhir.r4.utils.ToolingExtensions -import org.junit.Assert +import org.junit.Assert.assertThrows import org.junit.Before import org.junit.Ignore import org.junit.Rule @@ -2894,13 +2893,13 @@ class QuestionnaireViewModelTest { fun `resolveAnswerExpression() should return questionnaire item answer options for answer expression and choice column`() = runBlocking { val practitioner = - Practitioner() - .apply { - id = UUID.randomUUID().toString() - active = true - addName(HumanName().apply { this.family = "John" }) - } - .also { fhirEngine.create(it) } + Practitioner().apply { + id = UUID.randomUUID().toString() + active = true + addName(HumanName().apply { this.family = "John" }) + } + ApplicationProvider.getApplicationContext() + .dataCaptureConfiguration = DataCaptureConfig(xFhirQueryResolver = { listOf(practitioner) }) val questionnaire = Questionnaire().apply { @@ -2939,6 +2938,47 @@ class QuestionnaireViewModelTest { .isEqualTo("Practitioner/${practitioner.logicalId}") } + @Test + fun `resolveAnswerExpression() should throw exception when XFhirQueryResolver is not provided`() { + val questionnaire = + Questionnaire().apply { + addItem( + Questionnaire.QuestionnaireItemComponent().apply { + linkId = "a" + text = "answer expression question text" + type = Questionnaire.QuestionnaireItemType.REFERENCE + extension = + listOf( + Extension( + "http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-answerExpression", + Expression().apply { + this.expression = "Practitioner?active=true" + this.language = Expression.ExpressionLanguage.APPLICATION_XFHIRQUERY.toCode() + } + ), + Extension( + "http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-choiceColumn" + ) + .apply { + this.addExtension(Extension("path", StringType("id"))) + this.addExtension(Extension("label", StringType("name"))) + this.addExtension(Extension("forDisplay", BooleanType(true))) + } + ) + } + ) + } + state.set(EXTRA_QUESTIONNAIRE_JSON_STRING, printer.encodeResourceToString(questionnaire)) + val viewModel = QuestionnaireViewModel(context, state) + val exception = + assertThrows(null, IllegalStateException::class.java) { + runBlocking { viewModel.resolveAnswerExpression(questionnaire.itemFirstRep) } + } + assertThat(exception.message) + .isEqualTo( + "XFhirQueryResolver cannot be null. Please provide the XFhirQueryResolver via DataCaptureConfig." + ) + } // Test cases for submit button @Test @@ -3602,7 +3642,7 @@ class QuestionnaireViewModelTest { } val exception = - Assert.assertThrows(null, IllegalStateException::class.java) { + assertThrows(null, IllegalStateException::class.java) { createQuestionnaireViewModel(questionnaire) } assertThat(exception.message) @@ -3667,7 +3707,7 @@ class QuestionnaireViewModelTest { } val exception = - Assert.assertThrows(null, IllegalStateException::class.java) { + assertThrows(null, IllegalStateException::class.java) { createQuestionnaireViewModel(questionnaire) } assertThat(exception.message)