Skip to content

Commit 33fd86e

Browse files
committed
Validate extracted fhir resources while in debug
1 parent fede775 commit 33fd86e

File tree

8 files changed

+134
-3
lines changed

8 files changed

+134
-3
lines changed

android/engine/build.gradle.kts

+2
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,8 @@ dependencies {
157157
exclude(group = "ca.uhn.hapi.fhir")
158158
}
159159

160+
implementation(libs.hapi.fhir.validation) { exclude(module = "commons-logging") }
161+
160162
// Shared dependencies
161163
api(libs.glide)
162164
api(libs.knowledger)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
/*
2+
* Copyright 2021-2023 Ona Systems, Inc
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.smartregister.fhircore.engine.di
18+
19+
import ca.uhn.fhir.context.FhirContext
20+
import ca.uhn.fhir.validation.FhirValidator
21+
import dagger.Module
22+
import dagger.Provides
23+
import dagger.hilt.InstallIn
24+
import dagger.hilt.components.SingletonComponent
25+
import javax.inject.Singleton
26+
import org.hl7.fhir.common.hapi.validation.validator.FhirInstanceValidator
27+
28+
@Module
29+
@InstallIn(SingletonComponent::class)
30+
class FhirValidatorModule {
31+
32+
@Provides
33+
@Singleton
34+
fun provideFhirValidator(): FhirValidator {
35+
val fhirContext = FhirContext.forR4()
36+
val validatorModule = FhirInstanceValidator(fhirContext)
37+
return fhirContext.newValidator().apply { registerValidatorModule(validatorModule) }
38+
}
39+
}

android/engine/src/main/java/org/smartregister/fhircore/engine/task/FhirCarePlanGenerator.kt

+24-2
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,14 @@ package org.smartregister.fhircore.engine.task
1919
import androidx.annotation.VisibleForTesting
2020
import ca.uhn.fhir.context.FhirContext
2121
import ca.uhn.fhir.util.TerserUtil
22+
import ca.uhn.fhir.validation.FhirValidator
2223
import com.google.android.fhir.FhirEngine
2324
import com.google.android.fhir.get
2425
import com.google.android.fhir.logicalId
2526
import com.google.android.fhir.search.search
2627
import java.util.Date
2728
import javax.inject.Inject
29+
import javax.inject.Provider
2830
import javax.inject.Singleton
2931
import org.hl7.fhir.r4.model.ActivityDefinition
3032
import org.hl7.fhir.r4.model.Base
@@ -56,7 +58,9 @@ import org.smartregister.fhircore.engine.configuration.event.EventType
5658
import org.smartregister.fhircore.engine.data.local.DefaultRepository
5759
import org.smartregister.fhircore.engine.util.extension.addResourceParameter
5860
import org.smartregister.fhircore.engine.util.extension.asReference
61+
import org.smartregister.fhircore.engine.util.extension.checkResourceValid
5962
import org.smartregister.fhircore.engine.util.extension.encodeResourceToString
63+
import org.smartregister.fhircore.engine.util.extension.errorMessages
6064
import org.smartregister.fhircore.engine.util.extension.extractFhirpathDuration
6165
import org.smartregister.fhircore.engine.util.extension.extractFhirpathPeriod
6266
import org.smartregister.fhircore.engine.util.extension.extractId
@@ -75,6 +79,7 @@ constructor(
7579
val transformSupportServices: TransformSupportServices,
7680
val defaultRepository: DefaultRepository,
7781
val fhirResourceUtil: FhirResourceUtil,
82+
val fhirValidatorProvider: Provider<FhirValidator>,
7883
val workflowCarePlanGenerator: WorkflowCarePlanGenerator,
7984
) {
8085
private val structureMapUtilities by lazy {
@@ -186,7 +191,22 @@ constructor(
186191

187192
val carePlanTasks = output.contained.filterIsInstance<Task>()
188193

189-
if (carePlanModified) saveCarePlan(output)
194+
if (carePlanModified) {
195+
fhirValidatorProvider
196+
.get()
197+
.checkResourceValid(output)
198+
.filterNot { it.isSuccessful }
199+
.takeIf { it.isNotEmpty() }
200+
?.let {
201+
val errors = buildString {
202+
it.forEach { validationResult -> appendLine(validationResult.errorMessages) }
203+
}
204+
205+
throw IllegalStateException(errors)
206+
}
207+
208+
saveCarePlan(output)
209+
}
190210

191211
if (carePlanTasks.isNotEmpty()) {
192212
fhirResourceUtil.updateUpcomingTasksToDue(
@@ -208,7 +228,9 @@ constructor(
208228
carePlan.contained.clear()
209229

210230
// Save CarePlan only if it has activity, otherwise just save contained/dependent resources
211-
if (output.hasActivity()) defaultRepository.create(true, carePlan)
231+
if (output.hasActivity()) {
232+
defaultRepository.create(true, carePlan)
233+
}
212234

213235
dependents.forEach { defaultRepository.create(true, it) }
214236

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
/*
2+
* Copyright 2021-2023 Ona Systems, Inc
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.smartregister.fhircore.engine.util.extension
18+
19+
import ca.uhn.fhir.validation.FhirValidator
20+
import ca.uhn.fhir.validation.ResultSeverityEnum
21+
import ca.uhn.fhir.validation.ValidationResult
22+
import kotlin.coroutines.coroutineContext
23+
import kotlinx.coroutines.withContext
24+
import org.hl7.fhir.r4.model.Resource
25+
import org.smartregister.fhircore.engine.BuildConfig
26+
27+
suspend fun FhirValidator.checkResourceValid(vararg resource: Resource): List<ValidationResult> {
28+
if (!BuildConfig.BUILD_TYPE.contains("debug", ignoreCase = true)) return emptyList()
29+
30+
return withContext(coroutineContext) {
31+
resource.map { this@checkResourceValid.validateWithResult(it) }
32+
}
33+
}
34+
35+
val ValidationResult.errorMessages
36+
get() = buildString {
37+
for (validationMsg in messages.filter { it.severity == ResultSeverityEnum.ERROR }) {
38+
appendLine("${validationMsg.message} - ${validationMsg.locationString}")
39+
}
40+
}

android/engine/src/test/java/org/smartregister/fhircore/engine/task/FhirCarePlanGeneratorTest.kt

+5
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import ca.uhn.fhir.context.FhirContext
2121
import ca.uhn.fhir.context.FhirVersionEnum
2222
import ca.uhn.fhir.parser.IParser
2323
import ca.uhn.fhir.rest.gclient.ReferenceClientParam
24+
import ca.uhn.fhir.validation.FhirValidator
2425
import com.google.android.fhir.FhirEngine
2526
import com.google.android.fhir.SearchResult
2627
import com.google.android.fhir.get
@@ -45,6 +46,7 @@ import java.util.Calendar
4546
import java.util.Date
4647
import java.util.UUID
4748
import javax.inject.Inject
49+
import javax.inject.Provider
4850
import kotlin.time.Duration.Companion.seconds
4951
import kotlinx.coroutines.Dispatchers
5052
import kotlinx.coroutines.ExperimentalCoroutinesApi
@@ -128,6 +130,8 @@ class FhirCarePlanGeneratorTest : RobolectricTest() {
128130

129131
@Inject lateinit var workflowCarePlanGenerator: WorkflowCarePlanGenerator
130132

133+
@Inject lateinit var fhirValidatorProvider: Provider<FhirValidator>
134+
131135
@Inject lateinit var fhirPathEngine: FHIRPathEngine
132136

133137
@Inject lateinit var fhirEngine: FhirEngine
@@ -166,6 +170,7 @@ class FhirCarePlanGeneratorTest : RobolectricTest() {
166170
fhirPathEngine = fhirPathEngine,
167171
defaultRepository = defaultRepository,
168172
fhirResourceUtil = fhirResourceUtil,
173+
fhirValidatorProvider = fhirValidatorProvider,
169174
workflowCarePlanGenerator = workflowCarePlanGenerator,
170175
)
171176

android/gradle/libs.versions.toml

+2
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,7 @@ work-runtime-ktx = "2.8.1"
9898
work-testing = "2.8.1"
9999
workflow = "0.1.0-alpha02-preview9.1-SNAPSHOT"
100100
xercesImpl = "2.12.2"
101+
hapi-fhir = "6.0.1"
101102

102103
[libraries]
103104
accompanist-flowlayout = { group = "com.google.accompanist", name = "accompanist-flowlayout", version.ref = "accompanist-flowlayout" }
@@ -205,5 +206,6 @@ work-runtime-ktx = { group = "androidx.work", name = "work-runtime-ktx", version
205206
work-testing = { group = "androidx.work", name = "work-testing", version.ref = "work-testing" }
206207
workflow = { group = "org.smartregister", name = "workflow", version.ref = "workflow" }
207208
xercesImpl = { group = "xerces", name = "xercesImpl", version.ref = "xercesImpl" }
209+
hapi-fhir-validation = {group = "ca.uhn.hapi.fhir", name="hapi-fhir-validation", version.ref = "hapi-fhir"}
208210

209211
[plugins]

android/quest/src/main/java/org/smartregister/fhircore/quest/ui/questionnaire/QuestionnaireViewModel.kt

+17-1
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import androidx.lifecycle.LiveData
2222
import androidx.lifecycle.MutableLiveData
2323
import androidx.lifecycle.ViewModel
2424
import androidx.lifecycle.viewModelScope
25+
import ca.uhn.fhir.validation.FhirValidator
2526
import com.google.android.fhir.datacapture.mapping.ResourceMapper
2627
import com.google.android.fhir.datacapture.mapping.StructureMapExtractionContext
2728
import com.google.android.fhir.datacapture.validation.NotValidated
@@ -35,6 +36,7 @@ import dagger.hilt.android.lifecycle.HiltViewModel
3536
import java.util.Date
3637
import java.util.UUID
3738
import javax.inject.Inject
39+
import javax.inject.Provider
3840
import kotlinx.coroutines.SupervisorJob
3941
import kotlinx.coroutines.launch
4042
import kotlinx.coroutines.withContext
@@ -45,7 +47,6 @@ import org.hl7.fhir.r4.model.Group
4547
import org.hl7.fhir.r4.model.IdType
4648
import org.hl7.fhir.r4.model.ListResource
4749
import org.hl7.fhir.r4.model.ListResource.ListEntryComponent
48-
import org.hl7.fhir.r4.model.Patient
4950
import org.hl7.fhir.r4.model.Questionnaire
5051
import org.hl7.fhir.r4.model.QuestionnaireResponse
5152
import org.hl7.fhir.r4.model.RelatedPerson
@@ -66,7 +67,9 @@ import org.smartregister.fhircore.engine.util.extension.DEFAULT_PLACEHOLDER_PREF
6667
import org.smartregister.fhircore.engine.util.extension.appendOrganizationInfo
6768
import org.smartregister.fhircore.engine.util.extension.appendPractitionerInfo
6869
import org.smartregister.fhircore.engine.util.extension.asReference
70+
import org.smartregister.fhircore.engine.util.extension.checkResourceValid
6971
import org.smartregister.fhircore.engine.util.extension.cqfLibraryUrls
72+
import org.smartregister.fhircore.engine.util.extension.errorMessages
7073
import org.smartregister.fhircore.engine.util.extension.extractByStructureMap
7174
import org.smartregister.fhircore.engine.util.extension.extractId
7275
import org.smartregister.fhircore.engine.util.extension.extractLogicalIdUuid
@@ -93,6 +96,7 @@ constructor(
9396
val transformSupportServices: TransformSupportServices,
9497
val sharedPreferencesHelper: SharedPreferencesHelper,
9598
val fhirOperator: FhirOperator,
99+
val fhirValidatorProvider: Provider<FhirValidator>,
96100
val fhirPathDataExtractor: FhirPathDataExtractor,
97101
) : ViewModel() {
98102

@@ -206,6 +210,18 @@ constructor(
206210
context = context,
207211
)
208212

213+
val validationResults =
214+
bundle.entry
215+
.map { it.resource }
216+
.flatMap { fhirValidatorProvider.get().checkResourceValid(it) }
217+
val validationFailures = validationResults.filterNot { it.isSuccessful }
218+
if (validationFailures.isNotEmpty()) {
219+
val errorMessages = buildString {
220+
validationFailures.map { it.errorMessages }.forEach(this::appendLine)
221+
}
222+
throw IllegalStateException(errorMessages)
223+
}
224+
209225
saveExtractedResources(
210226
bundle = bundle,
211227
questionnaire = questionnaire,

android/quest/src/test/java/org/smartregister/fhircore/quest/ui/questionnaire/QuestionnaireViewModelTest.kt

+5
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ package org.smartregister.fhircore.quest.ui.questionnaire
1818

1919
import android.app.Application
2020
import androidx.test.core.app.ApplicationProvider
21+
import ca.uhn.fhir.validation.FhirValidator
2122
import com.google.android.fhir.FhirEngine
2223
import com.google.android.fhir.datacapture.mapping.ResourceMapper
2324
import com.google.android.fhir.db.ResourceNotFoundException
@@ -40,6 +41,7 @@ import io.mockk.verify
4041
import java.util.Date
4142
import java.util.UUID
4243
import javax.inject.Inject
44+
import javax.inject.Provider
4345
import kotlin.time.Duration.Companion.seconds
4446
import kotlinx.coroutines.ExperimentalCoroutinesApi
4547
import kotlinx.coroutines.runBlocking
@@ -108,6 +110,8 @@ class QuestionnaireViewModelTest : RobolectricTest() {
108110

109111
@Inject lateinit var sharedPreferencesHelper: SharedPreferencesHelper
110112

113+
@Inject lateinit var fhirValidatorProvider: Provider<FhirValidator>
114+
111115
@Inject lateinit var configService: ConfigService
112116

113117
@Inject lateinit var resourceDataRulesExecutor: ResourceDataRulesExecutor
@@ -180,6 +184,7 @@ class QuestionnaireViewModelTest : RobolectricTest() {
180184
fhirCarePlanGenerator = fhirCarePlanGenerator,
181185
resourceDataRulesExecutor = resourceDataRulesExecutor,
182186
fhirPathDataExtractor = fhirPathDataExtractor,
187+
fhirValidatorProvider = fhirValidatorProvider,
183188
fhirOperator = fhirOperator,
184189
),
185190
)

0 commit comments

Comments
 (0)